Koa鉴权

382 阅读3分钟

后端为什么要做鉴权

前后端分离以前,页面都是通过后台来渲染的,能不能访问页面直接由后台逻辑判断,前后端分离以后,页面元素由页面本身控制,所以页面间的路由是前端来控制的,当然,仅有前端做权限控制还是不够,后台还需要对每个接口进行验证

前端做权限控制是不够的,因为前端的路由仅仅是视觉上的控制,前端可以隐藏某个页面或者按钮,但是发送请求的方式还有很多,完全可以跳过操作页面来发送某个请求,所以就算前端的权限控制做的非常严密,后台依旧需要验证每个接口

前端的权限控制主要分三种,路由控制-跳转、视图控制-按钮级别 和 请求控制-请求拦截器

前端做完权限控制,后台还是需要验证每一个接口,这就是鉴权,现在前后端配合鉴权的方式主要有以下几种

  • session-cookie
  • Token验证 ( JWT )
  • OAuth ( 开放验证 )

Session/Cookie

cookie

http协议是一个无状态协议,服务器不知道是哪台浏览器访问了它,因此需要一个标识来让服务器区分每个浏览器,cookie就是这个管理服务器与客户端之间的状态标识

当浏览器第一次向服务器发送请求时,服务器在res头部设置set-cookie字段,浏览器收到响应就会设置cookie并存储,在下一次该浏览器向服务器发送请求时,就会在req头部带上cookie字段,服务器收到该cookie用以区分不同的浏览器,当然,这个cookie与某个用户的段颖关系应该在第一次访问时就存在服务器端了,这时就需要session了

session

session是会话的意思,浏览器第一次访问服务端,服务端就会创建一次会话,在会话中标识该浏览器信息,它与cookie的区别是session是缓存在服务端的,cookie则是缓存在客户端,他们都由服务端生成,为了弥补http协议无状态的缺陷

session - cookie认证

image-20211024112515882

session签名的使用

npm i koa-session -s

server.js

const Koa = require('koa')
const app = new Koa()

const session = require('koa-session')

// keys的签名:用来给cookie进行签名,原本为加密字符串,这里为理解简化
app.keys = ['session secret', 'anthor secret']

const SESSION_CONFIG = {
  key: 'Max:sess', // 设置cookie的key名字
  maxAge: 8640000, // 表示有效期,单位秒,这里为一天
  httpOnly: true, // 仅服务器修改
  signed: true // 签名cookie
}

// 作为中间件导入,写在静态资源目录和路由中间
app.use(session(SESSION_CONFIG, app))

// 测试
app.use(async ctx => {
  // 记录每一次访问,如果空则为0
  let n = ctx.session.count || 0
  // 每次访问 +n
  ctx.session.count = ++n
  ctx.body = `第${n}次访问`
})

app.listen(3000, function(){
  console.log('3000端口监听中');
})

将session存入redis数据库中

nas安装redis数据库

桌面新建 txt 写入以下配置,更改文件名为redis.conf

databases 16
maxmemory 1gb
maxmemory-policy allkeys-lru

File Station 创建目录上传配置文件

image-20211024154432064

群辉Docker 注册表搜 redis,安装last版本

打开映像 → 修改项目名 → 高级设置 → 存储空间添加配置文件和data目录

image-20211024183844443

本地端口与容器端口一致

image-20211024155213617

环境命令写入 redis-server /usr/local/etc/redis/redis.conf

image-20211024155401081

TIPS:redis数据库data数据默认就是一个/data/dump.rdb文件,需要备份的话保存缓存到此文件,备份此文件即可;需要恢复的话停掉docker redis 替换掉 data/dump.rdb即可,如图:

img

koa连接redis数据库
npm i redis koa-redis -s

连接nas的redis数据库失败,弃用

Token + JWT认证

token获得用户名和密码后传给服务端,服务端不需要将用户名和密码存放到redis中, 服务器会在当前路由下创建一个JWT签名秘钥,类似与一个令牌,返回给浏览器,浏览器一般会把令牌存放到localStorage中,当浏览器再次发送请求时,浏览器把令牌放在请求头给服务器端,服务器会拿这边的令牌进行对比,如果一致,直接放行,相对于session,token不需要存放到数据库中,对服务器压力小

image-20211024194051549

安装

// jwt中间件
npm i koa-jwt -s
// 用于生成token下发给浏览器
npm i jsonwebtoken -s

目录如下

image-20211025221643262

server.js 服务器

const Koa = require('koa')
const static = require('koa-static')
const bodyparser = require('koa-bodyparser')
const app = new Koa()
 
app.use(bodyparser())

app.use(static(__dirname + '/public'))

