Spring Authorization Server 0.2.3变化

目录

    • 引言
    • 联邦认证示例
    • public client默认设置
    • Introspection端点自定义
    • 访问令牌类型⭐️
    • 令牌生成器优化⭐️
    • 拆分Client认证逻辑OAuth2ClientAuthenticationProvider⭐️
    • 授权端点逻辑⭐️
    • 关于0.3.0版本中JwtEncoder相关变化⭐️

引言

Spring社区在2022-03-24 19:56发布了Spring Authorization Server 0.2.3版本,具体变化如下图:
Spring Authorization Server 0.2.3变化_第1张图片
接下来结合上图,聊聊新版本的变化。

注:
以下标题中标⭐️的是我这边需要关注的,后续扩展Spring Authorization Server均有涉及。

联邦认证示例

即通过Spring Security OAuth2 Client(Login)模块支持第三方登录,
社区给了一个示例:
https://github.com/spring-projects/spring-authorization-server/tree/main/samples/federated-identity-authorizationserver
示例中集成了Github和Google登录,
Spring Authorization Server 0.2.3变化_第2张图片
有兴趣的可以查看具体示例代码。

之前在做《Spring Authorization Server(2022-01-27 0.2.2版本)及自定义OIDC扩展实现》时,也实现了类似功能,具体效果如下图:

重点是下面那个 其他方式登录
后续有精力也可以参考社区示例修改成类似Configurer形式:
FederatedIdentityConfigurer extends AbstractHttpConfigurer

Spring Authorization Server 0.2.3变化_第3张图片

public client默认设置

即在注册Client信息时,若对应Public Client(即客户端认证方法仅支持none),
则自定开启PKCE和Consent确认。
具体修改可参见:
https://github.com/spring-projects/spring-authorization-server/commit/586c7daf2a69f72471a98240de1ec044ce256e59

Spring Authorization Server 0.2.3变化_第4张图片

Introspection端点自定义

新增加OAuth2TokenIntrospectionEndpointConfigurer配置类,可通过如下方式对introspection_endpoint(即OAuth2TokenIntrospectionEndpointFilter)进行自定义:
Spring Authorization Server 0.2.3变化_第5张图片
如想对令牌验证返回结果进行自定义,可参考OAuth2TokenIntrospectionAuthenticationProvider类进行扩展实现,
返回想要的TokenClaims即可,原相关实现逻辑如下图:
Spring Authorization Server 0.2.3变化_第6张图片

访问令牌类型⭐️

Spring Authorization Server 0.2.3变化_第7张图片

可参见OAuth2TokenFormat类,即访问令牌access_token支持如下两种类型(原来不可配置且只支持JWT):

  • SELF_CONTAINED(默认) - 自签JWT类型(包含claim信息如sub、scopes等内容)
  • REFERENCE - 引用类型(Support opaque access tokens),即生成96位随机字符串,具体claim信息存储在DB中

可通过RegisteredClient.TokenSettings.accessTokenFormat方法进行设置。

令牌生成器优化⭐️

原令牌生成逻辑直接耦合在Token端点的AuthenticationProvider中,现将令牌生成逻辑进行拆分,拆分为OAuth2TokenGenerator及其具体实现如下图:
Spring Authorization Server 0.2.3变化_第8张图片

  • OAuth2TokenGenerator
    • DelegatingOauth2TokenGenerator - 代理类,聚合多个生成器,依次遍历多个生成器,生成结果非空则直接返回结果
    • JwtGenerator - 生成JWT格式的access_token(适用于self_contained类型)和id_token
      • 可通过OAuth2TokenCustomizer进行扩展
      • access_token过期时间可通过RegisteredClient.TokenSettings.accessTokenTimeToLive方法进行设置,默认5分钟
      • id_token过期时间30分钟,目前不可配置(写死在代码中)
      • 此类即对应原0.2.2中OAuth2AuthorizationCodeAuthenticationProvider的access_token、id_token生成逻辑
    • OAuth2AccessTokenGenerator - 生成96位随机字符串access_token(适用于reference类型),且相应claims和过期时间等存在在DB中
      • 可通过OAuth2TokenCustomizer进行扩展
    • OAuth2RefreshTokenGenerator - 生成96位随机字符串refresh_token,过期时间存放在DB中
      • refresh_token过期时间可通过RegisteredClient.TokenSettings.refreshTokenTimeToLive方法进行设置,默认60分钟
      • 此类即对应原0.2.2中OAuth2AuthorizationCodeAuthenticationProvider的refresh_token生成逻辑
    • OAuth2AuthorizationCodeGenerator - 生成96位随机字符串授权码
      • 过期时间5分钟,目前不可配置(写死在代码中)
      • 此类为private static私有类,即对应原0.2.2中OAuth2AuthorizationCodeRequestAuthenticationProvider的code生成逻辑

