51工具盒子

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

使用 Spring Security 构建 OAuth 2.0 资源服务器

1、概览 {#1概览}

本文将带你了解如何使用 Spring Security 构建 OAuth 2.0 资源服务器(使用 JWT 和 Opaque Token,这两种由 Spring Security 支持的 Bearer Token)。

2、背景介绍 {#2背景介绍}

2.1、JWT 和 Opaque Token 是什么? {#21jwt-和-opaque-token-是什么}

JWT 或 JSON Web Token 是一种以广泛接受的 JSON 格式安全传输敏感信息的方式。其中包含的信息可能是关于用户的,也可能是关于 Token 本身的,例如其 expiry (有效期)和 issuer(签发者)。

Opaque Token 顾名思义,它所携带的信息是不透明的。Token 只是一个标识符,指向存储在授权服务器上的信息;它通过服务器端的自省(Introspection)进行验证。

2.2、资源服务器是什么? {#22资源服务器是什么}

在 OAuth 2.0 中,资源服务器是通过 OAuth Token 保护资源的应用。这些 Token 由授权服务器(通常是客户端应用)签发。资源服务器的工作是在向客户端提供资源之前验证 Token。

令牌的有效性由几个因素决定:

  • 该令牌是否来自配置的授权服务器?
  • 是否未过期?
  • 该资源是否为预期受众(audience)提供服务?
  • Token 是否具有访问所请求资源的必要权限?

来看一下 授权码模式 的顺序图,并观察所有相关的参与者:

Oauth2 授权码模式流程图

正如在步骤 8 中看到的,当客户端应用调用资源服务器的 API 访问受保护的资源时,它首先会转到授权服务器,以验证请求的 Authorization: Bearer Header 信息中包含的 Token,然后响应客户端。

本文的重点是第 9 步。

现在让进入代码实践部分。设置一个使用 Keycloak 的授权服务器、一个验证 JWT Token 的资源服务器、另一个验证 Opaque Token 的资源服务器,以及几个模拟客户端应用和验证响应的 JUnit 测试。

3、授权服务器 {#3授权服务器}

首先,创建一个授权服务器,也就是发放 Token 的服务器。

为此,我们直接在 Spring Boot 应用中嵌入 Keycloak。Keycloak 是一个开源身份和访问管理解决方案。由于本问的重点是资源服务器,这里不再深入。

嵌入式 Keycloak 服务器定义了两个客户端:fooClientbarClient,分别对应我们的两个资源服务器应用。

4、资源服务器 - 使用 JWT {#4资源服务器---使用-jwt}

资源服务器由四个主要部分组成:

  • Model:需要保护的资源
  • API:用于公开资源的 REST Controller
  • Security Configuration:用于定义 API 公开的受保护资源的访问控制的类
  • application.yml:用于声明属性的配置文件,包括有关授权服务器的信息

4.1、Maven 依赖 {#41maven-依赖}

添加 Spring Boot 的资源服务器支持 spring-boot-starter-oauth2-resource-server 依赖,该 Starter 默认包含了 Spring Security:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
    <version>2.7.5</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.13.0</version>
</dependency>

除此之外,还添加了 Web 支持。

出于演示目的,这里使用 Apache 的 commons-lang3 库随机生成资源。

4.2、Model {#42model}

使用 Foo POJO 作为受保护资源:

public class Foo {
    private long id;
    private String name;
    
    // 构造函数、get、set 方法省略
}

4.3. API {#43-api}

接着是 Rest Controller,可以对 Foo 进行操作:

@RestController
@RequestMapping(value = "/foos")
public class FooController {

    @GetMapping(value = "/{id}")
    public Foo findOne(@PathVariable Long id) {
        return new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));
    }

    @GetMapping
    public List findAll() {
        List fooList = new ArrayList();
        fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
        fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
        fooList.add(new Foo(Long.parseLong(randomNumeric(2)), randomAlphabetic(4)));
        return fooList;
    }

    @ResponseStatus(HttpStatus.CREATED)
    @PostMapping
    public void create(@RequestBody Foo newFoo) {
        logger.info("Foo created");
    }
}

如上,该 Controller 提供了 GET 所有 Foo、按 ID GET 一个 Foo 和 POST 一个 Foo 的功能。

4.4、Security 配置 {#44security-配置}

在配置类中定义资源的访问级别:

@Configuration
public class JWTSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(authz -> authz.antMatchers(HttpMethod.GET, "/foos/**")
            .hasAuthority("SCOPE_read")
            .antMatchers(HttpMethod.POST, "/foos")
            .hasAuthority("SCOPE_write")
            .anyRequest()
            .authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.jwt());
        return http.build();
    }
}

任何拥有 "具有 read scope 的 Access Token" 的人都可以 GET Foo。要 POST 一个新的 Foo,其 Token 必须具有 write scope。

还使用 oauth2ResourceServer() DSL 添加对 jwt() 的调用,以指明服务器支持的令牌类型。

4.5、application.yml {#45applicationyml}

在 application properties 中,除了常用的端口号和上下文路径外,还需要定义授权服务器的 issuer URI 路径,以便资源服务器发现其 Provider 配置

server: 
  port: 8081
  servlet: 
    context-path: /resource-server-jwt

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://localhost:8083/auth/realms/baeldung

资源服务器使用这些信息来验证从客户端应用输入的 JWT Token,如序列图中的步骤 9 所示。

要使用 issuer-uri 属性进行验证,授权服务器必须启动并运行。否则,资源服务器将无法启动。

如果需要独立启动它,则可以提供 jwk-set-uri 属性来指向授权服务器的端点,以公开公钥:

jwk-set-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

4.6、测试 {#46测试}

创建 JUnit 测试。启动并运行授权服务器和资源服务器。

测试,是否能通过具有 read scope 的 Token 从 resource-server-jwt GET Foo

@Test
public void givenUserWithReadScope_whenGetFooResource_thenSuccess() {
    String accessToken = obtainAccessToken("read");

    Response response = RestAssured.given()
      .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
      .get("http://localhost:8081/resource-server-jwt/foos");
    assertThat(response.as(List.class)).hasSizeGreaterThan(0);
}

在上述代码的第 3 行,从授权服务器获取了一个具有 read scope 的 Access Token,涵盖了序列图中的第 1 步到第 7 步。

第 8 步由 RestAssuredget() 调用执行。第 9 步由资源服务器根据配置执行,对用户来说是透明的。

5、资源服务器 - 使用 Opaque Token {#5资源服务器---使用-opaque-token}

接着,来看看处理 Opaque Token 的资源服务器的相同组件。

5.1、Maven 依赖 {#51maven-依赖}

要支持 Opaque Token,需要额外的 oauth2-oidc-sdk 依赖:

<dependency>
    <groupId>com.nimbusds</groupId>
    <artifactId>oauth2-oidc-sdk</artifactId>
    <version>8.19</version>
    <scope>runtime</scope>
</dependency>

5.2、Model 和 Controller {#52model-和-controller}

添加一个 Bar 资源:

public class Bar {
    private long id;
    private String name;
    
    // 构造函数、get、set 省略
}

还有一个 BarController,其端点与之前的 FooController 类似,用于提供对 Bar 的 GET 和 POST 操作。

5.3、application.yml {#53applicationyml}

application.yml 中,需要添加与授权服务器自省端点相对应的 introspection-uri。如前所述,这就是验证 Opaque Token 的方式:

server: 
  port: 8082
  servlet: 
    context-path: /resource-server-opaque

spring:
  security:
    oauth2:
      resourceserver:
        opaque:
          introspection-uri: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/token/introspect
          introspection-client-id: barClient
          introspection-client-secret: barClientSecret

5.4、Security 配置 {#54security-配置}

Bar 资源的访问级别也与 Foo 类似。该配置类还使用 oauth2ResourceServer() DSL 调用 opaqueToken(),以指示使用 Opaque Token 类型:

@Configuration
public class OpaqueSecurityConfig {

    @Value("${spring.security.oauth2.resourceserver.opaque.introspection-uri}")
    String introspectionUri;

    @Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-id}")
    String clientId;

    @Value("${spring.security.oauth2.resourceserver.opaque.introspection-client-secret}")
    String clientSecret;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(authz -> authz.antMatchers(HttpMethod.GET, "/bars/**")
            .hasAuthority("SCOPE_read")
            .antMatchers(HttpMethod.POST, "/bars")
            .hasAuthority("SCOPE_write")
            .anyRequest()
            .authenticated())
            .oauth2ResourceServer(oauth2 -> oauth2.opaqueToken
                (token -> token.introspectionUri(this.introspectionUri)
                .introspectionClientCredentials(this.clientId, this.clientSecret)));
        return http.build();
    }
}

还指定了授权服务器客户端对应的客户端凭证。之前在application.yml 中定义了这些凭证。

5.5、测试 {#55测试}

和 JWT 一样,通过 Junit 来测试使用 Opaque Token 的资源服务器。

测试,具有 write scope 的 Access Token 是否能将 Bar POST 到 resource-server-opaque

@Test
public void givenUserWithWriteScope_whenPostNewBarResource_thenCreated() {
    String accessToken = obtainAccessToken("read write");
    Bar newBar = new Bar(Long.parseLong(randomNumeric(2)), randomAlphabetic(4));

    Response response = RestAssured.given()
      .contentType(ContentType.JSON)
      .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
      .body(newBar)
      .log()
      .all()
      .post("http://localhost:8082/resource-server-opaque/bars");
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED.value());
}

如果响应的状态是 201 Created ,意味着资源服务器成功地验证了 Opaque Token,并成功地创建了 Bar 资源。

6、总结 {#6总结}

本文介绍了如何配置基于 Spring Security 的 Oauth2 资源服务器应用,以验证 JWT 和 Opaque Token。


Ref:https://www.baeldung.com/spring-security-oauth-resource-server

赞(1)
未经允许不得转载:工具盒子 » 使用 Spring Security 构建 OAuth 2.0 资源服务器