实现方案
后端生成两个token(token和refresh_token),token有效时间短,refresh_token有效时间长; 前端请求登录后,后端把这两个token传给前端,前端缓存下来;
- token未到期,前端可正常请求。
- token过期,后端会返回与前端约定好的相关参数(比如,响应码401),前端在返回拦截器中判断拦截,并将refresh_token替换掉已经失效的token去调用api_refresh_token的接口请求。后端则判断refresh_token是否过期。
(1)refresh_token未过期,后端重新生成token和refresh_token返给前端。前端接收到后,缓存token,并重新执行之前失败的接口。
(2)refresh_token过期,后端返回token失效,前端跳转到登录页。
分析
token和refresh_token有三个时间点需要去考虑;
token和refresh_token的时间点 | 需要实现的结果 |
---|---|
token和refersh_token都没失效 | 正常请求 |
token失效,refresh_token没失效 | 请求提示token失效,前端需要调api_refresh_token的请求,获取新的token |
token和refresh_token都失效 | 请求提示token失效,前端需要调api_refresh_token的请求,请求失败,退出登录 |
代码实现
前后端语言和框架分别通过vue和Java/spring security去实现。代码有不完善之处,今后会不断完善。
后端代码
登录生成token和refresh_token
@Component
public class LmsAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
UserService userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String username = authentication.getName();
User user = userService.getByUsername(username);
//生成jwt
String token = JwtUtil.generateToken(user,JwtUtil.EXPIRE_TIME);
//生成refresh_token
String refreshToken = JwtUtil.generateToken(user,JwtUtil.EXPIRE_TIME+JwtUtil.REFEASH_TIME_PLUS);
long userId = user.getId();//获取到userId
Map<String, Object> map = new HashMap<String, Object>();
map.put("token", token);
map.put("refreshToken", refreshToken);
ResultJson result = ResultJson.ok().data(map).message("登录成功");
response.setContentType("application/json;charset=UTF-8"); // 响应类型
PrintWriter out = response.getWriter();
out.write( JSON.toJSONString(result));
out.flush();
out.close();
}
}
token失效的处理
@Component
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//token失效返回401
ResultJson result = ResultJson.error().code(ResultCode.Unauthorized).message("token失效");
response.setContentType("text/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); //设置响应码
response.getWriter().write(JSON.toJSONString(result));
}
}
实现api_token_refresh接口
@GetMapping("/api-token-refresh")
public ResultJson refresh_token(Principal principal, HttpServletResponse response){
User user = userService.getByUsername(principal.getName());
//生成jwt
String token = JwtUtil.generateToken(user, JwtUtil.EXPIRE_TIME);
//refresh_token
String refreshToken = JwtUtil.generateToken(user, JwtUtil.EXPIRE_TIME + JwtUtil.REFEASH_TIME_PLUS);
Map<String, Object> map = new HashMap<>();
map.put("token", token);
map.put("refreshToken", refreshToken);
return ResultJson.ok().data(map);
}
前端代码
axios响应拦截,关于token失效的处理
import axios from "axios";
import { Notification } from "element-ui";
import router from "./router";
const request_domain = "http://127.0.0.1:8088";
axios.defaults.baseURL = request_domain; //全局使用的请求域名
//axios实例对象
const request = axios.create({
timeout: 5000,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
});
// 是否正在刷新的标记
let isRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
let requests = []
function refreshToken () {
// 我项目中 更新token 需要吧原有的token 换成refreshToken去请求 这里根据需求可以改动
window.localStorage.setItem('token', window.localStorage.refreshToken)
return request({method:'get',url: '/api-token-refresh'})
}
// 给实例添加一个setToken方法,用于登录后将最新token动态添加到header,同时将token保存在localStorage中
function setToken(token,refreshToken){
console.log("重新缓存token")
request.defaults.headers['Authorization'] = `Auth ${token}`
// 这里用到的存储是localStorage
window.localStorage.setItem('token', token)
window.localStorage.setItem('refreshToken', refreshToken)
}
axios响应拦截
request.interceptors.response.use(
(response) => {
// 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据
// 否则的话抛出错误
if (response.status === 200) {
console.log(response.data.code)
// 这里可以根据code值进行判断处理,需要与后端协商统一
if (response.data.code == 0) {
console.log("test");
} else if (response.data.code == 20001) {
console.log("20001报错");
Notification.error({
title: "错误",
message: response.data.message,
});
}
return Promise.resolve(response);
} else {
return Promise.reject(response);
}
},
// 服务器状态码不是2开头的的情况
// 这里可以跟你们的后台开发人员协商好统一的错误状态码
// 然后根据返回的状态码进行一些操作,例如登录过期提示,错误提示等等
// 下面列举几个常见的操作,其他需求可自行扩展
(error) => {
if (error.response.status) {
switch (error.response.status) {
case 401:
var config = error.config; //获取401失败请求的axios中的config配置数据
if (!isRefreshing) { //没有刷新
isRefreshing = true
return refreshToken().then(res => {//请求刷新token的接口
const { token ,refreshToken} = res.data.data
setToken(token,refreshToken) //将新的token和refresh_token保存到localStorage中
config.headers['Authorization'] = `Auth ${token}`
console.log('token过期刷新接口');
// 已经刷新了token,将所有队列中的请求进行重试
requests.forEach(cb => cb(token))
requests = []
return request(config)
},err=>{
Notification.error({
title: "401",
message: error.response.data.message,
});
router.push("/login"); //跳转到登录页
}).catch(res => {
console.error('refreshtoken error =>', res)
}).finally(() => { //无论是否有触发异常,该语句都会执行
isRefreshing = false
})
}else {
// 正在刷新token,将返回一个未执行resolve的promise
// 保存函数 等待执行
// 吧请求都保存起来 等刷新完成后再一个一个调用
new Promise((resolve) => {
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
requests.push((token) => {
config.headers['Authorization'] = `Auth ${token}`
resolve(request(config))
})
})
}
break;
default:
}
return Promise.reject(error.response);
}
}
);
export default request;
本文参考
https://blog.csdn.net/weixin_44115908/article/details/106063316