跳转至

實現 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. 測試資料準備

用 Java 方式寫入測試資料

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 預設會提供一個簡單的登出確認頁面。