上节回顾
- 嵌套路由
- 页面布局
- 状态管理 & 持久化
工作内容
- 身份认证
准备工作
-
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
测试结果
-
vs code
调试控制台
异常处理
上一步认证失败,抛出了ERROR
通过断点可以查看到错误信息,补充异常逻辑
官方推荐通过status
判断是否是认证错误
// 更新文件: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
继续测试
白名单
登录还需要认证?答案是不需要的。
不仅仅是登录,注册及其后续的静态图片访问,都不需要认证。
// 更新文件: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
继续测试
成功,完美。
Postman
测试身份认证
然而,用Postman
测试其它接口:
这是因为没有在请求头Headers
里加上认证信息Authorization
。
在登录接口的Tests
面板,借助右边的提示,设置全局变量token
为登录返回数据的token
。
// 更新Tests面板内容:
pm.test("Your test name", function () {
var jsonData = pm.response.json();
pm.globals.set("token", jsonData.data.token);
});
在需要身份认证的测试接口的Authorization
面板中,选择Type
为Bearer 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>
...
测试结果:
前端响应拦截
前端认证失败,就退回到登录/
页面,清空本地存储和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
至于为什么调用两次/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')
}
}
...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。