一、從Spring Security OAuth2官方文檔了解@EnableOAuth2Sso作用
spring-security-oauth2-boot 2.2.0.RELEASE Single Sign On文檔地址
先從第一段介紹開始,加上自己的分析:
-
@EnableOAuth2Sso
是使用在OAuth2 Client角色上的注解,從其包路徑也可以看出org.springframework.boot.autoconfigure.security.oauth2.client -
@EnableOAuth2Sso
單點登錄的原理簡單來說就是:標注有@EnableOAuth2Sso
的OAuth2 Client應用在通過某種OAuth2授權流程獲取訪問令牌后(一般是授權碼流程),通過訪問令牌訪問userDetails用戶明細這個受保護資源服務,獲取用戶信息后,將用戶信息轉換為Spring Security上下文中的認證后憑證Authentication,從而完成標注有@EnableOAuth2Sso
的OAuth2 Client應用自身的登錄認證的過程。整個過程是基於OAuth2的SSO單點登錄 -
SSO流程中需要訪問的用戶信息資源地址,可以通過
security.oauth2.resource.userInfoUri
配置指定 -
最后的通過訪問令牌訪問受保護資源后,在當前服務創建認證后憑證Authentication(登錄態)也可以不通過訪問userInfoUri實現,userInfoUri端點是需要用戶自己實現。默認情況
security.oauth2.resource.preferTokenInfo=true
,獲取用戶信息使用的是授權服務器的/check_token
端點,即TokenInfo,根據訪問令牌找到在授權服務器關聯的授予這個訪問令牌的用戶信息 -
Spring Security OAuth2 SSO整個流程實際上是 OAuth2 Client是一個運行在Server上的Webapp的典型場景,很適合使用授權碼流程
第二段主要講了下如何使用@EnableOAuth2Sso
:
-
使用
@EnableOAuth2Sso
的OAuth2 Client應用可以使用/login
端點用於觸發基於OAuth2的SSO流程,這個入口地址也可以通過security.oauth2.sso.login-path
來修改 -
如果針對一些安全訪問規則有自己的定制,說白了就是自己實現了Spring Security的
WebSecurityConfigurerAdapter
想自定義一些安全配置,但又想使用@EnableOAuth2Sso
的特性,可以在自己的WebSecurityConfigurerAdapter
上使用@EnableOAuth2Sso
注解,注解會在你的安全配置基礎上做“增強”,至於具體如何“增強”的,后面的源碼分析部分會詳細解釋注意:
如果是在自定義的AutoConfiguration自動配置類上使用
@EnableOAuth2Sso
,在第一次重定向到授權服務器時會出現問題,具體是因為通過@EnableOAuth2Client
添加的OAuth2ClientContextFilter
會被放到springSecurityFilterChain
這個Filter后面,導致無法攔截UserRedirectRequiredException
需重定向異常 -
如果沒有自己的
WebSecurityConfigurerAdapter
安全配置,也可以在任意配置類上使用@EnableOAuth2Sso
,除了添加OAuth2 SSO的增強外,還會有默認的基本安全配置
二、源碼分析@EnableOAuth2Sso作用
首先來看一下@EnableOAuth2Sso
的源碼
/**
* Enable OAuth2 Single Sign On (SSO). If there is an existing
* {@link WebSecurityConfigurerAdapter} provided by the user and annotated with
* {@code @EnableOAuth2Sso}, it is enhanced by adding an authentication filter and an
* authentication entry point. If the user only has {@code @EnableOAuth2Sso} but not on a
* WebSecurityConfigurerAdapter then one is added with all paths secured.
*
* @author Dave Syer
* @since 1.3.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableOAuth2Client
@EnableConfigurationProperties(OAuth2SsoProperties.class)
@Import({ OAuth2SsoDefaultConfiguration.class, OAuth2SsoCustomConfiguration.class,
ResourceServerTokenServicesConfiguration.class })
public @interface EnableOAuth2Sso {
}
可以看到主要做了幾件事
- 添加
@EnableOAuth2Client
- 啟用OAuth2 SSO相關的
OAuth2SsoProperties
配置文件 - 導入了3個配置類:
OAuth2SsoDefaultConfiguration
、OAuth2SsoCustomConfiguration
、ResourceServerTokenServicesConfiguration
@EnableOAuth2Client
@EnableOAuth2Client
從名稱就可以看出是專門給OAuth2 Client角色使用的注解,其可以獨立使用,具體功能需要單獨寫一篇來分析,大致看一下源碼,主要是導入了OAuth2ClientConfiguration
配置類
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(OAuth2ClientConfiguration.class)
public @interface EnableOAuth2Client {
}
而OAuth2ClientConfiguration
配置類主要做了三件事
- 向Servlet容器添加
OAuth2ClientContextFilter
- 創建request scope的Spring Bean:
AccessTokenRequest
- 創建session scope的Spring Bean:
OAuth2ClientContext
,OAuth2 Client上下文
大體上就是為OAuth2 Client角色創建相關環境
OAuth2SsoCustomConfiguration:OAuth2 SSO自定義配置
/**
* Configuration for OAuth2 Single Sign On (SSO) when there is an existing
* {@link WebSecurityConfigurerAdapter} provided by the user and annotated with
* {@code @EnableOAuth2Sso}. The user-provided configuration is enhanced by adding an
* authentication filter and an authentication entry point.
*
* @author Dave Syer
*/
@Configuration
@Conditional(EnableOAuth2SsoCondition.class) //OAuth2 SSO自定義配置生效條件
public class OAuth2SsoCustomConfiguration
implements ImportAware, BeanPostProcessor, ApplicationContextAware {
private Class<?> configType;
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
this.configType = ClassUtils.resolveClassName(importMetadata.getClassName(),
null);
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
/**
* BeanPostProcessor的初始化后方法
* 給用戶自定義的WebSecurityConfigurerAdapter添加Advice來增強:SsoSecurityAdapter
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
// 如果是WebSecurityConfigurerAdapter,並且就是添加@EnableOAuth2Sso的那個
if (this.configType.isAssignableFrom(bean.getClass())
&& bean instanceof WebSecurityConfigurerAdapter) {
ProxyFactory factory = new ProxyFactory();
factory.setTarget(bean);
factory.addAdvice(new SsoSecurityAdapter(this.applicationContext));
bean = factory.getProxy();
}
return bean;
}
/**
* 攔截用戶的WebSecurityConfigurerAdapter
* 在其init()初始化之前,添加SsoSecurityConfigurer配置
*/
private static class SsoSecurityAdapter implements MethodInterceptor {
private SsoSecurityConfigurer configurer;
SsoSecurityAdapter(ApplicationContext applicationContext) {
this.configurer = new SsoSecurityConfigurer(applicationContext);
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (invocation.getMethod().getName().equals("init")) {
Method method = ReflectionUtils
.findMethod(WebSecurityConfigurerAdapter.class, "getHttp");
ReflectionUtils.makeAccessible(method);
HttpSecurity http = (HttpSecurity) ReflectionUtils.invokeMethod(method,
invocation.getThis());
this.configurer.configure(http);
}
return invocation.proceed();
}
}
}
OAuth2SsoCustomConfiguration
自定義配置指的是如果用戶有自定義的WebSecurityConfigurerAdapter
安全配置的情況下,就在用戶自定義配置的基礎上做OAuth2 SSO的增強,具體分析為
- 首先必須在滿足
@Conditional(EnableOAuth2SsoCondition.class)
的情況下才可以使用,EnableOAuth2SsoCondition
條件指的是@EnableOAuth2Sso
注解被使用在WebSecurityConfigurerAdapter
上 - 可以看到
OAuth2SsoCustomConfiguration
配置類也是一個BeanPostProcessor
,其會在Spring初始化Bean的前后做處理,上面代碼中會在Sping初始化WebSecurityConfigurerAdapter
之后,並且就是添加了@EnableOAuth2Sso
注解的WebSecurityConfigurerAdapter
之后,為安全配置類做“增強”,添加了一個Advice為SsoSecurityAdapter
SsoSecurityAdapter
會在用戶添加了@EnableOAuth2Sso
注解的WebSecurityConfigurerAdapter
配置類調用init()
初始化方法之前,先添加一段子配置SsoSecurityConfigurer
,這個子配置就是實現基於OAuth2 SSO的關鍵
SsoSecurityConfigurer:OAuth2 SSO核心配置(增強)
class SsoSecurityConfigurer {
public void configure(HttpSecurity http) throws Exception {
OAuth2SsoProperties sso = this.applicationContext
.getBean(OAuth2SsoProperties.class);
// Delay the processing of the filter until we know the
// SessionAuthenticationStrategy is available:
http.apply(new OAuth2ClientAuthenticationConfigurer(oauth2SsoFilter(sso)));
addAuthenticationEntryPoint(http, sso);
}
- 添加
OAuth2ClientAuthenticationConfigurer
子配置,為了向springSecurityFilterChain過濾器鏈添加一個專門用於處理OAuth2 SSO的OAuth2ClientAuthenticationProcessingFilter
- 添加處理頁面及Ajax請求未認證時的AuthenticationEntryPoint認證入口
OAuth2ClientAuthenticationConfigurer
子配置是重點
// 創建OAuth2ClientAuthenticationProcessingFilter
private OAuth2ClientAuthenticationProcessingFilter oauth2SsoFilter(
OAuth2SsoProperties sso) {
OAuth2RestOperations restTemplate = this.applicationContext
.getBean(UserInfoRestTemplateFactory.class).getUserInfoRestTemplate();
ResourceServerTokenServices tokenServices = this.applicationContext
.getBean(ResourceServerTokenServices.class);
OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(
sso.getLoginPath());
filter.setRestTemplate(restTemplate);
filter.setTokenServices(tokenServices);
filter.setApplicationEventPublisher(this.applicationContext);
return filter;
}
// OAuth2ClientAuthenticationConfigurer子配置
private static class OAuth2ClientAuthenticationConfigurer
extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private OAuth2ClientAuthenticationProcessingFilter filter;
OAuth2ClientAuthenticationConfigurer(
OAuth2ClientAuthenticationProcessingFilter filter) {
this.filter = filter;
}
@Override
public void configure(HttpSecurity builder) throws Exception {
OAuth2ClientAuthenticationProcessingFilter ssoFilter = this.filter;
ssoFilter.setSessionAuthenticationStrategy(
builder.getSharedObject(SessionAuthenticationStrategy.class));
// 添加過濾器
builder.addFilterAfter(ssoFilter,
AbstractPreAuthenticatedProcessingFilter.class);
}
}
OAuth2ClientAuthenticationConfigurer
子配置將構造好的專門用於處理OAuth2 SSO場景的過濾器OAuth2ClientAuthenticationProcessingFilter
添加到springSecurityFilterChain過濾器鏈中,構造這個Filter時需要
OAuth2RestOperations
:專門用於和授權服務器、資源服務器做Rest交互的模板工具類ResourceServerTokenServices
:用於訪問Token資源服務的類SessionAuthenticationStrategy
:OAuth2 SSO認證完成后,使用Spring Security的會話策略
這一步,向springSecurityFilterChain過濾器鏈中添加OAuth2ClientAuthenticationConfigurer
是最核心的一步,整個OAuth2 SSO的交互都由這個Filter完成,OAuth2ClientAuthenticationConfigurer
的具體邏輯待后續分析
OAuth2SsoDefaultConfiguration:OAuth2 SSO默認配置
/**
* Configuration for OAuth2 Single Sign On (SSO). If the user only has
* {@code @EnableOAuth2Sso} but not on a {@code WebSecurityConfigurerAdapter} then one is
* added with all paths secured.
*
* @author Dave Syer
* @since 1.3.0
*/
@Configuration
@Conditional(NeedsWebSecurityCondition.class) //OAuth2Sso默認配置生效條件
public class OAuth2SsoDefaultConfiguration extends WebSecurityConfigurerAdapter {
private final ApplicationContext applicationContext;
public OAuth2SsoDefaultConfiguration(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
/**
* 1、添加/**都需要認證才能訪問的限制
* 2、添加SsoSecurityConfigurer配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests().anyRequest().authenticated();
new SsoSecurityConfigurer(this.applicationContext).configure(http);
}
/**
* OAuth2Sso默認配置生效條件
*/
protected static class NeedsWebSecurityCondition extends EnableOAuth2SsoCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
return ConditionOutcome.inverse(super.getMatchOutcome(context, metadata));
}
}
}
- 條件
NeedsWebSecurityCondition
與EnableOAuth2SsoCondition
相反,最后滿足當用戶使用了EnableOAuth2Sso
,但其沒有被放在自己定義的WebSecurityConfigurerAdapter
安全配置類上時,會進入OAuth2 SSO默認配置,從注釋信息也可以看出 OAuth2SsoDefaultConfiguration
繼承了WebSecurityConfigurerAdapter
,是一段Spring Security的安全配置- 添加滿足
/**
路徑的請求都需要authenticated()
認證,默認安全配置 - 和上面分析一樣,使用
SsoSecurityConfigurer
子配置,最終會為springSecurityFilterChain過濾器鏈中添加OAuth2ClientAuthenticationConfigurer
ResourceServerTokenServicesConfiguration:訪問Token資源服務的配置
主要作用是創建ResourceServerTokenServices
,用於通過訪問令牌獲取其相關的用戶憑據,或者讀取訪問令牌的完整信息,接口定義如下
public interface ResourceServerTokenServices {
/**
* Load the credentials for the specified access token.
* 加載指定訪問令牌的憑據
*
* @param accessToken The access token value.
* @return The authentication for the access token.
* @throws AuthenticationException If the access token is expired
* @throws InvalidTokenException if the token isn't valid
*/
OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
/**
* Retrieve the full access token details from just the value.
* 僅從值中檢索完整的訪問令牌詳細信息
*
* @param accessToken the token value
* @return the full access token with client id etc.
*/
OAuth2AccessToken readAccessToken(String accessToken);
}
具體的ResourceServerTokenServices
接口實現分為
- RemoteTokenServices:遠端的TokenService
- TokenInfoServices:訪問
/check_token
端點,根據訪問令牌找到在授權服務器關聯的授予這個訪問令牌的用戶信息 - UserInfoTokenServices:訪問用戶自定義的userInfo端點,根據訪問令牌訪問受保護資源userInfo
- TokenInfoServices:訪問
- JwtTokenServices:基於Json Web Token自包含令牌的TokenService
在通過以上ResourceServerTokenServices
接口實現獲取用戶信息后,就可以在使用@EnableOAuth2Sso
注解的OAuth2 Client上創建已認證的用戶身份憑證Authentication,完成登錄
三、總結
總的來說@EnableOAuth2Sso
注解幫助我們快速的將我們的OAuth2 Client應用接入授權服務器完成基於OAuth2的SSO流程,創建登錄狀態
無論是用戶有沒有自己的WebSecurityConfigurerAdapter
安全配置都可以使用@EnableOAuth2Sso
注解,如果有,@EnableOAuth2Sso
是在用戶的安全配置上做增強
增強的邏輯是在SpringSecurityFilterChain過濾器鏈上添加OAuth2ClientAuthenticationProcessingFilter
這個用於登錄認證的Filter,其使用的是OAuth2授權碼流程,以下都是這個Filter負責的功能
- 將用戶重定向到授權服務器獲取授權
- 根據code授權碼和OAuth2 clientId、secret獲取訪問令牌
- 最后使用
ResourceServerTokenServices
並攜帶訪問令牌獲取用戶信息,創建Authentication登錄后憑證,完成登錄