318 lines
23 KiB
Markdown
318 lines
23 KiB
Markdown
# 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 跳转):
|
||
|
||
```
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>登录</title>
|
||
<link rel="stylesheet" href="css/login.css" type="text/css">
|
||
</head>
|
||
<body>
|
||
<form class="login-page" action="/login" method="post">
|
||
<div class="form">
|
||
<h3>账户登录</h3>
|
||
<input type="text" placeholder="用户名" name="username" required="required" />
|
||
<input type="password" placeholder="密码" name="password" required="required" />
|
||
<button type="submit">登录</button>
|
||
</div>
|
||
</form>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
要怎么做才能让 Spring Security 跳转到我们自己定义的登录页面呢?很简单,只需要在<strong><i>BrowserSecurityConfig</strong></i>的<strong><i>configure</strong></i>中添加一些配置:
|
||
|
||
```
|
||
@Configuration
|
||
public class BrowserConfig extends WebSecurityConfigurerAdapter {
|
||
@Override
|
||
protected void configure(HttpSecurity http) throws Exception {
|
||
http.formLogin() // 表单登录
|
||
.loginPage("/login.html") // 自定义登录页
|
||
.loginProcessingUrl("/login") // 登录认证路径
|
||
.and()
|
||
.authorizeRequests() // 授权配置
|
||
.antMatchers("/login.html", "/css/</strong></i>", "/error").permitAll() // 无需认证
|
||
.anyRequest().authenticated() // 除antMatchers中配置路径外其他所有请求都需要认证
|
||
.and().csrf().disable();
|
||
}
|
||
}
|
||
```
|
||
|
||
上面代码中<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>:
|
||
|
||

|
||
|
||
## 配置用户信息的获取逻辑
|
||
|
||
Spring Security 默认会为我们生成一个用户名为 user,密码随机的用户实例,当然我们也可以定义自己用户信息的获取逻辑,只需要实现 Spring Security 提供的**_UserDetailService_**接口即可,该接口只有一个抽象方法**_loadUserByUsername_**,具体实现如下:
|
||
|
||
```
|
||
@Service
|
||
public class UserDetailService implements UserDetailsService {
|
||
@Autowired
|
||
private PasswordEncoder passwordEncoder;
|
||
@Override
|
||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||
return new User(username, passwordEncoder.encode("123456"), AuthorityUtils.createAuthorityList("admin"));
|
||
}
|
||
}
|
||
```
|
||
|
||
通过以上配置,我们定义了一个用户名随机,密码统一为 123456 的用户信息的获取逻辑。这样,当我们启动项目,访问<strong><i>http://localhost:8080/login</strong></i>,只需要输入任意用户名以及 123456 作为密码即可登录系统。
|
||
|
||
## 源码解析
|
||
|
||
### BrowserConfig 配置解析
|
||
|
||
我们首先来梳理下<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>中:
|
||
|
||

|
||
|
||
往上一步找到调用<strong><i>getHttp()</strong></i>的地方,在同个类的<strong><i>init()</strong></i>中:
|
||
|
||

|
||
|
||
往上一步找到调用<strong><i>init()</strong></i>的地方,在<strong><i>AbstractConfiguredSecurityBuilder</strong></i>的<strong><i>init()</strong></i>中:
|
||
|
||

|
||
|
||
在<strong><i>init()</strong></i>被调用时,它首先会遍历<strong><i>getConfigurers()</strong></i>返回的集合中的元素,调用其<strong><i>init()</strong></i>,点击<strong><i>getConfigurers()</strong></i>查看,发现其读取的是<strong><i>configurers</strong></i>属性的值,那么<strong><i>configurers</strong></i>是什么时候被赋值的呢?我们在同个类的<strong><i>add()</strong></i>中找到<strong><i>configurers</strong></i>被赋值的代码:
|
||
|
||

|
||
|
||
往上一步找到调用<strong><i>add()</strong></i>的地方,在同个类的<strong><i>apply()</strong></i>中:
|
||
|
||

|
||
|
||
往上一步找到调用<strong><i>apply()</strong></i>的地方,在<strong><i>WebSecurityConfiguration</strong></i>的<strong><i>setFilterChainProxySecurityConfigurer()</strong></i>中:
|
||
|
||

|
||
|
||
我们可以看到,在<strong><i>setFilterChainProxySecurityConfigurer()</strong></i>中,首先会实例化一个<strong><i>WebSecurity</strong></i>(<strong><i>AbstractConfiguredSecurityBuilder</strong></i>的实现类)的实例,遍历参数<strong><i>webSecurityConfigurers</strong></i>,将存储在其中的元素作为参数传递给<strong><i>WebSecurity</strong></i>的<strong><i>apply()</strong></i>,那么<strong><i>webSecurityConfigurers</strong></i>是什么时候被赋值的呢?我们根据<strong><i>@Value</strong></i>中的信息找到<strong><i>webSecurityConfigurers</strong></i>被赋值的地方,在<strong><i>AutowiredWebSecurityConfigurersIgnoreParents</strong></i>的<strong><i>getWebSecurityConfigurers()</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>中:
|
||
|
||

|
||
|
||
往上一步找到调用<strong><i>doBuild()</strong></i>的地方,在<strong><i>AbstractSecurityBuilder</strong></i>的<strong><i>build()</strong></i>中:
|
||
|
||

|
||
|
||
往上一步找到调用<strong><i>doBuild()</strong></i>的地方,在<strong><i>WebSecurityConfiguration</strong></i>的<strong><i>springSecurityFilterChain()</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>属性:
|
||
|
||

|
||
|
||
我们配置的<strong><i>.loginPage("/login.html")</strong></i>和<strong><i>.loginProcessingUrl("/login")</strong></i>在<strong><i>FormLoginConfigurer</strong></i>中:
|
||
|
||

|
||
|
||
配置的<strong><i>.antMatchers("/login.html", "/css/", "/error").permitAll()</strong></i>在<strong><i>ExpressionUrlAuthorizationConfigurer</strong></i>中:
|
||
|
||

|
||
|
||
这样,当我们访问除<strong><i>"/login.html", "/css/", "/error"</strong></i>以外的路径时,在<strong><i>AbstractSecurityInterceptor</strong></i>(<strong><i>FilterSecurityInterceptor</strong></i>的父类)的<strong><i>attemptAuthorization()</strong></i>中会抛出<strong><i>AccessDeniedException</strong></i>异常(最终由<strong><i>AuthenticationTrustResolverImpl</strong></i>的<strong></i>isAnonymous()</strong></i>进行判断)
|
||
|
||

|
||
|
||
当我们访问<strong><i>"/login.html", "/css/", "/error"</strong></i>这几个路径时,在<strong><i>AbstractSecurityInterceptor</strong></i>(<strong><i>FilterSecurityInterceptor</strong></i>的父类)的<strong><i>attemptAuthorization()</strong></i>中正常执行(最终由<strong><i>SecurityExpressionRoot</strong></i>的<strong><i>permitAll()</strong></i>进行判断)
|
||
|
||

|
||
|
||
### login.html 路径解析
|
||
|
||
当我们请求的资源需要经过认证时,Spring Security 会将请求重定向到我们自定义的登录页,那么 Spring 又是如何找到我们自定义的登录页的呢?下面就让我们来解析一下:
|
||
|
||
我们首先来到<strong><i>DispatcherServlet</strong></i>中,<strong><i>DispatcherServlet</strong></i>是 Spring Web 处理请求的入口。当 Spring Web 项目启动后,第一次接收到请求时,会调用其<strong><i>initStrategies()</strong></i>进行初始化:
|
||
|
||

|
||
|
||
我们重点关注<strong><i>initHandlerMappings(context);</strong></i>这句,<strong><i>initHandlerMappings()</strong></i>用于初始化处理器映射器(处理器映射器可以根据请求找到对应的资源),我们来到<strong><i>initHandlerMappings()</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>
|
||
|
||

|
||
|
||
<strong><i>weclomePageHandlerMapping:</strong></i>
|
||
|
||

|
||
|
||
<strong><i>beanNameHandlerMapping</strong></i>、<strong><i>routerFunctionMapping</strong></i>、<strong><i>resourceHandlerMapping</strong></i>在<strong><i>EnableWebMvcConfiguration</strong></i>的父类(<strong><i>WebMvcConfigurationSupport</strong></i>)中配置:
|
||
|
||
<strong><i>beanNameHandlerMapping:</strong></i>
|
||
|
||

|
||
|
||
<strong><i>routerFunctionMapping:</strong></i>
|
||
|
||

|
||
|
||
<strong><i>resourceHandlerMapping:</strong></i>
|
||
|
||

|
||
|
||
我们将目光锁定在<strong><i>resourceHandlerMapping</strong></i>上,当<strong><i>resourceHandlerMapping</strong></i>被初始化时,会调用<strong><i>addResourceHandlers()</strong></i>为<strong><i>registry</strong></i>添加资源处理器,我们找到实际被调用的<strong><i>addResourceHandlers()</strong></i>,在<strong><i>DelegatingWebMvcConfiguration</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>中:
|
||
|
||

|
||
|
||
可以看到这里实际调用的是<strong><i>delegates</strong></i>属性的<strong><i>addResourceHandlers()</strong></i>,<strong><i>delegates</strong></i>是一个 final 类型的集合,集合的元素由<strong><i>addWebMvcConfigurers()</strong></i>负责添加:
|
||
|
||

|
||
|
||
我们找到调用<strong><i>addWebMvcConfigurers()</strong></i>的地方,在<strong><i>DelegatingWebMvcConfiguration</strong></i>的<strong><i>setConfigurers()</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>:
|
||
|
||

|
||
|
||
我们进入参数<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>WebMvcAutoConfiguration</strong></i>的<strong><i>addResourceHandlers()</strong></i>中:
|
||
|
||

|
||
|
||
在<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>来验证这个过程。
|
||
|
||
请求首先来到<strong><i>DispatcherServlet</strong></i>的<strong><i>doDispatch()</strong></i>中,由于是对静态资源的请求,当程序走到<strong><i>mappedHandler = getHandler(processedRequest);</strong></i>时,通过<strong><i>getHandler()</strong></i>返回<strong><i>SimpleUrlHandlerMapping</strong></i>(即<strong><i>resourceHandlerMapping</strong></i>的类型)的<strong><i>HandlerExecutionChain</strong></i>:
|
||
|
||

|
||
|
||
然后由实际的处理器进行处理:
|
||
|
||

|
||
|
||
程序一路调试,来到<strong><i>ResourceHttpRequestHandler</strong></i>的<strong><i>handleRequest()</strong></i>中,通过调用<strong><i>Resource resource = getResource(request);</strong></i>找到请求对应的资源:
|
||
|
||

|
||
|
||
而在<strong><i>getResource()</strong></i>中,实际是将请求路径(即<strong><i>login.html</strong></i>)与前面配置的路径进行拼接(组合成<strong><i>/resources/login.html</strong></i>这样的路径),再通过类加载器来寻找资源。
|
||
|
||
后面源码深入过深,就不一一展开了,只截取其中比较重要的几段代码:
|
||
|
||
<strong><i>PathResourceResolver</strong></i>中:
|
||
|
||

|
||
|
||

|
||
|
||
<strong><i>ClassPathResource</strong></i>中:
|
||
|
||

|
||
|
||
<strong><i>ClassLoader</strong></i>中:
|
||
|
||

|
||
|
||
<strong><i>URLClassLoader</strong></i>中:
|
||
|
||

|
||
|
||
最终,类加载器会在如上两个路径下找到登录页并返回。
|
||
|
||
### UserDetailService 配置解析
|
||
|
||
我们定义的用户信息的获取逻辑是如何被 Spring Security 应用的呢?让我们通过阅读源码来了解一下。
|
||
|
||
还记得前面我们讲**_BrowserConfig 配置_**被加载的过程吗?**_UserDetailService_**也是在这个过程中被一起加载完成的,回到**BrowserConfig 配置解析**的第一幅图中,如下:
|
||
|
||

|
||
|
||
在断点处位置,<strong><i>authenticationManager()</strong></i>会返回一个**_AuthenticationManager_**实例,我们进入<strong><i>authenticationManager()</strong></i>中:
|
||
|
||

|
||
|
||
在<strong><i>authenticationManager()</strong></i>中,**_AuthenticationManager_**转由**_AuthenticationConfiguration_**中获取,我们进入<strong><i>getAuthenticationManager()</strong></i>中:
|
||
|
||

|
||
|
||
程序来到**_AuthenticationConfiguration_**的<strong><i>getAuthenticationManager()</strong></i>中,**_AuthenticationManager_**转由**_AuthenticationManagerBuilder_**中获取,我们进入<strong><i>build()</strong></i>中:
|
||
|
||

|
||
|
||
程序来到**_AbstractConfiguredSecurityBuilder_**的<strong><i>doBuild()</strong></i>中,这里在构建**_AuthenticationManager_**实例时,需要初始化 3 个配置类,我们重点关注第 3 个配置类:**_org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer_**,这个配置类是在**_AuthenticationConfiguration_**中引入的:
|
||
|
||

|
||
|
||
我们来到**_InitializeUserDetailsBeanManagerConfigurer_**的<strong><i>init()</strong></i>中:
|
||
|
||

|
||
|
||
这里会新建一个**_InitializeUserDetailsManagerConfigurer_**实例添加到**_AuthenticationManagerBuilder_**中。我们回到<strong><i>doBuild()</strong></i>中:
|
||
|
||

|
||
|
||
可以看到配置类变成了 5 个,其中就有刚刚新建的**_InitializeUserDetailsManagerConfigurer_**,程序接下来会调用各个配置类的<strong><i>configure()</strong></i>进行配置,我们来到**_InitializeUserDetailsManagerConfigurer_**的<strong><i>configure()</strong></i>中:
|
||
|
||

|
||
|
||
可以看到在<strong><i>configure()</strong></i>中,就会去 bean 工厂中寻找**_UserDetailsService_**类型的 bean,若是我们没有自定义**_UserDetailsService_**的实现类的话,Spring Security 默认会生成一个**_InMemoryUserDetailsManager_**的实例:
|
||
|
||

|
||
|
||
**_InMemoryUserDetailsManager_**是在**_UserDetailsServiceAutoConfiguration_**类中配置的:
|
||
|
||

|
||
|
||
解决完**_UserDetailsService_**的加载问题,现在我们来看看 Spring Security 是如何通过**_UserDetailsService_**获取用户信息的。
|
||
|
||
通过**Spring Boot 中开启 Spring Security**一节的学习我们知道,登录判断的逻辑是在**_UsernamePasswordAuthenticationFilter_**中进行的,因此我们在**_UsernamePasswordAuthenticationFilter_**的<strong><i>attemptAuthenticatio()</strong></i>中打上断点,然后启动项目,访问登录页,输入用户名和密码点击登录后,程序来到**_UsernamePasswordAuthenticationFilter_**中:
|
||
|
||

|
||
|
||
这里将验证的逻辑交由**_AuthenticationManager_**进行,我们进入<strong><i>authenticate()</strong></i>中:
|
||
|
||

|
||
|
||
程序来到**_ProviderManager_**的<strong><i>authenticate()</strong></i>中,这里将验证的逻辑委托给其父类进行,再次点击进入<strong><i>authenticate()</strong></i>中:
|
||
|
||

|
||
|
||
这里将验证的逻辑交由**_AuthenticationProvider_**进行,我们进入<strong><i>authenticate()</strong></i>中:
|
||
|
||

|
||
|
||
程序来到**_AbstractUserDetailsAuthenticationProvider_**的<strong><i>authenticate()</strong></i>中,这里会根据用户名去寻找对应的用户实例,我们进入<strong><i>retrieveUser()</strong></i>中:
|
||
|
||

|
||
|
||
程序来到**_DaoAuthenticationProvider_**的<strong><i>retrieveUser()</strong></i>中,可以看到正是在这里,会从**_UserDetailsService_**的<strong><i>loadUserByUsername()</strong></i>中寻找对应的用户信息。
|
||
|
||
## 参考
|
||
|
||
1. [Spring Security 自定义用户认证](https://mrbird.cc/Spring-Security-Authentication.html)
|