关于OAuth2TokenEndpointFilter整体调用逻辑:

AuthenticationConverter -> OAuth2AuthorizationGrantAuthenticationToken -> AuthenticaionProvider -> DelegatingOAuth2TokenGenerator

AuthenticationConverter
根据grant_type解析参数并转换为OAuth2AuthorizationGrantAuthenticationToken
OAuth2AuthorizationGrantAuthenticationToken AuthenticaionProvider
基本参数验证后使用OAuth2TokenGenerator生成对应的Token
DelegatingOAuth2TokenGenerator
AuthenticationProvider均会聚合对应的DelegatingOAuth2TokenGenerator
OAuth2AuthorizationCodeAuthenticationConverter OAuth2AuthorizationCodeAuthenticationToken OAuth2AuthorizationCodeAuthenticationProvider DelegatingOAuth2TokenGenerator(JwtGenerator, OAuth2AccessTokenGenerator, OAuth2RefreshTokenGenerator)
OAuth2RefreshTokenAuthenticationConverter OAuth2RefreshTokenAuthenticationToken OAuth2RefreshTokenAuthenticationProvider DelegatingOAuth2TokenGenerator(JwtGenerator, OAuth2AccessTokenGenerator, OAuth2RefreshTokenGenerator)
OAuth2ClientCredentialsAuthenticationConverter OAuth2ClientCredentialsAuthenticationToken OAuth2ClientCredentialsAuthenticationProvider DelegatingOAuth2TokenGenerator(JwtGenerator, OAuth2AccessTokenGenerator)

拆分Client认证逻辑OAuth2ClientAuthenticationProvider⭐️

OAuth2ClientAuthenticationProvider及以下拆分后的AuthenticationProver均被OAuth2ClientAuthenticationFilter调用,
即RP向OP发送获取token请求、检查token、吊销token时(POST /oauth2/token|introspect|revoke),OP端提供的认证逻辑。

0.2.2版本中OAuth2ClientAuthenticationProvider耦合了一堆Client认证逻辑,新版本0.2.3中拆分为:

  • ClientSecretAuthenticationProvider - 支持client_secret_basic、client_secret_post认证
    • 比较client_secret是否匹配
    • 支持OAuth2.1中confidential client - pkce验证码code_verifier验证(对应之前授权端点提交的挑战码code_challenge)
  • PublicClientAuthenticationProvider - 支持none认证(PKCE流程)
    • 支持pkce验证码code_verifier验证(对应之前授权端点提交的挑战码code_challenge)
  • JwtClientAssertionAuthenticationProvider - 支持urn:ietf:params:oauth:client-assertion-type:jwt-bearer认证
    • Http Form参数:client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=jwtXxx
    • authentication_method包含private_key_jwt、client_secret_jwt

原来想要扩展Client认证逻辑,如支持Public client(无法提供client_secret的场景)执行刷新令牌流程,需要覆盖修改整个OAuth2ClientAuthenticationProvider代码,现在0.2.3版本后仅需附加一个新的AuthenticationProver,该AuthenticationProver仅去实现Public client执行刷新令牌流程的认证场景即可,如:

grant_type == "refresh_token" && client_id != null && token_settings.allow_public_client_refresh_token 

关于OAuth2ClientAuthenticationFilter整体调用逻辑:

AuthenticationConverter -> OAuth2ClientAuthenticationToken -> AuthenticaionProvider

AuthenticationConverter
解析参数并转换为OAuth2ClientAuthenticationToken
AuthenticaionProvider
根据认证方法做具体客户端认证
JwtClientAssertionAuthenticationConverter JwtClientAssertionAuthenticationProvider
ClientSecretBasicAuthenticationConverter ClientSecretAuthenticationProvider
ClientSecretPostAuthenticationConverter ClientSecretAuthenticationProvider
PublicClientAuthenticationConverter PublicClientAuthenticationProvider

授权端点逻辑⭐️

这块不是新扩展的,就是逻辑比较复杂,所以就简单记录下。

