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 客户端。
因此,前端(通常是使用 Angular、React 或类似 UI 框架构建的 SPA 应用)可以代表终端户无缝访问这些应用上的数据。更重要的是:用户无需暴露自己的凭证。
3.2、Spring Cloud Gateway 作为 OAuth 2.0 资源服务器 {#32spring-cloud-gateway-作为-oauth-20-资源服务器}
在这里,网关充 "门卫" 的角色,在将每个请求发送到后端服务之前,都要确保该请求具有有效的 Access Token。此外,网关还能根据相关 Scope 检查 Token 是否具有访问特定资源的适当权限:
需要注意的是,这种权限检查主要是在粗粒度层面上进行的。细粒度访问控制(如对象/字段级权限)通常是在后端使用业务实现的。这种模式需要考虑的一个问题是,后端服务如何认证和授权任何转发请求。主要有两种情况:
- 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_customers
和 silver_customers
)以及两个角色(golden
和 silver
)。
现在,使用 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 的管理界面:
加几个用户来完成 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 如何获取用户角色的呢?毕竟,这不是像 scopes
或 email
那样的标准 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
检查网关日志,可以发现没有与请求转发相关的信息。这表明响应是在网关生成的。
- 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 的登录页面:
由于使用了 Maxwell Smart
的凭证,再次得到了一个更低的 price
:
测试结束时,使用匿名/隐身浏览器窗口,并使用 John Snow
的凭证测试该端点。
这次得到的是正常的 price
:
8、总结 {#8总结}
本文介绍了 OAuth 2.0 的一些认证、授权模式以及如何使用 Spring Cloud Gateway 实现这些模式。
参考:https://www.baeldung.com/spring-cloud-gateway-oauth2