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

拜托可不可以先点个赞证明你看过

每篇文章都是我学习的过程,可能有些思路看起来乱乱的,但这都是我珍贵的思绪留下的痕迹,随手帮我点个赞,加个关注吧 如果有用的话。要不然就找不到我啦。或者有什么需要讨论的,给我评论留言,让我觉得不孤独。谢谢大家。

上篇关于授权请求的文章我们分析了 用户在没有登录的情况下,访问 /oauth2/authorize端点,会引导用户去登录:

因为 /oauth/authorize 的目的是"让某个用户同意授权"------如果连用户是谁都不知道(即未登录),就无法确定"谁在授权",也就无法完成授权流程。

换句话说:

🔐 授权(Authorization)的前提是先知道"主体是谁"------也就是用户必须先认证(Authentication)。

OAuth2AuthorizationCodeRequestAuthenticationProvider

OAuth2AuthorizationCodeRequestAuthenticationProvider是专门用来处理授权请求的认证提供者

java 复制代码
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
   OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication = (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication;

   RegisteredClient registeredClient = this.registeredClientRepository
      .findByClientId(authorizationCodeRequestAuthentication.getClientId());
   if (registeredClient == null) {
      throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ParameterNames.CLIENT_ID,
            authorizationCodeRequestAuthentication, null);
   }
   ...
   Authentication principal = (Authentication) authorizationCodeRequestAuthentication.getPrincipal();
   if (!isPrincipalAuthenticated(principal)) {
           if (this.logger.isTraceEnabled()) {
              this.logger.trace("Did not authenticate authorization code request since principal not authenticated");
           }
   // Return the authorization request as-is where isAuthenticated() is false
   return authorizationCodeRequestAuthentication;
}
}

OAuth2AuthorizationCodeRequestAuthenticationProvider在对authorizationCodeRequestAuthentication授权请求进行认证的时候,会通过isPrincipalAuthenticated判断用户是否已经登录

通过调试发现未登录的时候是匿名token,认证之后就是usernamepasswordAuthenticationToken,并且是已经认证的。也就是说认证之后这步校验就可以通过了。

接下来:这里有个authorizationConsentService,通过这个service去查询OAuth2AuthorizationConsent

ini 复制代码
OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService
   .findById(registeredClient.getId(), principal.getName());
if (currentAuthorizationConsent != null) {
   authenticationContextBuilder.authorizationConsent(currentAuthorizationConsent);
}

1 OAuth2AuthorizationConsentService是什么?

  • OAuth2AuthorizationConsentService是一个服务,负责管理用户的"授权同意"记录。
  • 它的职责是:持久化地保存"用户是否同意过某个客户端的某些权限"

它的接口定义规范如下:对OAuth2AuthorizationConsent的增删改查功能

java 复制代码
public interface OAuth2AuthorizationConsentService {

   /**
    * Saves the {@link OAuth2AuthorizationConsent}.
    * @param authorizationConsent the {@link OAuth2AuthorizationConsent}
    */
   void save(OAuth2AuthorizationConsent authorizationConsent);

   /**
    * Removes the {@link OAuth2AuthorizationConsent}.
    * @param authorizationConsent the {@link OAuth2AuthorizationConsent}
    */
   void remove(OAuth2AuthorizationConsent authorizationConsent);

   /**
    * Returns the {@link OAuth2AuthorizationConsent} identified by the provided
    * {@code registeredClientId} and {@code principalName}, or {@code null} if not found.
    * @param registeredClientId the identifier for the {@link RegisteredClient}
    * @param principalName the name of the {@link Principal}
    * @return the {@link OAuth2AuthorizationConsent} if found, otherwise {@code null}
    */
   @Nullable
   OAuth2AuthorizationConsent findById(String registeredClientId, String principalName);

}
java 复制代码
.findById(registeredClient.getId(), principal.getName())

所以它查的是:

"用户 A 是否曾经同意过客户端 B 的授权请求?"

2. OAuth2AuthorizationConsent 是什么?

这是一个对象,表示 用户对某个客户端的授权同意记录

arduino 复制代码
public final class OAuth2AuthorizationConsent implements Serializable {

   private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID;

   private static final String AUTHORITIES_SCOPE_PREFIX = "SCOPE_";

   private final String registeredClientId;

   private final String principalName;

   private final Set<GrantedAuthority> authorities;
}   
字段 说明
registeredClientId 哪个客户端
principalName 哪个用户
authorities 用户曾经同意过的权限列表(如 read, write, profile

综上所述就是查询用户曾经是否同意过对某个client的授权。将其设置进authenticationContextBuilder

接下来是这段代码:判断是否需要弹出授权确认页。

kotlin 复制代码
if (this.authorizationConsentRequired.test(authenticationContextBuilder.build())) {
   String state = DEFAULT_STATE_GENERATOR.generateKey();
   OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
      .attribute(OAuth2ParameterNames.STATE, state)
      .build();

   if (this.logger.isTraceEnabled()) {
      this.logger.trace("Generated authorization consent state");
   }

   this.authorizationService.save(authorization);

   Set<String> currentAuthorizedScopes = (currentAuthorizationConsent != null)
         ? currentAuthorizationConsent.getScopes() : null;

   if (this.logger.isTraceEnabled()) {
      this.logger.trace("Saved authorization");
   }

   return new OAuth2AuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),
         registeredClient.getClientId(), principal, state, currentAuthorizedScopes, null);
}