考虑个问题,一次授权码流程中总共会经过OAuth2AuthorizationEndpointFilter授权端点/oauth2/authorize几次?
1)客户端首次跳转或重定向到授权端点GET /oauth2/authorize(由于未登录认证,则直接重定向到登录页面)
2)登录成功后通过SaveRequest获取前一个URI,即对应此授权端点,即登录成功后再次重定向到此GET /oauth2/authorize
3.1)若需要consentRequired,则重定向到consentUri确认页后,提交确认时会再请求到此POST /oauth2/authorize(授权范围scope确认无误后会再重定向回客户端redirect_uri)
3.2)若不需要consentRequired,由于用户已认证通过则直接重定向回客户端redirect_uri
4)之后再有Client请求此授权端点GET /oauth2/authorize,由于之前已经登录过,则直接重定向回对应客户端的redirect_uri

  • OAuth2AuthorizationEndpointFilter - 授权端点的具体实现逻辑
    • RequestMatcher
      • GET /oauth2/authorize - 授权端点
      • POST /oauth2/authorize?response_type=xxx&scope=openid… - 授权端点
      • POST/oauth2/authorize且不存在response_type参数 - Consent权限确认表单提交请求
    • OAuth2AuthorizationCodeRequestAuthenticationConverter - 提取认证参数OAuth2AuthorizationCodeRequestAuthenticationToken(通过consent区分类型,区别于consentRequired属性)
      http请求参数 授权端点 Consent提交请求
      client_id yes yes
      scope yes yes
      state yes yes
      response_type yes
      response_type=code
      no
      redirect_uri yes no
      code_challenge yes no
      code_challenge_method yes
      s256 | plain
      no
      additional parameters yes yes
    • OAuth2AuthorizationCodeRequestAuthenticationProvider - 具体的认证逻辑实现
      • authenticateAuthorizationConsent
        • 基础验证(state存在、用户认证通过、client_id合法)
        • 授权范围合法…
        • 保存OAuth2AuthorizationConsent
        • 生成授权码code
        • 更新OAuth2Authorization
        • 返回OAuth2AuthorizationCodeRequestAuthenticationToken(clientId, principal, authorization_uri, redirect_uri, authorizedScopes, request.state, authorizationCode)
      • authenticateAuthorizationRequest
        • 基础验证
          • 验证client_id是否合法、redirect_uri是否匹配、是否包含authorization_code授权类型
          • 验证当前请求的scope是否在RegisteredClient中包含(即请求的scope是否在注册Client时指定的范围内)
          • 验证PKCE code_challenge及code_challenge_method是否合法
        • 若之前已认证通过(SecurityContextHolder.getContext().getAuthentication())且非匿名认证AnonymousAuthenticationToken,也即通过Spring Security认证过(如formLogin),则直接返回OAuth2AuthorizationCodeRequestAuthenticationToken
        • 否则记录授权请求
        • 是否需要consent
          • clientSetting.REQUIRE_AUTHORIZATION_CONSENT
          • scope不是仅包含openid
          • 之前DB中OAuth2AuthorizationConsent存储的已确认的scope不完全包含当前请求的scope
        • 若需要consent,则直接返回OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, 新生成的state,scopes, consentRequired=true)
        • 若不需要consent
          • 生成授权码code(96位随机字符串,5分钟有效期,目前不可配置)
          • 生成并保存OAuth2Authorization(authorizationCode, scopes)记录
          • 返回OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, redirectUri, request.state,scopes, authorizationCode, consentRequired=false)
    • 若认证未通过(未颁发授权码 并且 !consentRequired),则继续Filter链触发登录
    • 需要consentRequired,则重定向达授权确认页(默认生成 或者 自定义consentUri)
      • consentUri?client_id=xx&state=xxx&scope=requestedScopes
    • 不需要consentRequired(已登录过、颁发授权码、无需consent或者已经consent),则重定向回客户端redirect_uri?code=xxx&state=request.state
    • 抛异常则重定向回客户端redirect_uri?state=request.state&error=error_code&error_description=xxx&error_uri=xxx

关于redirect_uri的验证可见下图:
Spring Authorization Server 0.2.3变化_第9张图片

关于0.3.0版本中JwtEncoder相关变化⭐️

Spring Authorization Server 0.2.3变化_第10张图片
以上截图来自:https://github.com/spring-projects/spring-authorization-server/issues/594

