iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0

到目前為止,我們已經學會了 JWTOIDC 的基本概念與實作方式。

今天開始進入專案案例,示範如何 同時支援兩種登入模式

  1. JWT:適合內部帳號密碼登入,簡單快速。
  2. OIDC:適合整合 Google、GitHub 等第三方身分認證。

一、架構設計思路

為了同時支援 JWT + OIDC,我們做了以下規劃:

  • JWT 流程
    • 使用者呼叫 /login API,輸入帳密 → 系統驗證 → 簽發 JWT → 後續 API 請求攜帶 Authorization: Bearer <token>
  • OIDC 流程
    • 使用者透過 /oauth2/authorization/google(或其他 Provider)登入 → OIDC Provider 驗證 → Spring Security 取得 OIDC Token → 可以直接存取受保護 API。

這兩種登入方式可以並存,不互相衝突。

二、程式碼解析

大家可以參考這個專案

https://github.com/AnsathSean/spring-security-30days.git

1. AuthController — 提供兩種登入與測試 API

@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。

2. JwtAuthenticationFilter — 驗證 JWT Token

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);
    }
}

  • 功能:攔截所有請求,檢查是否帶有合法 JWT。
  • 效果:只要 Authorization header 含有 Bearer <token>,就會被轉換成 Spring Security 的 Authentication

3. JwtUtil — JWT 工具類

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);
    }
}

  • 產生與驗證 JWT 的工具類。
  • Token 有效期目前設定為 1 分鐘,方便測試。實際上大家要測試的時候,可以調整到30秒,這樣會更快過期,可以測試過期的結果。

4. SecurityConfig — 整合 JWT + OIDC


@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/ 等公開路徑允許匿名訪問。
  • 其餘 API 必須經過 JWTOIDC 驗證。
  • JwtAuthenticationFilter 放在 UsernamePasswordAuthenticationFilter 之前,確保攔截請求並檢查 Token。

三、驗證流程測試

  1. JWT 模式

    • 呼叫 POST /http://localhost:8080/login

      { "username": "admin", "password": "password" }
      
    • 取得 JWT Token。

    https://ithelp.ithome.com.tw/upload/images/20251006/20152864vkNX1vnOfb.png

    • 使用 Token 呼叫 GET http://localhost:8080/hello-jwt

      Authorization: Bearer <token>
      
    • 預期結果:Hello, JWT 使用者:admin

https://ithelp.ithome.com.tw/upload/images/20251006/20152864ftlNVMBQsK.png

  1. OIDC 模式

    • 透過 http://localhost:8080/oauth2/authorization/google 進行登入。

    https://ithelp.ithome.com.tw/upload/images/20251006/20152864tckxljOgt7.png

    • 登入成功後,呼叫 GET /hello-oidc
    • 預期結果:顯示 Google 帳號的姓名與 Email。

https://ithelp.ithome.com.tw/upload/images/20251006/20152864vOG8KlkV4P.png

四、整合的意義

  • JWT:方便內部用戶快速驗證,適合簡單的 API 驗證情境。
  • OIDC:讓應用程式可以無縫整合外部身分系統(Google、GitHub、Facebook)。
  • 並存模式:兼顧彈性,滿足不同使用者來源。

今天我們完成了 JWT + OIDC 的整合,並能同時支援兩種登入方式。

這讓系統既能處理 內部帳號密碼登入,也能接入 外部身份提供者。已經具備基本的雙重登入邏輯。今天的分享就到這裡,我們就明天見囉!


上一篇
Day 26:ABAC 與資源授權
系列文
「站住 口令 誰」關於資安權限與授權的觀念教學,以Spring boot Security框架實作27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言