- 引言
- 联邦认证示例
- public client默认设置
- Introspection端点自定义
- 访问令牌类型⭐️
- 令牌生成器优化⭐️
- 拆分Client认证逻辑OAuth2ClientAuthenticationProvider⭐️
- 授权端点逻辑⭐️
- 关于0.3.0版本中JwtEncoder相关变化⭐️
Spring社区在2022-03-24 19:56发布了Spring Authorization Server 0.2.3版本,具体变化如下图:
接下来结合上图,聊聊新版本的变化。
联邦认证示例注:
以下标题中标⭐️的是我这边需要关注的,后续扩展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(2022-01-27 0.2.2版本)及自定义OIDC扩展实现》时,也实现了类似功能,具体效果如下图:
public client默认设置重点是下面那个 其他方式登录,
后续有精力也可以参考社区示例修改成类似Configurer形式:
FederatedIdentityConfigurer extends AbstractHttpConfigurer
即在注册Client信息时,若对应Public Client(即客户端认证方法仅支持none),
则自定开启PKCE和Consent确认。
具体修改可参见:
https://github.com/spring-projects/spring-authorization-server/commit/586c7daf2a69f72471a98240de1ec044ce256e59
新增加OAuth2TokenIntrospectionEndpointConfigurer配置类,可通过如下方式对introspection_endpoint(即OAuth2TokenIntrospectionEndpointFilter)进行自定义:
如想对令牌验证返回结果进行自定义,可参考OAuth2TokenIntrospectionAuthenticationProvider类进行扩展实现,
返回想要的TokenClaims即可,原相关实现逻辑如下图:
可参见OAuth2TokenFormat类,即访问令牌access_token支持如下两种类型(原来不可配置且只支持JWT):
- SELF_CONTAINED(默认) - 自签JWT类型(包含claim信息如sub、scopes等内容)
- REFERENCE - 引用类型(Support opaque access tokens),即生成96位随机字符串,具体claim信息存储在DB中
可通过RegisteredClient.TokenSettings.accessTokenFormat方法进行设置。
令牌生成器优化⭐️原令牌生成逻辑直接耦合在Token端点的AuthenticationProvider中,现将令牌生成逻辑进行拆分,拆分为OAuth2TokenGenerator及其具体实现如下图:
- 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生成逻辑
- 可通过OAuth2TokenCustomizer
- OAuth2AccessTokenGenerator - 生成96位随机字符串access_token(适用于reference类型),且相应claims和过期时间等存在在DB中
- 可通过OAuth2TokenCustomizer
进行扩展
- 可通过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) |
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=codeno redirect_uri yes no code_challenge yes no code_challenge_method yes
s256 | plainno 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)
- 基础验证
- authenticateAuthorizationConsent
- 若认证未通过(未颁发授权码 并且 !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
- RequestMatcher
以上截图来自: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; public class DefaultOidcTokenCustomer implements OAuth2TokenCustomizer{ private AbstractOidcTokenCustomerExtend abstractOidcTokenCustomerExtend = new AbstractOidcTokenCustomerExtend() { }; private Map > tokenTypeValue2ExtendFuncMap = new HashMap<>(3); 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); } @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) { 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); } 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()); } } 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); } public static abstract class AbstractOidcTokenCustomerExtend { public String registerThirdUser(JwtEncodingContext jwtEncodingContext) { return jwtEncodingContext.getPrincipal().getName(); } public void extendAccessToken(JwtEncodingContext jwtEncodingContext) { } public void extendRefreshToken(JwtEncodingContext jwtEncodingContext) { } public void extendIdToken(JwtEncodingContext jwtEncodingContext) { } } 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 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 authorizedScopes = jwtEncodingContext.getAuthorizedScopes(); jwtEncodingContext.getClaims().claims(claims -> this.extendIdTokenClaims(claims, authorizedScopes)); } public String registerThirdUser(Authentication thirdAuthInfo) { return thirdAuthInfo.getName(); } public void extendAccessTokenClaims(Map claims, Set authorizedScopes) { } public void extendIdTokenClaims(Map claims, Set authorizedScopes) { } } }



