定义
-
accessToken
:用户获取数据权限 -
refreshToken
:用来获取新的accessToken
双 token 验证机制,其中 accessToken 过期时间较短,refreshToken 过期时间较长。当 accessToken 过期后,使用 refreshToken 去请求新的 token。
双 token 验证流程
-
用户登录向服务端发送账号密码,登录失败返回客户端重新登录。登录成功服务端生成 accessToken 和 refreshToken,返回生成的 token 给客户端。
-
在请求拦截器中,请求头中携带 accessToken 请求数据,服务端验证 accessToken 是否过期。token 有效继续请求数据,token 失效返回失效信息到客户端。
-
客户端收到服务端发送的请求信息,在二次封装的 axios 的响应拦截器中判断是否有 accessToken 失效的信息,没有返回响应的数据。有失效的信息,就携带 refreshToken 请求新的 accessToken。
-
服务端验证 refreshToken 是否有效。有效,重新生成 token, 返回新的 token 和提示信息到客户端,无效,返回无效信息给客户端。
-
客户端响应拦截器判断响应信息是否有 refreshToken 有效无效。无效,退出当前登录。有效,重新存储新的 token,继续请求上一次请求的数据。
注意事项
-
短token失效,服务端拒绝请求,返回token失效信息,前端请求到新的短token如何再次请求数据,达到无感刷新的效果。
-
服务端白名单,成功登录前是还没有请求到token的,那么如果服务端拦截请求,就无法登录。定制白名单,让登录无需进行token验证。
后端代码
app.js
var expressJWT = require('express-jwt')
设置白名单
app.use(expressJWT({
secret:"123456",
algorithms:["HS256"],
//getToken是修改express-jwt获取请求头并判断的标准。默认是只有一个authorization,也就是我们请求头的Authorization字段,但是,如果我们想要他有多个判断时,比如,我们再要他判断一个pass,就可以像如下这么写
getToken: function fromHeaderOrQuerystring (req) {
console.log(req.headers);
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
return req.headers.authorization.split(' ')[1];
}
if (req.headers.pass && req.headers.pass.split(' ')[0] === 'Bearer') {
return req.headers.pass.split(' ')[1];
}
return null;
}
}).unless({path:["/login"]}))
//错位处理中间件,这里如果错误是UnauthorizedError,那么就证明accesstoken过期。
app.use(function (err, req, res, next) {
console.log(err);
if (err.name === 'UnauthorizedError') {
res.status(200).send({ code: 4003, msg: "未验证身份" });//这里status中一定要写200,否则页面会报错
} else {
next();
}
});
index.js
登录
router.post('/login', async (req, res) => {
let data = req.body
let routeinfo = await routerMoudel.find()
let user = await loginModel.find({ username: data.username })
if (user.length > 0) {
let accessToken = "Bearer " + jwt.sign({...user[0]}, "123456", { expiresIn: 5 })
let refreshToken = "Bearer " + jwt.sign({...user[0]}, "123456", { expiresIn: "1d" })
// let token = jwt.sign({ ...user[0] }, '123456', { expiresIn: '1d' })
if (user[0].password == data.password) {
res.send({
code: 200,
msg: "登陆成功",
routeinfo,
accessToken,
refreshToken
})
} else {
res.send({
code: 201,
msg: "密码错误"
})
}
} else {
res.send({
code: 203,
msg: "用户名错误"
})
}
})//刷新短token
router.get('/refresh', (req,res) => {
let accessToken = "Bearer " + jwt.sign({...user[0]}, "123456", { expiresIn: 5 })
let refreshToken = "Bearer " + jwt.sign({...user[0]}, "123456", { expiresIn: "1d" })
res.send({
code: 200,
accessToken,
refreshToken
})
})
前端代码
http/index.js
import axios from 'axios'
//全局变量,这些通常会在全局
export const ACCESS_TOKEN = 'a_tk' // 短token字段
export const REFRESH_TOKEN = 'r_tk' // 短token字段
export const AUTH = 'Authorization' // header头部 携带短token
export const PASS = 'pass' // header头部 携带长token//全局方法,这些通常也会在全局
// 存储短token
export const setAccessToken = (token) => localStorage.setItem(ACCESS_TOKEN, token)
// 存储长token
export const setRefershToken = (token) => localStorage.setItem(REFRESH_TOKEN, token)
// 获取短token
export const getAccessToken = () => localStorage.getItem(ACCESS_TOKEN)
// 获取长token
export const getRefershToken = () => localStorage.getItem(REFRESH_TOKEN)
// 删除短token
export const removeAccessToken = () => localStorage.removeItem(ACCESS_TOKEN)
// 删除长token
export const removeRefershToken = () => localStorage.removeItem(REFRESH_TOKEN)
let subscribes=[]
let flag=false // 设置开关,保证一次只能请求一次短token,防止客户多此操作,多次请求/*把过期请求添加在数组中*/
export const addRequest = (request) => {
subscribes.push(request)
}/*调用过期请求*/
export const retryRequest = () => {
console.log('重新请求上次中断的数据');
subscribes.forEach(request => request())
subscribes = []
}/*短token过期,携带token去重新请求token*/
export const refreshToken=()=>{
if(!flag){
flag = true;
let r_tk = getRefershToken() // 获取长token
if(r_tk){
server.get('/refresh',Object.assign({},{
headers:{[PASS]:r_tk}
})).then((res)=>{
//长token失效,退出登录 //这个功能没做
if(res.code===4006){
console.log('长token没了');
flag = false
removeRefershToken(REFRESH_TOKEN)
} else if(res.code===200){
// 存储新的token
setAccessToken(res.accessToken)
setRefershToken(res.refreshToken)
flag = false
// 重新请求数据
retryRequest()
}
})
}
}
}
//封装axios
const server = axios.create({
baseURL: 'http://localhost:3000', // 你的服务器
timeout: 1000 * 10,
headers: {
"Content-type": "application/json"
}
})/*请求拦截器*/
server.interceptors.request.use(config => {
// 获取短token,携带到请求头,服务端校验
let aToken = getAccessToken(ACCESS_TOKEN)
console.log(aToken);
config.headers[AUTH] = aToken
console.log(config.headers);
return config
})/*响应拦截器*/
server.interceptors.response.use(
async response => {
// 获取到配置和后端响应的数据
let { config, data } = response
console.log('响应提示信息:', data);
return new Promise((resolve, reject) => {
// 短token失效
if (data.code === 4003) {
console.log(config);
// 移除失效的短token
removeAccessToken(ACCESS_TOKEN)
// 把过期请求存储起来,用于请求到新的短token,再次请求,达到无感刷新
addRequest(() => resolve(server(config)))
// 携带长token去请求新的token
refreshToken()
} else {
// 有效返回相应的数据
resolve(data)
}}).catch((err)=>{
console.log(err);
})},
error => {
return Promise.reject(error)
}
)export default server
登录页面
<script setup>
import server from '../http/index'
import { ref } from "vue";
import { useRouter } from "vue-router";
import {setAccessToken,setRefershToken} from '../http/index'
const router = useRouter();
const form = ref({
username: "",
password: "",
});const onSubmit = async () => {
let res = await server.post("login", form.value);
console.log(res.routeinfo,'+++');
setAccessToken(res.accessToken);
setRefershToken(res.refreshToken);
form.value=""
sessionStorage.setItem("routerDate", JSON.stringify(res.routeinfo));
router.push("/container");
};</script>
<template >
<div>
<el-form :model="form" label-width="auto" style="max-width: 600px">
<el-form-item label="用户名">
<el-input v-model="form.username" />
</el-form-item><el-form-item label="密码">
<el-input v-model="form.password" />
</el-form-item><el-form-item>
<el-button type="primary" @click="onSubmit">Create</el-button>
<el-button>Cancel</el-button>
</el-form-item>
</el-form>
</div>
</template><style scoped>
</style>