到目前為止,我們已經學會了 JWT 與 OIDC 的基本概念與實作方式。
今天開始進入專案案例,示範如何 同時支援兩種登入模式:
為了同時支援 JWT + OIDC,我們做了以下規劃:
/login
API,輸入帳密 → 系統驗證 → 簽發 JWT → 後續 API 請求攜帶 Authorization: Bearer <token>
。/oauth2/authorization/google
(或其他 Provider)登入 → OIDC Provider 驗證 → Spring Security 取得 OIDC Token → 可以直接存取受保護 API。這兩種登入方式可以並存,不互相衝突。
大家可以參考這個專案
https://github.com/AnsathSean/spring-security-30days.git
@RestController
public class AuthController {
// ✅ JWT 登入
@PostMapping("/login")
public Map<String, String> login(@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");
return Map.of("accessToken", accessToken);
}
return Map.of("error", "帳號或密碼錯誤");
}
// ✅ 測試 JWT API
@GetMapping("/hello-jwt")
public String helloJwt() {
return "Hello, JWT 使用者:" +
SecurityContextHolder.getContext().getAuthentication().getName();
}
// ✅ 測試 OIDC API
@GetMapping("/hello-oidc")
public String helloOidc(@AuthenticationPrincipal OidcUser oidcUser) {
return "Hello, OIDC 使用者:" + oidcUser.getFullName() + " (" + oidcUser.getEmail() + ")";
}
}
/login
:驗證帳號密碼,簽發 JWT。/hello-jwt
:測試攜帶 JWT 後是否能成功訪問。/hello-oidc
:測試 OIDC 登入是否成功,並顯示使用者姓名與 Email。public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
Jws<Claims> claimsJws = JwtUtil.validateToken(token);
String username = claimsJws.getBody().getSubject();
String role = claimsJws.getBody().get("role", String.class);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, List.of(() -> role));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (Exception e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Invalid or Expired Token");
return;
}
}
filterChain.doFilter(request, response);
}
}
Authorization
header 含有 Bearer <token>
,就會被轉換成 Spring Security 的 Authentication
。public class JwtUtil {
private static final String SECRET = "mySecretKeymySecretKeymySecretKeymySecretKey"; // >=32 bytes
private static final Key key = Keys.hmacShaKeyFor(SECRET.getBytes());
public static String generateAccessToken(String username, String role) {
return Jwts.builder()
.setSubject(username)
.addClaims(Map.of("role", role))
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 60 * 1000)) // 1 分鐘
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public static Jws<Claims> validateToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin(form -> form.disable())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/login-with-refresh").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.defaultSuccessUrl("/hello-oidc", true) // 登入成功強制導到這裡
) // 啟用 OIDC Login
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
/login
、/
等公開路徑允許匿名訪問。JwtAuthenticationFilter
放在 UsernamePasswordAuthenticationFilter
之前,確保攔截請求並檢查 Token。JWT 模式
呼叫 POST /
http://localhost:8080/login
{ "username": "admin", "password": "password" }
取得 JWT Token。
使用 Token 呼叫 GET
http://localhost:8080/hello-jwt
:
Authorization: Bearer <token>
預期結果:Hello, JWT 使用者:admin
OIDC 模式
http://localhost:8080/oauth2/authorization/google
進行登入。GET /hello-oidc
。今天我們完成了 JWT + OIDC 的整合,並能同時支援兩種登入方式。
這讓系統既能處理 內部帳號密碼登入,也能接入 外部身份提供者。已經具備基本的雙重登入邏輯。今天的分享就到這裡,我們就明天見囉!