今天要做的:
/login-with-refresh
與 /refresh
的完整流程POST /login-with-refresh
→ 回 { accessToken, refreshToken }
POST /refresh
(帶 refreshToken
){ accessToken, refreshToken }
我們已經把兩個端點寫好了
// Login:同時回 accessToken 與 refreshToken
@PostMapping("/login-with-refresh")
public ResponseEntity<?> loginWithRefresh(@RequestBody Map<String, String> request) {
String username = request.get("username");
String password = request.get("password");
if ("admin".equals(username) && "password".equals(password)) {
String accessToken = JwtUtil.generateAccessToken(username, "ROLE_ADMIN");
String refreshToken = JwtUtil.generateRefreshToken(username);
return ResponseEntity.ok(Map.of(
"accessToken", accessToken,
"refreshToken", refreshToken
));
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "帳號或密碼錯誤"));
}
// 用 refreshToken 換新的 accessToken
@PostMapping("/refresh")
public ResponseEntity<?> refresh(@RequestBody Map<String, String> request) {
String refreshToken = request.get("refreshToken");
try {
Jws<Claims> claims = JwtUtil.validateToken(refreshToken);
String username = claims.getBody().getSubject();
String newAccessToken = JwtUtil.generateAccessToken(username, "ROLE_ADMIN");
return ResponseEntity.ok(Map.of(
"accessToken", newAccessToken,
"refreshToken", refreshToken // 目前重用舊的 refresh(簡化版)
));
} catch (ExpiredJwtException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Refresh Token 過期,請重新登入"));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "Refresh Token 無效"));
}
}
目前我們的 SecurityConfig
目前只 permitAll("/login", "/login-with-refresh")
,記得讓 /refresh
也能匿名呼叫:
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/login-with-refresh", "/refresh").permitAll()
.anyRequest().authenticated()
)
localStorage
/ indexedDB
。但風險是 XSS 可竊取 Token。/refresh
可以走 Cookie,不需 body 帶 token。SameSite=Lax/Strict
或搭配 CSRF Token。curl -X POST http://localhost:8080/login-with-refresh \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password"}'
回應:
{
"accessToken":"<短效>",
"refreshToken":"<長效>"
}
curl http://localhost:8080/hello \
-H "Authorization: Bearer <accessToken>"
curl -X POST http://localhost:8080/refresh \
-H "Content-Type: application/json" \
-d '{"refreshToken":"<refreshToken>"}'
成功會回新的 accessToken
(若有旋轉,也會回新的 refreshToken
)。
/refresh
回 401:Token 類型錯誤
type="refresh"
,或前端誤把 Access Token 傳來刷新。SECRET
不一致(簽發與驗證要同一把 key)。/hello
取不到使用者
JwtAuthenticationFilter
已 addFilterBefore(..., UsernamePasswordAuthenticationFilter.class)
。Authorization: Bearer <token>
。SameSite
設定與 CSRF Token 配置。純 API 通常先 csrf.disable()
,或採 REST + Cookie 流程時搭配 CSRF Token。加上了Refresh token,整個都不一樣了,雖然只是增加一個Refresh,實務上就會變得靈活很多,但同事也會有很多的邏輯需要調整,這次雖然沒有給大家專案,但有了之前的案例,我相信大家應該是有機會獨立寫出來,大家可以試著跟AI溝通,讓延伸前幾天的程式碼,新增Refresh token。
相信大家也會需要一點時間消化,大家可以重複看看,然後再休息一下,我們明天見!