首先了解的人肯定知道 这个**/oauth2/token**路径是用来申请token的,那那么他是怎么生效的和怎么配置的呢
OAuth2TokenEndpointConfigurer用来配置Spring Authorization Server的OAuth2TokenEndpointFilter过滤器,这个过滤器用来处理客户端发来的/oauth2/token(默认)请求。配置项有以下五个:


其中requestMatcher需要通过ProviderSettings自定义,这里并没有自定义入口。
自定义accessTokenRequestConverter的话,就意味着你修改了默认的配置。而默认的配置是一个委托类,包含了三种策略(参见上图)。如果你的自定义配置没有适配这三种授权方式,将会失去对这三种方式的支持。
一旦你通过OAuth2TokenEndpointConfigurer#authenticationProvider(AuthenticationProvider)方法进行了自定义,默认提供的三种AuthenticationProvider也将自动失效。
OAuth2TokenGenerator是所有类型Token生成器的抽象。 目前有以下几个实现


OAuth2TokenGenerator的配置也是在这里完成的。那么它是从哪里初始化的呢?
OAuth2TokenGenerator的初始化是借助于工具类OAuth2ConfigurerUtils的静态方法getTokenGenerator来生成。源码如下:

这里采用了单例懒加载设计:
1、先从SharedObject中获取,如果有就直接返回了。
2、如果SharedObject中没有就从Spring IoC中找找,再没有就开始初始化一个,初始化的逻辑专门分析,这里你把它当作黑盒,可以不打断你的思路。
3、初始化成功后,再放入SharedObject,下次再拿直接就有了。
上面初始化的步骤

你会发现它初始化的范式和上面OAuth2TokenGenerator差不多。优先从SharedObject中获取,没有就看看Spring IoC中有没有JwtEncoder或者JWKSource,有就能初始化一个JwtGenerator,无论有没有都会返回。
这里有一个很好玩的东西OAuth2TokenCustomizer,这个是干啥的呢,这个是如果你JWT有自定义需求,主要定义JWT的header和claims,就可以定义一个该类型的Spring Bean就可以了。
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer(){
return context -> {
Authentication principal = context.getPrincipal();
OAuth2Authorization authorization = context.getAuthorization();
Set<String> authorizedScopes = context.getAuthorizedScopes();
Authentication authorizationGrant = context.getAuthorizationGrant();
ProviderContext providerContext = context.getProviderContext();
RegisteredClient registeredClient = context.getRegisteredClient();
// 上面的都可以拿到
// 目的是为了定制jwt 的header 和 claims
JoseHeader.Builder headers = context.getHeaders();
context.getClaims().audience(Arrays.asList("client1","client2"))
.claim("some","any");
};
}
这个OAuth2AccessTokenGenerator其实就是个兜底的,它只能生成不透明令牌,如果JwtGenerator不存在,它就“扶正”了。你如果不喜欢使用JWT,就不要让JwtGenerator初始化。
类似于JwtGenerator的自定义接口OAuth2TokenCustomizer,OAuth2AccessTokenGenerator也有一个自定义接口OAuth2TokenCustomizer,这两个的用法非常类似,就不再赘述了。
最后也会初始化一个刷新Token的生成器OAuth2RefreshTokenGenerator,它是一个不透明令牌。

最终我们使用的是一个代理委托类,按照策略进行生成。
对于其它配置都是老面孔了,AuthenticationSuccessHandler和AuthenticationFailureHandler已经多次提及,在OAuth2TokenEndpointFilter中都提供了默认实现,如果不满足需要可自行扩展,这里就不再赘述。下面我们会对OAuth2TokenEndpointFilter进行一个简单的分析和实践
该过滤器时在OAuth2TokenEndpointConfigurer中添加

OAuth2TokenEndpointFilter的属性字段,可以很明显的看出来默认的拦截端点
OAuth2TokenEndpointFilter的类图


它会根据不同的授权方式AuthorizationGrantType(目前只支持authorization_code(授权码)、refresh_token(刷新)、client_credentials(客户)端三种授权方式)用不同策略提取请求中的授权信息,分别对应三种Token :
OAuth2AuthorizationCodeAuthenticationToken 授权码模式。
OAuth2RefreshTokenAuthenticationToken 刷新Token。
OAuth2ClientCredentialsAuthenticationToken 客户端凭据模式。
DelegatingAuthenticationConverter负责维护token请求参数提取的策略,它有三种策略。接下来我们来看看如何从token请求中提取参数,并封装为Authentication。
授权码模式的参数提取由OAuth2AuthorizationCodeAuthenticationConverter负责。它的提取策略是:

