承接昨天的 Day9 文章,我們已經完成了前端的 Google OAuth 登入流程。今天要來實作後端的認證功能,要驗證前端傳來的 Google ID Token,並簽發我們自己的 JWT Token 給前端使用。
這個設計讓我們的後端可以完全無狀態,不需要儲存 session,同時保持高安全性。
我們的認證流程如下:
前端取得 Google ID Token
↓
後端驗證 Google ID Token
↓
後端簽發自己的 JWT Token
↓
前端使用 JWT 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。在 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 的簽發與驗證:
@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 設計:
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 可能會變)。配置 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。
重點:
Google OAuth 設定提醒
在 Google Cloud Console 中:
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 設定正確,避免本地開發遇到錯誤。
重點:
常見問題與解決方案
今天我們完成了後端的認證系統實作,包括:
完整程式碼請參考 GitHub: 連結
相關檔案: