SpringBoot中双token实现无感刷新
目录
- 一、方案说明
- 1. 核心流程
- 2. 安全设计
- 二、前端实现(React示例)
- 1. AxIOS封装(src/utils/http.js)
- 2. 登录逻辑(src/pages/Login.js)
- 三、后端实现(Spring Boot)
- 1. JWT工具类(JwtUtil.Java)
- 2. 认证接口(AuthController.java)
- 3. Refresh Token服务(RefreshTokenService.java)
- 四、安全配置(SecurityConfig.java)
- 五、配置参数(application.yml)
- 六、数据库表结构(mysql)
一、方案说明
1. 核心流程
- 用户登录
- 提交账号密码 → 服务端验证 → 返回Access Token(前端存储) + Refresh Token(HttpOnly Cookie)
- 业务请求
- 请求头携带Access Token → 服务端验证有效性 → 有效则返回数据
- Token过期处理
- 若Access Token过期 → 前端拦截401错误 → 自动用Refresh Token请求新Token → 刷新后重试原请求
- Refresh Token失效
- 清除登录态 → 跳转登录页
2. 安全设计
- Access Token
- 存储:前端内存(如vuex/Redux)或
sessionStorage
- 有效期:2小时
- 传输:
Authorization: Bearer <token>
- 存储:前端内存(如vuex/Redux)或
- Refresh Token
- 存储:
HttpOnly + Secure + SameSite=Strict
Cookie - 有效期:7天
- 刷新机制:单次使用后更新,旧Token立即失效
- 存储:
二、前端实现(React示例)
1. Axios封装(src/utils/http.js)
import axios from 'axios'; const http = axios.create({ baseURL: process.env.REACT_APP_API_URL, }); // 请求拦截器:注入Access Token http.interceptors.request.use(config => { const accessToken = sessionStorage.getItem('access_token'); if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; } return config; }); // 响应拦截器:处理Token过期 http.interceptors.response.use( response => response, async error => { const originalRequest = error.config; // 检测401错误且未重试过 if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { // 发起刷新Token请求 const { accessToken } = await refreshToken(); // 存储新Token sessionStorage.setItem('access_token', accessToken); // 重试原请求 originalRequest.headers.Authorization = `Bearer ${accessToken}`; return http(originalRequest); } catch (refreshError) { // 刷新失败:清除Token,跳转登录 sessionStorage.removeItem('access_token'); window.location.href = '/login'; return Promise.reject(refreshError); } } return Promise.reject(error); } ); // 刷新Token函数 async function refreshToken() { const res = await axios.post( `${process.env.REACT_APP_API_URL}/auth/refresh`, {}, { withCredentials: t编程客栈rue } // 自动携带Cookie ); return res.data; } export default http;
2. 登录逻辑(src/pages/Login.js)
const LoginPage = () => { const handleSubmit = async (e) => { e.preventDefault(); try { const res = await axios.post('/auth/login', { username: 'user', password: 'pass' }, { withCredentials: true }); // 存储Access Token sessionStorage.setItem('access_token', res.data.accessToken); // 跳转主页 window.location.href = '/'; } catch (err) { alert('登录失败'); } }; return ( <form onSubmit={handleSubmit}> {/* 登录表单 */} </form> ); };
三、后端实现(Spring Boot)
1. JWT工具类(JwtUtil.java)
@Component public class JwtUtil { @Value("${jwt.secret}") private String secret; @Value("${jwt.access.expiration}") private Long accessExpiration; @Value("${jwt.refresh.expiration}") private Long refreshExpiration; // 生成Access Token public String generateAccessToken(UserDetails user) { return buildToken(user, accessExpiration); } // 生成Refresh Token public String generateRefreshToken(UserDetails user) { return buildToken(user, refreshExpiration); } private String buildToken(UserDetails user, Long expiration) { return Jwts.builder() .setSubject(user.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } // 验证Token public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { throw new JwtException("Token验证失败"); } } // 从Token中提取用户名 public String getUsernameFromToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody() .getSubject(); } }
2. 认证接口(AuthController.java)
@RestController @RequestMapping("/auth") public class AuthController { @Autowired private JwtUtil jwtUtil; @Autowired private UserDetailsService userDetailsService; @Autowired private RefreshTokenService refreshTokenService; // 登录接口 @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest request) { UserDetails user = userDetailsService.loadUserByUsername(www.devze.comrequest.getUsername()); // 密码验证 if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new BadCredentialsException("密码错误"); } // 生成Token String accessToken = jwtUtil.generateAccessToken(user); String refreshToken = jwtUtil.generateRefreshToken(user); // 存储Refresh Token refreshTokenService.saveRefreshToken(user.getUsername(), refreshToken); // 设置Refresh Token到Cookie ResponseCookie cookie = ResponseCookie.from("refreshToken", refreshToken) .httpOnly(true) .secure(true) .sameSite("Strict") .maxAge(jwtUtil.getRefreshExpiration() / 1000) .path("/auth/refresh") .build(); return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, cookie.toString()) .body(new AuthResponse(accessToken)); } // 刷新Token接口 @PostMapping("/refresh") public ResponseEntity<?> refreshToken(@CookieValue("refreshToken") String refreshToken) { // 验证Refresh Token if (!jwtUtil.validateToken(refreshToken)) { throw new JwtException("无效Token"); } String username = jwtUtil.getUsernameFromToken(refreshToken); // 检查是否与存储的Token一致 if (!refreshTokenService.validateRefreshToken(username, refreshToken)) { throw new JwtException("Token已失效"); } // 生成新Token UserDetails user = userDetailsService.loadUserByUsername(username); String newAccessToken = jwtUtil.generateAccessToken(user); String newRefreshToken = jwtUtil.generateRefreshToken(user); // 更新存储的Refresh Token refreshTokenService.updateRefreshToken(username, newRefreshToken); // 返回新Token ResponseCookie cookie = ResponseCookie.from("refreshToken", newRefreshToken) .httpOnly(true) .secure(true) .sameSite("Strict") .maxAge(jwtUtil.getRefreshExpiration() / 1000) .path("/auth/refresh") .build(); return ResponseEntity.ohttp://www.devze.comk() .header(HttpHeaders.SET_COOKIE, cookie.toString()) .body(new AuthResponse(newAccessToken)); } }
3. Refresh Token服务(RefreshTokenService.java)
@Service public class RefreshTokenService { @Autowired private RefreshTokenRepository repository; public void saveRefreshToken(String username, String token) { RefreshToken refreshToken = new RefreshToken(); refreshToken.setUsername(username); refreshToken.setToken(token); refreshToken.setExpiryDate(jwtUtil.getExpirationDateFromToken(token)); repository.save(refreshToken); } public boolean validateRefreshToken(String username, String token) { return repository.findByUsernameAndToken(username, token) .map(t -> t.getExpiryDate().after(new Date())) .orElse(false); } public void updateRefreshToken(String username, String newToken) { repository.deleteByUsername(username); saveRefreshToken(username, newToken); } }
四、安全配置(SecurityConfig.java)
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationFilter jwtFilter; @Override protected void configure(HttpSecurity http) throws Exception { RYHBtTpY http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/auth/**").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); } } @Component public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header = request.getHeader("Authorization"); if (header != null &amjavascriptp;& header.startsWith("Bearer ")) { String token = header.substring(7); if (jwtUtil.validateToken(token)) { String username = jwtUtil.getUsernameFromToken(token); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>()); SecurityContextHolder.getContext().setAuthentication(auth); } } chain.doFilter(request, response); } }
五、配置参数(application.yml)
jwt: secret: "your-256-bit-secret-key-here" # 通过环境变量注入 access: expiration: 7200000 # 2小时(毫秒) refresh: expiration: 604800000 # 7天(毫秒)
六、数据库表结构(MySQL)
CREATE TABLE refresh_tokens ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255) NOT NULL, token VARCHAR(512) NOT NULL, expiry_date DATETIME NOT NULL, UNIQUE KEY (username) );
此方案完整实现了双Token无感刷新机制,具备以下特点:
- 完整的前后端代码示例,可直接集成到项目中
- 遵循安全最佳实践(HttpOnly Cookie、短期Token)
- 支持并发请求处理和Token主动吊销
- 清晰的模块划分,易于扩展维护
到此这篇关于SpringBoot中双token实现无感刷新的文章就介绍到这了,更多相关SpringBoot 双token无感刷新内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
精彩评论