springboot集成JWT之雙重token的實(shí)現(xiàn)
一,單個(gè)token缺點(diǎn)
token一般存儲(chǔ)在瀏覽器中,容易被盜取,而為了防止被盜取長(zhǎng)期使用,token的有效時(shí)間必然無(wú)法不能設(shè)置太長(zhǎng),此舉也必然引起用戶(hù)的頻繁登錄,給用戶(hù)帶來(lái)不好的體驗(yàn)
二,雙重token(accessToken,refreshToken)
(一)設(shè)計(jì)思路
1,將accessToken作為是否登錄的標(biāo)識(shí),存儲(chǔ)在瀏覽器當(dāng)中。由于該token可瀏覽器看到,容易被盜取,可將有效時(shí)間設(shè)置的盡可能短一些,解決盜取長(zhǎng)期使用問(wèn)題
2,將refreshToken作為是否更新accessToken的標(biāo)識(shí),只要refreshToken不過(guò)期,則自動(dòng)更新accessToken,可將有效時(shí)間設(shè)置的長(zhǎng)些,解決頻繁登錄問(wèn)題
3,不直接將refreshToken存儲(chǔ)到瀏覽器上,而是將其存儲(chǔ)到accessToken的載荷里,后端通過(guò)獲取accessToken的載荷內(nèi)容獲取到refreshToken,防止refreshToken被盜取
4,accessToken構(gòu)成:
載荷:refreshToken
有效時(shí)間:盡可能短(這里我設(shè)置為30分鐘)
密鑰:用戶(hù)的密碼
refreshToken構(gòu)成:
載荷:用戶(hù)的id
有效時(shí)間:長(zhǎng)(這里我設(shè)置為一天)
密鑰:用戶(hù)的密碼
(二)后端代碼
1,TokenUtil(生成token和獲取用戶(hù)信息)
具體步驟看注釋
@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) { //生成過(guò)期時(shí)間 Calendar instance = Calendar.getInstance(); instance.add(Calendar.SECOND, time*60); return JWT.create().withAudience(adminId) //載荷 .withExpiresAt(instance.getTime()) //time分鐘后過(guò)期 .sign(Algorithm.HMAC256(sign)); // 密鑰 } //根據(jù)accessToken獲取用戶(hù)信息 // 1.通過(guò)accessToken的載荷拿到refreshToken // 2.通過(guò)refreshToken的載荷拿到userId // 3.調(diào)用根據(jù)用戶(hù)id獲取用戶(hù)信息的方法拿到用戶(hù)信息 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("獲取當(dāng)前登錄的accessToken失敗, token: {}", accessToken); return null; } String refreshToken = JWT.decode(accessToken).getAudience().get(0); if (StrUtil.isBlank(refreshToken)) { log.error("獲取當(dāng)前登錄的refreshToken失敗, token: {}", refreshToken); return null; } String userId = JWT.decode(refreshToken).getAudience().get(0); return staticAdminService.getById(Integer.valueOf(userId)); } catch (Exception e) { log.error("獲取當(dāng)前登錄的管理員信息失敗, token={}", accessToken, e); return null; } } }
2,JwtInterceptor(檢驗(yàn)accessToken是否合法)
實(shí)現(xiàn)思路:
(1)檢驗(yàn)accessToken是否為空
(2)通過(guò)accessToken的載荷內(nèi)容拿到refreshToken
(3)驗(yàn)證獲取到的refreshToken是否合法,若不合法,則accessToken為無(wú)效token,合法則返回密鑰
(4)通過(guò)該密鑰去判斷accessToken是否過(guò)期
@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"); } //執(zhí)行認(rèn)證 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 { // 用戶(hù)密碼加簽驗(yàn)證 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(password)).build(); jwtVerifier.verify(accessToken); // 驗(yàn)證token } catch (JWTVerificationException e) { throw new ServiceException(ErrorCode.TOKEN_ERROR.getCode(), ErrorCode.TOKEN_ERROR.getMsg()); } return true; } //驗(yàn)證refreshToken是否合法 public String verifyRefreshToken(String token){ // 獲取 token 中的adminId String adminId; User user; try { adminId = JWT.decode(token).getAudience().get(0); // 根據(jù)token中的userid查詢(xún)數(shù)據(jù)庫(kù) 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 { // 用戶(hù)密碼加簽驗(yàn)證 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build(); jwtVerifier.verify(token); // 驗(yàn)證token } catch (JWTVerificationException e) { throw new ServiceException(ErrorCode.REFRESE_TOKEN_ERROR.getCode(), ErrorCode.REFRESE_TOKEN_ERROR.getMsg()); } return user.getPassword(); } }
3, WebConfig(設(shè)置攔截規(guī)則)
@Configuration public class WebConfig implements WebMvcConfigurer { // 加自定義攔截器JwtInterceptor,設(shè)置攔截規(guī)則 @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,編寫(xiě)更新refreshToken的接口
實(shí)現(xiàn)思路:
(1)檢驗(yàn)accessToken是否為空
(2)通過(guò)accessToken的載荷內(nèi)容拿到refreshToken
(3)驗(yàn)證獲取到的refreshToken是否合法,不合法,則accessToken為無(wú)效token(此處無(wú)效token單指自己編寫(xiě),不是后端生成的token),不對(duì)accessToken進(jìn)行更新操作,合法則重新生成新的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); // 根據(jù)token中的userid查詢(xún)數(shù)據(jù)庫(kù) 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 { // 用戶(hù)密碼加簽驗(yàn)證 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build(); jwtVerifier.verify(refreshToken); // 驗(yàn)證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,登錄成功后存儲(chǔ)accessToken
存儲(chǔ)方式不限,在這我是存儲(chǔ)在localStorage里,并在vuex里共享該數(shù)據(jù)
//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請(qǐng)求攔截器處設(shè)置請(qǐng)求頭
instance.interceptors.request.use(function (config) { const accessToken = store.state.token.tokenInfo if (accessToken) { config.headers.token = accessToken } return config }, function (error) { // 對(duì)請(qǐng)求錯(cuò)誤做些什么 return Promise.reject(error) })
3,axios響應(yīng)攔截器配置
設(shè)置accessToken過(guò)期則判斷refreshToken是否過(guò)期,如果未過(guò)期,重新生成accessToken,并重新發(fā)送請(qǐng)求
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過(guò)期 const { data } = await refreshToken(store.state.token.tokenInfo) store.commit('token/setTokenInfo', data) return instance(response.config) } else if (response.data.code === 1014) { //refreshToken過(guò)期 window.location.href = '/login' } else { Vue.prototype.$message.error(response.data.msg) return Promise.reject(response.data.msg) } }, function (error) { // 對(duì)響應(yīng)錯(cuò)誤做點(diǎn)什么 return Promise.reject(error) })
4,守衛(wèi)路由配置
判斷是否有accessToken,是否在登錄注冊(cè)頁(yè)面,若都無(wú)則跳轉(zhuǎn)到登錄頁(yè),重新登錄
router.beforeEach(async (to, from, next) => { const token = store.state.token.tokenInfo if ( // 檢查用戶(hù)是否已登錄 !token && // ?? 避免無(wú)限重定向 to.path !== '/login' && to.path !== '/register' ) { // 將用戶(hù)重定向到登錄頁(yè)面 next({ path: '/login' }) } else { next() } })
到此這篇關(guān)于springboot集成JWT之雙重token的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)springboot 雙重token內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot權(quán)限認(rèn)證Sa-Token的使用總結(jié)
- SpringBoot項(xiàng)目引入token設(shè)置方式
- SpringBoot如何集成Token
- SpringBoot基于Redis實(shí)現(xiàn)token的在線(xiàn)續(xù)期的實(shí)踐
- springboot+vue實(shí)現(xiàn)Token自動(dòng)續(xù)期(雙Token方案)
- SpringBoot中Token登錄授權(quán)、續(xù)期和主動(dòng)終止的方案流程分析
- SpringBoot權(quán)限認(rèn)證-Sa-Token的使用詳解
- 使用SpringBoot簡(jiǎn)單實(shí)現(xiàn)無(wú)感知的刷新 Token功能
- springBoot整合jwt實(shí)現(xiàn)token令牌認(rèn)證的示例代碼
- springboot中通過(guò)jwt令牌校驗(yàn)及前端token請(qǐng)求頭進(jìn)行登錄攔截實(shí)戰(zhàn)記錄
- springboot實(shí)現(xiàn)token驗(yàn)證登陸狀態(tài)的示例代碼
- SpringBoot+Vue中的Token續(xù)簽機(jī)制
相關(guān)文章
完美解決springboot中使用mybatis字段不能進(jìn)行自動(dòng)映射的問(wèn)題
今天在springboot中使用mybatis的時(shí)候不能字段不能夠進(jìn)行自動(dòng)映射,接下來(lái)給大家給帶來(lái)了完美解決springboot中使用mybatis字段不能進(jìn)行自動(dòng)映射的問(wèn)題,需要的朋友可以參考下2023-05-05java連接hdfs ha和調(diào)用mapreduce jar示例
這篇文章主要介紹了Java API連接HDFS HA和調(diào)用MapReduce jar包,需要的朋友可以參考下2014-03-03SpringBoot整合Swagger2的完整過(guò)程記錄
Swagger是一款RESTful接口的文檔在線(xiàn)自動(dòng)生成、功能測(cè)試功能框架,這篇文章主要給大家介紹了關(guān)于SpringBoot整合Swagger2的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2021-09-09如何使用JWT的SpringSecurity實(shí)現(xiàn)前后端分離
這篇文章主要介紹了使用JWT的SpringSecurity實(shí)現(xiàn)前后端分離,登錄成功需要返回json數(shù)據(jù)登錄失敗需要返回json數(shù)據(jù)權(quán)限不足時(shí)返回json數(shù)據(jù)未登錄訪(fǎng)問(wèn)資源返回json數(shù)據(jù),需要的朋友可以參考下2024-08-08為什么Spring和IDEA都不推薦使用 @Autowired 注解
本文主要介紹了為什么Spring和IDEA都不推薦使用 @Autowired 注解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04Jenkins自動(dòng)部署SpringBoot項(xiàng)目實(shí)踐教程
這篇文章主要介紹了Jenkins自動(dòng)部署SpringBoot項(xiàng)目實(shí)踐教程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11