// 导入路由
const routerUser = require('./router/user')

// 注册路由
app.use(routerUser.routes()) // 中间件导入路由对象
app.use(routerUser.allowedMethods()) // 根据ctx.state 设置response响应头

app.use(async ctx => {
  ctx.body = 'Hello Koa!'
})

app.listen(3000, function() {
  console.log('3000端口监听中')
})

服务器端需要导入koa-bodyparser来解析请求体body内的数据

user.js 路由

const Router = require('koa-router')
const jwt = require('jsonwebtoken') // 生成令牌的模块
const jwtAuth = require('koa-jwt') // 认证令牌的模块

const secret = 'this is a secret' // 声明加密的秘钥

const router = new Router({
  prefix: '/user'
})

router.post('/login_token',async (ctx, next) => {
  // 从请求体中获取body
  const { body } = ctx.request;
  // 获取请求体内的用户名
  const userInfo = body.user;
  // 发送给服务器验证账号密码---
  // ....
  // 省略中间服务器验证账号密码操作,返回给客户端进行登录
  ctx.body = {
    ok: 1,
    message: '登陆成功',
    user: userInfo,
    token: jwt.sign({ // 调用jtw的sign方法生成token令牌
      data: userInfo, // 通过data属性计算出token字符串,由于签名不是加密的,令牌不要通过敏感信息生成
      exp: Math.floor(Date.now() / 1000) + 60 * 60 // 设置过期时间一分钟,防止token被逆运算解密
    }, secret) // sign方法第二个参数为事先声明的秘钥
  }
})

// 当客户端再次发送请求时已经携带了token
// 在需要权限验证的路由,第二个参数调用jwtAuth方法并传入秘钥,验证成功则放行
// 接下来进行数据操作,并响应给客户端
router.get('/getUser_token', jwtAuth({secret}), async (ctx, next) => {
  // 省略服务器请求过程,返回给客户端数据
  ctx.body = {
    message: '数据获取成功',
    userInfo: ctx.state.user.data
  }
})

module.exports = router

jsonwebtoken 负责令牌生成、koa-jwt 负责令牌认证 和 加密秘钥 在需要权限验证的路由引入模块

登录请求内通过jwt.sign方法生成,发送需要权限的请求没在路由内通过 jwtAuth({秘钥}) 来验证

如果权限验证需要更高的层级,可以把jwtAuth中间件,放到server.js内

expressJWT.unless的使用 - 簡書

// 引入中间件
const jwtAuth = require('koa-jwt')
// 配置秘钥
const secret = 'this is a secret'
// 使用中间件,传入秘钥,unless()方法排除不需要验证的路径,可以使用字符串、正则表达式或者指定请求方式
app.use(jwtAuth({secret}).unless({
  path:['/user']
}))

login_token.html 登录页面

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <input type="text" v-model='user'>
    <input type="password" v-model='pwd'>
    <br>
    <button @click="handleLogin">登录</button>
    <button @click="handleLogout">退出</button>
    <button @click="getUser">获取用户</button>
  </div>
  <script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
  <script src="https://cdn.bootcss.com/axios/0.19.0/axios.js"></script>
  <script>
    // 添加请求拦截器---------------------------------------
    axios.interceptors.request.use((config) => {
      // 对请求数据做处理,判断客户端是否存在token
      const token = localStorage.getItem('token')
      if (token) {
        // 如果存在的话,则每个http请求都在config.headers加上token
        // Bearer是jwt的认证头部信息,注意后面必须有一个字符串
        config.headers.common['authorization'] = 'Bearer ' + token
      }
      return config
    }),(err) => {
      // 对请求错误的处理
      return Promise.reject(err)
    }

    new Vue({
      el: '#app',
      data() {
        return {
          user: 'maxuan',
          pwd: '123'
        }
      },
      methods: {
        // 发起登录请求
        async handleLogin() {
          // 发送用户名密码
          const res = await axios.post('/user/login_token', {
            user: this.user,
            pwd: this.pwd
          })
          console.log(res)
          // 服务器响应后,将token保存到localStorage
          localStorage.setItem('token', res.data.token)
        },
        // 退出操作直接删除本地存储,不需要对服务器发送请求
        handleLogout() {
          localStorage.removeItem('token')
        },
        // 用户登录后本地存在令牌,这个请求会被拦截,并在当前请求config.headers内添加token令牌
        // 如果在当前请求发送时本地没有令牌,服务器会响应令牌验证失败
        async getUser() {
          const res = await axios.get('/user/getUser_token')
          console.log(res)
        }
      }
    })
  </script>
  
</body>
</html>

OAuth 开放授权

概念

