经过前面两篇文章的分析我们知道:OAuth2AuthorizationEndpointFilter是用来处理/oauth/authorize端点的。

OAuth2AuthorizationEndpointFilter 的核心职责就是处理两种关键请求:

  1. 授权请求(Authorization Request)  —— 通常是 GET /oauth2/authorize
  2. 同意请求(Consent Submission)  —— 通常是 POST /oauth2/authorize(用户点击“同意”按钮后提交的表单)

本篇我们主要讨论的是 同意请求阶段。

请求经过OAuth2AuthorizationEndpointFilter后,需要使用converter将原始请求转换成Authentication对象

因为 Spring Security 的核心设计原则是:一切认证/授权流程都必须基于 Authentication 对象。

OAuth2AuthorizationConsentAuthenticationConverter转换请求

因为OAuth2AuthorizationEndpointFilter持有`OAuth2AuthorizationConsentAuthenticationConverter

OAuth2AuthorizationEndpointFilter 构造器:

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()流程如下:

@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的构造器:

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的篇幅太长了,我只想截取代码来描述这个过程,看都校验了哪些内容

image.png

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

image.png

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

image.png

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

image.png

它在防止“用户同意的权限”超过“客户端最初请求的权限”。

换句话说:

requestedScopes.containsAll(authorizedScopes)

等价于:

也就是:同意的不能比请求的多

Spring Authorization Server 的这个检查,正是为了防止:

核心规则:用户只能从客户端请求的权限中“选择同意或拒绝”,但不能“添加新权限”。

插一句:突然明白了OAuth2AuthorizationConsentAuthenticationToken 啥叫授权确认,就是用户告诉授权服务器我同意给客户端某些权限,这种确认要通过表单的形式提交给授权服务器。

image.png

  • 查找当前用户(principalName)是否曾经同意过这个客户端(clientId)的某些权限
  • 比如:用户 alice 曾经同意 web-client 访问 email 和 profile
  • 获取用户之前同意过的权限集合
  • 如果没同意过,就是空集合
  • 遍历当前客户端请求的权限
  • 如果某个权限(如 email)恰好是用户之前已经同意过,就自动加到 authorizedScopes 中

目的是:“如果用户上次同意过某个权限,但这次在同意页面忘了勾选,就自动把那个权限加回来,避免用户误操作导致授权丢失。”

image.png

它在构建一个新的或更新已有的“用户对客户端的授权同意记录”(Authorization Consent),准备保存到数据库。

换句话说:

image.png

这段代码是 Spring Authorization Server 的 “授权决策终审阶段” ,它:

  1. ✅ 检查用户是否最终授权了任何权限(通过 authorities 是否为空)
  2. ❌ 若无授权 → 清理数据 + 抛出 access_denied
  3. ✅ 若有授权 → 构建并保存最新的授权记录(consent)

image.png

这段代码的作用是:

“根据用户的授权决策,生成一个一次性、短期有效的授权码(authorization_code),用于后续换取 access_token。”

image.png

这段代码的作用是:

这是 授权服务器在用户同意后,对授权状态的最终固化

image.png

有时候我就有疑问为啥 OAuth2AuthorizationConsentAuthenticationProvider 最后返回的是OAuth2AuthorizationCodeRequestAuthenticationToken呢?

OAuth2AuthorizationConsentAuthenticationProvider 并不是整个流程的终点,而是一个“中间处理器”
它的职责是:处理用户同意逻辑,然后“升级”原始的授权请求,使其变成一个“已认证”的状态
最终返回的 OAuth2AuthorizationCodeRequestAuthenticationToken,表示:
“授权码请求 now 已成功完成,可以重定向了”

也就是说最开始就是OAuth2AuthorizationCodeRequestAuthenticationToken,最开始就是在请求授权码,而中间加了一步用户授权确认的机制,经过确认之后授权码请求才算完成,所以最终返回的是已经认证的,并且包含授权码的OAuth2AuthorizationCodeRequestAuthenticationToken

OAuth2AuthorizationEndpointFilter 处理认证通过的回调

返回经过认证的OAuth2AuthorizationCodeRequestAuthenticationToken后,就可以重定向到客户端了,OAuth2AuthorizationEndpointFilter 封装了一个authenticationSuccessHandler 用来处理授权码

image.png

authenticationSuccessHandler最后就是将code重定向到getRedirectUri带回code

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);
}

撒花完结

终于跟完了整个授权确认的流程,但是可能有很多有瑕疵的地方,不过没关系,我会继续努力弥补写的不好的地方,开心,开心。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]