1

上节回顾

  • 嵌套路由
  • 页面布局
  • 状态管理 & 持久化

工作内容

  • 身份认证

准备工作

  • npm i -S crypto-js // 先切换到/server目录下
  • npm i -S koa-jwt // 先切换到/server目录下
  • npm i -S jsonwebtoken // 先切换到/server目录下

业务逻辑

JWT简介

JWT对象为一个长字串,字符之间通过"."分隔符分为三个子串。
JWT的三个部分:JWT头、有效载荷和签名。
一旦JWT签发,在有效期内将会一直有效。

  • JWT使用的核心步骤

    • 签名
    • 认证
    • 解码
  • 这里使用jsonwebtoken对载荷进行签名,jsonwebtoken结合koa-jwt进行签名认证,最终得到签名前的数据ctx.state[<key>]

工具库:签名

// 新建文件:/server/config/auth.js
const AES = require("crypto-js/aes");
const secretText = 'jwt.secret.text';
const key = 'jwt.secret.key'
// // Encrypt
// var ciphertext = CryptoJS.AES.encrypt('my message', 'secret key 123').toString();

// // Decrypt
// var bytes  = CryptoJS.AES.decrypt(ciphertext, 'secret key 123');
// var originalText = bytes.toString(CryptoJS.enc.Utf8);

// console.log(originalText); // 'my message'
module.exports = {
  secret: AES.encrypt(secretText, key).toString(),
  authKey: 'auth'
}

可以通过ctx.state[<authKey>],即ctx.state.auth获取有效荷载。

// 新建文件:server/utils/auth.js
const jwt = require('jsonwebtoken')
const { secret } = require('../config/auth')
// 定义超时时间:token的有效时长
const expiresIn = '2h';

module.exports = {
  sign: function(payload) {
    // 推荐对payload进行加密。
    const token = jwt.sign(payload, secret, {
      expiresIn
    });

    return token;
  },
  vertify: function(ctx, decodeToken, token){
    
  }
}

工具库:认证

// 更新文件:server/utils/auth.js
...
  vertify: function(ctx, decodeToken, token){
    let result = true;
    try{
      jwt.verify(token, secret);
      result = false;
    }catch(e) {

    }
    return result;
  }
...

vertify返回true表明token已无效或认证失败;
vertify返回false表明token仍有效,且已经成功;

解析

//更新文件:server/app.js
...
const koaJwt = require('koa-jwt');
const { secret, authKey } = require('./config/auth');
const { vertify } = require('./utils/auth');
...
app.use(koaJwt({  //要放到路由前边,否则,无效
  secret,
  key: authKey,
  // jwt是否被废除
  isRevoked: vertify
}))
...
  • JWT认证必须放到路由前方,否则,路由逻辑都走完了,再进行认证,有什么用
  • koa-jwt帮助实现认证逻辑,认证失败,抛出错误。
  • koa-jwt类似koa-body,将有效载荷解析,存储到ctx.state[<authKey>]ctx.state.auth)中。

服务端登录返回token

// 更新文件:
...
const auth = require('../utils/auth');
...
async function login (ctx) {
...
  if(user) {
    const token = auth.sign({
      id: user.id,
      account
    })
    ctx.body = {
      code: '200',
      data: {
        token,
        id: user.id,
        account,
        alias: user.alias
      },
      msg: '登陆成功'
    }
  }
...
}
  • Postman测试结果

NotFound

  • vs code 调试控制台

AuthenticationError

异常处理

上一步认证失败,抛出了ERROR
monitor
通过断点可以查看到错误信息,补充异常逻辑
官方推荐通过status判断是否是认证错误
remmand

// 更新文件:server/app.js
···
// 中间件的错误处理
app.use(function(ctx, next){
  return next().catch((err) => {
    console.log(err)
    if (err.name === 'ValidationError') {
      ctx.body = {
        code: '403',
        data: null,
        msg: err.message
      }
    } else if (401 == err.status) { //认证错误
      ctx.status = 401; // 更新HTTP Response Code
      ctx.body = {
        code: '401',
        data: null,
        msg: `${err.message}\n请先登陆`
      }
    } else {
      throw err;
    }
  });
});
···
  • Postman继续测试

