iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0
佛心分享-SideProject30

我的時間到底去哪裡了!? – 個人時間數據系統開發挑戰系列 第 10

後端認證實作:Google OAuth + JWT 無狀態認證系統

  • 分享至 

  • xImage
  •  

前言與承接

承接昨天的 Day9 文章,我們已經完成了前端的 Google OAuth 登入流程。今天要來實作後端的認證功能,要驗證前端傳來的 Google ID Token,並簽發我們自己的 JWT Token 給前端使用。

這個設計讓我們的後端可以完全無狀態,不需要儲存 session,同時保持高安全性。

認證架構概覽

我們的認證流程如下:

前端取得 Google ID Token
    ↓
後端驗證 Google ID Token
    ↓
後端簽發自己的 JWT Token
    ↓
前端使用 JWT Token 進行後續請求

登入沒有分權限,基本上使用者只有已登入/沒登入,兩種狀態

Google ID Token 驗證實作

首先配置 Google ID Token 驗證器:

@Configuration
public class GoogleVerifierConfig {
    private final AppConfig appConfig;  // 依賴注入配置
    
    public GoogleVerifierConfig(AppConfig appConfig) {
        this.appConfig = appConfig;  // Spring 自動注入
    }
    
    @Bean
    public GoogleIdTokenVerifier googleIdTokenVerifier() throws Exception {
        String clientId = appConfig.getGoogle().getOauth().getClientId();
        return new GoogleIdTokenVerifier.Builder(
                GoogleNetHttpTransport.newTrustedTransport(),
                GsonFactory.getDefaultInstance()
        )
                .setAudience(Collections.singletonList(clientId))
                .build();
    }
}

這裡做什麼:建立一個 Google ID Token 驗證器,用來確認前端送來的 Token 真的是Google ID。

重點:

  • audience 必須對到前端的 clientId。
  • 用 Google 官方提供的金鑰與簽章機制,不需要特別寫程式處理。

在 Controller 中驗證 ID Token:

@PostMapping("/google")
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleAuthRequest request) {
    try {
        GoogleIdToken idToken = verifier.verify(request.idToken());
        if (idToken == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        
        GoogleIdToken.Payload payload = idToken.getPayload();
        String googleSub = payload.getSubject();
        String email = (String) payload.get("email");
        String name = (String) payload.get("name");
        
        // 回傳JWT...
        
    } catch (Exception e) {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
    }
}

這裡做什麼:接收前端送來的 Google ID Token,驗證後取出 payload(sub、email、name)。

JWT 服務實作

實作 JWT 的簽發與驗證:

@Service
public class JwtService {
    
    public String issue(Long userId, String email) {
        Date now = new Date();
        Date exp = new Date(now.getTime() + expirationMinutes * 60L * 1000L);
        return Jwts.builder()
                .setSubject(userId.toString())
                .claim("email", email)
                .setIssuedAt(now)
                .setExpiration(exp)
                .signWith(secretKey, SignatureAlgorithm.HS256)
                .compact();
    }
    
    public Jws<Claims> parse(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token);
    }
}

這裡做什麼:負責簽發與解析 JWT。

JWT payload 設計:

  • subject: 用戶 ID
  • email: 用戶email
  • issuedAt: 發行時間
  • expiration: 過期時間(4 小時)

用戶管理與自動註冊

User 實體設計:

@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "google_sub", nullable = false, unique = true)
    private String googleSub;  // Google 用戶唯一標識
    
    @Column(name = "email")
    private String email;
    
    @Column(name = "name")
    private String name;
}

自動註冊邏輯:

Optional<User> optional = userRepository.findByGoogleSub(googleSub);
User user = optional.orElseGet(() -> {
    User u = new User();
    u.setGoogleSub(googleSub);
    return u;
});
user.setEmail(email);
user.setName(name);
user = userRepository.save(user);

這裡做什麼:User資料表設計與自動註冊邏輯。

重點:

  • google_sub 是唯一標識,比 email 穩定(因為 email 可能會變)。
  • 登入時如果沒有找到使用者,就自動註冊一筆到資料庫。
  • 有找到就更新 email/name,保持資料最新。

Spring Security 整合

配置 Spring Security:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/auth/**", "/health", "/api-docs/**", "/swagger-ui/**").permitAll()
                .anyRequest().authenticated()
            )
            .cors(Customizer.withDefaults());
            
        http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
}

這裡做什麼:定義整個系統的安全規則,負責攔截請求,然後透過jwtAuthFilter進行認證。

重點:

  • csrf.disable():因為是 REST API,不需要 CSRF。
  • sessionCreationPolicy.STATELESS:完全無狀態,不保留 session。
  • /auth/**/swagger-ui/** 等 endpoint 開放,其他都需要認證。
  • jwtAuthFilter 放進 Filter 鏈,專門處理 Authorization header。

JWT Filter 處理:

@Component
public class JwtAuthFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
        String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (authorization != null && authorization.startsWith("Bearer ")) {
            String token = authorization.substring(7);
            try {
                Jws<Claims> jws = jwtService.parse(token);
                String subject = jws.getBody().getSubject();
                Authentication authentication = new UserIdAuthentication(Long.parseLong(subject));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } catch (Exception ignored) {
                SecurityContextHolder.clearContext();
            }
        }
        filterChain.doFilter(request, response);
    }
}

這裡做什麼:驗證 Authorization header 中的 JWT。

重點:

  • 如果 JWT 有效,就建立 Authentication 放進 SecurityContext。
  • 如果無效,清掉 Context,不讓請求通過。
  • 最後交給 filterChain 繼續處理。

環境配置與測試

Google OAuth 設定提醒

在 Google Cloud Console 中:

  • Authorized JavaScript origins: http://localhost:3000
  • Authorized redirect URIs: http://localhost:3000(如果使用 redirect 模式)

CORS 安全設定

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(List.of("http://localhost:3000")); // 只開必要的來源
    configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
    configuration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
    configuration.setAllowCredentials(true);
    return source;
}

這裡做什麼:確保 Google OAuth 與 CORS 設定正確,避免本地開發遇到錯誤。

重點:

  • Google Cloud Console → 設定正確的 Authorized origins / redirect URIs
  • CORS 設定 → 只開放前端實際用到的 domain。
  • 常見問題:aud 不符(clientId 錯)、token 過期、CORS 設定錯誤。

常見問題與解決方案

  1. Google ID Token 驗證失敗
  • 檢查 Client ID 是否正確
  • 確認 Token 未過期
  • 檢查 Google OAuth 設定
  1. CORS 錯誤
  • 檢查前端 URL 是否在允許清單中
  • 確認 CORS 配置正確

總結

今天我們完成了後端的認證系統實作,包括:

  • Google ID Token 驗證
  • JWT 簽發與驗證
  • 用戶自動註冊
  • Spring Security 整合

完整程式碼請參考 GitHub連結

相關檔案:

  • GoogleVerifierConfig.java - Google ID Token 驗證器配置
  • JwtService.java - JWT 服務
  • JwtAuthFilter.java - JWT 認證過濾器
  • SecurityConfig.java - Spring Security 配置
  • GoogleAuthController.java - Google 登入控制器

上一篇
前端登入實作:Google OAuth + JWT
下一篇
為什麼需要資料庫遷移?用 Flyway 管理 Spring Boot 專案的資料庫內容
系列文
我的時間到底去哪裡了!? – 個人時間數據系統開發挑戰15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言