OAuth 2.0 Resource Server With Spring Security 5 – 使用Spring Security 5的OAuth 2.0资源服务器

最后修改: 2020年 8月 12日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll learn how to set up an OAuth 2.0 resource server using Spring Security 5.

在本教程中,我们将学习如何使用Spring Security 5建立一个OAuth 2.0资源服务器

We’ll do this using JWTs, as well as opaque tokens, the two kinds of bearer tokens supported by Spring Security.

我们将使用JWT以及不透明令牌(Spring Security支持的两种不记名令牌)来实现这一点。

Before we jump in to the implementation and code samples, we’ll first establish some background.

在我们进入实现和代码示例之前,我们首先要建立一些背景。

2. A Little Background

2.一点背景

2.1. What Are JWTs and Opaque Tokens?

2.1.什么是JWTs和不透明令牌?

JWT, or JSON Web Token, is a way to transfer sensitive information securely in the widely-accepted JSON format. The contained information could be about the user, or about the token itself, such as its expiry and issuer.

JWT,即JSON网络令牌,是一种以广泛接受的JSON格式安全传输敏感信息的方式。所包含的信息可以是关于用户的,也可以是关于令牌本身的,例如它的到期日和发行者。

On the other hand, an opaque token, as the name suggests, is opaque in terms of the information it carries. The token is just an identifier that points to the information stored at the authorization server; it gets validated via introspection at the server’s end.

另一方面,不透明令牌,顾名思义,就其携带的信息而言是不透明的。令牌只是一个标识符,指向存储在授权服务器上的信息;它在服务器端通过自省得到验证。

2.2. What Is a Resource Server?

2.2.什么是资源服务器?

In the context of OAuth 2.0, a resource server is an application that protects resources via OAuth tokens. These tokens are issued by an authorization server, typically to a client application. The job of the resource server is to validate the token before serving a resource to the client.

在OAuth 2.0中,资源服务器是一个通过OAuth令牌保护资源的应用程序。这些令牌是由授权服务器发出的,通常是发给客户端应用程序。资源服务器的工作是在向客户端提供资源之前验证该令牌。

A token’s validity is determined by several things:

一个令牌的有效性是由几个方面决定的。

  • Did this token come from the configured authorization server?
  • Is it unexpired?
  • Is this resource server its intended audience?
  • Does the token have the required authority to access the requested resource?

To visualize this, let’s look at a sequence diagram for the authorization code flow, and see all the actors in action:

为了直观地了解这一点,让我们看一下授权代码流的序列图,看看所有的角色都在行动。

As we can see in step 8, when the client application calls the resource server’s API to access a protected resource, it first goes to the authorization server to validate the token contained in the request’s Authorization: Bearer header, and then responds to the client.

正如我们在步骤8中看到的,当客户端应用程序调用资源服务器的API来访问受保护资源时,它首先去授权服务器验证请求的Authorization:Bearer标头,然后响应客户端。

Step 9 is what we’ll focus on in this tutorial.

第9步是我们在本教程中的重点。

So now let’s jump into the code part. We’ll set up an authorization server using Keycloak, a resource server validating JWT tokens, another resource server validating opaque tokens, and a couple of JUnit tests to simulate client apps and verify responses.

所以现在让我们跳到代码部分。我们将使用Keycloak建立一个授权服务器,一个验证JWT令牌的资源服务器,另一个验证不透明令牌的资源服务器,以及几个JUnit测试来模拟客户端应用程序并验证响应。

3. Authorization Server

3.授权服务器

First, we’ll set up an authorization server, the thing that issues tokens.

首先,我们要建立一个授权服务器,也就是发行令牌的东西。

For this, we’ll use Keycloak embedded in a Spring Boot Application. Keycloak is an open-source identity and access management solution. Since we’re focusing on the resource server in this tutorial, we won’t delve any deeper into it.

为此,我们将使用嵌入Spring Boot应用程序的Keycloak。Keycloak是一个开源的身份和访问管理解决方案。由于我们在本教程中专注于资源服务器,所以我们不会对它进行更深入的研究。

