SpringBoot中雙token實現(xiàn)無感刷新
更新時間:2025年07月10日 09:31:35 作者:悟能不能悟
本文介紹雙Token無感刷新機制,前端React與后端SpringBoot實現(xiàn),采用HttpOnly Cookie和短期Token設(shè)計,具有一定的參考價值,感興趣的可以了解一下
一、方案說明
1. 核心流程
- ?用戶登錄?
- 提交賬號密碼 → 服務(wù)端驗證 → 返回Access Token(前端存儲) + Refresh Token(HttpOnly Cookie)
- ?業(yè)務(wù)請求?
- 請求頭攜帶Access Token → 服務(wù)端驗證有效性 → 有效則返回數(shù)據(jù)
- ?Token過期處理?
- 若Access Token過期 → 前端攔截401錯誤 → 自動用Refresh Token請求新Token → 刷新后重試原請求
- ?Refresh Token失效?
- 清除登錄態(tài) → 跳轉(zhuǎn)登錄頁
2. 安全設(shè)計
- ?Access Token?
- 存儲:前端內(nèi)存(如Vuex/Redux)或
sessionStorage - 有效期:2小時
- 傳輸:
Authorization: Bearer <token>
- 存儲:前端內(nèi)存(如Vuex/Redux)或
- ?Refresh Token?
- 存儲:
HttpOnly + Secure + SameSite=StrictCookie - 有效期:7天
- 刷新機制:單次使用后更新,舊Token立即失效
- 存儲:
二、前端實現(xiàn)(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;
});
// 響應(yīng)攔截器:處理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 {
// 發(fā)起刷新Token請求
const { accessToken } = await refreshToken();
// 存儲新Token
sessionStorage.setItem('access_token', accessToken);
// 重試原請求
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return http(originalRequest);
} catch (refreshError) {
// 刷新失?。呵宄齌oken,跳轉(zhuǎn)登錄
sessionStorage.removeItem('access_token');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
// 刷新Token函數(shù)
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);
// 跳轉(zhuǎn)主頁
window.location.href = '/';
} catch (err) {
alert('登錄失敗');
}
};
return (
<form onSubmit={handleSubmit}>
{/* 登錄表單 */}
</form>
);
};三、后端實現(xiàn)(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);
// 設(shè)置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服務(wù)(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);
}
}五、配置參數(shù)(application.yml)
jwt:
secret: "your-256-bit-secret-key-here" # 通過環(huán)境變量注入
access:
expiration: 7200000 # 2小時(毫秒)
refresh:
expiration: 604800000 # 7天(毫秒)六、數(shù)據(jù)庫表結(jié)構(gòu)(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) );
此方案完整實現(xiàn)了雙Token無感刷新機制,具備以下特點:
- 完整的前后端代碼示例,可直接集成到項目中
- 遵循安全最佳實踐(HttpOnly Cookie、短期Token)
- 支持并發(fā)請求處理和Token主動吊銷
- 清晰的模塊劃分,易于擴展維護
到此這篇關(guān)于SpringBoot中雙token實現(xiàn)無感刷新的文章就介紹到這了,更多相關(guān)SpringBoot 雙token無感刷新內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot解決Class path contains multiple 
這篇文章主要介紹了springboot解決Class path contains multiple SLF4J bindings問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07
SpringBoot數(shù)據(jù)庫常見錯誤DataIntegrityViolationException的原因及解決方案
在SpringBoot+MyBatis/MyBatis-Plus開發(fā)過程中,數(shù)據(jù)庫操作是核心部分之一,然而,開發(fā)者經(jīng)常會遇到 org.springframework.dao.DataIntegrityViolationException異常本文將通過兩個典型案例,深入分析DataIntegrityViolationException的常見原因,并提供完整的解決方案2025-07-07
Springmvc中的轉(zhuǎn)發(fā)重定向和攔截器的示例
本篇文章主要介紹了Springmvc中的轉(zhuǎn)發(fā)重定向和攔截器的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05
mybatis collection關(guān)聯(lián)查詢多個參數(shù)方式
在使用MyBatis進行關(guān)聯(lián)查詢時,往往需要根據(jù)多個參數(shù)進行查詢,例如,使用evtId和businessType作為查詢條件,同時在resultMap中配置id和businessType1作為結(jié)果映射,這種情況下,可以通過<sql>標簽定義參數(shù)模板,或者使用@Param注解指定參數(shù)名稱2024-10-10

