别告诉我你还不会OAuth 2.0授权过滤器:OAuth2AuthorizationEndpointFilter第三篇

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

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

  1. 授权请求(Authorization Request) ------ 通常是 GET /oauth2/authorize
  2. 同意请求(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 访问 emailprofile
  • 获取用户之前同意过的权限集合
  • 如果没同意过,就是空集合
  • 遍历当前客户端请求的权限
  • 如果某个权限(如 email)恰好是用户之前已经同意过 ,就自动加到 authorizedScopes

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

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

换句话说:

🔧 "记住用户同意了哪些权限" ,以便下次登录时使用(比如自动授权、跳过同意页等)。

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

  1. 检查用户是否最终授权了任何权限 (通过 authorities 是否为空)
  2. 若无授权 → 清理数据 + 抛出 access_denied
  3. 若有授权 → 构建并保存最新的授权记录(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);
}

撒花完结

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

相关推荐
编程迪3 小时前
找活招工系统源码 雇员雇主小程序 后端JAVA前端uniapp
java·spring boot·uni-app
_Walli_3 小时前
k8s集群搭建(一)-------- 基础准备
java·容器·kubernetes
天天摸鱼的java工程师3 小时前
如何快速判断几十亿个数中是否存在某个数?—— 八年 Java 开发的实战避坑指南
java·后端
老邓计算机毕设3 小时前
Springboot乐家流浪猫管理系统16lxw(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
武子康3 小时前
大数据-88 Spark Super Word Count 全流程实现(Scala + MySQL)
大数据·后端·spark
知其然亦知其所以然3 小时前
别再只会背八股了!一文带你彻底搞懂UNION与UNION ALL的区别
后端·mysql·面试
羑悻3 小时前
再续传输层协议UDP :从低可靠到极速传输的协议重生之路,揭秘无连接通信的二次进化密码!
后端
就是帅我不改3 小时前
99%的Java程序员都踩过的高并发大坑
后端·面试
BingoGo3 小时前
PHP Swoole/WebMan/Laravel Octane 等长驻进程框架内存泄露诊断与解决方案
后端·php