有的应用会停工第三方应用登录,比如提供微信、QQ登录,这归功于 OAuth2.0,他允许客户端访问我们的资源服务器( 微信 ),我们就是资源的拥有者,这需要我们允许我们的客户端能够通过认证服务器 ( 在这里指微信,认证服务器和资源服务器可以分开也可以是部署在同一个服务器上 ) 的认证,很明显,OAuth2.0 提供了四种角色:

  • 资源服务器
  • 资源的拥有者
  • 客户端应用
  • 认证服务器

它们之间的交流实现了OAuth2.0 整个认证授权过程

OAuth2.0登录的原理,根据4种不同的模式有所不同,这里使用授权码模式下OAuth2.0的登录过程,其他的模式自行搜索

原理图:

image-20211026000605607

注册github OAuth认证

登录github后,头像点击Setting,左侧点击下方Developer settings

image-20211026001634104image-20211026083125562

点击Oauth Apps 注册一个新的应用

image-20211026083221872

填写表单,测试本地地址可以填写localhost:3000,下方回调地址要在后端声明这个接口,这些信息都是可以后期修改的

image-20211026083639120

注册完成后会获得一个 Client ID,点击按钮创建一个秘钥,蓝框提示现在就复制这个秘钥,之后会部分***隐藏

image-20211026084557691

登录链接

点击这个链接发送github登录请求

<a href="/user/login_github">github登录</a>

配置路由

const Router = require('koa-router')
// 安装导入axios模块,用于向github发送请求
const axios = require('axios') 

const router = new Router({
  prefix: '/user'
})

// 配置gthub申请到的客户端id和秘钥
const config = {
  client_id: '4127668463b9134d10ce',
  client_secret: '6619105764703081e435e2a0b31930ca39c36beb'
}

// 客户端点击github登录,向服务器发送请求,服务器端返回重定向到github的授权登录页面-------------------
router.get('/login_github', async (ctx, next) => {
  // 传入申请好的客户端id,拼接github授权页面地址
  const path = `http://github.com/login/oauth/authorize?client_id=${config.client_id}`
  // 重定向到github授权地址
  ctx.redirect(path)
})

// 授权登录页面点击登录,github向客户端发送客户端需要访问的服务器回调请求路由地址----------------------
router.get('/oauth/github/callback',async ctx => {
  const code = ctx.query.code // github回调传回的授权码
  // 带着授权码code、client_id、clien_secret 向github认证服务器请求token
  const params = {
    client_id: config.client_id,
    client_secret: config.client_secret,
    code: code
  }
  // 服务器携带客户端id、秘钥、github授权码,向github发送这个登录请求------------------------------
  let res = await axios.post('https://github.com/login/oauth/access_token', params)
  // github响应中获得token
  const access_token = new URLSearchParams(res.data).get('access_token')
  // 请求头headers 设置 token,服务器向github发起请求获取用户信息--------------------------------
  res = await axios.get('https://api.github.com/user',{
    headers:{
      Authorization: 'token ' + access_token
    }
  })
  // 打印github返回的用户信息
  console.log(res.data);
  // 授权完成,重定向到登陆成功页面
  ctx.redirect('/login_success.html')
})

module.exports = router

这是从github获取到的用户信息

{
  login: 'Maxuann',
  id: 62932596,
  node_id: 'MDQ6VXNlcjYyOTMyNTk2',
  avatar_url: 'https://avatars.githubusercontent.com/u/62932596?v=4',
  gravatar_id: '',
  url: 'https://api.github.com/users/Maxuann',
  html_url: 'https://github.com/Maxuann',
  followers_url: 'https://api.github.com/users/Maxuann/followers',
  following_url: 'https://api.github.com/users/Maxuann/following{/other_user}',
  gists_url: 'https://api.github.com/users/Maxuann/gists{/gist_id}',
  starred_url: 'https://api.github.com/users/Maxuann/starred{/owner}{/repo}',
  subscriptions_url: 'https://api.github.com/users/Maxuann/subscriptions',
  organizations_url: 'https://api.github.com/users/Maxuann/orgs',
  repos_url: 'https://api.github.com/users/Maxuann/repos',
  events_url: 'https://api.github.com/users/Maxuann/events{/privacy}',
  received_events_url: 'https://api.github.com/users/Maxuann/received_events',
  type: 'User',
  site_admin: false,
  name: null,
  company: null,
  blog: '',
  location: null,
  email: null,
  hireable: null,
  bio: null,
  twitter_username: null,
  public_repos: 0,
  public_gists: 0,
  followers: 0,
  following: 0,
  created_at: '2020-03-31T08:45:58Z',
  updated_at: '2021-10-26T07:32:48Z'
}