Our embedded Keycloak Server has two clients defined, fooClient and barClient, corresponding to our two resource server applications.

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

4. Resource Server – Using JWTs

4.资源服务器 – 使用JWTs

Our resource server will have four main components:

我们的资源服务器将有四个主要组成部分。

  • Model – the resource to protect
  • API – a REST controller to expose the resource
  • Security Configuration – a class to define access control for the protected resource that the API exposes
  • application.yml – a config file to declare properties, including information about the authorization server

After we take a quick look at the dependencies, we’ll go through these components one by one for our resource server handling JWT tokens.

在我们快速浏览了依赖关系之后,我们将为我们处理JWT令牌的资源服务器逐一浏览这些组件。

4.1. Maven Dependencies

4.1.Maven的依赖性

Mainly, we’ll need the spring-boot-starter-oauth2-resource-server, Spring Boot’s starter for resource server support. This starter includes Spring Security by default, so we don’t need to add it explicitly:

主要是,我们需要spring-boot-starter-oauth2-resource-server,这是Spring Boot支持资源服务器的启动器。这个启动器默认包括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.12.0</version>
</dependency>

Apart from that, we’ll also add web support.

除此以外,我们还将增加网络支持。

For our demonstration purposes, we’ll generate resources randomly, instead of getting them from a database, with some help from Apache’s commons-lang3 library.

为了我们的演示目的,我们将随机生成资源,而不是从数据库中获取它们,这需要Apache的commons-lang3库的一些帮助。

4.2. Model

4.2.模型

Keeping it simple, we’ll use Foo, a POJO, as our protected resource:

为了保持简单,我们将使用Foo,一个POJO,作为我们的受保护资源。

public class Foo {
    private long id;
    private String name;
    
    // constructor, getters and setters
}

4.3. API

4.3.API

Here’s our rest controller to make Foo available for manipulation:

这里是我们的休息控制器,使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");
    }
}

As is evident, we have the provision to GET all Foos, GET a Foo by id, and POST a Foo.

显而易见,我们有规定来获取所有的Foos,通过id来获取一个Foo,以及POST一个Foo

4.4. Security Configuration

4.4.安全配置

In this configuration class, we’ll define access levels for our resource:

在这个配置类中,我们将为我们的资源定义访问级别。

@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();
    }
}

Anyone with an access token having the read scope can get Foos. In order to POST a new Foo, their token should have a write scope.

任何拥有范围的访问令牌的人都可以获得Foos。为了发布一个新的Foo,他们的令牌应该有write范围。

Additionally, we’ll add a call to jwt() using the oauth2ResourceServer() DSL to indicate the type of tokens supported by our server here.

此外,我们将使用oauth2ResourceServer() DSL添加对jwt()的调用,以表明我们的服务器在这里支持的令牌类型

4.5. application.yml

4.5.application.yml

In the application properties, in addition to the usual port number and context-path, we need to define the path to our authorization server’s issuer URI so that the resource server can discover its provider configuration:

在应用程序属性中,除了通常的端口号和上下文路径外,我们需要定义我们的授权服务器的发行者URI的路径,以便资源服务器能够发现其供应商配置

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

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

The resource server uses this information to validate the JWT tokens coming in from the client application, as per Step 9 of our sequence diagram.

资源服务器使用这些信息来验证来自客户端应用程序的JWT令牌,正如我们的顺序图的第9步。

For this validation to work using the issuer-uri property, the authorization server must be up and running. Otherwise, the resource server won’t start.

为了使这个验证能够使用issuer-uri属性,授权服务器必须启动并运行。否则,资源服务器将不会启动。

If we need to start it independently, then we can supply the jwk-set-uri property instead to point to the authorization server’s endpoint exposing public keys:

如果我们需要独立启动它,那么我们可以提供jwk-set-uri属性来代替,以指向授权服务器暴露公钥的端点。

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

And that’s all we need to get our server to validate JWT tokens.

这就是我们需要让我们的服务器验证JWT令牌的全部内容。

4.6. Testing

4.6.测试

