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扩展实现》时,也实现了类似功能,具体效果如下图:
重点是下面那个 其他方式登录,
后续有精力也可以参考社区示例修改成类似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):
可通过RegisteredClient.TokenSettings.accessTokenFormat方法进行设置。
原令牌生成逻辑直接耦合在Token端点的AuthenticationProvider中,现将令牌生成逻辑进行拆分,拆分为OAuth2TokenGenerator及其具体实现如下图:
关于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中拆分为:
原来想要扩展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
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 |
以上截图来自: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) {
}
}
}