<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 {
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* Jwt过滤器
*/
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public 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();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
// 使用BCryptPasswordEncoder作为security默认的passwordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
这个过滤器是实现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 {
@Autowired
private RedisCache redisCache;
@Value("${token.expiration}")
private Long expiration;
@Autowired
private IUserMapper userMapper;
@Override
protected 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;
}
// 获取token
String 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;
}
// 解析token
String 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);
}
// 存入SecurityContextHolder
UsernamePasswordAuthenticationToken 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 url
timeout: 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 = config
return config
}
)
// response interceptor
service.interceptors.response.use(
response => {
if (response.headers['token-refreshed']) {
console.log('Token刷新成功');
// 如果有Token-Refreshed头部,更新本地存储中的Token
localStorage.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标记,尝试刷新Token
if (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 无感刷新机制。如有任何问题或建议,欢迎在评论区留言讨论。