Testing OAuth 2.0

对于 OAuth 2.0 特定场景,Spring Security 提供测试支持,简化了授权流模拟,从而避免了与授权服务器进行烦琐的交互。

例如,您可以使用 mockOidcLogin() 模拟由 OidcUser 实例表示的 OIDC 登录,该实例包含声明和已授予的权限。同样,您可以使用 mockOAuth2Login() 模拟由 OAuth2User 实例表示的 OAuth 2.0 登录,该实例包含属性和已授予的权限。

此外,Spring Security 还提供了用于模拟 OAuth 2.0 客户端的 mockOAuth2Client(),以及用于模拟 JWT 和不透明令牌身份验证的 mockJwt() 和 mockOpaqueToken()。

这些测试支持方法允许您直接配置必需的属性,例如权限、声明和主体,从而简化了测试流程并使您能够专注于授权逻辑,而无需处理授权服务器的复杂性。

对于 OAuth 2.0,the same principles covered earlier still apply:最终,这取决于你的测试方法期望在 `SecurityContextHolder`中有什么。

When it comes to OAuth 2.0, the same principles covered earlier still apply: Ultimately, it depends on what your method under test is expecting to be in the SecurityContextHolder.

考虑以下控制器示例:

Consider the following example of a controller:

  • Java

  • Kotlin

@GetMapping("/endpoint")
public Mono<String> foo(Principal user) {
    return Mono.just(user.getName());
}
@GetMapping("/endpoint")
fun foo(user: Principal): Mono<String> {
    return Mono.just(user.name)
}

它没有什么是 OAuth2 特定的,因此你可以 xref:reactive/test/method.adoc#test-erms[use @WithMockUser 并可以正常运行。

Nothing about it is OAuth2-specific, so you can use @WithMockUser and be fine.

但是,如果控制器绑定到 Spring Security OAuth 2.0 支持的某些方面,请考虑这种情况:

However, consider a case where your controller is bound to some aspect of Spring Security’s OAuth 2.0 support:

  • Java

  • Kotlin

@GetMapping("/endpoint")
public Mono<String> foo(@AuthenticationPrincipal OidcUser user) {
    return Mono.just(user.getIdToken().getSubject());
}
@GetMapping("/endpoint")
fun foo(@AuthenticationPrincipal user: OidcUser): Mono<String> {
    return Mono.just(user.idToken.subject)
}

在这种情况下,Spring Security 的测试支持很方便。

In that case, Spring Security’s test support is handy.

Testing OIDC Login

使用 WebTestClient 测试 preceding section 中所示的方法需要使用授权服务器模拟某种类型的授权流。这是一项艰巨的任务,这就是 Spring Security 附带支持以移除此样板代码的原因。

Testing the method shown in the webflux-testing-oauth2 with WebTestClient requires simulating some kind of grant flow with an authorization server. This is a daunting task, which is why Spring Security ships with support for removing this boilerplate.

例如,我们可以使用 SecurityMockServerConfigurers#oidcLogin 方法告诉 Spring Security 包含一个默认 OidcUser

For example, we can tell Spring Security to include a default OidcUser by using the SecurityMockServerConfigurers#oidcLogin method:

  • Java

  • Kotlin

client
    .mutateWith(mockOidcLogin()).get().uri("/endpoint").exchange();
client
    .mutateWith(mockOidcLogin())
    .get().uri("/endpoint")
    .exchange()

该行将关联的 MockServerRequest 配置为 OidcUser,其中包括一个简单的 OidcIdToken、一个 OidcUserInfo 和一个已授予权限的 Collection

That line configures the associated MockServerRequest with an OidcUser that includes a simple OidcIdToken, an OidcUserInfo, and a Collection of granted authorities.

具体来说,它包括一个 OidcIdToken,其中 sub 声明设置为 user

Specifically, it includes an OidcIdToken with a sub claim set to user:

  • Java

  • Kotlin

assertThat(user.getIdToken().getClaim("sub")).isEqualTo("user");
assertThat(user.idToken.getClaim<String>("sub")).isEqualTo("user")

它还包括一个没有设置声明的 OidcUserInfo

It also includes an OidcUserInfo with no claims set:

  • Java

  • Kotlin

assertThat(user.getUserInfo().getClaims()).isEmpty();
assertThat(user.userInfo.claims).isEmpty()

它还包括一个权限 Collection,其中只有一个权限 SCOPE_read

It also includes a Collection of authorities with just one authority, SCOPE_read:

  • Java

  • Kotlin

