docs: update readme guideline

pull/100/head
yanglbme 4 years ago
parent 091a094deb
commit f1802196a4

@ -14,9 +14,11 @@
本项目主要用于记录框架及中间件源码的阅读经验、个人理解及解析,希望能够使阅读源码变成一件简单有趣,且有价值的事情,抽空更新中... (如果本项目对您有帮助,请 watch、star、fork 素质三连一波,鼓励一下作者,谢谢)
- Netlify: https://schunter.netlify.app
- Gitee Pages: https://doocs.gitee.io/source-code-hunter
- ~~Gitee Pages: https://doocs.gitee.io/source-code-hunter~~
- GitHub Pages: https://doocs.github.io/source-code-hunter
注:😶 Gitee Pages 站点遭 Gitee 官方误判为“包含违禁违规内容”,惨遭下线。
## Spring 系列
### IoC 容器

@ -1,10 +1,10 @@
# Spring Security自定义用户认证
# Spring Security 自定义用户认证
在**Spring Boot中开启Spring Security**一节中我们简单地搭建了一个Spring Boot + Spring Security的项目其中登录页、用户名和密码都是由Spring Security自动生成的。Spring Security支持我们自定义认证的过程如使用自定义的登录页替换默认的登录页用户信息的获取逻辑、登录成功或失败后的处理逻辑等。这里将在上一节的源码基础上进行改造。
在**Spring Boot 中开启 Spring Security**一节中我们简单地搭建了一个 Spring Boot + Spring Security 的项目,其中登录页、用户名和密码都是由 Spring Security 自动生成的。Spring Security 支持我们自定义认证的过程,如使用自定义的登录页替换默认的登录页,用户信息的获取逻辑、登录成功或失败后的处理逻辑等。这里将在上一节的源码基础上进行改造。
## 配置自定义登录页
为了方便起见,我们直接在<strong><i>src/main/resources/resources</strong></i>目录下创建一个<strong><i>login.html</strong></i>不需要Controller跳转
为了方便起见,我们直接在<strong><i>src/main/resources/resources</strong></i>目录下创建一个<strong><i>login.html</strong></i>(不需要 Controller 跳转):
```
<!DOCTYPE html>
@ -27,7 +27,7 @@
</html>
```
要怎么做才能让Spring Security跳转到我们自己定义的登录页面呢很简单只需要在<strong><i>BrowserSecurityConfig</strong></i><strong><i>configure</strong></i>中添加一些配置:
要怎么做才能让 Spring Security 跳转到我们自己定义的登录页面呢?很简单,只需要在<strong><i>BrowserSecurityConfig</strong></i><strong><i>configure</strong></i>中添加一些配置:
```
@Configuration
@ -46,14 +46,15 @@ public class BrowserConfig extends WebSecurityConfigurerAdapter {
}
```
上面代码中<strong><i>.loginPage("/login.html")</strong></i>指定了跳转到登录页面的请求URL<strong><i>.loginProcessingUrl("/login")</strong></i>对应登录页面form表单的<strong><i>action="/login"</strong></i><strong><i>.antMatchers("/login.html", "/css/", "/error").permitAll()</strong></i>表示跳转到登录页面的请求不被拦截。
上面代码中<strong><i>.loginPage("/login.html")</strong></i>指定了跳转到登录页面的请求 URL<strong><i>.loginProcessingUrl("/login")</strong></i>对应登录页面 form 表单的<strong><i>action="/login"</strong></i><strong><i>.antMatchers("/login.html", "/css/", "/error").permitAll()</strong></i>表示跳转到登录页面的请求不被拦截。
这时候启动系统,访问<strong><i>http://localhost:8080/hello</strong></i>,会看到页面已经被重定向到了<strong><i>http://localhost:8080/login.html</strong></i>
这时候启动系统,访问<strong><i>http://localhost:8080/hello</strong></i>,会看到页面已经被重定向到了<strong><i>http://localhost:8080/login.html</strong></i>
![img](../../images/SpringSecurity/d6bd19a2-08d3-4ba6-921c-5b5f57370a16.jpg)
## 配置用户信息的获取逻辑
Spring Security默认会为我们生成一个用户名为user密码随机的用户实例当然我们也可以定义自己用户信息的获取逻辑只需要实现Spring Security提供的***UserDetailService***接口即可,该接口只有一个抽象方法***loadUserByUsername***,具体实现如下:
Spring Security 默认会为我们生成一个用户名为 user密码随机的用户实例当然我们也可以定义自己用户信息的获取逻辑只需要实现 Spring Security 提供的**_UserDetailService_**接口即可,该接口只有一个抽象方法**_loadUserByUsername_**,具体实现如下:
```
@Service
@ -67,13 +68,13 @@ public class UserDetailService implements UserDetailsService {
}
```
通过以上配置我们定义了一个用户名随机密码统一为123456的用户信息的获取逻辑。这样当我们启动项目访问<strong><i>http://localhost:8080/login</strong></i>只需要输入任意用户名以及123456作为密码即可登录系统。
通过以上配置,我们定义了一个用户名随机,密码统一为 123456 的用户信息的获取逻辑。这样,当我们启动项目,访问<strong><i>http://localhost:8080/login</strong></i>,只需要输入任意用户名以及 123456 作为密码即可登录系统。
## 源码解析
### BrowserConfig配置解析
### BrowserConfig 配置解析
我们首先来梳理下<strong><i>BrowserConfig</strong></i>中的配置是如何被Spring Security所加载的。
我们首先来梳理下<strong><i>BrowserConfig</strong></i>中的配置是如何被 Spring Security 所加载的。
首先找到调用<strong><i>BrowserConfig</strong></i><strong><i>configure()</strong></i>的地方,在其父类<strong><i>WebSecurityConfigurerAdapter</strong></i><strong><i>getHttp()</strong></i>中:
@ -103,7 +104,7 @@ public class UserDetailService implements UserDetailsService {
![img](../../images/SpringSecurity/eb7a5916-4049-4682-9a11-10f1f1f94c74.png)
我们重点看第二句代码可以看到这里会提取存储在bean工厂中类型为<strong><i>WebSecurityConfigurer.class</strong></i>的bean<strong><i>BrowserConfig</strong></i>正是<strong><i>WebSecurityConfigurerAdapter</strong></i>的实现类。
我们重点看第二句代码,可以看到这里会提取存储在 bean 工厂中类型为<strong><i>WebSecurityConfigurer.class</strong></i> bean<strong><i>BrowserConfig</strong></i>正是<strong><i>WebSecurityConfigurerAdapter</strong></i>的实现类。
解决完<strong><i>configurers</strong></i>的赋值问题,我们回到<strong><i>AbstractConfiguredSecurityBuilder</strong></i><strong><i>init()</strong></i>处,找到调用该方法的地方,在同个类的<strong><i>doBuild()</strong></i>中:
@ -117,7 +118,7 @@ public class UserDetailService implements UserDetailsService {
![img](../../images/SpringSecurity/a5c61feb-ca72-4768-94bd-1b0a8cf8af70.png)
至此,我们分析完了<strong><i>BrowserConfig</strong></i>被Spring Security加载的过程。现在我们再来看看当我们自定义的配置被加载完后<strong><i>http</strong></i>各属性的变化,在<strong><i>BrowserConfig</strong></i><strong><i>configure()</strong></i>末尾打上断点,当程序走到断点处时,查看<strong><i>http</strong></i>属性:
至此,我们分析完了<strong><i>BrowserConfig</strong></i> Spring Security 加载的过程。现在我们再来看看当我们自定义的配置被加载完后,<strong><i>http</strong></i>各属性的变化,在<strong><i>BrowserConfig</strong></i><strong><i>configure()</strong></i>末尾打上断点,当程序走到断点处时,查看<strong><i>http</strong></i>属性:
![img](../../images/SpringSecurity/6c72c09b-742c-4415-851a-8ca5292a4969.png)
@ -137,11 +138,11 @@ public class UserDetailService implements UserDetailsService {
![img](../../images/SpringSecurity/4612c27e-dd9f-4e60-92dc-fc9858496ec5.png)
### login.html路径解析
### login.html 路径解析
当我们请求的资源需要经过认证时Spring Security会将请求重定向到我们自定义的登录页那么Spring又是如何找到我们自定义的登录页的呢下面就让我们来解析一下
当我们请求的资源需要经过认证时Spring Security 会将请求重定向到我们自定义的登录页,那么 Spring 又是如何找到我们自定义的登录页的呢?下面就让我们来解析一下:
我们首先来到<strong><i>DispatcherServlet</strong></i>中,<strong><i>DispatcherServlet</strong></i>是Spring Web处理请求的入口。当Spring Web项目启动后第一次接收到请求时会调用其<strong><i>initStrategies()</strong></i>进行初始化:
我们首先来到<strong><i>DispatcherServlet</strong></i>中,<strong><i>DispatcherServlet</strong></i> Spring Web 处理请求的入口。当 Spring Web 项目启动后,第一次接收到请求时,会调用其<strong><i>initStrategies()</strong></i>进行初始化:
![img](../../images/SpringSecurity/964ec4a4-6039-4205-8a87-ea2febcc00b6.png)
@ -149,7 +150,7 @@ public class UserDetailService implements UserDetailsService {
![img](../../images/SpringSecurity/0a97b011-34ed-4d57-945e-95c8e6bafc8e.png)
可以看到,当程序走到<strong><i>initHandlerMappings()</strong></i>中时会从bean工厂中找出<strong><i>HandlerMapping.class</strong></i>类型的bean将其存储到<strong><i>handlerMappings</strong></i>属性中。这里看到一共找到5个分别是<strong><i>requestMappingHandlerMapping</strong></i>(将请求与标注了<strong><i>@RequestMapping</strong></i>的方法进行关联)、<strong><i>weclomePageHandlerMapping</strong></i>(将请求与主页进行关联)、<strong><i>beanNameHandlerMapping</strong></i>将请求与同名的bean进行关联<strong><i>routerFunctionMapping</strong></i>(将请求与<strong><i>RouterFunction</strong></i>进行关联)、<strong><i>resourceHandlerMapping</strong></i>将请求与静态资源进行关联这5个bean是在<strong><i>WebMvcAutoConfiguration$EnableWebMvcConfiguration</strong></i>中配置的:
可以看到,当程序走到<strong><i>initHandlerMappings()</strong></i>中时,会从 bean 工厂中找出<strong><i>HandlerMapping.class</strong></i>类型的 bean将其存储到<strong><i>handlerMappings</strong></i>属性中。这里看到一共找到 5 个,分别是:<strong><i>requestMappingHandlerMapping</strong></i>(将请求与标注了<strong><i>@RequestMapping</strong></i>的方法进行关联)、<strong><i>weclomePageHandlerMapping</strong></i>(将请求与主页进行关联)、<strong><i>beanNameHandlerMapping</strong></i>(将请求与同名的 bean 进行关联)、<strong><i>routerFunctionMapping</strong></i>(将请求与<strong><i>RouterFunction</strong></i>进行关联)、<strong><i>resourceHandlerMapping</strong></i>(将请求与静态资源进行关联),这 5 bean 是在<strong><i>WebMvcAutoConfiguration$EnableWebMvcConfiguration</strong></i>中配置的:
<strong><i>requestMappingHandlerMapping:</strong></i>
@ -177,11 +178,11 @@ public class UserDetailService implements UserDetailsService {
![img](../../images/SpringSecurity/6eca7b58-80f9-4e98-8483-62b4ef751854.png)
可以看到这里实际调用的是<strong><i>configurers</strong></i>属性的<strong><i>addResourceHandlers()</strong></i>,而<strong><i>configurers</strong></i>是一个final类型的成员变量其值是<strong><i>WebMvcConfigurerComposite</strong></i>的实例,我们来到<strong><i>WebMvcConfigurerComposite</strong></i>中:
可以看到这里实际调用的是<strong><i>configurers</strong></i>属性的<strong><i>addResourceHandlers()</strong></i>,而<strong><i>configurers</strong></i>是一个 final 类型的成员变量,其值是<strong><i>WebMvcConfigurerComposite</strong></i>的实例,我们来到<strong><i>WebMvcConfigurerComposite</strong></i>中:
![img](../../images/SpringSecurity/c365e5ab-a7a3-4ebf-8a25-09cd2e049f22.png)
可以看到这里实际调用的是<strong><i>delegates</strong></i>属性的<strong><i>addResourceHandlers()</strong></i><strong><i>delegates</strong></i>是一个final类型的集合集合的元素由<strong><i>addWebMvcConfigurers()</strong></i>负责添加:
可以看到这里实际调用的是<strong><i>delegates</strong></i>属性的<strong><i>addResourceHandlers()</strong></i><strong><i>delegates</strong></i>是一个 final 类型的集合,集合的元素由<strong><i>addWebMvcConfigurers()</strong></i>负责添加:
![img](../../images/SpringSecurity/895afead-ea6b-4ac6-8138-fbe0a223daf9.png)
@ -189,11 +190,11 @@ public class UserDetailService implements UserDetailsService {
![img](../../images/SpringSecurity/a20e06a4-5a43-4edf-bea1-53fc9bc929e9.png)
可以看到当<strong><i>setConfigurers()</strong></i>被初始化时Spring会往参数<strong><i>configurers</strong></i>中传入两个值,我们关注第一个值,是一个<strong><i>WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter</strong></i>的实例,注意它的属性<strong><i>resourceProperties</strong></i>,是一个<strong><i>WebProperties$Resources</strong></i>的实例,默认情况下,在实例化<strong><i>WebMvcAutoConfigurationAdapter</strong></i>时,由传入参数<strong><i>webProperties</strong></i>进行赋值:<strong><i>webProperties.getResources()</strong></i>
可以看到当<strong><i>setConfigurers()</strong></i>被初始化时Spring 会往参数<strong><i>configurers</strong></i>中传入两个值,我们关注第一个值,是一个<strong><i>WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter</strong></i>的实例,注意它的属性<strong><i>resourceProperties</strong></i>,是一个<strong><i>WebProperties$Resources</strong></i>的实例,默认情况下,在实例化<strong><i>WebMvcAutoConfigurationAdapter</strong></i>时,由传入参数<strong><i>webProperties</strong></i>进行赋值:<strong><i>webProperties.getResources()</strong></i>
![img](../../images/SpringSecurity/97b6f22c-414d-4a16-aa8e-c3deece2f7cd.png)
我们进入参数<strong><i>webProperties</strong></i>的类中,可以看到<strong><i>getResources()</strong></i>是直接实例化了一个<strong><i>Resources</strong></i>,其属性<strong><i>staticLocations</strong></i>是一个含有4个值的final类型的字符串数组这4个值正是Spring寻找静态文件的地方
我们进入参数<strong><i>webProperties</strong></i>的类中,可以看到<strong><i>getResources()</strong></i>是直接实例化了一个<strong><i>Resources</strong></i>,其属性<strong><i>staticLocations</strong></i>是一个含有 4 个值的 final 类型的字符串数组,这 4 个值正是 Spring 寻找静态文件的地方:
![img](../../images/SpringSecurity/4d84fe43-2646-4a6f-a580-f39f6416d02d.png)
@ -203,7 +204,7 @@ public class UserDetailService implements UserDetailsService {
![img](../../images/SpringSecurity/ccd16b38-d724-4c03-949e-3b6ba03268a9.png)
<strong><i>addResourceHandlers()</strong></i>中,会为<strong><i>registry</strong></i>添加两个资源处理器,当请求路径是“/webjars/”时会在”classpath:/META-INF/resources/webjars/“路径下寻找对应的资源,当请求路径是“/**”时会在”classpath:/META-INF/resources/“、”classpath:/resources/“、”classpath:/static/“、”classpath:/public/“路径下寻找对应的资源。
<strong><i>addResourceHandlers()</strong></i>中,会为<strong><i>registry</strong></i>添加两个资源处理器,当请求路径是“/webjars/”时会在”classpath:/META-INF/resources/webjars/“路径下寻找对应的资源,当请求路径是“/\*\*”时会在”classpath:/META-INF/resources/“、”classpath:/resources/“、”classpath:/static/“、”classpath:/public/“路径下寻找对应的资源。
现在我们通过访问<strong><i>http://localhost:8080/login.html</strong></i>来验证这个过程。
@ -243,75 +244,74 @@ public class UserDetailService implements UserDetailsService {
最终,类加载器会在如上两个路径下找到登录页并返回。
### UserDetailService配置解析
### UserDetailService 配置解析
我们定义的用户信息的获取逻辑是如何被Spring Security应用的呢让我们通过阅读源码来了解一下。
我们定义的用户信息的获取逻辑是如何被 Spring Security 应用的呢?让我们通过阅读源码来了解一下。
还记得前面我们讲***BrowserConfig配置***被加载的过程吗?***UserDetailService***也是在这个过程中被一起加载完成的,回到**BrowserConfig配置解析**的第一幅图中,如下:
还记得前面我们讲**_BrowserConfig 配置_**被加载的过程吗?**_UserDetailService_**也是在这个过程中被一起加载完成的,回到**BrowserConfig 配置解析**的第一幅图中,如下:
![img](../../images/SpringSecurity/68490740-e03c-4353-b5fc-ac99c0cf0435.png)
在断点处位置,<strong><i>authenticationManager()</strong></i>会返回一个***AuthenticationManager***实例,我们进入<strong><i>authenticationManager()</strong></i>中:
在断点处位置,<strong><i>authenticationManager()</strong></i>会返回一个**_AuthenticationManager_**实例,我们进入<strong><i>authenticationManager()</strong></i>中:
![img](../../images/SpringSecurity/e7a1a684-64db-41d0-a5c0-d9a841d86cc1.png)
<strong><i>authenticationManager()</strong></i>中,***AuthenticationManager***转由***AuthenticationConfiguration***中获取,我们进入<strong><i>getAuthenticationManager()</strong></i>中:
<strong><i>authenticationManager()</strong></i>中,**_AuthenticationManager_**转由**_AuthenticationConfiguration_**中获取,我们进入<strong><i>getAuthenticationManager()</strong></i>中:
![img](../../images/SpringSecurity/d9ae84ae-d60c-4d9c-a7fd-cddeb1142f95.png)
程序来到***AuthenticationConfiguration***的<strong><i>getAuthenticationManager()</strong></i>中,***AuthenticationManager***转由***AuthenticationManagerBuilder***中获取,我们进入<strong><i>build()</strong></i>中:
程序来到**_AuthenticationConfiguration_**的<strong><i>getAuthenticationManager()</strong></i>中,**_AuthenticationManager_**转由**_AuthenticationManagerBuilder_**中获取,我们进入<strong><i>build()</strong></i>中:
![img](../../images/SpringSecurity/19f71152-f456-4db7-a1d5-f79aaa37253b.png)
程序来到***AbstractConfiguredSecurityBuilder***的<strong><i>doBuild()</strong></i>中,这里在构建***AuthenticationManager***实例时需要初始化3个配置类我们重点关注第3个配置类***org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer***,这个配置类是在***AuthenticationConfiguration***中引入的:
程序来到**_AbstractConfiguredSecurityBuilder_**的<strong><i>doBuild()</strong></i>中,这里在构建**_AuthenticationManager_**实例时,需要初始化 3 个配置类,我们重点关注第 3 个配置类:**_org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer_**,这个配置类是在**_AuthenticationConfiguration_**中引入的:
![img](../../images/SpringSecurity/9f06c823-645d-413b-8bb6-1d81b8f329ea.png)
我们来到***InitializeUserDetailsBeanManagerConfigurer***的<strong><i>init()</strong></i>中:
我们来到**_InitializeUserDetailsBeanManagerConfigurer_**的<strong><i>init()</strong></i>中:
![img](../../images/SpringSecurity/cd7d6cb9-c6e7-4570-aab8-309adcb15e16.png)
这里会新建一个***InitializeUserDetailsManagerConfigurer***实例添加到***AuthenticationManagerBuilder***中。我们回到<strong><i>doBuild()</strong></i>中:
这里会新建一个**_InitializeUserDetailsManagerConfigurer_**实例添加到**_AuthenticationManagerBuilder_**中。我们回到<strong><i>doBuild()</strong></i>中:
![img](../../images/SpringSecurity/3ea76980-417d-4c0c-9330-e0bb241c6a47.png)
可以看到配置类变成了5个其中就有刚刚新建的***InitializeUserDetailsManagerConfigurer***,程序接下来会调用各个配置类的<strong><i>configure()</strong></i>进行配置,我们来到***InitializeUserDetailsManagerConfigurer***的<strong><i>configure()</strong></i>中:
可以看到配置类变成了 5 个,其中就有刚刚新建的**_InitializeUserDetailsManagerConfigurer_**,程序接下来会调用各个配置类的<strong><i>configure()</strong></i>进行配置,我们来到**_InitializeUserDetailsManagerConfigurer_**的<strong><i>configure()</strong></i>中:
![img](../../images/SpringSecurity/ec39a9f6-c97d-4b7d-8843-d20358c1d194.png)
可以看到在<strong><i>configure()</strong></i>中,就会去bean工厂中寻找***UserDetailsService***类型的bean若是我们没有自定义***UserDetailsService***的实现类的话Spring Security默认会生成一个***InMemoryUserDetailsManager***的实例:
可以看到在<strong><i>configure()</strong></i>中,就会去 bean 工厂中寻找**_UserDetailsService_**类型的 bean若是我们没有自定义**_UserDetailsService_**的实现类的话Spring Security 默认会生成一个**_InMemoryUserDetailsManager_**的实例:
![img](../../images/SpringSecurity/c6a7370c-4afb-4c5d-aa35-ba9c3406b1ed.png)
***InMemoryUserDetailsManager***是在***UserDetailsServiceAutoConfiguration***类中配置的:
**_InMemoryUserDetailsManager_**是在**_UserDetailsServiceAutoConfiguration_**类中配置的:
![img](../../images/SpringSecurity/476f8954-abe3-4e26-bfe1-8c5b4abbf0e0.png)
解决完***UserDetailsService***的加载问题现在我们来看看Spring Security是如何通过***UserDetailsService***获取用户信息的。
解决完**_UserDetailsService_**的加载问题,现在我们来看看 Spring Security 是如何通过**_UserDetailsService_**获取用户信息的。
通过**Spring Boot中开启Spring Security**一节的学习我们知道,登录判断的逻辑是在***UsernamePasswordAuthenticationFilter***中进行的,因此我们在***UsernamePasswordAuthenticationFilter***的<strong><i>attemptAuthenticatio()</strong></i>中打上断点,然后启动项目,访问登录页,输入用户名和密码点击登录后,程序来到***UsernamePasswordAuthenticationFilter***中:
通过**Spring Boot 中开启 Spring Security**一节的学习我们知道,登录判断的逻辑是在**_UsernamePasswordAuthenticationFilter_**中进行的,因此我们在**_UsernamePasswordAuthenticationFilter_**的<strong><i>attemptAuthenticatio()</strong></i>中打上断点,然后启动项目,访问登录页,输入用户名和密码点击登录后,程序来到**_UsernamePasswordAuthenticationFilter_**中:
![img](../../images/SpringSecurity/1282014b-fc29-4c2b-9316-9fdd638653c9.png)
这里将验证的逻辑交由***AuthenticationManager***进行,我们进入<strong><i>authenticate()</strong></i>中:
这里将验证的逻辑交由**_AuthenticationManager_**进行,我们进入<strong><i>authenticate()</strong></i>中:
![img](../../images/SpringSecurity/5d511c93-3614-40e0-b3c9-9673c573d60f.png)
程序来到***ProviderManager***的<strong><i>authenticate()</strong></i>中,这里将验证的逻辑委托给其父类进行,再次点击进入<strong><i>authenticate()</strong></i>中:
程序来到**_ProviderManager_**的<strong><i>authenticate()</strong></i>中,这里将验证的逻辑委托给其父类进行,再次点击进入<strong><i>authenticate()</strong></i>中:
![img](../../images/SpringSecurity/8dddd63e-c567-4b41-a9d9-8ef8aa6f2a92.png)
这里将验证的逻辑交由***AuthenticationProvider***进行,我们进入<strong><i>authenticate()</strong></i>中:
这里将验证的逻辑交由**_AuthenticationProvider_**进行,我们进入<strong><i>authenticate()</strong></i>中:
![img](../../images/SpringSecurity/ec796a9b-7c65-49a2-9f9f-7685af7bd57b.png)
程序来到***AbstractUserDetailsAuthenticationProvider***的<strong><i>authenticate()</strong></i>中,这里会根据用户名去寻找对应的用户实例,我们进入<strong><i>retrieveUser()</strong></i>中:
程序来到**_AbstractUserDetailsAuthenticationProvider_**的<strong><i>authenticate()</strong></i>中,这里会根据用户名去寻找对应的用户实例,我们进入<strong><i>retrieveUser()</strong></i>中:
![img](../../images/SpringSecurity/3980e264-c073-456a-b808-715edd85633a.png)
程序来到***DaoAuthenticationProvider***的<strong><i>retrieveUser()</strong></i>中,可以看到正是在这里,会从***UserDetailsService***的<strong><i>loadUserByUsername()</strong></i>中寻找对应的用户信息。
程序来到**_DaoAuthenticationProvider_**的<strong><i>retrieveUser()</strong></i>中,可以看到正是在这里,会从**_UserDetailsService_**的<strong><i>loadUserByUsername()</strong></i>中寻找对应的用户信息。
## 参考
1. [Spring Security自定义用户认证](https://mrbird.cc/Spring-Security-Authentication.html)
1. [Spring Security 自定义用户认证](https://mrbird.cc/Spring-Security-Authentication.html)

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

Loading…
Cancel
Save