POST /oauth2/token HTTP/1.1
Host: localhost:9000
Content-Type: application/x-www-form-urlencoded
Content-Length: 218
grant_type=authorization_code&code=BAVzaAx8TtTTRE-E_CoQJ8-Bu9-APXjZOqVwPm7JxTQox3ko6d3aTM-m_p4aWoeEII6UDg5X9StWvTW5m9_0IvdCD2pJlaHVkLGnwXWzjXRPtU9hJVMvfI8VR-t8UvCL&redirect_uri=http%3A%2F%2F127.0.0.1%3A8082%2Ffoo%2Fbar&client_id=felord&client_secret=secret
POST /oauth2/token HTTP/1.1
Host: localhost:9000
Authorization: Basic ZmVsb3JkOnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
Content-Length: 218
grant_type=authorization_code&code=BAVzaAx8TtTTRE-E_CoQJ8-Bu9-APXjZOqVwPm7JxTQox3ko6d3aTM-m_p4aWoeEII6UDg5X9StWvTW5m9_0IvdCD2pJlaHVkLGnwXWzjXRPtU9hJVMvfI8VR-t8UvCL&redirect_uri=http%3A%2F%2F127.0.0.1%3A8082%2Ffoo%2Fbar
POST /oauth2/token HTTP/1.1
Host: localhost:9000
Content-Type: application/x-www-form-urlencoded
Content-Length: 1008
grant_type=authorization_code&code=5e7apu0SF720WKmQwVj-wx3lsEDkug1suSnqiXhvB6RwRkOjNrWN43n6DLmKXpcz3RaHG5gSFnvIth97nw-ltKRaDOtSkvl9LfN9YrivhfKG4Ln0Wqe1gmoXvhpyvD45&redirect_uri=http%3A%2F%2F127.0.0.1%3A8082%2Ffoo%2Fbar&client_id=felord&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=eyJ4NXQjUzI1NiI6IlZ4YTJKMllTcnRFSkxPZlI2LU9zMXRPaXJfWXIzS0s2OVI5anJ1cTlzdmciLCJraWQiOiJqb3NlIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJmZWxvcmQiLCJhdWQiOiJodHRwOlwvXC9sb2NhbGhvc3Q6OTAwMFwvb2F1dGgyXC90b2tlbiIsImlzcyI6ImZlbG9yZCIsImV4cCI6MTY1MDI2NDg4NCwiaWF0IjoxNjUwMjY0ODI0LCJqdGkiOiI4MDk5ZWI1Mi05ZTI1LTQ2OTgtYjQwMS1iMjc5MmNhNmI0YmIifQ.sjZBUP5-uQzNQo634B-WCL2yYZt5fktfqeCXLS8qqdCTsGQrm07RGVk774h-VImg3CF5-0v2_aA5CfI1ESMNTmNwfyPLzDWpzInDI6_MX-tLta67TXGButvov0SrXjI8NPcw3IIlfQ91TBs5Msx_W-zpL4A_Px0cr8JuCFiAf092E_Yi7nTqJqwuETopIcSnPDeJsw9ReYsaHEbJ-2570IPcJP357t7RDT7JCJJYIruweIMO6fMAGTksz2cOQNmXd-bDcNE5Oaqm8vZ_2vRF4LuJ19WKM_RHQKwIId9yRrTsRd4rjlCHPYj95NZSyfqWeVtUezDMCjnPit3PaY31oQ
access_token过期后,OAuth2客户端可以携带refresh_token通过**/oauth2/token**去请求一个新的令牌。参数提取由OAuth2RefreshTokenAuthenticationConverter负责。它的提取策略为:

替换refresh_token=即可,这里就不再一一演示了。 客户端凭据模式由OAuth2ClientCredentialsAuthenticationConverter负责提取参数。它的提取策略为:

根据Spring Security的范式,封装为AuthenticationToken自然要交给认证管理器AuthenticationManager,由它检索出对应的AuthenticationProvider来认证AuthenticationToken。
根据上一篇的讲解,我们可以知道默认提供了三个AuthenticationProvider:
认证成功后会交给authenticationSuccessHandler进行处理,它的逻辑为:

