一、从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登录后凭证,完成登录