For testing, we’ll set up a JUnit. In order to execute this test, we need the authorization server, as well as the resource server, up and running.

为了测试,我们将设置一个JUnit。为了执行这个测试,我们需要授权服务器以及资源服务器启动并运行。

Let’s verify that we can get Foos from resource-server-jwt with a read scoped token in our test:

让我们验证一下,我们可以在测试中用read范围的令牌从resource-server-jwt获得Foos。

@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);
}

In the above code, at Line #3, we obtain an access token with a read scope from the authorization server, covering Steps 1 through 7 of our sequence diagram.

在上面的代码中,在第3行,我们从授权服务器获得了一个范围为read的访问令牌,涵盖了我们顺序图中的第1到7步。

Step 8 is performed by the RestAssured‘s get() call. Step 9 is performed by the resource server with the configurations we saw, and is transparent to us as users.

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

5. Resource Server – Using Opaque Tokens

5.资源服务器 – 使用不透明令牌

Next, let’s see the same components for our resource server handling opaque tokens.

接下来,让我们看看我们的资源服务器处理不透明令牌的相同组件。

5.1. Maven Dependencies

5.1.Maven的依赖性

To support opaque tokens, we’ll need the additional oauth2-oidc-sdk dependency:

为了支持不透明令牌,我们需要额外的oauth2-oidc-sdk依赖。

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

5.2. Model and Controller

5.2.模型和控制器

For this one, we’ll add a Bar resource:

对于这个,我们将添加一个Bar资源。

public class Bar {
    private long id;
    private String name;
    
    // constructor, getters and setters
}

We’ll also have a BarController, with endpoints similar to our FooController before, to dish out Bars.

我们还将有一个BarController,,其端点与之前的FooController相似,用于分配Bar

5.3. application.yml

5.3.application.yml

In the application.yml here, we’ll need to add an introspection-uri corresponding to our authorization server’s introspection endpoint. As mentioned before, this is how an opaque token gets validated:

在这里的application.yml中,我们需要添加一个introspection-uri,与我们的授权服务器的自省端点相对应。如前所述,不透明令牌就是这样被验证的:

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 Configuration

5.4.安全配置

Keeping access levels similar to that of Foo for the Bar resource as well, this configuration class also makes a call to opaqueToken() using the oauth2ResourceServer() DSL to indicate the use of the opaque token type:

对于Bar资源也保持与Foo类似的访问级别,该配置类还使用oauth2ResourceServer() DSL对opaqueToken()进行了调用,以表明使用不透明的令牌类型

@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();
    }
}

Here we’ll also specify the client credentials corresponding to the authorization server’s client that we’ll be using. We defined these earlier in our application.yml.

在这里我们还将指定与我们将使用的授权服务器的客户端相对应的客户端证书。我们在前面的application.yml中定义了这些。

5.5. Testing

5.5.测试

We’ll set up a JUnit for our opaque token-based resource server, similar to what we did for the JWT one.

我们将为我们的基于不透明令牌的资源服务器设置一个JUnit,与我们为JWT所做的类似。

In this case, we’ll check if a write scoped access token can POST a Bar to resource-server-opaque:

在这种情况下,我们将检查一个write范围内的访问令牌是否可以将Bar发布到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());
}

If we get a status of CREATED back, it means the resource server successfully validated the opaque token and created the Bar for us.

如果我们得到一个CREATED的状态,这意味着资源服务器成功地验证了不透明的令牌,并为我们创建了Bar

6. Conclusion

6.结语

In this article, we learned how to configure a Spring Security based resource server application for validating JWTs, as well as opaque tokens.

在这篇文章中,我们学习了如何配置一个基于Spring Security的资源服务器应用程序,以验证JWT以及不透明的令牌。

As we saw, with minimal setup, Spring made it possible to seamlessly validate the tokens with an issuer and send resources to the requesting party (in our case, a JUnit test).

正如我们所看到的,通过最小的设置,Spring可以无缝地验证发行人的令牌,并将资源发送到请求方(在我们的案例中,是JUnit测试)。

As always, the source code is available over on GitHub.

一如既往,源代码可在GitHub上获得