前言
最近同项目的伙伴告诉我们一个“新词汇”——双Token登录机制,emmmmm,确实没了解过,据说是在实现token长期有效的同时,防止token被第三方盗用,提高用户信息的安全性。于是,在了解了大概之后,我找了很多篇文章去学习双Token的实现过程,总结如下。
什么是双Token机制?
一般我们实现用户登录是:用户登录向服务端发送账号密码信息,登录失败返回客户端重新填写并发送用户信息;登录成功服务端生成token并返回token给客户端,客户端将token存本地。
那么问题来了,token的有效期应该是多久呢?
短期 Token:
- 安全性:短期 Token 更加安全,因为它们在相对较短的时间内失效,即使被泄露,攻击者的窗口期也很有限。
- 用户体验:短期 Token需要用户在较短的时间内重新登录,这可能会对用户的体验产生影响。用户可能需要频繁重新输入凭证,这可能会变得繁琐。
- 敏感操作:对于执行敏感操作的令牌,如支付、更改密码等,短期 Token 更有意义。这样即使用户忘记登出,也不会对安全性产生大的威胁。
长期 Token:
- 用户体验:长期 Token 可以改善用户体验,因为用户不需要频繁重新登录。这在对用户友好性方面很有价值。
- 资源访问频率:对于需要频繁访问资源的应用,长期 Token 可能更合适,以减少频繁的重新登录。
- 维护成本:长期 Token 可能需要更多的维护成本,包括处理 Token 过期、刷新 Token 等。
为了中和短期Token和长期Token出现的弊端,在实际应用中,通常使用一种混合策略来平衡这些因素,此时,就出现了双Token机制。
Access Token
:用于获取访问资源或执行操作的授权,有效期短。客户端发送请求时,在请求头携带此accessToken。Refresh Token
:用来验证用户的身份,刷新accessToken,有效期长。当accessToken过期时,向服务端传递refreshToken来刷新accessToken。
其实,安全方面也会存在双token劫持,只是双token的写法能相对降低风险。
双Token的实现流程
实现思路:
- 用户登录向服务端发送账号密码信息,登录失败返回客户端重新填写并发送用户信息;登录成功服务端生成accessToken和refreshToken并返给客户端,客户端将token存本地。
- 当客户端想服务端发起请求时,在请求头携带accessToken发送给服务端,服务端验证accessToken是否过期,若未过期则正常请求数据;若过期,服务端通过code码将accessToken失效信息返回给客户端。
- 客户端在响应拦截器中添加拦截判断,若返回accessToken失效信息,则在请求头携带refreshToken ,重新发起请求,获取新的accessToken。
- 服务端验证 refreshToken 是否失效。若未过期,则重新生成accessToken返给客户端;若过期,服务端通过code码将refreshToken 失效信息返回给客户端。
- 客户端在响应拦截器中添加拦截判断,若返回refreshToken 失效信息,则提示用户需要重新登录,获取新的双token。
实现代码:
setToken.js
// 设置
export function setToken (tokenKey, token){
return localStorage.setItem(tokenKey,token)
}
// 获取
export function getToken (tokenKey){
return localStorage.getItem(tokenKey)
}
// 删除
export function removeToken(tokenKey){
return localStorage.removeItem(tokenKey)
}
request.js
import axios from 'axios';
import router from './router';
import {
setToken, getToken, removeToken } from './setToken.js';
// 封装 baseURL
const request = axios.create({
baseURL: 'http://****',
timeout: 10000, //请求的超时毫秒数
contentType: 'application/json',
});
// 获取refreshToken
let refreshToken = getToken('refreshToken') || "";
// 判断是否开启刷新token:不刷新
let isrefreshToken = false;
// 如果没有refreshToken(没登录||过期了),就开启刷新token
if (!getToken("refreshToken")) {
isrefreshToken = false;
if (!getToken('refreshToken')) {
isrefreshToken = true;
}
}
// 添加请求拦截器
request.interceptors.request.use((config) => {
// 获取accessToken
let token = getToken('accessToken');
// 如果有token
if (token) {
// 并且token没过期
if (!isrefreshToken) {
config.headers['x-token'] = getToken('accessToken') || '';
}
}
// 如果有refreshToken
if (refreshToken) {
// 且需要刷新token
if (isrefreshToken) {
config.headers['x-token'] = getToken('refreshToken');
}
}
return config;
}),
(error) => {
return Promise.reject(error);
};
// 添加响应拦截器
request.interceptors.response.use((response) => {
// 对响应数据做些什么
console.log('响应状态码', response.data.code);
let code = response.data.code;
// 还没有设置refreshToken请求头,需要设置一下再次发送请求
if (!refreshToken && getToken('refreshToken') != null) {
refreshToken = getToken('refreshToken');
return request(response.config);
}
if (code == 401 || code == 1021) {
//accessToken过期了,需要带着refreshToken,去换取新的token
refreshToken = getToken('refreshToken');
isrefreshToken = true;
// 相当于重新走一遍刚刚的请求
return request(response.config);
}
if (code == 1024) {
setToken('accessToken', response.data.data);
isrefreshToken = false;
return request(response.config);
} else if (code == 1023) {
// 将本地token删除
removeToken('refreshToken');
removeToken('accessToken');
// 跳转到登录页面,重新登录
router.push('/login');
//返回信息,让用户重新登录
isrefreshToken = true;
alert('登录已超期,请重新登录');
}
return response;
}),
(error) => {
return Promise.reject(error);
};
// 向外暴露 request
export default request;
可能理解的不够全面,请批评指正!
推荐学习文章:
前端双token策略(uniapp-vue3-ts版)
基于OAuth2.0的refreshToken前端刷新方案与演示demo