1. 程式人生 > >【SpringSecurityOAuth2】原始碼分析@EnableOAuth2Sso在Spring Security OAuth2 SSO單點登入場景下的作用

【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個配置類:OAuth2SsoDefaultConfigurationOAuth2SsoCustomConfigurationResourceServerTokenServicesConfiguration


@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));
        }
    }
}
  • 條件NeedsWebSecurityConditionEnableOAuth2SsoCondition相反,最後滿足當用戶使用了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
  • JwtTokenServices:基於Json Web Token自包含令牌的TokenService

在通過以上ResourceServerTokenServices介面實現獲取使用者資訊後,就可以在使用@EnableOAuth2Sso註解的OAuth2 Client上建立已認證的使用者身份憑證Authentication,完成登入


三、總結

總的來說@EnableOAuth2Sso註解幫助我們快速的將我們的OAuth2 Client應用接入授權伺服器完成基於OAuth2的SSO流程,建立登入狀態

無論是使用者有沒有自己的WebSecurityConfigurerAdapter安全配置都可以使用@EnableOAuth2Sso註解,如果有,@EnableOAuth2Sso是在使用者的安全配置上做增強

增強的邏輯是在SpringSecurityFilterChain過濾器鏈上新增OAuth2ClientAuthenticationProcessingFilter這個用於登入認證的Filter,其使用的是OAuth2授權碼流程,以下都是這個Filter負責的功能