【SpringSecurityOAuth2】原始碼分析@EnableOAuth2Sso在Spring Security OAuth2 SSO單點登入場景下的作用
目錄
- 一、從Spring Security OAuth2官方文件瞭解@EnableOAuth2Sso作用
- 二、原始碼分析@EnableOAuth2Sso作用
- @EnableOAuth2Client
- OAuth2SsoCustomConfiguration:OAuth2 SSO自定義配置
- SsoSecurityConfigurer:OAuth2 SSO核心配置(增強)
- OAuth2SsoDefaultConfiguration:OAuth2 SSO預設配置
- ResourceServerTokenServicesConfiguration:訪問Token資源服務的配置
- 三、總結
一、從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登入後憑證,完成登入