- 引言
- 联邦认证示例
- 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;
/**
* 默认的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) {
}
}
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)