assertThat(user.getAuthorities()).hasSize(1);
assertThat(user.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_read"));
assertThat(user.authorities).hasSize(1)
assertThat(user.authorities).containsExactly(SimpleGrantedAuthority("SCOPE_read"))

Spring Security 确保 OidcUser 实例可用于 xref:servlet/integrations/mvc.adoc#mvc-authentication-principal[the @AuthenticationPrincipal 注释。

Spring Security makes sure that the OidcUser instance is available forthe @AuthenticationPrincipal annotation.

此外,它还将 OidcUser 链接到 OAuth2AuthorizedClient 的一个简单实例,并将其存储到模拟 ServerOAuth2AuthorizedClientRepository 中。如果您的测试 use the @RegisteredOAuth2AuthorizedClient annotation,这可能会派上用场。

Further, it also links the OidcUser to a simple instance of OAuth2AuthorizedClient that it deposits into a mock ServerOAuth2AuthorizedClientRepository. This can be handy if your tests webflux-testing-oauth2-client..

Configuring Authorities

在许多情况下,您的方法受过滤器或方法安全性保护,需要您的 Authentication 拥有某些已授予的权限以允许请求。

In many circumstances, your method is protected by filter or method security and needs your Authentication to have certain granted authorities to allow the request.

在这些情况下,您可以使用 authorities() 方法提供您需要的已授予权限:

In those cases, you can supply what granted authorities you need by using the authorities() method:

  • Java

  • Kotlin

client
    .mutateWith(mockOidcLogin()
        .authorities(new SimpleGrantedAuthority("SCOPE_message:read"))
    )
    .get().uri("/endpoint").exchange();
client
    .mutateWith(mockOidcLogin()
        .authorities(SimpleGrantedAuthority("SCOPE_message:read"))
    )
    .get().uri("/endpoint").exchange()

Configuring Claims

虽然 Spring Security 中默认授予权限,但在 OAuth 2.0 中我们也具有声明。

While granted authorities are common across all of Spring Security, we also have claims in the case of OAuth 2.0.

例如,假设您有一个 user_id 声明,用于表示您系统中的用户 ID。您可以在控制器中像下面这样访问它:

Suppose, for example, that you have a user_id claim that indicates the user’s ID in your system. You might access it as follows in a controller:

  • Java

  • Kotlin

@GetMapping("/endpoint")
public Mono<String> foo(@AuthenticationPrincipal OidcUser oidcUser) {
    String userId = oidcUser.getIdToken().getClaim("user_id");
    // ...
}
@GetMapping("/endpoint")
fun foo(@AuthenticationPrincipal oidcUser: OidcUser): Mono<String> {
    val userId = oidcUser.idToken.getClaim<String>("user_id")
    // ...
}

在这种情况下,您可以使用 idToken() 方法来指定声明:

In that case, you can specify that claim with the idToken() method:

  • Java

  • Kotlin

client
    .mutateWith(mockOidcLogin()
        .idToken(token -> token.claim("user_id", "1234"))
    )
    .get().uri("/endpoint").exchange();
client
    .mutateWith(mockOidcLogin()
        .idToken { token -> token.claim("user_id", "1234") }
    )
    .get().uri("/endpoint").exchange()

之所以这样做,是因为 OidcUserOidcIdToken 中收集其声明。

That works because OidcUser collects its claims from OidcIdToken.

Additional Configurations

另外,还有其他用于进一步配置身份验证的方法,具体取决于您的控制器期望的数据:

There are additional methods, too, for further configuring the authentication, depending on what data your controller expects:

  • userInfo(OidcUserInfo.Builder): Configures the OidcUserInfo instance

  • clientRegistration(ClientRegistration): Configures the associated OAuth2AuthorizedClient with a given ClientRegistration

  • oidcUser(OidcUser): Configures the complete OidcUser instance

如果您具有以下情况,最后的那个将非常方便:* 自己的 OidcUser 实现,或者* 需要更改名称属性

That last one is handy if you: * Have your own implementation of OidcUser or * Need to change the name attribute

例如,假设您的授权服务器在 user_name 声明中发送负责人姓名,而不是在 sub 声明中。在这种情况下,您可以手动配置 OidcUser

For example, suppose that your authorization server sends the principal name in the user_name claim instead of the sub claim. In that case, you can configure an OidcUser by hand:

  • Java

  • Kotlin

OidcUser oidcUser = new DefaultOidcUser(
        AuthorityUtils.createAuthorityList("SCOPE_message:read"),
        OidcIdToken.withTokenValue("id-token").claim("user_name", "foo_user").build(),
        "user_name");

client
    .mutateWith(mockOidcLogin().oidcUser(oidcUser))
    .get().uri("/endpoint").exchange();
val oidcUser: OidcUser = DefaultOidcUser(
    AuthorityUtils.createAuthorityList("SCOPE_message:read"),
    OidcIdToken.withTokenValue("id-token").claim("user_name", "foo_user").build(),
    "user_name"
)

client
    .mutateWith(mockOidcLogin().oidcUser(oidcUser))
    .get().uri("/endpoint").exchange()

Testing OAuth 2.0 Login

testing OIDC login 一样,测试 OAuth 2.0 登录也会带来类似的挑战:模拟授权流。正因如此,Spring Security 还为非 OIDC 用例提供了测试支持。

As with webflux-testing-oidc-login, testing OAuth 2.0 Login presents a similar challenge: mocking a grant flow. Because of that, Spring Security also has test support for non-OIDC use cases.

假设我们有一个控制器,它以 OAuth2User 的身份获取登录用户:

Suppose that we have a controller that gets the logged-in user as an OAuth2User:

  • Java

  • Kotlin

@GetMapping("/endpoint")
public Mono<String> foo(@AuthenticationPrincipal OAuth2User oauth2User) {
    return Mono.just(oauth2User.getAttribute("sub"));
}
@GetMapping("/endpoint")
fun foo(@AuthenticationPrincipal oauth2User: OAuth2User): Mono<String> {
    return Mono.just(oauth2User.getAttribute("sub"))
}

在这种情况下,我们可以使用 SecurityMockServerConfigurers#oauth2User 方法告诉 Spring Security 包含一个默认 OAuth2User

In that case, we can tell Spring Security to include a default OAuth2User by using the SecurityMockServerConfigurers#oauth2User method:

  • Java

  • Kotlin

client
    .mutateWith(mockOAuth2Login())
    .get().uri("/endpoint").exchange();
client
    .mutateWith(mockOAuth2Login())
    .get().uri("/endpoint").exchange()

前面的示例会使用 OAuth2User 配置关联的 MockServerRequest,它包含 Map 个简单的属性和 Collection 个已授予的权限。

The preceding example configures the associated MockServerRequest with an OAuth2User that includes a simple Map of attributes and a Collection of granted authorities.

具体来说,它包含一个 Map,其键/值对为 sub/user

Specifically, it includes a Map with a key/value pair of sub/user:

  • Java

  • Kotlin

assertThat((String) user.getAttribute("sub")).isEqualTo("user");
assertThat(user.getAttribute<String>("sub")).isEqualTo("user")

它还包括一个权限 Collection,其中只有一个权限 SCOPE_read

It also includes a Collection of authorities with just one authority, SCOPE_read:

  • Java

  • Kotlin

assertThat(user.getAuthorities()).hasSize(1);
assertThat(user.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_read"));
assertThat(user.authorities).hasSize(1)
assertThat(user.authorities).containsExactly(SimpleGrantedAuthority("SCOPE_read"))

Spring Security 执行必要的工作以确保 OAuth2User 实例对于 xref:servlet/integrations/mvc.adoc#mvc-authentication-principal[the @AuthenticationPrincipal 注释可用。

Spring Security does the necessary work to make sure that the OAuth2User instance is available for the @AuthenticationPrincipal annotation.

另外,它还可以将 OAuth2User 链接到 OAuth2AuthorizedClient 的一个简单实例,并将该实例存储在模拟 ServerOAuth2AuthorizedClientRepository 中。如果您的测试 use the @RegisteredOAuth2AuthorizedClient annotation,这可能会非常方便。

Further, it also links that OAuth2User to a simple instance of OAuth2AuthorizedClient that it deposits in a mock ServerOAuth2AuthorizedClientRepository. This can be handy if your tests webflux-testing-oauth2-client.

Configuring Authorities

在许多情况下,您的方法受过滤器或方法安全性保护,需要您的 Authentication 拥有某些已授予的权限以允许请求。

In many circumstances, your method is protected by filter or method security and needs your Authentication to have certain granted authorities to allow the request.

在这种情况下,您可以使用 authorities() 方法提供您需要的已授予权限:

In this case, you can supply the granted authorities you need by using the authorities() method:

  • Java

  • Kotlin

client
    .mutateWith(mockOAuth2Login()
        .authorities(new SimpleGrantedAuthority("SCOPE_message:read"))
    )
    .get().uri("/endpoint").exchange();
client
    .mutateWith(mockOAuth2Login()
        .authorities(SimpleGrantedAuthority("SCOPE_message:read"))
    )
    .get().uri("/endpoint").exchange()

Configuring Claims

虽然 Spring Security 中默认授予权限,但在 OAuth 2.0 中我们也具有声明。

While granted authorities are quite common across all of Spring Security, we also have claims in the case of OAuth 2.0.

例如,假设您有一个 user_id 属性,用于表示您系统中的用户 ID。您可以在控制器中像下面这样访问它:

Suppose, for example, that you have a user_id attribute that indicates the user’s ID in your system. You might access it as follows in a controller:

  • Java

  • Kotlin

@GetMapping("/endpoint")
public Mono<String> foo(@AuthenticationPrincipal OAuth2User oauth2User) {
    String userId = oauth2User.getAttribute("user_id");
    // ...
}
@GetMapping("/endpoint")
fun foo(@AuthenticationPrincipal oauth2User: OAuth2User): Mono<String> {
    val userId = oauth2User.getAttribute<String>("user_id")
    // ...
}

在这种情况下,您可以使用 attributes() 方法来指定属性:

In that case, you can specify that attribute with the attributes() method:

  • Java

  • Kotlin

client
    .mutateWith(mockOAuth2Login()
        .attributes(attrs -> attrs.put("user_id", "1234"))
    )
    .get().uri("/endpoint").exchange();
client
    .mutateWith(mockOAuth2Login()
        .attributes { attrs -> attrs["user_id"] = "1234" }
    )
    .get().uri("/endpoint").exchange()

Additional Configurations

另外,还有其他用于进一步配置身份验证的方法,具体取决于您的控制器期望的数据:

There are additional methods, too, for further configuring the authentication, depending on what data your controller expects:

  • clientRegistration(ClientRegistration): Configures the associated OAuth2AuthorizedClient with a given ClientRegistration

  • oauth2User(OAuth2User): Configures the complete OAuth2User instance

如果您具有以下情况,最后的那个将非常方便:* 自己的 OAuth2User 实现,或者* 需要更改名称属性

That last one is handy if you: * Have your own implementation of OAuth2User or * Need to change the name attribute

例如,假设您的授权服务器在 user_name 声明中发送负责人姓名,而不是在 sub 声明中。在这种情况下,您可以手动配置 OAuth2User

For example, suppose that your authorization server sends the principal name in the user_name claim instead of the sub claim. In that case, you can configure an OAuth2User by hand:

  • Java

  • Kotlin

OAuth2User oauth2User = new DefaultOAuth2User(
        AuthorityUtils.createAuthorityList("SCOPE_message:read"),
        Collections.singletonMap("user_name", "foo_user"),
        "user_name");

client
    .mutateWith(mockOAuth2Login().oauth2User(oauth2User))
    .get().uri("/endpoint").exchange();
val oauth2User: OAuth2User = DefaultOAuth2User(
    AuthorityUtils.createAuthorityList("SCOPE_message:read"),
    mapOf(Pair("user_name", "foo_user")),
    "user_name"
)

client
    .mutateWith(mockOAuth2Login().oauth2User(oauth2User))
    .get().uri("/endpoint").exchange()

Testing OAuth 2.0 Clients

无论您的用户如何进行身份验证,您都可能有其他令牌和客户端注册,它们适用于您正在测试的请求。例如,您的控制器可能依赖于客户端凭据授权,以获取与用户完全无关的令牌:

Independent of how your user authenticates, you may have other tokens and client registrations that are in play for the request you are testing. For example, your controller may rely on the client credentials grant to get a token that is not associated with the user at all:

  • Java

  • Kotlin

@GetMapping("/endpoint")
public Mono<String> foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2AuthorizedClient authorizedClient) {
    return this.webClient.get()
        .attributes(oauth2AuthorizedClient(authorizedClient))
        .retrieve()
        .bodyToMono(String.class);
}
import org.springframework.web.reactive.function.client.bodyToMono

// ...

@GetMapping("/endpoint")
fun foo(@RegisteredOAuth2AuthorizedClient("my-app") authorizedClient: OAuth2AuthorizedClient?): Mono<String> {
    return this.webClient.get()
        .attributes(oauth2AuthorizedClient(authorizedClient))
        .retrieve()
        .bodyToMono()
}

使用 Gemini 模拟与授权服务器的握手可能会很麻烦。相反,你可以使用 SecurityMockServerConfigurers#oauth2Client 将一个 OAuth2AuthorizedClient 添加到一个模拟的 ServerOAuth2AuthorizedClientRepository

Simulating this handshake with the authorization server can be cumbersome. Instead, you can use SecurityMockServerConfigurers#oauth2Client to add a OAuth2AuthorizedClient to a mock ServerOAuth2AuthorizedClientRepository:

  • Java

  • Kotlin

client
    .mutateWith(mockOAuth2Client("my-app"))
    .get().uri("/endpoint").exchange();
client
    .mutateWith(mockOAuth2Client("my-app"))
    .get().uri("/endpoint").exchange()

这会创建一个拥有一个简单 ClientRegistration、一个 OAuth2AccessToken 和一个资源所有者名称的 OAuth2AuthorizedClient

This creates an OAuth2AuthorizedClient that has a simple ClientRegistration, a OAuth2AccessToken, and a resource owner name.

具体来说,它包含一个 ClientRegistration,其客户端 ID 为 test-client,客户端机密为 test-secret

Specifically, it includes a ClientRegistration with a client ID of test-client and a client secret of test-secret:

  • Java

  • Kotlin

assertThat(authorizedClient.getClientRegistration().getClientId()).isEqualTo("test-client");
assertThat(authorizedClient.getClientRegistration().getClientSecret()).isEqualTo("test-secret");
assertThat(authorizedClient.clientRegistration.clientId).isEqualTo("test-client")
assertThat(authorizedClient.clientRegistration.clientSecret).isEqualTo("test-secret")

它还包含一个资源所有者名称 user

It also includes a resource owner name of user:

  • Java

  • Kotlin

assertThat(authorizedClient.getPrincipalName()).isEqualTo("user");
assertThat(authorizedClient.principalName).isEqualTo("user")

它还包含一个 OAuth2AccessToken,含有一个范围 read

It also includes an OAuth2AccessToken with one scope, read:

  • Java

  • Kotlin

assertThat(authorizedClient.getAccessToken().getScopes()).hasSize(1);
assertThat(authorizedClient.getAccessToken().getScopes()).containsExactly("read");
assertThat(authorizedClient.accessToken.scopes).hasSize(1)
assertThat(authorizedClient.accessToken.scopes).containsExactly("read")

你之后可以使用控制器方法中的 @RegisteredOAuth2AuthorizedClient 照常检索客户端。

You can then retrieve the client as usual by using @RegisteredOAuth2AuthorizedClient in a controller method.

Configuring Scopes

在许多情况下,OAuth 2.0 访问令牌会带有一组范围。考虑一个控制器如何检查范围的示例:

In many circumstances, the OAuth 2.0 access token comes with a set of scopes. Consider the following example of how a controller can inspect the scopes:

  • Java

  • Kotlin

@GetMapping("/endpoint")
public Mono<String> foo(@RegisteredOAuth2AuthorizedClient("my-app") OAuth2AuthorizedClient authorizedClient) {
    Set<String> scopes = authorizedClient.getAccessToken().getScopes();
    if (scopes.contains("message:read")) {
        return this.webClient.get()
            .attributes(oauth2AuthorizedClient(authorizedClient))
            .retrieve()
            .bodyToMono(String.class);
    }
    // ...
}
import org.springframework.web.reactive.function.client.bodyToMono

// ...

@GetMapping("/endpoint")
fun foo(@RegisteredOAuth2AuthorizedClient("my-app") authorizedClient: OAuth2AuthorizedClient): Mono<String> {
    val scopes = authorizedClient.accessToken.scopes
    if (scopes.contains("message:read")) {
        return webClient.get()
            .attributes(oauth2AuthorizedClient(authorizedClient))
            .retrieve()
            .bodyToMono()
    }
    // ...
}

给定一个检查范围的控制器,你可以使用 accessToken() 方法配置范围:

Given a controller that inspects scopes, you can configure the scope by using the accessToken() method:

  • Java

  • Kotlin

client
    .mutateWith(mockOAuth2Client("my-app")
        .accessToken(new OAuth2AccessToken(BEARER, "token", null, null, Collections.singleton("message:read")))
    )
    .get().uri("/endpoint").exchange();
client
    .mutateWith(mockOAuth2Client("my-app")
        .accessToken(OAuth2AccessToken(BEARER, "token", null, null, setOf("message:read")))
)
.get().uri("/endpoint").exchange()

Additional Configurations

你还可以使用其他方法进一步配置认证,具体取决于你的控制器所期望的数据:

You can also use additional methods to further configure the authentication depending on what data your controller expects:

  • principalName(String); Configures the resource owner name

  • clientRegistration(Consumer<ClientRegistration.Builder>): Configures the associated ClientRegistration

  • clientRegistration(ClientRegistration): Configures the complete ClientRegistration

如果您想使用真实的 ClientRegistration,最后一个方法非常方便。

That last one is handy if you want to use a real ClientRegistration

例如,假设你想使用应用程序某个 ClientRegistration 的定义,如 application.yml 中指定的。

For example, suppose that you want to use one of your application’s ClientRegistration definitions, as specified in your application.yml.

在这种情况下,你的测试可以自动装配 ReactiveClientRegistrationRepository 并查找你的测试所需的定义:

In that case, your test can autowire the ReactiveClientRegistrationRepository and look up the one your test needs:

  • Java

  • Kotlin

@Autowired
ReactiveClientRegistrationRepository clientRegistrationRepository;

// ...

client
    .mutateWith(mockOAuth2Client()
        .clientRegistration(this.clientRegistrationRepository.findByRegistrationId("facebook").block())
    )
    .get().uri("/exchange").exchange();
@Autowired
lateinit var clientRegistrationRepository: ReactiveClientRegistrationRepository

// ...

client
    .mutateWith(mockOAuth2Client()
        .clientRegistration(this.clientRegistrationRepository.findByRegistrationId("facebook").block())
    )
    .get().uri("/exchange").exchange()

Testing JWT Authentication

要在资源服务器上进行授权请求,你需要一个持有者令牌。如果你的资源服务器针对 JWT 进行配置,则持有者令牌需要签名,然后按照 JWT 规范进行编码。这一切都可能相当令人生畏,尤其是当这不是你测试的重点时。

To make an authorized request on a resource server, you need a bearer token. If your resource server is configured for JWTs, the bearer token needs to be signed and then encoded according to the JWT specification. All of this can be quite daunting, especially when this is not the focus of your test.

幸运的是,有很多简单的方法可以帮你克服这个困难,让你的测试专注于授权而不用表现为持有者令牌。我们将在接下来的两个小节中了解其中的两个。

Fortunately, there are a number of simple ways in which you can overcome this difficulty and let your tests focus on authorization and not on representing bearer tokens. We look at two of them in the next two subsections.

mockJwt() WebTestClientConfigurer

第一个方法是使用 WebTestClientConfigurer。最简单的方法是像以下所示那样使用 SecurityMockServerConfigurers#mockJwt 方法:

The first way is with a WebTestClientConfigurer. The simplest of these would be to use the SecurityMockServerConfigurers#mockJwt method like the following:

  • Java

  • Kotlin

client
    .mutateWith(mockJwt()).get().uri("/endpoint").exchange();
client
    .mutateWith(mockJwt()).get().uri("/endpoint").exchange()

此示例创建一个模拟 Jwt,并通过任何认证 API 传递它,以便你的授权机制可以验证它。

This example creates a mock Jwt and passes it through any authentication APIs so that it is available for your authorization mechanisms to verify.

默认情况下,它创建的 JWT 具有以下特征:

By default, the JWT that it creates has the following characteristics:

{
  "headers" : { "alg" : "none" },
  "claims" : {
    "sub" : "user",
    "scope" : "read"
  }
}

如果经过测试,结果 Jwt 将以下面的方式传递:

The resulting Jwt, were it tested, would pass in the following way:

  • Java

  • Kotlin

assertThat(jwt.getTokenValue()).isEqualTo("token");
assertThat(jwt.getHeaders().get("alg")).isEqualTo("none");
assertThat(jwt.getSubject()).isEqualTo("sub");
assertThat(jwt.tokenValue).isEqualTo("token")
assertThat(jwt.headers["alg"]).isEqualTo("none")
assertThat(jwt.subject).isEqualTo("sub")

请注意,你可以配置这些值。

Note that you configure these values.

你还可以使用相应的方法来配置任何标头或声明:

You can also configure any headers or claims with their corresponding methods:

  • Java

  • Kotlin

client
	.mutateWith(mockJwt().jwt(jwt -> jwt.header("kid", "one")
		.claim("iss", "https://idp.example.org")))
	.get().uri("/endpoint").exchange();
client
    .mutateWith(mockJwt().jwt { jwt -> jwt.header("kid", "one")
        .claim("iss", "https://idp.example.org")
    })
    .get().uri("/endpoint").exchange()
  • Java

  • Kotlin

client
	.mutateWith(mockJwt().jwt(jwt -> jwt.claims(claims -> claims.remove("scope"))))
	.get().uri("/endpoint").exchange();
client
    .mutateWith(mockJwt().jwt { jwt ->
        jwt.claims { claims -> claims.remove("scope") }
    })
    .get().uri("/endpoint").exchange()

在此,scopescp 声明和在正常的承载令牌请求中处理它们的方式相同。但是,只要提供测试所需的 GrantedAuthority 实例列表,就可以简单地覆盖它:

The scope and scp claims are processed the same way here as they are in a normal bearer token request. However, this can be overridden simply by providing the list of GrantedAuthority instances that you need for your test:

  • Java

  • Kotlin

client
	.mutateWith(mockJwt().authorities(new SimpleGrantedAuthority("SCOPE_messages")))
	.get().uri("/endpoint").exchange();
client
    .mutateWith(mockJwt().authorities(SimpleGrantedAuthority("SCOPE_messages")))
    .get().uri("/endpoint").exchange()

或者,如果你有一个自定义 JwtCollection<GrantedAuthority> 转换器,你也可以使用它来派生权限:

Alternatively, if you have a custom Jwt to Collection<GrantedAuthority> converter, you can also use that to derive the authorities:

  • Java

  • Kotlin

client
	.mutateWith(mockJwt().authorities(new MyConverter()))
	.get().uri("/endpoint").exchange();
client
    .mutateWith(mockJwt().authorities(MyConverter()))
    .get().uri("/endpoint").exchange()

您还可以指定一个完整的 Jwt, 对于它,{security-api-url}org/springframework/security/oauth2/jwt/Jwt.Builder.html[Jwt.Builder] 非常方便:

You can also specify a complete Jwt, for which {security-api-url}org/springframework/security/oauth2/jwt/Jwt.Builder.html[Jwt.Builder] is quite handy:

  • Java

  • Kotlin

Jwt jwt = Jwt.withTokenValue("token")
    .header("alg", "none")
    .claim("sub", "user")
    .claim("scope", "read")
    .build();

client
	.mutateWith(mockJwt().jwt(jwt))
	.get().uri("/endpoint").exchange();
val jwt: Jwt = Jwt.withTokenValue("token")
    .header("alg", "none")
    .claim("sub", "user")
    .claim("scope", "read")
    .build()

client
    .mutateWith(mockJwt().jwt(jwt))
    .get().uri("/endpoint").exchange()

authentication() and WebTestClientConfigurer

第二种方法是使用 authentication() Mutator。你可以实例化你自己的 JwtAuthenticationToken 并将其提供给你的测试:

The second way is by using the authentication() Mutator. You can instantiate your own JwtAuthenticationToken and provide it in your test:

  • Java

  • Kotlin

Jwt jwt = Jwt.withTokenValue("token")
    .header("alg", "none")
    .claim("sub", "user")
    .build();
Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("SCOPE_read");
JwtAuthenticationToken token = new JwtAuthenticationToken(jwt, authorities);

client
	.mutateWith(mockAuthentication(token))
	.get().uri("/endpoint").exchange();
val jwt = Jwt.withTokenValue("token")
    .header("alg", "none")
    .claim("sub", "user")
    .build()
val authorities: Collection<GrantedAuthority> = AuthorityUtils.createAuthorityList("SCOPE_read")
val token = JwtAuthenticationToken(jwt, authorities)

client
    .mutateWith(mockAuthentication<JwtMutator>(token))
    .get().uri("/endpoint").exchange()

请注意,作为替代方案,你还可以用 @MockBean 注解来模拟 ReactiveJwtDecoder bean 本身。

Note that, as an alternative to these, you can also mock the ReactiveJwtDecoder bean itself with a @MockBean annotation.

Testing Opaque Token Authentication

JWTs 类似,不透明令牌需要一个授权服务器来验证其有效性,这使得测试变得更加困难。为了帮助解决这个问题,Spring Security 为不透明令牌提供了测试支持。

Similar to webflux-testing-jwt, opaque tokens require an authorization server in order to verify their validity, which can make testing more difficult. To help with that, Spring Security has test support for opaque tokens.

假设你有一个控制器,它将身份验证获取为 BearerTokenAuthentication

Suppose you have a controller that retrieves the authentication as a BearerTokenAuthentication:

  • Java

  • Kotlin

@GetMapping("/endpoint")
public Mono<String> foo(BearerTokenAuthentication authentication) {
    return Mono.just((String) authentication.getTokenAttributes().get("sub"));
}
@GetMapping("/endpoint")
fun foo(authentication: BearerTokenAuthentication): Mono<String?> {
    return Mono.just(authentication.tokenAttributes["sub"] as String?)
}

在那种情况下,你可以使用 SecurityMockServerConfigurers#opaqueToken 方法告诉 Spring Security 包含一个默认 BearerTokenAuthentication

In that case, you can tell Spring Security to include a default BearerTokenAuthentication by using the SecurityMockServerConfigurers#opaqueToken method:

  • Java

  • Kotlin

client
    .mutateWith(mockOpaqueToken())
    .get().uri("/endpoint").exchange();
client
    .mutateWith(mockOpaqueToken())
    .get().uri("/endpoint").exchange()

此示例使用 BearerTokenAuthentication 配置关联的 MockHttpServletRequest,其中包含一个简单的 OAuth2AuthenticatedPrincipal、一连串属性和一连串已授予的权限。

This example configures the associated MockHttpServletRequest with a BearerTokenAuthentication that includes a simple OAuth2AuthenticatedPrincipal, a Map of attributes, and a Collection of granted authorities.

具体来说,它包含一个 Map,其键/值对为 sub/user

Specifically, it includes a Map with a key/value pair of sub/user:

  • Java

  • Kotlin

assertThat((String) token.getTokenAttributes().get("sub")).isEqualTo("user");
assertThat(token.tokenAttributes["sub"] as String?).isEqualTo("user")

它还包括一个权限 Collection,其中只有一个权限 SCOPE_read

It also includes a Collection of authorities with just one authority, SCOPE_read:

  • Java

  • Kotlin

assertThat(token.getAuthorities()).hasSize(1);
assertThat(token.getAuthorities()).containsExactly(new SimpleGrantedAuthority("SCOPE_read"));
assertThat(token.authorities).hasSize(1)
assertThat(token.authorities).containsExactly(SimpleGrantedAuthority("SCOPE_read"))

Spring Security 会执行必要的工作以确保 BearerTokenAuthentication 实例可用于您的控制器方法。

Spring Security does the necessary work to make sure that the BearerTokenAuthentication instance is available for your controller methods.

Configuring Authorities

在许多情况下,您的方法受过滤器或方法安全性保护,需要您的 Authentication 拥有某些已授予的权限以允许请求。

In many circumstances, your method is protected by filter or method security and needs your Authentication to have certain granted authorities to allow the request.

在这种情况下,您可以使用 authorities() 方法提供您需要的已授予权限:

In this case, you can supply what granted authorities you need using the authorities() method:

  • Java

  • Kotlin

client
    .mutateWith(mockOpaqueToken()
        .authorities(new SimpleGrantedAuthority("SCOPE_message:read"))
    )
    .get().uri("/endpoint").exchange();
client
    .mutateWith(mockOpaqueToken()
        .authorities(SimpleGrantedAuthority("SCOPE_message:read"))
    )
    .get().uri("/endpoint").exchange()

Configuring Claims

虽然已授予的权限在所有 Spring Security 中都很常见,但我们也有在 OAuth 2.0 中的属性。

While granted authorities are quite common across all of Spring Security, we also have attributes in the case of OAuth 2.0.

例如,假设您有一个 user_id 属性,用于表示您系统中的用户 ID。您可以在控制器中像下面这样访问它:

Suppose, for example, that you have a user_id attribute that indicates the user’s ID in your system. You might access it as follows in a controller:

  • Java

  • Kotlin

@GetMapping("/endpoint")
public Mono<String> foo(BearerTokenAuthentication authentication) {
    String userId = (String) authentication.getTokenAttributes().get("user_id");
    // ...
}
@GetMapping("/endpoint")
fun foo(authentication: BearerTokenAuthentication): Mono<String?> {
    val userId = authentication.tokenAttributes["user_id"] as String?
    // ...
}

在这种情况下,您可以使用 attributes() 方法来指定属性:

In that case, you can specify that attribute with the attributes() method:

  • Java

  • Kotlin

client
    .mutateWith(mockOpaqueToken()
        .attributes(attrs -> attrs.put("user_id", "1234"))
    )
    .get().uri("/endpoint").exchange();
client
    .mutateWith(mockOpaqueToken()
        .attributes { attrs -> attrs["user_id"] = "1234" }
    )
    .get().uri("/endpoint").exchange()

Additional Configurations

你还可以使用其他方法进一步配置身份验证,具体取决于你的控制器需要的什么数据。

You can also use additional methods to further configure the authentication, depending on what data your controller expects.

其中一个这样的方法是 principal(OAuth2AuthenticatedPrincipal),你可以使用它来配置构成 BearerTokenAuthentication 的完整 OAuth2AuthenticatedPrincipal 实例。

One such method is principal(OAuth2AuthenticatedPrincipal), which you can use to configure the complete OAuth2AuthenticatedPrincipal instance that underlies the BearerTokenAuthentication.

在以下情况下,它会很方便:* 你有 OAuth2AuthenticatedPrincipal 自己的实现,或者 * 你想指定不同的主体名称

It is handy if you: * Have your own implementation of OAuth2AuthenticatedPrincipal or * Want to specify a different principal name

例如,假设你的授权服务器在 user_name 属性中发送主体名称,而不是在 sub 属性中。在这种情况下,你可以手动配置 OAuth2AuthenticatedPrincipal

For example, suppose that your authorization server sends the principal name in the user_name attribute instead of the sub attribute. In that case, you can configure an OAuth2AuthenticatedPrincipal by hand:

  • Java

  • Kotlin

Map<String, Object> attributes = Collections.singletonMap("user_name", "foo_user");
OAuth2AuthenticatedPrincipal principal = new DefaultOAuth2AuthenticatedPrincipal(
        (String) attributes.get("user_name"),
        attributes,
        AuthorityUtils.createAuthorityList("SCOPE_message:read"));

client
    .mutateWith(mockOpaqueToken().principal(principal))
    .get().uri("/endpoint").exchange();
val attributes: Map<String, Any> = mapOf(Pair("user_name", "foo_user"))
val principal: OAuth2AuthenticatedPrincipal = DefaultOAuth2AuthenticatedPrincipal(
    attributes["user_name"] as String?,
    attributes,
    AuthorityUtils.createAuthorityList("SCOPE_message:read")
)

client
    .mutateWith(mockOpaqueToken().principal(principal))
    .get().uri("/endpoint").exchange()

请注意,作为使用 mockOpaqueToken() 测试支持的替代方案,你还可以用 @MockBean 注解来模拟 OpaqueTokenIntrospector bean 本身。

Note that, as an alternative to using mockOpaqueToken() test support, you can also mock the OpaqueTokenIntrospector bean itself with a @MockBean annotation.