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()
之所以这样做,是因为 OidcUser
从 OidcIdToken
中收集其声明。
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 theOidcUserInfo
instance -
clientRegistration(ClientRegistration)
: Configures the associatedOAuth2AuthorizedClient
with a givenClientRegistration
-
oidcUser(OidcUser)
: Configures the completeOidcUser
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 associatedOAuth2AuthorizedClient
with a givenClientRegistration
-
oauth2User(OAuth2User)
: Configures the completeOAuth2User
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 associatedClientRegistration
-
clientRegistration(ClientRegistration)
: Configures the completeClientRegistration
如果您想使用真实的 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()
在此,scope
和 scp
声明和在正常的承载令牌请求中处理它们的方式相同。但是,只要提供测试所需的 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()
或者,如果你有一个自定义 Jwt
至 Collection<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.