springboot集成JWT之双重token的实现
作者:编梦小匠
一,单个token缺点
token一般存储在浏览器中,容易被盗取,而为了防止被盗取长期使用,token的有效时间必然无法不能设置太长,此举也必然引起用户的频繁登录,给用户带来不好的体验
二,双重token(accessToken,refreshToken)
(一)设计思路
1,将accessToken作为是否登录的标识,存储在浏览器当中。由于该token可浏览器看到,容易被盗取,可将有效时间设置的尽可能短一些,解决盗取长期使用问题
2,将refreshToken作为是否更新accessToken的标识,只要refreshToken不过期,则自动更新accessToken,可将有效时间设置的长些,解决频繁登录问题
3,不直接将refreshToken存储到浏览器上,而是将其存储到accessToken的载荷里,后端通过获取accessToken的载荷内容获取到refreshToken,防止refreshToken被盗取
4,accessToken构成:
载荷:refreshToken
有效时间:尽可能短(这里我设置为30分钟)
密钥:用户的密码
refreshToken构成:
载荷:用户的id
有效时间:长(这里我设置为一天)
密钥:用户的密码
(二)后端代码
1,TokenUtil(生成token和获取用户信息)
具体步骤看注释
@Component
@Slf4j
public class TokenUtils {
private static IUserService staticAdminService;
@Resource
private IUserService adminService;
@PostConstruct
public void setUserService() {
staticAdminService = adminService;
}
// 生成token
public static String genToken(String adminId, String sign, Integer time) {
//生成过期时间
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, time*60);
return JWT.create().withAudience(adminId) //载荷
.withExpiresAt(instance.getTime()) //time分钟后过期
.sign(Algorithm.HMAC256(sign)); // 密钥
}
//根据accessToken获取用户信息
// 1.通过accessToken的载荷拿到refreshToken
// 2.通过refreshToken的载荷拿到userId
// 3.调用根据用户id获取用户信息的方法拿到用户信息
public static User getCurrentAdmin() {
String accessToken = null;
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
accessToken = request.getHeader("token");
System.out.println("access"+accessToken);
if (StrUtil.isBlank(accessToken)) {
log.error("获取当前登录的accessToken失败, token: {}", accessToken);
return null;
}
String refreshToken = JWT.decode(accessToken).getAudience().get(0);
if (StrUtil.isBlank(refreshToken)) {
log.error("获取当前登录的refreshToken失败, token: {}", refreshToken);
return null;
}
String userId = JWT.decode(refreshToken).getAudience().get(0);
return staticAdminService.getById(Integer.valueOf(userId));
} catch (Exception e) {
log.error("获取当前登录的管理员信息失败, token={}", accessToken, e);
return null;
}
}
}
2,JwtInterceptor(检验accessToken是否合法)
实现思路:
(1)检验accessToken是否为空
(2)通过accessToken的载荷内容拿到refreshToken
(3)验证获取到的refreshToken是否合法,若不合法,则accessToken为无效token,合法则返回密钥
(4)通过该密钥去判断accessToken是否过期
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private IUserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String accessToken = request.getHeader("token");
if (StrUtil.isBlank(accessToken)) {
accessToken = request.getParameter("token");
}
//执行认证
if (StrUtil.isBlank(accessToken)) {
throw new ServiceException(ErrorCode.TOKEN_NO_EXIST.getCode(), ErrorCode.TOKEN_NO_EXIST.getMsg());
}
String refreshToken;
try{
refreshToken = JWT.decode(accessToken).getAudience().get(0);
}catch (Exception e){
throw new ServiceException(ErrorCode.TOKEN_ERROR.getCode(), ErrorCode.TOKEN_ERROR.getMsg());
}
String password = verifyRefreshToken(refreshToken);
try {
// 用户密码加签验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(password)).build();
jwtVerifier.verify(accessToken); // 验证token
} catch (JWTVerificationException e) {
throw new ServiceException(ErrorCode.TOKEN_ERROR.getCode(), ErrorCode.TOKEN_ERROR.getMsg());
}
return true;
}
//验证refreshToken是否合法
public String verifyRefreshToken(String token){
// 获取 token 中的adminId
String adminId;
User user;
try {
adminId = JWT.decode(token).getAudience().get(0);
// 根据token中的userid查询数据库
user = userService.getById(Integer.parseInt(adminId));
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
throw new ServiceException(ErrorCode.REFRESE_TOKEN_ERROR.getCode(), ErrorCode.REFRESE_TOKEN_ERROR.getMsg());
}
if (user == null) {
throw new ServiceException(ErrorCode.USER_NO_EXIST.getCode(), ErrorCode.USER_NO_EXIST.getMsg());
}
try {
// 用户密码加签验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
jwtVerifier.verify(token); // 验证token
} catch (JWTVerificationException e) {
throw new ServiceException(ErrorCode.REFRESE_TOKEN_ERROR.getCode(), ErrorCode.REFRESE_TOKEN_ERROR.getMsg());
}
return user.getPassword();
}
}
3, WebConfig(设置拦截规则)
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 加自定义拦截器JwtInterceptor,设置拦截规则
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor()).addPathPatterns("/**").excludePathPatterns("/user/login","/user/register","/refreshToken/refresh");
}
@Bean
public JwtInterceptor jwtInterceptor(){
return new JwtInterceptor();
}
}
4,登陆接口处生成accessToken和refreshToken
String refreshToken= TokenUtils.genToken(user.getId().toString(),user.getPassword(),24*60); String accessToken=TokenUtils.genToken(refreshToken,user.getPassword(),30);
5,编写更新refreshToken的接口
实现思路:
(1)检验accessToken是否为空
(2)通过accessToken的载荷内容拿到refreshToken
(3)验证获取到的refreshToken是否合法,不合法,则accessToken为无效token(此处无效token单指自己编写,不是后端生成的token),不对accessToken进行更新操作,合法则重新生成新的accessToken
@RestController
@RequestMapping("/refreshToken")
public class refreshTokenController {
@Autowired
private IUserService userService;
@GetMapping("/refresh")
public Result refresh(@RequestParam String accessToken) {
if (StrUtil.isBlank(accessToken)) {
throw new ServiceException(ErrorCode.TOKEN_NO_EXIST.getCode(), ErrorCode.TOKEN_NO_EXIST.getMsg());
}
String refreshToken;
try{
refreshToken = JWT.decode(accessToken).getAudience().get(0);
}catch (Exception e){
throw new ServiceException(ErrorCode.TOKEN_ERROR.getCode(), ErrorCode.TOKEN_ERROR.getMsg());
}
if(StrUtil.isBlank(refreshToken)){
return Result.error(ErrorCode.REFRESH_TOKEN_NULL.getCode(), ErrorCode.REFRESH_TOKEN_NULL.getMsg());
}
// 获取 token 中的adminId
String adminId;
User user;
try {
adminId = JWT.decode(refreshToken).getAudience().get(0);
// 根据token中的userid查询数据库
user = userService.getById(Integer.parseInt(adminId));
System.out.println(user);
} catch (Exception e) {
e.printStackTrace();
throw new ServiceException(ErrorCode.REFRESE_TOKEN_ERROR.getCode(), ErrorCode.REFRESE_TOKEN_ERROR.getMsg());
}
if (user == null) {
throw new ServiceException(ErrorCode.USER_NO_EXIST.getCode(), ErrorCode.USER_NO_EXIST.getMsg());
}
try {
// 用户密码加签验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
jwtVerifier.verify(refreshToken); // 验证token
} catch (JWTVerificationException e) {
throw new ServiceException(ErrorCode.REFRESE_TOKEN_ERROR.getCode(), ErrorCode.REFRESE_TOKEN_ERROR.getMsg());
}
String currentAccessToken= TokenUtils.genToken(refreshToken,user.getPassword(),30);
return Result.success(currentAccessToken);
}
}
(三)前端代码
1,登录成功后存储accessToken
存储方式不限,在这我是存储在localStorage里,并在vuex里共享该数据
//storage.js
const TOKEN_KEY = 'tk'
export const getInfo = () => {
const token = localStorage.getItem(TOKEN_KEY)
return token || ''
}
export const setInfo = (token) => {
localStorage.setItem(TOKEN_KEY, token)
}
export const removeInfo = () => {
localStorage.removeItem(TOKEN_KEY)
}
//token模块
import { getInfo, setInfo ,removeInfo} from '@/utils/storage'
export default {
namespaced: true,
state () {
return {
tokenInfo: getInfo()
}
},
mutations: {
setTokenInfo (state, info) {
state.tokenInfo = info
setInfo(state.tokenInfo)
},
removeTokenInfo () {
removeInfo()
}
},
actions: {}
}
store.commit('token/setTokenInfo', data)
2,axios请求拦截器处设置请求头
instance.interceptors.request.use(function (config) {
const accessToken = store.state.token.tokenInfo
if (accessToken) {
config.headers.token = accessToken
}
return config
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error)
})
3,axios响应拦截器配置
设置accessToken过期则判断refreshToken是否过期,如果未过期,重新生成accessToken,并重新发送请求
instance.interceptors.response.use(async function (response) {
if (response.data.code === 200) {
return response.data
} else if (response.data.code === 1008 || response.data.code === 1009 || response.data.code === 1010) {
//accessToken过期
const { data } = await refreshToken(store.state.token.tokenInfo)
store.commit('token/setTokenInfo', data)
return instance(response.config)
} else if (response.data.code === 1014) {
//refreshToken过期
window.location.href = '/login'
} else {
Vue.prototype.$message.error(response.data.msg)
return Promise.reject(response.data.msg)
}
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error)
})
4,守卫路由配置
判断是否有accessToken,是否在登录注册页面,若都无则跳转到登录页,重新登录
router.beforeEach(async (to, from, next) => {
const token = store.state.token.tokenInfo
if (
// 检查用户是否已登录
!token &&
// ❗️ 避免无限重定向
to.path !== '/login' && to.path !== '/register'
) {
// 将用户重定向到登录页面
next({ path: '/login' })
} else {
next()
}
})到此这篇关于springboot集成JWT之双重token的实现的文章就介绍到这了,更多相关springboot 双重token内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
您可能感兴趣的文章:
- SpringBoot权限认证Sa-Token的使用总结
- SpringBoot项目引入token设置方式
- SpringBoot如何集成Token
- SpringBoot基于Redis实现token的在线续期的实践
- springboot+vue实现Token自动续期(双Token方案)
- SpringBoot中Token登录授权、续期和主动终止的方案流程分析
- SpringBoot权限认证-Sa-Token的使用详解
- 使用SpringBoot简单实现无感知的刷新 Token功能
- springBoot整合jwt实现token令牌认证的示例代码
- springboot中通过jwt令牌校验及前端token请求头进行登录拦截实战记录
- springboot实现token验证登陆状态的示例代码
- SpringBoot+Vue中的Token续签机制
