经过前面两篇文章的分析我们知道:OAuth2AuthorizationEndpointFilter
是用来处理/oauth/authorize
端点的。
OAuth2AuthorizationEndpointFilter
的核心职责就是处理两种关键请求:
- 授权请求(Authorization Request) ------ 通常是
GET /oauth2/authorize
- 同意请求(Consent Submission) ------ 通常是
POST /oauth2/authorize
(用户点击"同意"按钮后提交的表单)
本篇我们主要讨论的是 同意请求阶段。
在授权确认页面点击确认:会向提交oauth2/authorize端点提交post表单请求 表单参数:client_id=oidc-client&state=WJwb_Ex16hovx1oLhwyu2RoUukAF0drYtTUwluq1vdw%3D&scope=profile
请求经过OAuth2AuthorizationEndpointFilter
后,需要使用converter将原始请求转换成Authentication对象
因为 Spring Security 的核心设计原则是:一切认证/授权流程都必须基于 Authentication
对象。
OAuth2AuthorizationConsentAuthenticationConverter转换请求
因为OAuth2AuthorizationEndpointFilter
持有`OAuth2AuthorizationConsentAuthenticationConverter
OAuth2AuthorizationEndpointFilter
构造器:
java
public OAuth2AuthorizationEndpointFilter(AuthenticationManager authenticationManager,
String authorizationEndpointUri) {
this.authenticationManager = authenticationManager;
this.authorizationEndpointMatcher = createDefaultRequestMatcher(authorizationEndpointUri);
// @formatter:off
this.authenticationConverter = new DelegatingAuthenticationConverter(
Arrays.asList(
new OAuth2AuthorizationCodeRequestAuthenticationConverter(),
new OAuth2AuthorizationConsentAuthenticationConverter()));
// @formatter:on
}
所以当POST表单请求过来的时候,调用convert() 方法 ,将原始请求转换成 OAuth2AuthorizationConsentAuthenticationToken
convert()
流程如下:
java
@Override
public Authentication convert(HttpServletRequest request) {
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getFormParameters(request);
if (!"POST".equals(request.getMethod()) || parameters.getFirst(OAuth2ParameterNames.RESPONSE_TYPE) != null) {
return null;
}
String authorizationUri = request.getRequestURL().toString();
// client_id (REQUIRED)
String clientId = parameters.getFirst(OAuth2ParameterNames.CLIENT_ID);
if (!StringUtils.hasText(clientId) || parameters.get(OAuth2ParameterNames.CLIENT_ID).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID);
}
Authentication principal = SecurityContextHolder.getContext().getAuthentication();
if (principal == null) {
principal = ANONYMOUS_AUTHENTICATION;
}
// state (REQUIRED)
String state = parameters.getFirst(OAuth2ParameterNames.STATE);
if (!StringUtils.hasText(state) || parameters.get(OAuth2ParameterNames.STATE).size() != 1) {
throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.STATE);
}
// scope (OPTIONAL)
Set<String> scopes = null;
if (parameters.containsKey(OAuth2ParameterNames.SCOPE)) {
scopes = new HashSet<>(parameters.get(OAuth2ParameterNames.SCOPE));
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.CLIENT_ID) && !key.equals(OAuth2ParameterNames.STATE)
&& !key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, (value.size() == 1) ? value.get(0) : value.toArray(new String[0]));
}
});
return new OAuth2AuthorizationConsentAuthenticationToken(authorizationUri, clientId, principal, state, scopes,
additionalParameters);
}
上面的过程主要就是提取表单参数,然后创建OAuth2AuthorizationConsentAuthenticationToken
并返回
👉 它不处理"是否同意" ,只是把请求信息打包成一个标准对象。
✅ OAuth2AuthorizationConsentAuthenticationToken
是一个代表"用户是否同意授权"的认证令牌(Authentication Token)
OAuth2AuthorizationConsentAuthenticationToken的构造器:
less
public OAuth2AuthorizationConsentAuthenticationToken(String authorizationUri, String clientId,
Authentication principal, String state, @Nullable Set<String> scopes,
@Nullable Map<String, Object> additionalParameters) {
super(Collections.emptyList());
Assert.hasText(authorizationUri, "authorizationUri cannot be empty");
Assert.hasText(clientId, "clientId cannot be empty");
Assert.notNull(principal, "principal cannot be null");
Assert.hasText(state, "state cannot be empty");
this.authorizationUri = authorizationUri;
this.clientId = clientId;
this.principal = principal;
this.state = state;
this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
this.additionalParameters = Collections.unmodifiableMap(
(additionalParameters != null) ? new HashMap<>(additionalParameters) : Collections.emptyMap());
setAuthenticated(true);
}
OAuth2AuthorizationConsentAuthenticationProvider 认证OAuth2AuthorizationConsentAuthenticationToken
OAuth2AuthorizationConsentAuthenticationProvider的篇幅太长了,我只想截取代码来描述这个过程,看都校验了哪些内容

✅ 它在用 state
参数,查找之前保存的"待完成授权记录",并防止 CSRF 攻击。 通过state去查找当时保存的 OAuth2Authorization
,如果找到了说明这个state是合法的,因为这个state是服务器端生成的,攻击者给不出这样的state。如果查不到,说明授权确认的这个页面是攻击者创建的

✅ 它在确保:当前登录的用户,就是当初发起授权请求的那个用户。

✅ 它在确保:当前提交"同意请求"的客户端,和最初发起授权请求的客户端是同一个。

✅ 它在防止"用户同意的权限"超过"客户端最初请求的权限"。
换句话说:
🔐 客户端只能拿到它"申请过"的权限,不能通过用户操作"多拿"权限。
java
requestedScopes.containsAll(authorizedScopes)
等价于:
"用户同意的权限集合" ⊆ "客户端请求的权限集合"
也就是:同意的不能比请求的多。
Spring Authorization Server 的这个检查,正是为了防止:
🔹 客户端或用户通过篡改表单,获取超出原始请求的权限
核心规则:用户只能从客户端请求的权限中"选择同意或拒绝",但不能"添加新权限"。
插一句:突然明白了OAuth2AuthorizationConsentAuthenticationToken
啥叫授权确认,就是用户告诉授权服务器我同意给客户端某些权限,这种确认要通过表单的形式提交给授权服务器。

- 查找当前用户(
principalName
)是否曾经同意过 这个客户端(clientId
)的某些权限 - 比如:用户
alice
曾经同意web-client
访问email
和profile
- 获取用户之前同意过的权限集合
- 如果没同意过,就是空集合
- 遍历当前客户端请求的权限
- 如果某个权限(如
email
)恰好是用户之前已经同意过 ,就自动加到authorizedScopes
中
目的是:"如果用户上次同意过某个权限,但这次在同意页面忘了勾选,就自动把那个权限加回来,避免用户误操作导致授权丢失。"

✅ 它在构建一个新的或更新已有的"用户对客户端的授权同意记录"(Authorization Consent),准备保存到数据库。
换句话说:
🔧 "记住用户同意了哪些权限" ,以便下次登录时使用(比如自动授权、跳过同意页等)。

这段代码是 Spring Authorization Server 的 "授权决策终审阶段" ,它:
- ✅ 检查用户是否最终授权了任何权限 (通过
authorities
是否为空) - ❌ 若无授权 → 清理数据 + 抛出
access_denied
- ✅ 若有授权 → 构建并保存最新的授权记录(consent)

✅ 这段代码的作用是:
"根据用户的授权决策,生成一个一次性、短期有效的授权码(authorization_code),用于后续换取 access_token。"

✅ 这段代码的作用是:
"基于原始授权会话,构建一个更新后的授权对象,包含用户同意的权限和生成的授权码,并清除敏感属性(如 state),然后将其保存到数据库中。"
这是 授权服务器在用户同意后,对授权状态的最终固化。

有时候我就有疑问为啥 OAuth2AuthorizationConsentAuthenticationProvider 最后返回的是OAuth2AuthorizationCodeRequestAuthenticationToken
呢?
✅ OAuth2AuthorizationConsentAuthenticationProvider
并不是整个流程的终点,而是一个"中间处理器" 。
它的职责是:处理用户同意逻辑,然后"升级"原始的授权请求,使其变成一个"已认证"的状态 。
最终返回的 OAuth2AuthorizationCodeRequestAuthenticationToken
,表示:
"授权码请求 now 已成功完成,可以重定向了" 。
也就是说最开始就是OAuth2AuthorizationCodeRequestAuthenticationToken
,最开始就是在请求授权码,而中间加了一步用户授权确认的机制,经过确认之后授权码请求才算完成,所以最终返回的是已经认证的,并且包含授权码的OAuth2AuthorizationCodeRequestAuthenticationToken
OAuth2AuthorizationEndpointFilter 处理认证通过的回调
返回经过认证的OAuth2AuthorizationCodeRequestAuthenticationToken
后,就可以重定向到客户端了,OAuth2AuthorizationEndpointFilter 封装了一个authenticationSuccessHandler
用来处理授权码

authenticationSuccessHandler最后就是将code重定向到getRedirectUri
带回code
scss
private void sendAuthorizationResponse(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;
UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromUriString(authorizationCodeRequestAuthentication.getRedirectUri())
.queryParam(OAuth2ParameterNames.CODE,
authorizationCodeRequestAuthentication.getAuthorizationCode().getTokenValue());
if (StringUtils.hasText(authorizationCodeRequestAuthentication.getState())) {
uriBuilder.queryParam(OAuth2ParameterNames.STATE,
UriUtils.encode(authorizationCodeRequestAuthentication.getState(), StandardCharsets.UTF_8));
}
// build(true) -> Components are explicitly encoded
String redirectUri = uriBuilder.build(true).toUriString();
this.redirectStrategy.sendRedirect(request, response, redirectUri);
}
撒花完结
终于跟完了整个授权确认的流程,但是可能有很多有瑕疵的地方,不过没关系,我会继续努力弥补写的不好的地方,开心,开心。