實現 Gateway 作為 OAuth2 Client 對接 Google
1. 必備依賴 (pom.xml)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. 設定 SecurityConfig
在 Gateway 建立一個配置類。這裡的重點是設定哪些路徑需要登入,哪些可以放行(例如靜態資源或 Eureka 註冊中心頁面)。
package com.example.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.csrf(csrf -> csrf.disable()) // 微服務架構通常關閉 CSRF,或由後端處理
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/login/**", "/public/**").permitAll() // 開放登入頁與公開路徑
.anyExchange().authenticated() // 其他所有請求都必須登入
)
.oauth2Login(Customizer.withDefaults()); // 啟動 OAuth2 登入功能
return http.build();
}
}
沒有在 pathMatchers 裡面的路徑
因為設定了 .anyExchange().authenticated(),Gateway 發現你沒登入,會自動把瀏覽器導向 Google 的登入頁面。
3. YAML 配置 (application.yml)
是讓 Spring 知道要連去 Google 的哪裡。
spring:
security:
oauth2:
client:
registration:
google:
client-id: "你的_CLIENT_ID"
client-secret: "你的_CLIENT_SECRET"
scope:
- openid
- email
- profile
how to get google client-id & client-secret
如何將 Token 傳遞給後端 (User/Order Service)?
這是最關鍵的一步。當 Gateway 登入成功後,它持有 Token,但後端的微服務還不知道使用者是誰。我們可以透過 Gateway Filter 把資訊塞進 Header 裡。
在 application.yml 中利用內建的 TokenRelay 過濾器:
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/users/**
filters:
- TokenRelay= # 關鍵:這會自動將 OAuth2 Token 放入轉發請求的 Authorization Header 中
4. 攔截請求
攔截所有經過 Gateway 的請求,檢查目前是否有登入的 OAuth2 用戶,並將其身分資訊放入 Header。
package com.example.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class UserHeaderFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return ReactiveSecurityContextHolder.getContext()
.map(securityContext -> securityContext.getAuthentication())
.filter(authentication -> authentication instanceof OAuth2AuthenticationToken)
.cast(OAuth2AuthenticationToken.class)
.map(authentication -> {
// 從 Google 的屬性中提取 Email String email = authentication.getPrincipal().getAttribute("email");
// 將 Email 塞入 Header 中轉發給後端
ServerHttpRequest request = exchange.getRequest().mutate()
.header("X-User-Email", email)
.build();
return exchange.mutate().request(request).build();
})
.defaultIfEmpty(exchange)
.flatMap(chain::filter);
}
@Override
public int getOrder() {
// 設定優先權,通常設為較小的值(例如 0 或 -1)確保它早點執行
return 0;
}
}
5. 後端服務(如 order-service)如何接收?
現在 Gateway 已經把 Email 塞進了 X-User-Email 這個 Header,你的 order-service 只需要在 Controller 裡接收即可:
@GetMapping("/my-orders")
public List<OrderDTO> getMyOrders(@RequestHeader("X-User-Email") String userEmail) {
System.out.println("當前登入用戶: " + userEmail);
// 根據 email 去資料庫查訂單...
return orderService.findByEmail(userEmail);
}
6. 流程說明
-
用戶端發起請求。
-
Gateway 發現沒登入,跳轉 Google OAuth。
-
登入成功後回到 Gateway。
-
Global Filter 運行,從
SecurityContext撈出 Google 給的資料。 -
Gateway 將資料(Email)封裝進 HTTP Header,轉發給 微服務。
7. 如何測試
瀏覽器直接存取
OAuth2 的登入流程(Authorization Code Grant)是基於瀏覽器重新導向的。你不需要寫任何 HTML,Spring Security 會自動幫你處理跳轉。
直接訪問受保護的 API: 打開瀏覽器,直接在網址列輸入你的 Gateway 地址與後端路由,例如: http://localhost:8080/orders/user/1
因為由 gateway 轉發所以要加上 /api => http://localhost:8080/api/orders/user/1
8. 測試資料準備
9. 測試時常見的「坑」
9.1. 雖然 console email, SQL 有印,但 404
路徑匹配但資料為空 代碼返回 NOT_FOUND 導致
return orderRepository.findById(id)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
9.2. Redirect URI 不匹配
在瀏覽器輸入 127.0.0.1:8080,但 Google Console 設定的是 localhost:8080,這會噴錯。請確保兩者完全一致。
9.3. 如何「登出」?
因為瀏覽器會記錄 Session,如果你想換個帳號測試,可以訪問: http://localhost:8080/logout Spring Security 預設會提供一個簡單的登出確認頁面。