持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,
为什么要刷新token
token在2h会过期,过期后就要重新进行登录。
这两个小时是根据用户打开页面登录后计算的,中途如果两个小时都没使用,再次进来就要重新登陆。
长期使用用户也需要频繁登录,造成困扰。
如何刷新token
看到了很多方法,大致有以下几种,每种方法都有优劣。
方法一: 后端返回过期时间,前端判断当前token是否过期,过期请求刷新token。
缺点:恶意篡改时间就会拦截失败;后端需要额外提供一个token过期时间字段
方法二:定时器,定时刷新token
缺点:消耗资源
方法三:在响应拦截器中,判断token过期后,调用刷新token接口
缺点:如果发多个请求,token过期后就会使请求时间延长,请求超时用户体验不好。
方法四:改良版方法三。在响应拦截器中写节流定时器,调用刷新token接口。
方法五:纯后端:使用两个token,一个是真正的access_token时效短(2h),另外一个refresh_token时效长(7day)用于刷新token。用redis存储access_token,设置过期时间。
情况1:refresh_token过期,重新登录获取access_token
情况2:refresh_token没过期,access_token过期,不用登录,后端刷新access_token。
一开始我是用了方法4,后来考虑不想让前端涉及这块,就实践了方法五。以下放下两种方法的实践。
实践--方法四:改良版方法三。在响应拦截器中写节流定时器,调用刷新token接口
前端部分
util.js。一个节流函数
/**
* 节流函数
* @param {Function} fn
* @param {Number} wait
* @returns {Function}
*/
export function throttle(fn, wait) {
var context, args
var previous = 0
return function () {
var now = +new Date()
context = this
args = arguments
if (now - previous > wait) {
fn.apply(context, args)
previous = now
}
}
}
token.js 刷新token方法,如果过期会返回新的token
import { throttle } from './util'
import store from '@/store'
import storage from 'store'
import { refreshToken } from '@/api/user'
import { ACCESS_TOKEN } from '@/store/mutation-types'
export const refreshAccessToken = throttle(async function() {
const token = storage.get(ACCESS_TOKEN)
try {
const res = await refreshToken(token || '')
console.log('refreshAccessToken', res)
if (res.code === 0) {
// // 设置客户端的token
storage.set(ACCESS_TOKEN, res.data, 2 * 60 * 60 * 1000)
// // 设置vuex的token
store.commit('SET_TOKEN', res.data)
}
} catch (e) {
console.log(e)
}
}, 1000*60*30)
export default refreshAccessToken
request.js 在拦截器中调用
// request interceptor
request.interceptors.request.use(config => {
const token = storage.get(ACCESS_TOKEN)
// 如果 token 存在
// 让每个请求携带自定义 token 请根据实际情况自行修改
if (token) {
config.headers.Authorization = 'Bearer ' + token
}
if (!config.url.startsWith('/user/login')) {
refreshAccessToken()
}
return config
}, errorHandler)
后端代码
user.js
我token是使用jwt签发的,jwt.verify用于验证之前token是否过期,每30min刷新token返回前端,如果token过期就说明用户超过2h30min没有shi'y要重新登陆了。
async refreshToken(token) {
const { app } = this;
try {
const decoded = app.jwt.verify(token, app.config.jwt.secret);
console.log('decoded', decoded);
return app.jwt.sign({
ticket: decoded.ticket,
accountId: decoded.accountId,
yachid: decoded.yachid,
teamId: decoded.teamId,
name: decoded.name,
},
app.config.jwt.secret,
{
expiresIn: '50s',
});
} catch (e) {
this.ctx.loggerError(e, 'YACH', '刷新token');
return {};
}
}
方法五:纯后端:使用两个token,一个是真正的access_token时效短(2h),另外一个refresh_token时效长(7day)用于刷新token。用redis存储access_token,设置过期时间。
后端使用node写的,使用的egg.js.这里的ticket对应的是access_token,jwt生成的token是refresh_token。
思路:
登录时ticket存在redis中,过期时间设置2h
在需要ticket的接口先进行刷新判断操作,刷新ticket方法写在中间件中
刷新ticket方法内容,判断ticket是否快过期或者已过期,如果是则重新请求ticket存储。
service/user.js
这里controller代码就省略不放出来了
//用户登录后
async login(params) {
const { app } = this;
try {
const result = await this.request(`${this.serverUrl}/api/v1/sso/verify`, { data: params }); //获取用户信息接口
if (result.errcode === 0) {
const ticket = params.ticket,
accountId = result.data.account_id;
//查找用户如果数据库中没有就插入
const data = await this.findOrInsertUserInfo(ticket, accountId);
// redis中存储ticket
await app.redis.set('ticket', ticket, 'EX', 60 * 60 * 2);
//签发一个refresh_token,七天后过期,过期后请求状态码都返回401
return app.jwt.sign({
ticket,
accountId,
yachid: data.yachid,
teamId: data.teamId,
name: data.name,
},
app.config.jwt.secret,
{
expiresIn: '7d',
});
}
this.ctx.throw(403, result.errmsg);
} catch (e) {
this.ctx.loggerError(e, 'YACH', '验证用户');
return {};
}
}
//获取ticket
async getTicket() {
const { appid, appkey } = this.yachConfig;
try {
const result = await this.request(`${this.serverUrl}/basic/get_ticket`, { data: { appid, appkey } });
if (result.errcode === 0) {
return result.ticket;
}
return '';
} catch (e) {
this.ctx.loggerError(e, 'YACH', '获取ticket');
return '';
}
}
middleware/refreshToken.js
'use strict';
let state = true;
module.exports = (options, app) => {
return async function(ctx, next) {
const res = await app.redis.ttl('ticket');
if (res < (60 * 10) && state) { // 濒临过期时间
// 更新ticket
state = false;
const ticket = await ctx.service.user.getTicket();
ctx.state.user.ticket = ticket;
// 重新设置redis中ticket为2h
await app.redis.setex('ticket', 60 * 60 * 2, ticket);
state = true;
}
await next();
};
};
router.js
'use strict';
/**
* @param {Egg.Application} app - egg application
*/
module.exports = app => {
const { router, controller } = app;
//引入中间件中刷新方法
const refreshToken = app.middleware.refreshToken();
// user
//在指定路由(需要用到ticket的接口)用刷新方法
router.get('/api/user/getUserInfo', refreshToken, controller.user.getUserInfo);
router.get('/api/user/updateUserInfo', refreshToken, controller.user.updateUserInfo);
};
总结
最后用了方法五,个人觉得挺合适,前端不用涉及,也不会像定时任务那么消耗性能。