SpringBoot+React中双token实现无感刷新
作者:悟能不能悟
本文主要介绍了基于AccessToken和RefreshToken的双Token无感刷新机制,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
一、方案说明
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=StrictCookie - 有效期: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: true } // 自动携带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(request.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.ok()
.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 {
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 && 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+React中双token实现无感刷新的文章就介绍到这了,更多相关双token无感刷新内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
