SpringSecurity5 附加篇:实现 Github 第三方登陆

回到序章

来来来,点这

Github 申请需要的东西

  • 进入 Github-OAuthApp页面
  • 点击 New OAuth App
  • 填入需要的参数
    Github-OAuthApp创建页面
  • 点击 Register application
    Github-OAuthApp详细页面
  • 点击 Generate a new client secret
    Github-OAuthApp生成secret值

SpringBoot 程序代码

导入需要的依赖包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@SpringBootApplication
@Controller
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

// 测试用,登录后才能请求
@GetMapping("/test")
public String test() {
return "test";
}
}

security 配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import java.nio.charset.StandardCharsets;

@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(a -> a
.anyRequest().authenticated()
)
.oauth2Login()
;
}
}

配置文件

1
2
3
4
5
6
7
8
spring:
security:
oauth2:
client:
registration:
github:
clientId: client-id
clientSecret: client-secret

启动,测试

启动项目,访问 http://localhost:8080/login 会进入登陆界面,或者直接访问 http://localhost:8080/test 会直接进行 github 登陆。

Github-OAuth-springboot登陆页面

代码上传的 Github 仓库

Emmmm…. 跟上面有些许出入

  • 增加了自定义 login 页面
  • 自定义 redirectUri

源码解析

启动的时候,观察比较日志,有这么一段

增加 security 配置之前

1
2021-03-29 18:20:44.444  INFO 17200 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@32118208, org.springframework.security.web.context.SecurityContextPersistenceFilter@7bcecef6, org.springframework.security.web.header.HeaderWriterFilter@de7e193, org.springframework.security.web.csrf.CsrfFilter@2582b0ef, org.springframework.security.web.authentication.logout.LogoutFilter@34d713a2, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@22ad1bae, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@36aab105, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@414f87a9, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@46e3559f, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@93824eb, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5a97b17c, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@1ddc6db2, org.springframework.security.web.session.SessionManagementFilter@35ef439e, org.springframework.security.web.access.ExceptionTranslationFilter@74606204, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@a146b11]

增加 security 配置之后

1
2021-03-29 18:19:00.218  INFO 9972 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@6393bf8b, org.springframework.security.web.context.SecurityContextPersistenceFilter@706ddbc8, org.springframework.security.web.header.HeaderWriterFilter@478b0739, org.springframework.security.web.csrf.CsrfFilter@6ea246af, org.springframework.security.web.authentication.logout.LogoutFilter@1450131a, org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter@25a94b55, org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter@6adc5b9c, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@2d2b6960, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@76d7881e, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@144ab983, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@77429040, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@4aa31ffc, org.springframework.security.web.session.SessionManagementFilter@51f95f0d, org.springframework.security.web.access.ExceptionTranslationFilter@7edb6fca, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1c57f6b2]

对比之后,可以发现多了两个 Filter

1
2
org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter
org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter

在解析前,先了解一下整个的过程:

  1. 用户访问 /login ,然后点击 Github 的按钮请求 Github 授权
  2. 跳转到 Github 的授权页面,填入信息,点击授权
  3. 授权后,Github 将必要的参数加上,然后调用 Github 应用中填写的回调地址
  4. 拿着参数去 Github 获取令牌

进入 OAuth2AuthorizationRequestRedirectFilter 查看其处理的逻辑

OAuth2AuthorizationRequestRedirectFilter处理截图

可以看到是通过 authorizationRequestResolver 解析器解析请求,再往上翻翻,看一下初始化的代码,可以看到默认实现是 DefaultOAuth2AuthorizationRequestResolver

OAuth2AuthorizationRequestRedirectFilter初始化截图

进入 DefaultOAuth2AuthorizationRequestResolver 看看

DefaultOAuth2AuthorizationRequestResolver处理截图

如截图所示,组装完各种参数后,会跳转到 Github 的授权页面,参考 OAuth2AuthorizationRequestRedirectFilter 的处理的后续代码,上面源码解析的第一张截图中也有这部分代码

1
2
3
4
if (authorizationRequest != null) {
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}

如此,过程的 第一步 便结束了;

第二步是纯 Github 的授权,跟我们没啥关系;

看第三步,会请求回调地址

此时便是看 OAuth2LoginAuthenticationFilter 了,这个就是响应授权服务器的回调地址,根据构造函数,可以看到默认情况下监听的是 /login/oauth2/code/* 请求

OAuth2LoginAuthenticationFilter初始化截图

由于它继承 AbstractAuthenticationProcessingFilter,所以咱就直接看 attemptAuthentication 方法是它的处理逻辑

OAuth2LoginAuthenticationFilter处理截图

其实做的事情也很简单,就是进行授权结果的解剖,再封装成为 Authentication 认证对象。

贴一下请求的过程

这里是少了输入 Github 账号密码的请求的,因为麻烦,懒得截。

Github 回调前

Gitihub-OAuth登陆请求过程

Github 回调后

Gitihub-OAuth登陆回调后请求过程

默认提供的第三方

在 maven 工具可以看到 spring-boot-starter-oauth2-client 的依赖包如下:

oauth2-client依赖的包

其中有个 config 的 Jar 包,进去之后找到 org.springframework.security.config.oauth2.client,里面有个枚举是 CommonOAuth2Provider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public enum CommonOAuth2Provider {
GOOGLE {
public Builder getBuilder(String registrationId) {
Builder builder = this.getBuilder(registrationId, ClientAuthenticationMethod.BASIC, "{baseUrl}/{action}/oauth2/code/{registrationId}");
builder.scope(new String[]{"openid", "profile", "email"});
builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth");
builder.tokenUri("https://www.googleapis.com/oauth2/v4/token");
builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs");
builder.issuerUri("https://accounts.google.com");
builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo");
builder.userNameAttributeName("sub");
builder.clientName("Google");
return builder;
}
},
GITHUB {
public Builder getBuilder(String registrationId) {
Builder builder = this.getBuilder(registrationId, ClientAuthenticationMethod.BASIC, "{baseUrl}/{action}/oauth2/code/{registrationId}");
builder.scope(new String[]{"read:user"});
builder.authorizationUri("https://github.com/login/oauth/authorize");
builder.tokenUri("https://github.com/login/oauth/access_token");
builder.userInfoUri("https://api.github.com/user");
builder.userNameAttributeName("id");
builder.clientName("GitHub");
return builder;
}
},
FACEBOOK {
public Builder getBuilder(String registrationId) {
Builder builder = this.getBuilder(registrationId, ClientAuthenticationMethod.POST, "{baseUrl}/{action}/oauth2/code/{registrationId}");
builder.scope(new String[]{"public_profile", "email"});
builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth");
builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token");
builder.userInfoUri("https://graph.facebook.com/me?fields=id,name,email");
builder.userNameAttributeName("id");
builder.clientName("Facebook");
return builder;
}
},
OKTA {
public Builder getBuilder(String registrationId) {
Builder builder = this.getBuilder(registrationId, ClientAuthenticationMethod.BASIC, "{baseUrl}/{action}/oauth2/code/{registrationId}");
builder.scope(new String[]{"openid", "profile", "email"});
builder.userNameAttributeName("sub");
builder.clientName("Okta");
return builder;
}
};

private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}";

private CommonOAuth2Provider() {
}

protected final Builder getBuilder(String registrationId, ClientAuthenticationMethod method, String redirectUri) {
Builder builder = ClientRegistration.withRegistrationId(registrationId);
builder.clientAuthenticationMethod(method);
builder.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE);
builder.redirectUri(redirectUri);
return builder;
}

public abstract Builder getBuilder(String var1);
}