在《SpringSecurity系列 之 集成第三方登录》中,我们基于SpringSecurity实现了集成的用户名密码、短信验证码和Github三种登录方式,其中,基于Github实现的登录,其实已经在SpringSecurity Oauth2中提供了一套实现流程,而且可以通过简单的配置就完成,我们下面尝试使用基于Oauth2的方式来实现Github的登录。
在使用Github实现登录的时候,首先需要在Github账户进行OAuth配置,和《SpringSecurity系列 之 集成第三方登录》中的配置方式一样,这里不再重复。
完成Github配置后,我们需要得到Client ID、Client secrets和Authorization callback URL三个参数值,其中Authorization callback URL可以自定义,对应Github登录成功进行回调的地址。
完成了上述准备工作,我们开始基于Oauth2实现Github的登录。首先,增加Oauth2所需要的的依赖,如下所示:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
<version>2.2.5.RELEASEversion>
dependency>
然后,完成application.yml配置,具体内容如下:
server:
port: 8080
servlet:
context-path: /oauth2
tomcat:
uri-encoding: UTF-8
security:
oauth2:
client:
#对应Github账号配置的Client ID
client-id: xxxxxx
#对应Github账号配置的Client secrets
client-secret: xxxxxx
accessTokenUri: https://github.com/login/oauth/access_token
userAuthorizationUri: https://github.com/login/oauth/authorize
clientAuthenticationScheme: form
#对应Github账号配置的Authorization callback URL
registered-redirect-uri: ${site.baseUrl}/github_login
use-current-uri: false
resource:
userInfoUri: https://api.github.com/user
preferTokenInfo: false
sso:
#对应Github账号配置的Authorization callback URL
login-path: /github_login
然后,配置启动类的单点登录,即在启动类上增加@EnableOAuth2Sso注解,如下所示:
@SpringBootApplication
@EnableOAuth2Sso
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
最后,增加一个测试Controller类,默认配置时,所有地址都需要鉴权后才可以访问,所以"/index"地址需要登录后,才能够进行访问,如下所示:
@RestController
public class IndexController {
@GetMapping("/index")
public String index(){
return "Welcome to the index!";
}
}
经过上述配置,我们启动服务,当我们访问http://localhost:8080/oauth2/index 地址时,会自动跳转到Github的授权登录页,输入Github的用户名密码,登录授权成功后,就可以成功访问到了http://localhost:8080/oauth2/index 地址。
在初始化Oauth2的时候,有两个入口,其中一个就是在启动类上添加的@EnableOAuth2Sso注解,还有一个就是SpringBoot会加载META-INF/spring.factories文件中的配置类。
我们这里首先来分析@EnableOAuth2Sso注解主要实现了那些配置的初始化。首先,@EnableOAuth2Sso注解的定义如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableOAuth2Client
@EnableConfigurationProperties(OAuth2SsoProperties.class)
@Import({ OAuth2SsoDefaultConfiguration.class, OAuth2SsoCustomConfiguration.class,
ResourceServerTokenServicesConfiguration.class })
public @interface EnableOAuth2Sso {
}
在定义@EnableOAuth2Sso注解时,又用到了另外三个注解@EnableOAuth2Client、@Import、@EnableConfigurationProperties,我们分别进行分析:
@EnableConfigurationProperties
通过@EnableConfigurationProperties(OAuth2SsoProperties.class)注解实现OAuth2SsoProperties属性对象的初始化,这里主要初始化了单点登录的登录页地址,即对应application.yml配置文件中的security.oauth2.sso.login-path属性值。
@ConfigurationProperties(prefix = "security.oauth2.sso")
public class OAuth2SsoProperties {
public static final String DEFAULT_LOGIN_PATH = "/login";
private String loginPath = DEFAULT_LOGIN_PATH;
public String getLoginPath() {
return this.loginPath;
}
public void setLoginPath(String loginPath) {
this.loginPath = loginPath;
}
}
@EnableOAuth2Client注解
@EnableOAuth2Client注解,主要是通过@Import注解引入了OAuth2ClientConfiguration配置类,实现如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(OAuth2ClientConfiguration.class)
public @interface EnableOAuth2Client {
}
在OAuth2ClientConfiguration配置类中,又通过@Bean注解,实现了OAuth2ClientContextFilter对象、AccessTokenRequest对象(生命周期为request)和OAuth2ClientContext对象三个对象的注入,具体实现可以查看OAuth2ClientConfiguration类的源码。
@Import
在@EnableOAuth2Sso上的@Import注解,主要用来加载OAuth2SsoDefaultConfiguration、OAuth2SsoCustomConfiguration和ResourceServerTokenServicesConfiguration三个配置类。其中OAuth2SsoDefaultConfiguration 是配置默认的WebSecurity相关配置,而OAuth2SsoCustomConfiguration是配置自定义的WebSecurity相关配置,ResourceServerTokenServicesConfiguration 是配置ResourceServerTokenServices相关内容。
而OAuth2SsoDefaultConfiguration和OAuth2SsoCustomConfiguration两个配置同时只会有一个生效,当@EnableOAuth2Sso注解在继承了WebSecurityConfigurerAdapter配置类的实现类上时,自定义配置类OAuth2SsoCustomConfiguration会生效,否则,默认的OAuth2SsoDefaultConfiguration配置类会生效。
这里我们先分析一下OAuth2SsoDefaultConfiguration配置类,其中根据NeedsWebSecurityCondition 决定了OAuth2SsoDefaultConfiguration 配置类是否生效,而NeedsWebSecurityCondition 和 EnableOAuth2SsoCondition 条件类生效正好相反,而EnableOAuth2SsoCondition则是OAuth2SsoCustomConfiguration配置类是否生效的的判断条件,所以两个配置类只会有一个生效。
@Configuration
@Conditional(NeedsWebSecurityCondition.class)
public class OAuth2SsoDefaultConfiguration extends WebSecurityConfigurerAdapter {
private final ApplicationContext applicationContext;
public OAuth2SsoDefaultConfiguration(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests().anyRequest().authenticated();
new SsoSecurityConfigurer(this.applicationContext).configure(http);
}
//
protected static class NeedsWebSecurityCondition extends EnableOAuth2SsoCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
return ConditionOutcome.inverse(super.getMatchOutcome(context, metadata));
}
}
}
在上述OAuth2SsoDefaultConfiguration 类的configure(HttpSecurity http)方法中,实现了默认的HttpSecurity 配置。实现了所有请求,都需要认证的默认配置,同时通过创建SsoSecurityConfigurer配置类增加了一些额外的配置。
//SsoSecurityConfigurer.java
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);
}
&esmp;在上述SsoSecurityConfigurer配置类configure(HttpSecurity http)方法中,首先获取了OAuth2SsoProperties 参数对象,前面已经完成了该对象的初始化。然后又通过oauth2SsoFilter()方法创建了OAuth2ClientAuthenticationProcessingFilter对象,该对象类似于UsernamePasswordAuthenticationFilter,主要用于Oauth2认证的过滤器。然后,又通过创建OAuth2ClientAuthenticationConfigurer配置类,把OAuth2ClientAuthenticationProcessingFilter对象设置到了SpringSecurity过滤器链中,最后再通过http.apply()方法把OAuth2ClientAuthenticationConfigurer配置对象应用到HttpSecurity 中。其中,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);
}
}
在SsoSecurityConfigurer配置类configure(HttpSecurity http)方法中,最后又通过addAuthenticationEntryPoint()方法,实现了AuthenticationEntryPoint对象的配置,其中为异常处理器配置默认配置了LoginUrlAuthenticationEntryPoint 和 HttpStatusEntryPoint两个AuthenticationEntryPoint对象,其中LoginUrlAuthenticationEntryPoint 对象 loginFormUrl参数还是用了前面配置的单点登录的地址。
&emp;而ResourceServerTokenServicesConfiguration 配置类,主要用来ResourceServerTokenServices相关内容,默认会初始化UserInfoTokenServices等对象。这个时候会使用到ResourceServerProperties参数对象。
在Oauth2初始化过程中,除了前面根据@EnableOAuth2Sso注解实现的一部分初始化内容外,还有一部分内容是通过SpringBoot加载META-INF/spring.factories配置文件进行加载的。
首先,在spring-security-oauth2-autoconfigure.jar中存在META-INF/spring.factories配置文件,内容如下:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.security.oauth2.OAuth2AutoConfiguration
根据SpringBoot运行机制,在SpringBoot项目启动时,就会加载OAuth2AutoConfiguration配置类。而配置类OAuth2AutoConfiguration的实现如下:
@Configuration
@ConditionalOnClass({ OAuth2AccessToken.class, WebMvcConfigurer.class })
@Import({ OAuth2AuthorizationServerConfiguration.class,
OAuth2MethodSecurityConfiguration.class, OAuth2ResourceServerConfiguration.class,
OAuth2RestOperationsConfiguration.class })
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(OAuth2ClientProperties.class)
public class OAuth2AutoConfiguration {
private final OAuth2ClientProperties credentials;
public OAuth2AutoConfiguration(OAuth2ClientProperties credentials) {
this.credentials = credentials;
}
@Bean
public ResourceServerProperties resourceServerProperties() {
return new ResourceServerProperties(this.credentials.getClientId(),
this.credentials.getClientSecret());
}
}
在OAuth2AutoConfiguration 配置类中,初始化了OAuth2ClientProperties、ResourceServerProperties 两个参数对象,其中OAuth2ClientProperties通过@EnableConfigurationProperties注解初始化,ResourceServerProperties 对象通过@Bean注解实现,并把OAuth2ClientProperties中的clientId和clientSecret作为参数传递其中。
通过@Import注解,又引入了OAuth2AuthorizationServerConfiguration、OAuth2MethodSecurityConfiguration、OAuth2ResourceServerConfiguration、OAuth2RestOperationsConfiguration四个配置类,分别对应授权服务、安全表达式处理器、资源服务器和客户端四类内容,如下所示:
其中,
当我们使用@EnableOAuth2Sso注解启用单点登录时,主要是启用了OAuth2RestOperationsConfiguration配置类,在该配置类中,注入了ClientCredentialsResourceDetails、DefaultOAuth2ClientContext和FilterRegistrationBean等对象,其中FilterRegistrationBean对象实现了OAuth2ClientContextFilter过滤器的添加,具体实现如下:
@Bean
public FilterRegistrationBean<OAuth2ClientContextFilter> oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter, SecurityProperties security) {
FilterRegistrationBean<OAuth2ClientContextFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(filter);
registration.setOrder(security.getFilter().getOrder() - 10);
return registration;
}
}
在这部分的初始化过程中,主要实现了把前面已经实例化的OAuth2ClientContextFilter过滤器对象添加到SpringMVC的过滤器链中,同时还注入了一个生命周期为request的DefaultOAuth2ClientContext对象。
当 访问http://localhost:8080/oauth2/index地址(未认证)时,首先,经过OAuth2ClientContextFilter过滤器,然后进入SpringSecurity过滤器链FilterChainProxy中,因为没有经过认证,所以会最终会跳转到登录页面,具体可以参考《未认证的请求是如何重定向到登录地址的?》。因为我们配置了单点登录的登录页地址"/github_login",所以跳转地址如下所示:
再次跳转访问"/github_login"地址时,经过OAuth2ClientContextFilter过滤器,然后进入SpringSecurity过滤器链FilterChainProxy中的OAuth2ClientAuthenticationProcessingFilter过滤器,这个时候因为访问的是登录地址,所以会执行到attemptAuthentication()方法进行处理,因为没有授权,所以在通过restTemplate.getAccessToken()获取token时,会抛出UserRedirectRequiredException异常,该异常会被OAuth2ClientContextFilter过滤器捕获,然后调用redirectUser()方法,最终跳转到了github的认证地址。
经过上述重定向,就跳转到了Github提供的授权认证界面,输入用户名密码,并进行登录。这个时候,验证成功后,又会跳回/github_login地址,,而且这次携带了一个state状态码。
这个时候,再次请求/github_login地址时,因为携带了state参数,所以这次再attemptAuthentication()方法中,通过调用restTemplate.getAccessToken()方法获取token时,就不会再抛出异常了,而是获取到了accessToken信息。然后继续执行attemptAuthentication()方法中后续代码,又调用tokenServices.loadAuthentication(accessToken.getValue());获取用户信息,这个时候就开始执行认证成功后的相关逻辑了。