前言
平时使用中,是否有过这样的场景,我们经常使用的 app
很久很久都不会让我们重新登录,但如果我们一个礼拜没上线,就需要重新上线了,这就是本篇文章文章要提到的 双token无感刷新
了
看了后,也会介绍跟单 token 的对比,会感觉,还是双token给力哈
本篇会使用 nestjs + react(umi-request) 做一个示范,让大家体会一下双 token 的魅力
双token无感刷新
双 token 就是两个 token,当用户登陆的时候,服务端会给客户端两个 token,一个token一般期限为 0.5小时(个人也是感觉太短,自己根据情况,可以适当延长1小时1天都行,越短安全性越高)
token
就是令牌,带着token
相当于我们拿着身份证似
的,后端可以直接办理事情,客户端使用最短的 token
用来与后台交互
,长的token
作为 更新token
的时候使用,这样半个小时后,客户端的短token
过期时,就用长 token 获取新的 两个token
,然后客户端拿着新的 短token
重发原来请求即可
这样单token也可以,为什么要双token?
第一个,单token等过期了就没办法续了,不过期客户端就得保存一个时间戳,在过期之前更换新的 token,但很可能也会出现类似的情况,假设期限7天,你设置5天期限更新,但第六、七天用户没更新,那么第八天就需要重新登录,用户感觉两天没上线就要重新登陆,期限短点也行,但仍然出现另外一个问题,如果你这个 token 被别人劫持拿到了,那么就可以肆意挥霍好几天了,安全性就出现问题了,如果是双 token 呢,一般短的,持续时间为 30s,别人即使拿到了,也会很快到期,还得重新想办法拿新的,这就无形之间增加了成本,双token的优势就出现了,并且无需前端设置时间校验,只需要出现 401 的时候,重新调用接口,重发请求即可,也方便后台调整过期时间
nestjs 代码
除了登录返回两个 token,刷新也是返回两个 token
话不多说直接上代码
js
@ApiOperation({
summary: '登陆'
})
@Public() // @UseGuards(UserGuard) @Guards()
@APIResponse(TokenDto)
@Post('login')
login(
@Body() loginInfo: LoginDto,
) {
return this.authService.login(loginInfo);
}
@ApiOperation({
summary: '刷新'
})
@APIResponse(TokenDto)
@Post('refresh_token')
refreshToken(
@ReqUser() user: User,
) {
return this.authService.refreshToken(user);
}
generateToken(
user: User
) {
return {
access_token: this.jwtService.sign({
id: user.id
}, {
expiresIn: '0.003h' //1小时过期
}), //将token返回给客户端
refresh_token: this.jwtService.sign({
id: user.id,
}, {
expiresIn: '7d' //7天过期
})
}
}
async login(
loginInfo: LoginDto,
) {
let user = await this.userRepository.findOne({
where: {
account: loginInfo.account,
},
});
if (!user || user.password !== loginInfo.password) {
return ResponseData.fail('用户名或者密码不正确')
}
return ResponseData.ok({
user,
...this.generateToken(user)
})
}
async refreshToken(
user: User,
) {
//获取新token
return this.generateToken(user)
}
这样我们的后台就写好了,就等前端来测试了
前端代码
使用 umi-request 先顶一个一个全局 request 把,实际使用一般用代理,prefix一般不写,或者只写接口前缀
js
export const request = extend({
prefix: "http://localhost:3000/api",
timeout: 15000,
requestType: 'form',
});
再写个登录和刷新的接口
js
export async function loginUser(account: string, password: string) {
let res = await request.post('/user/login', {
data: {
account,
password
}
})
if (Math.floor(res.status / 200) === 2) {
localStorage.setItem('access_token', res.data.access_token)
localStorage.setItem('refresh_token', res.data.refresh_token)
}
return res
}
export async function refreshToken() {
let res = await request.post('/user/refresh_token', {
headers: {
'token': localStorage.getItem('refresh_token') || ''
} || {},
})
localStorage.setItem('access_token', res.data?.access_token)
localStorage.setItem('refresh_token', res.data?.refresh_token)
return res
}
比那些我们的请求拦截器,当 header中不存在 token 时,我们加入 token 即可(登陆时加不加都不影响,不用可以加代码)
js
//请求request请求数据拦截器,其发生在我们写的请求之后,实际发出请求之前
request.interceptors.request.use((url, options) => {
//假设请求钱我们需要将一些令牌统一放到header中(根据实际需要处理)
//将token和加密后的sign放到一起
let headers: any = options.headers
if (!headers.token) {
headers['token'] = localStorage.getItem('access_token')
}
//设置完成后,要返回我们的的参数
return {
url,
options: {
...options,
headers
}
}
})
编写我们的响应拦截器,当 401
时,我们就拦截
我们的响应,这时我们就开始使用长token
直接刷新token
,为什么上面要加入判断不包含刷新接口呢,为了避免我们的 长token 也过期
,那时再 401 就死循环了,或者后台故障也会死循环
这里会发现我们刷新了token
后,然后直接调用了我们原来的接口(这样就可以无缝衔接了),只不过 umi-request不可以直接重发,我们就直接根据类型请求我们的接口就行了
这样就完了么,并没有,这样我们的请求的 401 的接口就真的 401 失败了,前面 两个await
就是为了能够让新token获取的新内容,覆盖掉初始调用接口的response
(前提是我们的接口要设置 getResponse: true
),这样就会额外返回一个 interceptors
中独有的 response
了,然后我们直接返回新的 reponse,用户真的无感刷新了(ps:如果失败了,也只是正常失败,而不是401,除非两个token都过期才会401)
js
request.interceptors.response.use(async (response, options) => {
if (response.status === 401 && !options.url.includes('/refresh_token')) {
//为什么await,因为这样,上一个401的请求就会被阻塞,我们此时直接重新refresh,并且重新请求
let res = await refreshToken(options)
//处理请求,没看到直接重新请求的类
//这里code是我专门给接口准备的罢了,就是为了区分接口,实际上使用自己的成功码即可
if (res.code === 1) {
if (options.method === "GET") {
let res = await request.get(options.url, {
params: options.params,
headers: {
...options.headers,
token: ''
},
getResponse: true
})
return res.response
} else if (options.method === "POST") {
let res = await request.post(options.url, {
data: options.data,
headers: {
...options.headers,
token: ''
},
getResponse: true,
})
return res.response
}
}
}
return response;
})
看看运行效果,虽然浏览器有 401 出现,但是接口数据正常返回了,就说厉害不厉害
最后
需要的话就用起来吧,稳得一批,如果是前端,这个长 token 就别七天了,最好一两天就过期,避免别人登陆自己的账号搞事情,哈哈😂