在集成0.2.x版本时,会发现JwtEncodingContext关联的底层实现JwtClaimsSet等均已被@Deprecated标识,
而在扩展Token相关Claims(实现自定义OAuth2TokenCustomizer)时会用到相关类,
考虑到后续0.3.0版本JwtEncoding相关实现会有变化,此处扩展最好隔离底层实现,可以参见以下我的实现:

注:集成的工程如需扩展Token Claims,仅需实现AbstractOidcTokenClaimsCustomerExtend即可。

import com.neusoft.oscoe.oauth.authserver.constant.Oauth2Constants;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.OAuth2TokenType;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

/**
 * 默认的OIDC Token定制化实现
 *
 * @author luohq
 * @date 2022-04-21 14:59
 */
public class DefaultOidcTokenCustomer implements OAuth2TokenCustomizer<JwtEncodingContext> {

    /**
     * 自定义Token扩展(默认空实现)
     */
    private AbstractOidcTokenCustomerExtend abstractOidcTokenCustomerExtend = new AbstractOidcTokenCustomerExtend() {
    };

    /**
     * Map(token类型值, 自定义扩展实现)
     */
    private Map<String, Consumer<JwtEncodingContext>> tokenTypeValue2ExtendFuncMap = new HashMap<>(3);

    /**
     * 构造函数
     *
     * @param abstractOidcTokenCustomerExtend 自定义Token扩展
     */
    public DefaultOidcTokenCustomer(AbstractOidcTokenCustomerExtend abstractOidcTokenCustomerExtend) {
        //设置非空自定义token扩展
        if (null != abstractOidcTokenCustomerExtend) {
            this.abstractOidcTokenCustomerExtend = abstractOidcTokenCustomerExtend;
        }

        //设置Map(token类型值, 自定义扩展实现)
        this.tokenTypeValue2ExtendFuncMap.put(OAuth2TokenType.ACCESS_TOKEN.getValue(), this::extendAccessTokenInner);
        this.tokenTypeValue2ExtendFuncMap.put(OAuth2TokenType.REFRESH_TOKEN.getValue(), this.abstractOidcTokenCustomerExtend::extendRefreshToken);
        this.tokenTypeValue2ExtendFuncMap.put(OidcParameterNames.ID_TOKEN, this::extendIdTokenInner);
    }

    /**
     * 内部token扩展实现
     *
     * @param jwtEncodingContext token上下文
     */
    @Override
    public void customize(JwtEncodingContext jwtEncodingContext) {
        //token类型
        OAuth2TokenType tokenType = jwtEncodingContext.getTokenType();
        //根据token类型扩展对应的token(依次扩展accessToken -> refreshToken -> idToken)
        //详细扩展逻辑参见 org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider -> authenticate)
        this.tokenTypeValue2ExtendFuncMap.get(tokenType.getValue()).accept(jwtEncodingContext);
    }

    private void extendAccessTokenInner(JwtEncodingContext jwtEncodingContext) {
        /** 第三方登录,调用第三方用户自动注册逻辑(非OAuth2 Client第三方登录的情况均为UniLoginAuthenticationToken)  */
        if (jwtEncodingContext.getPrincipal().getClass().getName().equals("org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken")) {
            String newRegUserId = this.abstractOidcTokenCustomerExtend.registerThirdUser(jwtEncodingContext);
            //重置newRegUserId
            this.resetNewRegUserIdInJwtContext(newRegUserId, jwtEncodingContext);
        }

        //调用自定义扩展
        this.abstractOidcTokenCustomerExtend.extendAccessToken(jwtEncodingContext);
    }

    /**
     * 重置claims.sub和Auth2Authorization.principalName为newRegUserId
     *
     * @param newRegUserId 新注册的用户ID
     * @param jwtEncodingContext jwt编码上下文
     */
    private void resetNewRegUserIdInJwtContext(String newRegUserId, JwtEncodingContext jwtEncodingContext) {
        try {
            //覆盖claims.sub为新注册用户ID
            jwtEncodingContext.getClaims().claim("sub", newRegUserId);

            //重置OAuth2Authorization.pincipalName
            OAuth2Authorization oAuth2Authorization = jwtEncodingContext.getAuthorization();
            Field principalNameField = OAuth2Authorization.class.getDeclaredField("principalName");
            principalNameField.setAccessible(true);
            principalNameField.set(oAuth2Authorization, newRegUserId);
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new AuthenticationServiceException(ex.getMessage());
        }
    }
    /**
     * 内部idToken扩展(扩展sid)
     *
     * @param jwtEncodingContext token上下文
     */
    private void extendIdTokenInner(JwtEncodingContext jwtEncodingContext) {
        //获取登录时的sessionId(避免再次调用RequestContextHolder.getRequestAttributes().getSessionId()获取sessionId而导致额外创建新的session)
        String loginSessionId = jwtEncodingContext.getAuthorization().getAttribute(Oauth2Constants.AUTHORIZATION_ATTRS.SESSION_ID);
        //idToken默认添加sid
        if (StringUtils.hasText(loginSessionId)) {
            jwtEncodingContext.getClaims().claim(Oauth2Constants.CLAIMS.SID, loginSessionId);
        }
        //调用自定义扩展
        this.abstractOidcTokenCustomerExtend.extendIdToken(jwtEncodingContext);
    }


