51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

Spring Cloud Gateway 和 Oauth2

1、概览 {#1概览}

Spring Cloud Gateway 是一个响应式的轻量级网关,是 Spring Cloud 体系中一个比较重要的组件。本文将带你了解如何在其基础上快速实现 OAuth 2.0 认证、授权。

2、OAuth 2.0 快速回顾 {#2oauth-20-快速回顾}

OAuth 2.0 标准是一个成熟的标准,在互联网上广泛使用,是用户和应用安全访问资源的一种安全机制。

其中涉及的关键术语如下:

  • Resource(资源):只有经过授权的客户端才能检索的任何类型的信息。
  • Client(客户端):消费资源的应用,通常通过 REST API 消费资源。
  • Resource Server(资源服务器):负责向授权客户端提供资源的服务。
  • Resource Owner(资源所有者):实体(人或应用),拥有资源,并最终负责向客户端授予对该资源的访问权限。
  • Token(令牌):客户端获取的一段信息,并作为请求的一部分发送给资源服务器以进行身份验证。
  • Identity Provider(身份提供商,即 IDP):验证用户凭证并向客户端发放 Access Token。
  • Authentication Flow(认证模式/流程):客户端获得有效 Token 必须经过的一系列步骤。

你可以通过 Auth0 的相关文档了解更多详细内容。

3、OAuth 2.0 模式 {#3oauth-20-模式}

Spring Cloud Gateway 主要用于以下用途之一:

  • OAuth Client(客户端)
  • OAuth Resource Server(资源服务器)

我们来逐个了解。

3.1、Spring Cloud Gateway 作为 OAuth 2.0 客户端 {#31spring-cloud-gateway-作为-oauth-20-客户端}

在这种情况下,任何未经身份认证的传入请求都将触发授权码流程。一旦网关获取到 Token,它将在向后端服务发起请求时使用该 Token。

OAuth 2.0 授权码模式

在实际应用中,一个很好的例子是聚合了 "社交应用" 的应用:对于每个支持的社交应用,网关将充当 OAuth 2.0 客户端。

因此,前端(通常是使用 Angular、React 或类似 UI 框架构建的 SPA 应用)可以代表终端户无缝访问这些应用上的数据。更重要的是:用户无需暴露自己的凭证。

3.2、Spring Cloud Gateway 作为 OAuth 2.0 资源服务器 {#32spring-cloud-gateway-作为-oauth-20-资源服务器}

在这里,网关充 "门卫" 的角色,在将每个请求发送到后端服务之前,都要确保该请求具有有效的 Access Token。此外,网关还能根据相关 Scope 检查 Token 是否具有访问特定资源的适当权限:

Spring Cloud Gateway 作为资源服务器

需要注意的是,这种权限检查主要是在粗粒度层面上进行的。细粒度访问控制(如对象/字段级权限)通常是在后端使用业务实现的。这种模式需要考虑的一个问题是,后端服务如何认证和授权任何转发请求。主要有两种情况:

  • Token 传播:API 网关将收到的 Token 原封不动地转发到后端。
  • Token 替换:在发送请求前,API Gateway 会用另一个 Token 替换收到的 Token。

本文只介绍 "Token 传播" 的示例,这是最常见的情况。

4、示例项目概览 {#4示例项目概览}

构建一个 "报价" 的示例项目,展示如何使用 Spring Cloud Gateway 与上述的 OAuth 模式。

该项目暴露一个端点:/quotes/{symbol}。访问该端点需要一个由配置的 "Identity Provider" 签发的有效 Access Token。

在本例中,使用嵌入式 Keycloak Identity Provider,添加一个新的客户端应用和几个用户进行测试。

后台服务会根据与请求相关联的用户返回不同的报价。拥有黄金角色(Gold Role)的用户会得到较低的价格,而其他人则会得到正常价格。

使用 Spring Cloud Gateway 来作为这个服务的前端,并且只需更改几行配置,就能够将其角色从 OAuth 客户端切换为资源服务器。

5、项目设置 {#5项目设置}

5.1、Keycloak IdP {#51keycloak-idp}

本文中使用的嵌入式 Keycloak 只是一个普通的 Spring Boot 应用。

GitHub Clone 并使用 Maven 构建它:

$ git clone https://github.com/Baeldung/spring-security-oauth
$ cd oauth-rest/oauth-authorization/server
$ mvn install

注:本项目目前使用 Java 13+ 为构建版本,但在 Java 11 下也能正常构建和运行。只需在 Maven 命令中添加 -Djava.version=11 即可。

接下来,把 src/main/resources/baeldung-domain.json 替换为 这个版本。修改后的版本具有与原始版本相同的配置,并增加了一个客户端应用(quotes-client)、两个用户组(golden_customerssilver_customers)以及两个角色(goldensilver)。

现在,使用 spring-boot:run maven 插件启动服务器:

$ mvn spring-boot:run
... many, many log messages omitted
2022-01-16 10:23:20.318
  INFO 8108 --- [           main] c.baeldung.auth.AuthorizationServerApp   : Started AuthorizationServerApp in 23.815 seconds (JVM running for 24.488)
2022-01-16 10:23:20.334
  INFO 8108 --- [           main] c.baeldung.auth.AuthorizationServerApp   : Embedded Keycloak started: http://localhost:8083/auth to use keycloak

服务器启动后,使用浏览器访问 http://localhost:8083/auth/admin/master/console/#/realms/baeldung 。使用管理员凭证(bael-admin / pass)登录后,将看到 Realm 的管理界面:

Keycloak Realm 管理界面

加几个用户来完成 IdP 设置。第一个用户是 Maxwell Smart,他是 golden_customers 组的成员。第二个用户是 John Snow,不属于任何组。

使用提供的配置,golden_customers 组的成员将自动获得 gold 角色。

5.2、后端服务 {#52后端服务}

后台需要常规的 Spring Boot Reactive MVC 依赖,外加 Resource Server Starter 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    <version>2.6.2</version>
</dependency>

当使用 Spring Boot 的 parent POM 或 dependency management 部分中的相应 BOM 时 可以省略依赖的版本号。这也是推荐的做法。

在 main 类,使用 @EnableWebFluxSecurity 启用 Web Flux Security:

@SpringBootApplication
@EnableWebFluxSecurity
public class QuotesApplication {    
    public static void main(String[] args) {
        SpringApplication.run(QuotesApplication.class);
    }
}

端点使用 BearerAuthenticationToken 来检查当前用户是否拥有 gold 角色:

@RestController
public class QuoteApi {
    private static final GrantedAuthority GOLD_CUSTOMER = new SimpleGrantedAuthority("gold");

    @GetMapping("/quotes/{symbol}")
    public Mono<Quote> getQuote(@PathVariable("symbol") String symbol,
      BearerTokenAuthentication auth ) {
        
        Quote q = new Quote();
        q.setSymbol(symbol);        
        if ( auth.getAuthorities().contains(GOLD_CUSTOMER)) {
            q.setPrice(10.0);
        }
        else {
            q.setPrice(12.0);
        }
        return Mono.just(q);
    }
}

现在,Spring 如何获取用户角色的呢?毕竟,这不是像 scopesemail 那样的标准 Claim。实际上,这里没有什么魔法:我们必须提供一个自定义的 ReactiveOpaqueTokenIntrospection,从 Keycloak 返回的自定义字段中提取这些角色。这个可在线获取的 Bean 基本上与 Spring 关于 这个主题的文档 中所示的相同,只是针对我们的自定义字段进行了一些细微的更改。

还必须提供访问 Identity Provider 所需的配置属性:

spring.security.oauth2.resourceserver.opaquetoken.introspection-uri=http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect
spring.security.oauth2.resourceserver.opaquetoken.client-id=quotes-client
spring.security.oauth2.resourceserver.opaquetoken.client-secret=<CLIENT SECRET>

最后,可以将其导入 IDE 或从 Maven 中运行。为此,项目的 POM 中包含一个 Profile:

$ mvn spring-boot:run -Pquotes-application

现在,应用在 http://localhost:8085/quotes 上为请求提供服务。可以使用 curl 检查它是否正常响应:

$ curl -v http://localhost:8085/quotes/BAEL

不出所料,收到了 "401 Unauthorized" 响应,因为没有发送 Authorization Header。

6、Spring Cloud Gateway 作为 OAuth 2.0 资源服务器 {#6spring-cloud-gateway-作为-oauth-20-资源服务器}

作为资源服务器的 Spring Cloud Gateway 应用的安全配置与普通资源服务并无不同。

因此,添加与后端服务相同的 Starter 依赖关系:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    <version>3.1.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    <version>2.6.2</version>
</dependency>

在启动类中添加 @EnableWebFluxSecurity

@SpringBootApplication
@EnableWebFluxSecurity
public class ResourceServerGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ResourceServerGatewayApplication.class,args);
    }
}

与安全相关的配置属性与后台使用的相同:

spring:
  security:
    oauth2:
      resourceserver:
        opaquetoken:
          introspection-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect
          client-id: quotes-client
          client-secret: <code class="language-css"><CLIENT SECRET>

接下来,只需添加路由声明即可:

  # ... 其他配置忽略
  cloud:
    gateway:
      routes:
      - id: quotes
        uri: http://localhost:8085
        predicates:
        - Path=/quotes/**

关于路由的详细用法,可以参阅 中文文档

注意,除了 Security 依赖和 properties 外,我们没有更改网关本身的任何内容。

使用 spring-boot:run 运行网关程序,并指定带有所需设置的特定 Profile:

$ mvn spring-boot:run -Pgateway-as-resource-server

6.1、测试资源服务器 {#61测试资源服务器}

首先,必须确保 Keycloak、后台服务和网关都在运行。

接下来,需要从 Keycloak 获取 Access Token。在这种情况下,最直接的方法就是使用 "密码授权模式"(又称 "资源所有者")。这意味着向 Keycloak 发送 POST 请求,传递其中一个用户的用户名/密码,以及客户端 ID 和客户端应用的 Secret:

$ curl -L -X POST \
  'http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'client_id=quotes-client' \
  --data-urlencode 'client_secret=0e082231-a70d-48e8-b8a5-fbfb743041b6' \
  --data-urlencode 'grant_type=password' \
  --data-urlencode 'scope=email roles profile' \
  --data-urlencode 'username=john.snow' \
  --data-urlencode 'password=1234'

响应将是一个 JSON 对象,其中包含 Access Token 和其他值:

{
    "access_token": "...omitted",
    "expires_in": 300,
    "refresh_expires_in": 1800,
    "refresh_token": "...omitted",
    "token_type": "bearer",
    "not-before-policy": 0,
    "session_state": "7fd04839-fab1-46a7-a179-a2705dab8c6b",
    "scope": "profile email"
}

现在,可以使用返回的 Access Token 访问 /quotes API:

$ curl --location --request GET 'http://localhost:8086/quotes/BAEL' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer xxxx...'

会响应一个 JSON 格式的 Quote

{
  "symbol":"BAEL",
  "price":12.0
}

重复这个过程,这次使用 Maxwell Smart 的 Access Token:

{
  "symbol":"BAEL",
  "price":10.0
}

如你所见,这次返回的 price 较低,这意味着后台能够正确识别相关用户。

还可以使用不带 Authorization Header 的 curl 请求,检查未经身份认证的请求是否会转发到后台:

$ curl  http://localhost:8086/quotes/BAEL

检查网关日志,可以发现没有与请求转发相关的信息。这表明响应是在网关生成的。

  1. Spring Cloud Gateway 作为 OAuth 2.0 客户端 {#7-spring-cloud-gateway-作为-oauth-20-客户端}

使用与资源服务器相同的启动类。

事实上,比较这两个版本,唯一明显的不同之处在于配置属性。在这里,需要使用 issuer-uri 属性或各种端点(authorization、token 和 introspection)的单独设置来配置 Identity Provider 的详细信息。

还需要定义应用客户端 registration 的详细信息,其中包括请求的 scope。这些 scope 会告知 IdP 哪些信息项将通过 introspection 机制提供:

  # 其他属性省略
  security:
    oauth2:
      client:
        provider:
          keycloak:
            issuer-uri: http://localhost:8083/auth/realms/baeldung
        registration:
          quotes-client:
            provider: keycloak
            client-id: quotes-client
            client-secret: <CLIENT SECRET>
            scope:
            - email
            - profile
            - roles

最后,路由定义部分有一个重要变化。必须在任何需要传播 Access Token 的路由中添加 TokenRelay Filter:

spring:
  cloud:
    gateway:
      routes:
      - id: quotes
        uri: http://localhost:8085
        predicates:
        - Path=/quotes/**
        filters:
        - TokenRelay=

或者,如果我们希望所有路由都启动授权模式,可以在默认过滤器(default-filters)部分添加 TokenRelay Filter:

spring:
  cloud:
    gateway:
      default-filters:
      - TokenRelay=
      routes:
  # 省略其他的路由定义

7.1、测试作为 OAuth 2.0 客户端的 Spring Cloud Gateway {#71测试作为-oauth-20-客户端的-spring-cloud-gateway}

在测试设置中,需要确保项目的三个部分都在运行。不过,这次使用不同的 Spring Profile 来运行网关,该 Profile 包含使网关成为 OAuth 2.0 客户端所需的属性。

示例项目的 POM 中包含一个 Profile,可以使用该 Profile 启动项目:

$ mvn spring-boot:run -Pgateway-as-oauth-client

网关运行后,使用浏览器访问 http://localhost:8087/quotes/BAEL 进行测试。如果一切正常,你将被重定向到 IdP 的登录页面:

Keycloak 登录页

由于使用了 Maxwell Smart 的凭证,再次得到了一个更低的 price

服务响应的数据,price 更低

测试结束时,使用匿名/隐身浏览器窗口,并使用 John Snow 的凭证测试该端点。

这次得到的是正常的 price

服务响应的数据,price 正常

8、总结 {#8总结}

本文介绍了 OAuth 2.0 的一些认证、授权模式以及如何使用 Spring Cloud Gateway 实现这些模式。


参考:https://www.baeldung.com/spring-cloud-gateway-oauth2

赞(1)
未经允许不得转载:工具盒子 » Spring Cloud Gateway 和 Oauth2