授权码模式访问oauth2/token端点的流程
我们在使用授权码模式通过code换取accessToken的时候,通常会调用oauth2/tokenendpoint,http请求如下:
POST /oauth2/token HTTP/1.1
Host: localhost:9000
Content-Type: application/x-www-form-urlencoded
Content-Length: 271
grant_type=authorization_code&code=jnAhK1dAGV42GQNK4hhxF14-xx61c6PplDSz1ziW63V8XWq42A3Nzi1ZYSv9UZTATkSFk_WXAF0OchWZo1Eb9xFupOJ0FFXnDzr3RsfcZqsoE91B5U2awlvnA3dRfNRV&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/oidc-client&client_id=oidc-client&client_secret=secret
HTTP Request最终会进入到授权服务器的OAuth2TokenEndpointFilter过滤器中,在授权码的模式中,过滤器需要对授权码进行认证。
tips: 我们都知道在spring security中,所有的认证都要转换成authentication对象,这是一种认证令牌,经过 authenticationManager的认证之后,返回一个完全认证的令牌。
所以OAuth2TokenEndpointFilter使用AuthenticationConverter将HTTP request转换成Authentication
tips: AuthenticationConverter的接口签名如下:
Authentication convert(HttpServletRequest request);
接受一个HttpServletRequest作为参数,提取里面的信息,转换成待认证的Authentication令牌
OAuth2AuthorizationCodeAuthenticationConverter
由于我们使用的是授权码模式,所以AuthenticationConverter的实现类使用的是OAuth2AuthorizationCodeAuthenticationConverter 看名称就知道啦:oauth2授权码认证的converter。
tips:OAuth2TokenEndpointFilter,内部持有一个“委托式认证转换器”,其内部持有一组“策略”,每个策略负责处理一种
grant_type。
this.authenticationConverter = new DelegatingAuthenticationConverter(
Arrays.asList(
new OAuth2AuthorizationCodeAuthenticationConverter(),
new OAuth2RefreshTokenAuthenticationConverter(),
new OAuth2ClientCredentialsAuthenticationConverter(),
new OAuth2DeviceCodeAuthenticationConverter(),
new OAuth2TokenExchangeAuthenticationConverter())
);
OAuth2AuthorizationCodeAuthenticationToken
OAuth2AuthorizationCodeAuthenticationConverter 提取请求参数,转换成OAuth2AuthorizationCodeAuthenticationToken,下图中我提取了关键逻辑,主要是看最后的返回值,返回一个OAuth2AuthorizationCodeAuthenticationToken
@Override
public Authentication convert(HttpServletRequest request) {
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getFormParameters(request);
// grant_type (REQUIRED)
String grantType = parameters.getFirst(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
// code (REQUIRED)
String code = parameters.getFirst(OAuth2ParameterNames.CODE);
// redirect_uri (REQUIRED)
// Required only if the "redirect_uri" parameter was included in the authorization
// request
String redirectUri = parameters.getFirst(OAuth2ParameterNames.REDIRECT_URI);
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) && !key.equals(OAuth2ParameterNames.CLIENT_ID)
&& !key.equals(OAuth2ParameterNames.CODE) && !key.equals(OAuth2ParameterNames.REDIRECT_URI)) {
additionalParameters.put(key, (value.size() == 1) ? value.get(0) : value.toArray(new String[0]));
}
});
return new OAuth2AuthorizationCodeAuthenticationToken(code, clientPrincipal, redirectUri, additionalParameters);
}
OAuth2AuthorizationCodeAuthenticationProvider
OAuth2AuthorizationCodeAuthenticationProvider用来对OAuth2AuthorizationCodeAuthenticationToken进行认证
tips:OAuth2TokenEndpointFilter 持有AuthenticationManager对令牌进行认证,但实际根据令牌类型的不同,需要使用不同的AuthenticationProvider来进行认证。OAuth2AuthorizationCodeAuthenticationProvider就是负责对 OAuth2AuthorizationCodeAuthenticationToken进行认证,认证之后返回OAuth2AccessTokenAuthenticationToken,代表认证的结果。
OAuth2AccessTokenAuthenticationToken
终于讲到今天的主角了OAuth2AccessTokenAuthenticationToken,经过OAuth2AuthorizationCodeAuthenticationProvider 认证之后返回的结果就是它:
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken,
additionalParameters);
构造器:
public OAuth2AccessTokenAuthenticationToken(RegisteredClient registeredClient, Authentication clientPrincipal,
OAuth2AccessToken accessToken, @Nullable OAuth2RefreshToken refreshToken,
Map<String, Object> additionalParameters) {
super(Collections.emptyList());
Assert.notNull(registeredClient, "registeredClient cannot be null");
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
Assert.notNull(accessToken, "accessToken cannot be null");
Assert.notNull(additionalParameters, "additionalParameters cannot be null");
this.registeredClient = registeredClient; // 表示在授权服务器中注册的客户端应用(Client Application)
this.clientPrincipal = clientPrincipal;
this.accessToken = accessToken;// 实际颁发给客户端的访问令牌(Access Token)。
this.refreshToken = refreshToken; // 刷新令牌(Refresh Token),用于在访问令牌过期后获取新的访问令牌
this.additionalParameters = additionalParameters;
}
它扩展了 AbstractAuthenticationToken,表明它是一个用于在认证流程中传递认证信息的 认证令牌(Authentication Token) 。也就是经过认证之后,颁发的令牌和刷新令牌已经保存在这个类中了。
AuthenticationSuccessHandler
当OAuth2TokenEndpointFilter拿到OAuth2AccessTokenAuthenticationToken 之后,AuthenticationSuccessHandler就派上用场了,见名之意:认证成功处理器。
public interface AuthenticationSuccessHandler {
/**
* Called when a user has been successfully authenticated.
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication the <tt>Authentication</tt> object which was created during
* the authentication process.
*/
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException;
}
tips: AuthenticationSuccessHandler是一个接口,里面提供了一个方法onAuthenticationSuccess,当认证成功之后,调用这个方法进行响应。OAuth2TokenEndpointFilter默认使用的是OAuth2AccessTokenResponseAuthenticationSuccessHandler实现来 用来将accesToken返回给用户。
OAuth2TokenEndpointFilter部分代码
private AuthenticationSuccessHandler authenticationSuccessHandler = new OAuth2AccessTokenResponseAuthenticationSuccessHandler();
OAuth2AccessTokenAuthenticationToken accessTokenAuthentication = (OAuth2AccessTokenAuthenticationToken) this.authenticationManager
.authenticate(authorizationGrantAuthentication);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, accessTokenAuthentication);
OAuth2AccessTokenResponseAuthenticationSuccessHandler 默认的成功处理器
OAuth2AccessTokenResponseAuthenticationSuccessHandler都怎么处理的呢?
-
将OAuth2AccessTokenAuthenticationToken包装成
OAuth2AccessTokenResponse -
利用
Converter<OAuth2AccessTokenResponse, Map<String, Object>>将OAuth2AccessTokenResponse转成map结构。 -
利用
MappingJackson2HttpMessageConverter将map转换成HTTP响应
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();
OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
.tokenType(accessToken.getTokenType())
.scopes(accessToken.getScopes());
if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
}
if (refreshToken != null) {
builder.refreshToken(refreshToken.getTokenValue());
}
if (!CollectionUtils.isEmpty(additionalParameters)) {
builder.additionalParameters(additionalParameters);
}
OAuth2AccessTokenResponse accessTokenResponse = builder.build();
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
this.accessTokenResponseConverter.write(accessTokenResponse, null, httpResponse);
}
我去掉了一些代码发现整个流程使用了构建者模式,最后build出一个OAuth2AccessTokenResponse实例
OAuth2AccessTokenResponse的关键属性
最后一步利用HttpMessageConverter将响应返回
private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
tips:
HttpMessageConverter是 Spring 框架中一个用于在 HTTP 请求/响应 和 Java 对象之间进行转换的组件。
它负责:
- 把 HTTP 请求体(如 JSON) 转成 Java 对象(反序列化) ✅
read - 把 Java 对象 转成 HTTP 响应体(如 JSON) (序列化) ✅
write
在写出响应之前转换成map:
加一层map我们可以手动控制每一个字段的输出:
这种设计模式叫什么?
这其实是典型的: ✅ DTO + Converter 模式(或 适配器模式)
OAuth2AccessTokenResponse:业务模型(Java 对象)Map<String, Object>:协议模型(符合 RFC 的键值对)Converter:适配层,负责两者之间的转换
类似于你在做 API 开发时,不会直接返回数据库实体类,而是转成一个
ApiResponseDTO。
Map<String, Object> 在这里是“协议中间表示层”,它不是多余的,而是为了确保生成的 JSON 严格符合 OAuth2 标准,而不是“看起来像”的 JSON。
正确的 OAuth2 响应应该是:
{
"access_token": "abc123",
"expires_in": 3600,
"refresh_token": "def456",
"token_type": "Bearer",
"scope": "read write"
}
所以这个converter的作用就是使其结果符合RFC 6749 的规范!
OAuth2ParameterNames类中定义了很多oauth2的标准参数和字段:
要不要统一响应格式
{
"code": 200,
"msg": "success",
"data": { ... }
}
可能有的公司的业务API会有统一的返回结构,那么我们要不要自定义呢?
我的建议是:不要
因为这是“协议”不是“API”,OAuth2 是一个 开放标准(Open Standard)
所有标准 SDK 都是按 RFC 6749 写死的逻辑,不会去猜你的 data 字段在哪。
你的授权服务器必须返回:
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "abc123",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "xyz789",
"scope": "read write"
}
这样所有客户端 SDK 才能正确读取:
json.get("access_token") → "abc123" // ✅ 成功
tips:虽然不建议,如果想要自定义,可以通过实现自己的AuthenticationSuccessHandler,来统一封装返回结果