写入响应的逻辑由DefaultOAuth2AccessTokenResponseMapConverter负责,我觉得有必要学习一下,它是HttpMessageConverter的一个实现。它首先它借助于内部的DefaultMapOAuth2AccessTokenResponseConverter将OAuth2AccessTokenResponse转换为Map
如果想改变返回体的结构,可以通过其setAccessTokenResponseParametersConverter方法改写DefaultMapOAuth2AccessTokenResponseConverter。
/oauth2/token返回的token只能交给OAuth2客户端使用,不能交给其它User Agent使用,这是非常不安全的。只有注册在授权服务器具有client_id的才是OAuth2客户端。很多同学都混淆了这一点,我们只能通过令牌中继间接的通过已授权的OAuth2客户端来使用token请求资源。
上面分析了OAuth2TokenEndpointFilter,它处理了授权码模式、刷新模式、客户端凭据模式三种Token请求的逻辑,正好对应了三个AuthenticationProvider。

这个名字的类有两个,请认准
org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider

获取当前验证码对应的OAuth2客户端信息。
然后根据携带的授权码code检索出在之前授权码请求中存储的授权信息OAuth2Authorization进行条件判定,以证明本次请求合规。
首先,查询的客户端信息和请求的客户端要一致,如果发现冒用也要强行过期掉OAuth2Authorization。
其次,redirectUri也要一致。
授权码必须在有效期内,否则也不行。
组装构造AccessToken的上下文,包含了:
通过Token生成器将步骤⑥的上下文对象转换为OAuth2Token,这里实际是Jwt。
依据步骤⑦生成的Jwt初始化OAuth2AccessToken。
会把OAuth2AccessToken一起写入OAuth2Authorization,如果令牌是是Jwt风格,会把令牌包含的claims也写进去,后面的刷新Token、ID Token都是这样。
刷新Token的生成,步骤非常简单不再赘述,值得一提的是它需要同时满足以下条件:
OIDC专属的Id Token的生成,条件是请求必须属于OIDC认证。
步骤⑧⑩⑾生成的Token信息都会被存入 OAuth2Authorization,同时本次授权的授权码会被主动作废,OAuth2AuthorizationService会对OAuth2Authorization的持久化进行更新。
最后注册客户端信息RegisteredClient、客户端认证信息OAuth2ClientAuthenticationToken、访问令牌OAuth2AccessToken、刷新令牌(可能为null)OAuth2RefreshToken,以及可能包含OidcIdToken的additionalParameters组成了OAuth2AccessTokenAuthenticationToken返回
Spring Authorization Server刷新Token的逻辑由该AuthenticationProvider负责。刷新令牌请求中包含了以下三个重要的东西:
记住:刷新令牌携带的scope必须是没有受过权的,否则将抛出异常;另外如果不携带该参数则刷新后的访问令牌默认范围是上次授权的范围。
这个AuthenticationProvider逻辑上非常简单,依旧是对参数进行了校验,生成token的步骤和OAuth2AuthorizationCodeAuthenticationProvider类似。一个重要的差别就是,如果你的客户端设置TokenSettings开启了重用刷新令牌isReuseRefreshTokens=ture,那么刷新令牌就可以重复使用。
客户端凭据模式的Token签发由该AuthenticationProvider负责。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication =
(OAuth2ClientCredentialsAuthenticationToken) authentication;
//①获取当前验证码对应的OAuth2客户端信息。
OAuth2ClientAuthenticationToken clientPrincipal =
getAuthenticatedClientElseThrowInvalidClient(clientCredentialsAuthentication);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// ②判断是不是客户端凭据模式。
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.CLIENT_CREDENTIALS)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
//③如果请求中携带的授权范围scope不为空,就校验一下有没有超出客户端定义的范围;如果没有超出就按照请求中的去设定;如果请求中不携带scope的话就把客户端定义的全部范围赋予给本次请求。
Set<String> authorizedScopes = registeredClient.getScopes(); // Default to configured scopes
if (!CollectionUtils.isEmpty(clientCredentialsAuthentication.getScopes())) {
for (String requestedScope : clientCredentialsAuthentication.getScopes()) {
if (!registeredClient.getScopes().contains(requestedScope)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
}
authorizedScopes = new LinkedHashSet<>(clientCredentialsAuthentication.getScopes());
}
// ④组装构造AccessToken的上下文,细节参见OAuth2AuthorizationCodeAuthenticationProvider。
// @formatter:off
OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(clientPrincipal)
.providerContext(ProviderContextHolder.getProviderContext())
.authorizedScopes(authorizedScopes)
.tokenType(OAuth2TokenType.ACCESS_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrant(clientCredentialsAuthentication)
.build();
// @formatter:on
// ⑤通过Token生成器将步骤④的上下文对象转换为OAuth2Token,这里通常是Jwt。
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
// ⑥依据步骤⑤生成的OAuth2Token初始化OAuth2AccessToken。
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
// @formatter:off
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(clientPrincipal.getName())
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.attribute(OAuth2Authorization.AUTHORIZED_SCOPE_ATTRIBUTE_NAME, authorizedScopes);
// @formatter:on
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
} else {
authorizationBuilder.accessToken(accessToken);
}
OAuth2Authorization authorization = authorizationBuilder.build();
// ⑦把授权信息持久化。
this.authorizationService.save(authorization);
//⑧生成已授权的信息OAuth2AccessTokenAuthenticationToken。
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken);
}
大体上,都是通过token端点传递参数,然后根据各自的AuthenticationConverter从请求中提取参数并封装成Authentication,Authentication会按照授权类型(grant_type)的策略从三种AuthenticationProvider中选择一个定向处理,根据不同的逻辑生成最终授权的OAuth2AccessTokenAuthenticationToken。
{
"id": "a1b0c6cb-45d2-46ab-a418-552e025ce719",
"access_token_expires_at": null,
"access_token_issued_at": null,
"access_token_metadata": null,
"access_token_scopes": null,
"access_token_type": null,
"access_token_value": null,
"attributes": "{\"@class\":\"java.util.Collections$UnmodifiableMap\",\"java.security.Principal\":{\"@class\":\"org.springframework.security.authentication.UsernamePasswordAuthenticationToken\",\"authorities\":[\"java.util.Collections$UnmodifiableRandomAccessList\",[{\"@class\":\"org.springframework.security.core.authority.SimpleGrantedAuthority\",\"authority\":\"ROLE_USER\"}]],\"details\":{\"@class\":\"org.springframework.security.web.authentication.WebAuthenticationDetails\",\"remoteAddress\":\"0:0:0:0:0:0:0:1\",\"sessionId\":\"9BA3FCEFA50ED24B42259BA062C6C4B2\"},\"authenticated\":true,\"principal\":{\"@class\":\"org.springframework.security.core.userdetails.User\",\"password\":null,\"username\":\"felord\",\"authorities\":[\"java.util.Collections$UnmodifiableSet\",[{\"@class\":\"org.springframework.security.core.authority.SimpleGrantedAuthority\",\"authority\":\"ROLE_USER\"}]],\"accountNonExpired\":true,\"accountNonLocked\":true,\"credentialsNonExpired\":true,\"enabled\":true},\"credentials\":null},\"org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest\":{\"@class\":\"org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest\",\"authorizationUri\":\"http://localhost:9000/oauth2/authorize\",\"authorizationGrantType\":{\"value\":\"authorization_code\"},\"responseType\":{\"value\":\"code\"},\"clientId\":\"e2fa7e64-249b-46f0-ae1d-797610e88615\",\"redirectUri\":\"http://127.0.0.1:8082/foo/bar\",\"scopes\":[\"java.util.Collections$UnmodifiableSet\",[\"message.read\",\"message.write\"]],\"state\":\"noihJd3-Mc_nEbAx8As0aUEwJraRUHQAbcfQ87F2FtE=\",\"additionalParameters\":{\"@class\":\"java.util.Collections$UnmodifiableMap\"},\"authorizationRequestUri\":\"http://localhost:9000/oauth2/authorize?response_type=code&client_id=e2fa7e64-249b-46f0-ae1d-797610e88615&scope=message.read%20message.write&state=noihJd3-Mc_nEbAx8As0aUEwJraRUHQAbcfQ87F2FtE%3D&redirect_uri=http://127.0.0.1:8082/foo/bar\",\"attributes\":{\"@class\":\"java.util.Collections$UnmodifiableMap\"}},\"state\":\"-PS0r6frBqz8QTBlFpQXKdJnBlZG5FMmHQ5Nziwuy0Y=\"}",
"authorization_code_expires_at": null,
"authorization_code_issued_at": null,
"authorization_code_metadata": null,
"authorization_code_value": null,
"authorization_grant_type": "authorization_code",
"oidc_id_token_claims": null,
"oidc_id_token_expires_at": null,
"oidc_id_token_issued_at": null,
"oidc_id_token_metadata": null,
"oidc_id_token_value": null,
"principal_name": "test",
"refresh_token_expires_at": null,
"refresh_token_issued_at": null,
"refresh_token_metadata": null,
"refresh_token_value": null,
"registered_client_id": "2c9c20818099c695018099cbca030000",
"state": "-PS0r6frBqz8QTBlFpQXKdJnBlZG5FMmHQ5Nziwuy0Y="
}