    /**
     * 自定义扩展适配器
* * 注: * 该类实现暂不稳定,后续升级SAS 0.3.0后JwtEncodingContext包名及其底层实现会调整, * 如有扩展需要,目前0.2.x版本可基于AbstractOidcTokenClaimsCustomerExtend进行扩展(兼容后续0.3版本) * */
public static abstract class AbstractOidcTokenCustomerExtend { /** * 注册第三方用户为当前系统用户,并返回注册后的当前系统用户ID
* 注:用注册后的用户ID作为token.claim.sub * @param jwtEncodingContext * @return 注册后的用户ID(对应于当前授权服务端的用户) */
public String registerThirdUser(JwtEncodingContext jwtEncodingContext) { return jwtEncodingContext.getPrincipal().getName(); } /** * 扩展IdToken * * @param jwtEncodingContext token上下文 */ public void extendAccessToken(JwtEncodingContext jwtEncodingContext) { } /** * 扩展RefreshToken * * @param jwtEncodingContext token上下文 */ public void extendRefreshToken(JwtEncodingContext jwtEncodingContext) { } /** * 扩展AccessToken * * @param jwtEncodingContext token上下文 */ public void extendIdToken(JwtEncodingContext jwtEncodingContext) { } } /** * 自定义Claims扩展适配器
* * 注: * 该类屏蔽了SAS 0.2和后续0.3版本会发生变化的部分,较为稳定, * 后续升级SAS为0.3版本后,仅需调整此框架实现,通过此类集成的工程可不受影响 */
public static abstract class AbstractOidcTokenClaimsCustomerExtend extends AbstractOidcTokenCustomerExtend { @Deprecated @Override public String registerThirdUser(JwtEncodingContext jwtEncodingContext) { Authentication thirdAuthInfo = jwtEncodingContext.getPrincipal(); return this.registerThirdUser(thirdAuthInfo); } @Deprecated @Override public void extendAccessToken(JwtEncodingContext jwtEncodingContext) { Set<String> authorizedScopes = jwtEncodingContext.getAuthorizedScopes(); jwtEncodingContext.getClaims().claims(claims -> this.extendAccessTokenClaims(claims, authorizedScopes)); } @Deprecated @Override public void extendRefreshToken(JwtEncodingContext jwtEncodingContext) { super.extendRefreshToken(jwtEncodingContext); } @Deprecated @Override public void extendIdToken(JwtEncodingContext jwtEncodingContext) { Set<String> authorizedScopes = jwtEncodingContext.getAuthorizedScopes(); jwtEncodingContext.getClaims().claims(claims -> this.extendIdTokenClaims(claims, authorizedScopes)); } /** * 注册第三方用户为当前系统用户,并返回注册后的当前系统用户ID
* 注:用注册后的用户ID作为token.claim.sub * * @param thirdAuthInfo 第三方用户认证信息(如借助Spring Security OAuth2 Client(集成三方登录oauth2Login)则对应OAuth2AuthenticationToken类型 * @return 注册后的用户ID(对应于当前授权服务端的用户) */
public String registerThirdUser(Authentication thirdAuthInfo) { return thirdAuthInfo.getName(); } /** * 扩展AccessToken * * @param claims AccessToken属性Map * @param authorizedScopes 当前客户端授权范围 */ public void extendAccessTokenClaims(Map<String, Object> claims, Set<String> authorizedScopes) { } /** * 扩展IdToken * * @param claims IdToken属性Map * @param authorizedScopes 当前客户端授权范围 */ public void extendIdTokenClaims(Map<String, Object> claims, Set<String> authorizedScopes) { } } }

你可能感兴趣的:(#,OAuth2.0,&,OIDC,1.0,#,springboot,java,oauth2,oidc,spring,authorization)