?作者簡介:熱愛Java后端開發的一名學習者,大家可以跟我一起討論各種問題喔。
🍎個人主頁:Hhzzy99
🍊個人信條:堅持就是勝利!
💞當前專欄:項目實踐
🥭本文內容:實現雙Token的無感刷新。
雙 Token 無感刷新機制實現
- 后端依賴
- 安全配置
- Jwt過濾器 *
- 前端的配置
在現代 Web 應用開發中,前后端分離已經成為一種趨勢。Vue.js 作為前端框架,Java
作為后端語言的組合被廣泛應用。在用戶認證方面,JWT因為其無狀態、易于擴展等特點也備受青睞。本文將詳細介紹如何在 Vue 前端和 Java
后端實現雙 Token 的無感刷新機制。
后端依賴
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><!-- 其他依賴 -->
</dependencies>
安全配置
配置Jwt過濾器,以及認證失敗過濾器。
@Configuration
@EnableWebSecurity
public class SecurityConfig {/*** 認證失敗處理類*/@Autowiredprivate AuthenticationEntryPointImpl unauthorizedHandler;/*** Jwt過濾器*/@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf().disable().cors().and().addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class).headers().cacheControl().disable().and().exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers("/user/login", "/user/forgetPassword/**", "/user/sendUpdatePasswordEmailCode/**", "/user/register", "/swagger-ui.html", "/user/sendEmailLoginCode", "/user/verifyEmailLoginCode/**").permitAll().antMatchers("/admin/**").hasRole("ADMIN").anyRequest().authenticated().and().headers().frameOptions().disable();return http.build();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}// 使用BCryptPasswordEncoder作為security默認的passwordEncoder@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}
Jwt過濾器 *
這個過濾器是實現token刷新機制的核心,每次前端的請求攜帶accessToken與refreshToken過來,此過濾器拿到之后,先對accessToken進行解析,如果解析失敗(過期),那么接下來會對refreshToken進行解析,解析完成之后,如果沒有過期,就會生成新的accessToken與refreshToken返回給前端,并且設置一個新的請求頭Token-Refreshed,值可以隨便設,前端能拿到就好。
package com.hblog.backend.config;import com.alibaba.fastjson2.JSON;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hblog.backend.entity.LoginUser;
import com.hblog.backend.entity.User;
import com.hblog.backend.exception.BusinessException;
import com.hblog.backend.exception.EnumException;
import com.hblog.backend.mapper.IUserMapper;
import com.hblog.backend.response.CommonResponse;
import com.hblog.backend.utli.*;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;/*** @ClassName: JwtAuthenticationFilter* @author: Hhzzy99* @date: 2024/3/17 16:09* description:繼承每個請求只會經過一次的過濾器*/
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Value("${token.expiration}")private Long expiration;@Autowiredprivate IUserMapper userMapper;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 獲取當前請求路徑String requestPath = request.getRequestURI();// 排除不需要過濾的路徑if (requestPath.equals("/user/login") || requestPath.equals("/user/register")) {filterChain.doFilter(request, response);return;}// 獲取tokenString accessToken = request.getHeader("access_token");String refreshToken = request.getHeader("refresh_token");if ("null".equals(accessToken) || "".equals(accessToken) || "undefined".equals(accessToken) || null == accessToken) {// 放行filterChain.doFilter(request, response);return;}// 解析tokenString userId = "";boolean isRefresh = false;try {userId = JwtUtils.parseJWT(accessToken).getSubject();} catch (Exception e) {isRefresh = true;e.printStackTrace();}if (isRefresh) {try {userId = JwtUtils.parseJWT(refreshToken).getSubject();accessToken = JwtUtils.createJWT(userId);refreshToken = JwtUtils.createRefreshToken(userId);User loginUser = userMapper.getUserById(Long.valueOf(userId));Integer ttl = expiration.intValue() / 1000;log.warn("@@@@@@@@@@@@@@@@@@刷新token@@@@@@@@@@@@@@@@@@@@@");redisCache.setCacheObject("userInfo:" + userId, loginUser, ttl, TimeUnit.SECONDS);writeTokenResponse(response, accessToken, refreshToken);return;} catch (Exception e1) {throw new BusinessException(EnumException.THE_LOGIN_HAS_EXPIRED);}}// 從redis里面獲取用戶信息User loginUser = redisCache.getCacheObject("userInfo:" + userId);if (Objects.isNull(loginUser)) {throw new BusinessException(EnumException.THE_LOGIN_HAS_EXPIRED);}// 存入SecurityContextHolderUsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);// 放行filterChain.doFilter(request, response);}private void writeTokenResponse(HttpServletResponse response, String accessToken, String refreshToken) throws IOException {Map<String, String> tokenMap = new HashMap<>();tokenMap.put("accessToken", accessToken);tokenMap.put("refreshToken", refreshToken);Map<String, String> headers = new HashMap<>();headers.put("Token-Refreshed", "true");CommonResponse<Map<String, String>> commonResponse = new CommonResponse<>(200, "Token refreshed successfully", tokenMap);WebUtil.renderString(response, headers, JSON.toJSONString(commonResponse));}
}
前端的配置
前端對所有的axios請求進行全局配置,先在每次請求的時候設置好請求頭accessToken與refreshToken,并且將每次請求都保存起來,如果在請求時后端解析到accessToken失效,并且返回了新的accessToken與refreshToken,在請求頭拿到了后端設置好的Token-Refreshed,此時就可以重新將新的accessToken與refreshToken保存在瀏覽器本地,并且重新發送之前保存好的請求,就可以實現無感刷新。
request.js
import axios from 'axios'
import {ref} from "vue";// create an axios instance
const service = axios.create({baseURL: '/api', // url = base url + request urltimeout: 20000 // request timeout
})const retryRequest = ref(null)// request interceptor
service.interceptors.request.use(config => {// 加入頭信息配置if (localStorage.getItem("access_token") !== null && localStorage.getItem("access_token") !== undefined){config.headers['access_token'] = localStorage.getItem("access_token")}if (localStorage.getItem("refresh_token") !== null && localStorage.getItem("refresh_token") !== undefined){config.headers['refresh_token'] = localStorage.getItem("refresh_token")}retryRequest.value = configreturn config}
)// response interceptor
service.interceptors.response.use(response => {if (response.headers['token-refreshed']) {console.log('Token刷新成功');// 如果有Token-Refreshed頭部,更新本地存儲中的TokenlocalStorage.setItem('access_token', response.data.data.accessToken);localStorage.setItem('refresh_token', response.data.data.refreshToken);console.log("繼續")// 繼續發送原始請求return axios(retryRequest.value)}return response;},async error => {const originalRequest = error.config;// 如果是Token過期導致的401錯誤,并且沒有retry標記,嘗試刷新Tokenif (error.response.status === 401 && !originalRequest._retry) {originalRequest._retry = true;const refreshToken = localStorage.getItem('refresh_token');if (refreshToken) {try {const response = await axios.post('/refresh-token', { refreshToken });const { accessToken, refreshToken: newRefreshToken } = response.data.data;localStorage.setItem('access_token', accessToken);localStorage.setItem('refresh_token', newRefreshToken);// 更新原始請求的Authorization頭部originalRequest.headers['access_token'] = accessToken;// 重新發送原始請求return instance(originalRequest);} catch (refreshError) {// 刷新Token失敗,跳轉到登錄頁或執行其他處理console.error('Token刷新失敗:', refreshError);// 這里可以跳轉到登錄頁或者執行其他處理}}}return Promise.reject(error);}
)export default service
通過以上步驟,我們就可以實現雙 Token 無感刷新機制。該機制通過短期有效的訪問 Token 和長期有效的刷新 Token 相結合,在 Token 過期時自動刷新。
本示例僅展示了基礎的實現方式,實際生產環境中還需要考慮更多安全性和健壯性的問題。
希望這篇文章能幫助你更好地理解和實現 JWT 雙 Token 無感刷新機制。如有任何問題或建議,歡迎在評論區留言討論。