authorizationConsentRequired方法是干嘛的啊?

scss 复制代码
private static boolean isAuthorizationConsentRequired(
      OAuth2AuthorizationCodeRequestAuthenticationContext authenticationContext) {
   if (!authenticationContext.getRegisteredClient().getClientSettings().isRequireAuthorizationConsent()) {
      return false;
   }
   // 'openid' scope does not require consent
   if (authenticationContext.getAuthorizationRequest().getScopes().contains(OidcScopes.OPENID)
         && authenticationContext.getAuthorizationRequest().getScopes().size() == 1) {
      return false;
   }

   if (authenticationContext.getAuthorizationConsent() != null && authenticationContext.getAuthorizationConsent()
      .getScopes()
      .containsAll(authenticationContext.getAuthorizationRequest().getScopes())) {
      return false;
   }

   return true;
}

这是一个非常核心且关键的方法,它决定了在 OAuth2 授权流程中:

🔍 用户是否需要看到"授权确认页面"(Consent Page)------也就是那个经典的 "Do you allow XXX to access your data?" 的弹窗。
作用:判断当前授权请求是否需要用户手动确认授权(即是否要显示"同意授权"页面)。

  • 返回 true → 需要用户确认(显示 Consent 页面)
  • 返回 false → 不需要用户确认(自动放行,直接生成 code
✅ 条件 1:客户端是否配置为"不需要授权确认"

检查客户端是否在注册时明确设置为 "不需要用户确认"

✅ 条件 2:是否只请求了 openid scope(纯 OIDC 登录)

判断:

  • 请求的 scopes 中是否包含 openid
  • 并且 只有 openid 这一个 scope

如果是,则不需要用户确认

📌 为什么?

因为 openid scope 的含义是:

"使用 OAuth2 做用户登录(身份认证),不访问任何用户数据。"

这本质上是 OpenID Connect(OIDC)的"登录"行为,而不是"授权访问资源"。

✅ 条件 3:用户是否已经同意过这些权限(Consent 已存在)

🔍 做了什么?

判断:

  • 用户是否曾经同意过这个客户端的授权
  • 并且 历史同意的权限范围(scopes)包含了本次请求的所有 scopes

如果是,则不需要再次确认

在需要弹出授权确认页的情况下

kotlin 复制代码
if (this.authorizationConsentRequired.test(authenticationContextBuilder.build())) {
   String state = DEFAULT_STATE_GENERATOR.generateKey();
   OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
      .attribute(OAuth2ParameterNames.STATE, state)
      .build();

   if (this.logger.isTraceEnabled()) {
      this.logger.trace("Generated authorization consent state");
   }

   this.authorizationService.save(authorization);

   Set<String> currentAuthorizedScopes = (currentAuthorizationConsent != null)
         ? currentAuthorizationConsent.getScopes() : null;

   if (this.logger.isTraceEnabled()) {
      this.logger.trace("Saved authorization");
   }

   return new OAuth2AuthorizationConsentAuthenticationToken(authorizationRequest.getAuthorizationUri(),
         registeredClient.getClientId(), principal, state, currentAuthorizedScopes, null);
}

如果要弹出确认页,框架做了如上操作

🔹 1. 生成 state

  • 这个 state 不是 OAuth 2.0 中客户端传来的 state,而是 授权服务器内部生成的"同意页状态"

  • 目的:

    • 🔐 防止 CSRF 攻击 :当用户提交"同意"表单时,必须携带这个 state,服务器会验证它是否匹配。

🔹 2. 构建 OAuth2Authorization 对象

  • 这是一个 待确认的授权上下文对象,但它还不是最终的授权记录。

  • 它包含了:

    • 客户端信息(registeredClient
    • 当前用户(principal
    • 原始授权请求参数(authorizationRequest
    • 刚生成的 state
  • 注意:此时 authorization 的状态是"未完成",还没有 authorization_code

🔹 3. 保存授权上下文

🔹 4. 获取用户之前已经授权过的 scope(如果有),用于在同意页上显示"已授权权限"。

🔹 5. 返回 OAuth2AuthorizationConsentAuthenticationToken

  • 这个 AuthenticationToken 是一个信号,告诉 Spring Security:

    "现在需要跳转到'授权确认页',让用户决定是否同意!"

而且返回的是一个经过认证的OAuth2AuthorizationCodeRequestAuthenticationToken。

less 复制代码
public OAuth2AuthorizationCodeRequestAuthenticationToken(String authorizationUri, String clientId,
      Authentication principal, OAuth2AuthorizationCode authorizationCode, @Nullable String redirectUri,
      @Nullable String state, @Nullable Set<String> scopes) {
   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.notNull(authorizationCode, "authorizationCode cannot be null");
   this.authorizationUri = authorizationUri;
   this.clientId = clientId;
   this.principal = principal;
   this.authorizationCode = authorizationCode;
   this.redirectUri = redirectUri;
   this.state = state;
   this.scopes = Collections.unmodifiableSet((scopes != null) ? new HashSet<>(scopes) : Collections.emptySet());
   this.additionalParameters = Collections.emptyMap();
   setAuthenticated(true);
}

跳转确认页

上面的代码返回以后,代码又回到了OAuth2AuthorizationEndpointFilter的authentication方法中

kotlin 复制代码
Authentication authenticationResult = this.authenticationManager.authenticate(authentication);
// 代码回到这里
if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken) {
   if (this.logger.isTraceEnabled()) {
      this.logger.trace("Authorization consent is required");
   }
   sendAuthorizationConsent(request, response,
         (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication,
         (OAuth2AuthorizationConsentAuthenticationToken) authenticationResult);
   return;
}

判断如果返回的是OAuth2AuthorizationCodeRequestAuthenticationToken(这也是一个认证通过的令牌),就调用sendAuthorizationConsent进行跳转

ini 复制代码
private void sendAuthorizationConsent(HttpServletRequest request, HttpServletResponse response,
      OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication,
      OAuth2AuthorizationConsentAuthenticationToken authorizationConsentAuthentication) throws IOException {

   String clientId = authorizationConsentAuthentication.getClientId();
   Authentication principal = (Authentication) authorizationConsentAuthentication.getPrincipal();
   Set<String> requestedScopes = authorizationCodeRequestAuthentication.getScopes();
   Set<String> authorizedScopes = authorizationConsentAuthentication.getScopes();
   String state = authorizationConsentAuthentication.getState();

   if (hasConsentUri()) {
      String redirectUri = UriComponentsBuilder.fromUriString(resolveConsentUri(request))
         .queryParam(OAuth2ParameterNames.SCOPE, String.join(" ", requestedScopes))
         .queryParam(OAuth2ParameterNames.CLIENT_ID, clientId)
         .queryParam(OAuth2ParameterNames.STATE, state)
         .toUriString();
      this.redirectStrategy.sendRedirect(request, response, redirectUri);
   }
   else {
      if (this.logger.isTraceEnabled()) {
         this.logger.trace("Displaying generated consent screen");
      }
      DefaultConsentPage.displayConsent(request, response, clientId, principal, requestedScopes, authorizedScopes,
            state, Collections.emptyMap());
   }
}

private boolean hasConsentUri() {
   return StringUtils.hasText(this.consentPage);
}

但是我们没有配置consentPage属性,所以会生成一个默认的授权确认页面,

从默认页面我们可以看出 如果用户点击同意授权,那么就还会再oauth2/authorize的url提交表单post请求用来表示同意授权。

点击确认:提交post请求 http://localhost:9000/oauth2/authorize

请求参数:client_id=oidc-client&state=WJwb_Ex16hovx1oLhwyu2RoUukAF0drYtTUwluq1vdw%3D&scope=profile

总结:

本篇又花时间讨论了: 如果在用户登录的情况下,访问/oauth2/authorize请求授权的时候,如果需要授权确认页,就弹出确认页。点击Post提交的的时候才会换取code

下一篇文章就会讨论post 提交授权请求的流程,记得关注啊。另外本篇文章的源码中,有很多很好的设计模式,比如builder设计模式,我希望后期我能有时间整理下spring框架中,使用的优雅的设计模式,如果感兴趣的话,可以关注哈,谢谢大家。

相关推荐
shelterremix6 小时前
Integer缓存池
java·开发语言
啥都不懂的小小白6 小时前
微服务多级缓存:从问题到实战(小白也能看懂的亿级流量方案)
缓存·微服务·架构
Doris_LMS6 小时前
在Linux系统中安装Jenkins(保姆级别)
java·linux·jenkins·ci
LDM>W<6 小时前
EasyMeeting-注册登录
java·腾讯会议
本就是菜鸟何必心太浮7 小时前
python中`__annotations__` 和 `inspect` 模块区别??
java·前端·python
叫我阿柒啊7 小时前
Java全栈工程师的面试实战:从基础到复杂问题的完整解析
java·数据库·spring boot·微服务·vue3·测试·全栈开发
liang_jy7 小时前
Java volatile
android·java·面试
聚客AI7 小时前
💥下一代推理引擎:vLLM如何重塑AI服务架构?
人工智能·架构·llm
花花无缺7 小时前
函数和方法的区别
java·后端·python