401

白名单

登录还需要认证?答案是不需要的。
不仅仅是登录,注册及其后续的静态图片访问,都不需要认证。

// 更新文件:server/app.js
  secret,
  key: authKey,
  // jwt是否被废除
  isRevoked: vertify
}).unless({
  // 返回true就是忽略认证
  custom: function(ctx) {
    const { method, path, query } = ctx;
    if(path === '/'){
      return true;
    }
    if(path === '/users' && query.action) {
      return true;
    }
    return false;
  }
}));
  • 链式调用unless,返回true即为忽略认证。
  • unless的具体用法同koa-unless,这里使用的custom是自定义忽略规则。
  • Postman继续测试

success
成功,完美。

Postman测试身份认证

然而,用Postman测试其它接口:
Authentication
这是因为没有在请求头Headers里加上认证信息Authorization
image.png
在登录接口的Tests面板,借助右边的提示,设置全局变量token为登录返回数据的token

// 更新Tests面板内容:
pm.test("Your test name", function () {
    var jsonData = pm.response.json();
    pm.globals.set("token", jsonData.data.token);
});

image.png
在需要身份认证的测试接口的Authorization面板中,选择TypeBearer Token,值为变量{{token}}

前端请求拦截

到目前为止,接口没有什么问题了,但,前端页面请求,没有添加Authorization

// 更新文件:client/src/views/login/index.vue
...
async function onLogin () {
...
        if (res && res.code === '200') {
            const {token, ...user} = res.data // 新增
            localStorage.setItem('token', res.data.token) // 新增
            this.$store.commit('putLoginer', user)
            this.$router.replace('/home')
          }
...
}
...

在登录时,获取token,进行本地存储。

// 更新文件:client/src/utils/http.js
...
instance.interceptors.request.use(async (config) => {
  const token = await localStorage.getItem('token')
  token && (config.headers['Authorization'] = `Bearer ${token}`)
  return config
}, function (error) {
  console.log('------request===========', error)
  // Do something with request error
  return Promise.reject(error)
})
...
  • 拦截请求,token存在时,将token赋值给config.headers['Authorization']
  • 赋值是以Bearer 为前缀的,这是规范,要求这样处理。
// 仅为了测试身份认证,测试完,就把该文件还原。
// 更新文件:client/src/views/homePage/index.vue
...
<script>
import http from '@/utils/http'

export default {
  async created () {
    const res = await http.get('/users')
    console.log(res)
  }
}
</script>
...

测试结果:
auth.gif

前端响应拦截

前端认证失败,就退回到登录/页面,清空本地存储和vuex。(登出时,也是这些步骤,登出逻辑以前已经处理过了)

// 更新文件:client/src/utils/http.js
...
import router from '@/router'
import store from '@/store'
...
instance.interceptors.response.use(
  async res => {
    if (/^20./.test(res.status)) {
      return res.data
    }
    if (/^40./.test(res.status)) {
      router.push('/')
      await localStorage.clear()
      store.commit('resetVuex')
      return {
        code: '401',
        msg: '请重新登陆'
      }
    }
    console.log('------response=======', res)
    return res
  },
  error => {
    return Promise.reject(error)
  }
)

测试结果如下:退回到登录/页面,清空本地存储和vuex
logout.gif

至于为什么调用两次/users接口,因为/home下认证一次,/login调用了一次。

这里改进一下/login的调用,让如果有token的时候,认证成功,直接进入首页。(偷懒:专门做一个认证接口比较好)

// 更新文件:client/src/views/login/index.vue
...
  async created () {
    const res = await http.get('/users')
    if (res.code === '200') {
      this.$router.replace('/home')
    }
  }
  ...

参考文档

koa-jwt
jsonwebtoken


米花儿团儿
1.3k 声望75 粉丝