# Spring Security 请求全过程解析
Spring Security 是一款基于 Spring 的安全框架,主要包含认证和授权两大安全模块,和另外一款流行的安全框架 Apache Shiro 相比,它拥有更为强大的功能。Spring Security 也可以轻松的自定义扩展以满足各种需求,并且对常见的 Web 安全攻击提供了防护支持。如果你的 Web 框架选择的是 Spring,那么在安全方面 Spring Security 会是一个不错的选择。
这里我们使用 Spring Boot 来集成 Spring Security,Spring Boot 版本为**_2.5.3_**,Spring Security 版本为**_5.5.1_**。
## 开启 Spring Security
使用 IDEA 创建一个 Spring Boot 项目,然后引入**_spring-boot-starter-security_**:
```java
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.projectlombok:lombok:1.18.8'
annotationProcessor 'org.projectlombok:lombok:1.18.8'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
```
接下来我们创建一个**_HelloController_**,对外提供一个/hello服务:
```java
@RestController
public class HelloController {
@GetMapping("hello")
public String hello() {
return "hello world";
}
}
```
这时候我们直接启动项目,访问http://localhost:8080/hello,可以看到页面跳转到一个登陆页面:
![image-20210811091508157](../../images/SpringSecurity/image-20210811091508157.png)
默认的用户名为 user,密码由 Sping Security 自动生成,回到 IDEA 的控制台,可以找到密码信息:
```java
Using generated security password: 4f06ba04-37e9-4bdd-a085-3305260da0d6
```
输入用户名 user,密码 4f06ba04-37e9-4bdd-a085-3305260da0d6 后,我们便可以成功访问/hello接口。
## 基本原理
Spring Security 默认为我们开启了一个简单的安全配置,下面让我们来了解其原理。
当 Spring Boot 项目配置了 Spring Security 后,Spring Security 的整个加载过程如下图所示:
![image-20210811091633434](../../images/SpringSecurity/image-20210811091633434.png)
而当我们访问http://localhost:8080/hello时,代码的整个执行过程如下图所示:
![image-20210811091659121](../../images/SpringSecurity/image-20210811091659121.png)
如上图所示,Spring Security 包含了众多的过滤器,这些过滤器形成了一条链,所有请求都必须通过这些过滤器后才能成功访问到资源。
下面我们通过 debug 来验证这个过程:
首先,通过前面可以知道,当有请求来到时,最先由**_DelegatingFilterProxy_**负责接收,因此在**_DelegatingFilterProxy_**的doFilter()的首行打上断点:
![image-20210811091719470](../../images/SpringSecurity/image-20210811091719470.png)
接着**_DelegatingFilterProxy_**会将请求委派给**_FilterChainProxy_**进行处理,在**_FilterChainProxy_**的首行打上断点:
![img](../../images/SpringSecurity/56ac5128-eab7-4b92-912f-ff50bac68a4f.png)
**_FilterChainProxy_**会在doFilterInternal()中生成一个内部类**_VirtualFilterChain_**的实例,以此来调用 Spring Security 的整条过滤器链,在**_VirtualFilterChain_**的doFilter()首行打上断点:
![image-20210811091755498](../../images/SpringSecurity/image-20210811091755498.png)
接下来**_VirtualFilterChain_**会通过**_currentPosition_**依次调用存在**_additionalFilters_**中的过滤器,其中比较重要的几个过滤器有:**_UsernamePasswordAuthenticationFilter_**、**_DefaultLoginPageGeneratingFilter_**、**_AnonymousAuthenticationFilter_**、**_ExceptionTranslationFilter_**、**_FilterSecurityInterceptor_**,我们依次在这些过滤器的doFilter()的首行打上断点:
![image-20210811091815473](../../images/SpringSecurity/image-20210811091815473.png)
准备完毕后,我们启动项目,然后访问http://localhost:8080/hello,程序首先跳转到**_DelegatingFilterProxy_**的断点上:
![image-20210811091833065](../../images/SpringSecurity/image-20210811091833065.png)
此时**_delegate_**还是 null 的,接下来依次执行代码,可以看到**_delegate_**最终被赋值一个**_FilterChainProxy_**的实例:
![img](../../images/SpringSecurity/f045b025-bd97-4222-8a02-51634be6745b.png)
接下来程序依次跳转到**_FilterChainProxy_**的doFilter()和**_VirtualFilterChain_**的doFilter()中:
![img](../../images/SpringSecurity/90d3e369-510f-45cb-982d-241d2eedb55c.png)
![image-20210811092048784](../../images/SpringSecurity/image-20210811092048784.png)
接着程序跳转到**_AbstractAuthenticationProcessingFilter_**(**_UsernamePasswordAuthenticationFilter_**的父类)的doFilter()中,通过requiresAuthentication()判定为 false(是否是 POST 请求):
![img](../../images/SpringSecurity/2e5440bc-9488-4213-a030-0d25153bb2ea.png)
接着程序跳转到**_DefaultLoginPageGeneratingFilter_**的doFilter()中,通过isLoginUrlRequest()判定为 false(请求路径是否是/login):
![img](../../images/SpringSecurity/47a7bca4-d858-4cb1-b126-347805b74053.png)
接着程序跳转到**_AnonymousAuthenticationFilter_**的doFilter()中,由于是首次请求,此时SecurityContextHolder.getContext().getAuthentication()为 null,因此会生成一个**_AnonymousAuthenticationToken_**的实例:
![img](../../images/SpringSecurity/6b1aded6-5229-47ba-b192-78a7c2622b8c.png)
接着程序跳转到**_ExceptionTranslationFilter_**的doFilter()中,**_ExceptionTranslationFilter_**负责处理**_FilterSecurityInterceptor_**抛出的异常,我们在 catch 代码块的首行打上断点:
**![img](../../images/SpringSecurity/8efa0b1c-2b32-4d5b-9655-985374326e10.png)**
接着程序跳转到**_FilterSecurityInterceptor_**的doFilter()中,依次执行代码后程序停留在其父类(**_AbstractSecurityInterceptor_**)的attemptAuthorization()中:
![img](../../images/SpringSecurity/d6e99143-6207-43a5-8d04-f0c81baa11b4.png)
**_accessDecisionManager_**是**_AccessDecisionManager_**(访问决策器)的实例,**_AccessDecisionManager_**主要有 3 个实现类:**_AffirmativeBased_**(一票通过),**ConsensusBased**(少数服从多数)、UnanimousBased(一票否决),此时**_AccessDecisionManager_**的的实现类是**_AffirmativeBased_**,我们可以看到程序进入**_AffirmativeBased_**的decide()中:
![img](../../images/SpringSecurity/6724647c-34ee-4a57-8cfa-b46f57400d14.png)
从上图可以看出,决策的关键在voter.vote(authentication, object, configAttributes)这句代码上,通过跟踪调试,程序最终进入**_AuthenticationTrustResolverImpl_**的isAnonymous()中:
![img](../../images/SpringSecurity/4beaa02f-a93d-4d95-9ad1-0d7213cb0e46.png)
isAssignableFrom()判断前者是否是后者的父类,而**_anonymousClass_**被固定为**_AnonymousAuthenticationToken.class_**,参数**_authentication_**由前面**_AnonymousAuthenticationFilter_**可以知道是**_AnonymousAuthenticationToken_**的实例,因此isAnonymous()返回 true,**_FilterSecurityInterceptor_**抛出**_AccessDeniedException_**异常,程序返回**_ExceptionTranslationFilter_**的 catch 块中:
![img](../../images/SpringSecurity/8e1ac9db-5987-484d-abf4-4c6535c60cc6.png)
接着程序会依次进入**_DelegatingAuthenticationEntryPoint_**、**_LoginUrlAuthenticationEntryPoint_**中,最后由**_LoginUrlAuthenticationEntryPoint_**的commence()决定重定向到/login:
![img](../../images/SpringSecurity/1b03bdd4-6773-4b39-a664-fdf65d104403.png)
后续对/login的请求同样会经过之前的执行流程,在**_DefaultLoginPageGeneratingFilter_**的doFilter()中,通过isLoginUrlRequest()判定为 true(请求路径是否是/login),直接返回**_login.html_**,也就是我们开头看到的登录页面。
当我们输入用户名和密码,点击**_Sign in_**,程序来到**_AbstractAuthenticationProcessingFilter_**的doFilter()中,通过requiresAuthentication()判定为 true(是否是 POST 请求),因此交给其子类**_UsernamePasswordAuthenticationFilter_**进行处理,**_UsernamePasswordAuthenticationFilter_**会将用户名和密码封装成一个**_UsernamePasswordAuthenticationToken_**的实例并进行校验,当校验通过后会将请求重定向到我们一开始请求的路径:/hello。
后续对/hello的请求经过过滤器链时就可以一路开绿灯直到最终交由**_HelloController_**返回"Hello World"。
## 参考
1. [Spring Security Reference](https://docs.spring.io/spring-security/site/docs/current/reference/html5/)
2. [Spring Boot 中开启 Spring Security](https://mrbird.cc/Spring-Boot&Spring-Security.html)