|
|
|
@ -1,319 +0,0 @@
|
|
|
|
|
# Spring Security 自定义用户认证
|
|
|
|
|
|
|
|
|
|
在**Spring Boot 中开启 Spring Security**一节中我们简单地搭建了一个 Spring Boot + Spring Security 的项目,其中登录页、用户名和密码都是由 Spring Security 自动生成的。Spring Security 支持我们自定义认证的过程,如使用自定义的登录页替换默认的登录页,用户信息的获取逻辑、登录成功或失败后的处理逻辑等。这里将在上一节的源码基础上进行改造。
|
|
|
|
|
|
|
|
|
|
## 配置自定义登录页
|
|
|
|
|
|
|
|
|
|
为了方便起见,我们直接在`src/main/resources/resources`目录下创建一个`login.html`(不需要 Controller 跳转):
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
<!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 跳转到我们自己定义的登录页面呢?很简单,只需要在 `BrowserSecurityConfig` 的 `configure` 中添加一些配置:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@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/**", "/error").permitAll() // 无需认证
|
|
|
|
|
.anyRequest().authenticated() // 其他所有请求都需要认证
|
|
|
|
|
.and()
|
|
|
|
|
.csrf().disable(); // 禁用 CSRF
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
上面代码中`.loginPage("/login.html")`指定了跳转到登录页面的请求 URL,`.loginProcessingUrl("/login")`对应登录页面 form 表单的`action="/login"`,`.antMatchers("/login.html", "/css/", "/error").permitAll()`表示跳转到登录页面的请求不被拦截。
|
|
|
|
|
|
|
|
|
|
这时候启动系统,访问`http://localhost:8080/hello`,会看到页面已经被重定向到了`http://localhost:8080/login.html`:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
## 配置用户信息的获取逻辑
|
|
|
|
|
|
|
|
|
|
Spring Security 默认会为我们生成一个用户名为 user,密码随机的用户实例,当然我们也可以定义自己用户信息的获取逻辑,只需要实现 Spring Security 提供的**_UserDetailService_**接口即可,该接口只有一个抽象方法**_loadUserByUsername_**,具体实现如下:
|
|
|
|
|
|
|
|
|
|
```java
|
|
|
|
|
@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 的用户信息的获取逻辑。这样,当我们启动项目,访问`http://localhost:8080/login`,只需要输入任意用户名以及 123456 作为密码即可登录系统。
|
|
|
|
|
|
|
|
|
|
## 源码解析
|
|
|
|
|
|
|
|
|
|
### BrowserConfig 配置解析
|
|
|
|
|
|
|
|
|
|
我们首先来梳理下 `BrowserConfig` 中的配置是如何被 Spring Security 所加载的。
|
|
|
|
|
|
|
|
|
|
首先找到调用 `BrowserConfig` 的 `configure()` 的地方,在其父类 `WebSecurityConfigurerAdapter` 的 `getHttp()` 中:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
往上一步找到调用 `getHttp()` 的地方,在同个类的 `init()` 中:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
往上一步找到调用`init()`的地方,在 `AbstractConfiguredSecurityBuilder` 的`init()`中:
|
|
|
|
|
|
|
|
|
|

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

|
|
|
|
|
|
|
|
|
|
往上一步找到调用`add()`的地方,在同个类的`apply()`中:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
往上一步找到调用`apply()`的地方,在`WebSecurityConfiguration`的`setFilterChainProxySecurityConfigurer()`中:
|
|
|
|
|
|
|
|
|
|

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

|
|
|
|
|
|
|
|
|
|
我们重点看第二句代码,可以看到这里会提取存储在 bean 工厂中类型为`WebSecurityConfigurer.class`的 bean,而`BrowserConfig`正是`WebSecurityConfigurerAdapter`的实现类。
|
|
|
|
|
|
|
|
|
|
解决完`configurers`的赋值问题,我们回到`AbstractConfiguredSecurityBuilder`的`init()`处,找到调用该方法的地方,在同个类的`doBuild()`中:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
往上一步找到调用`doBuild()`的地方,在`AbstractSecurityBuilder`的`build()`中:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
往上一步找到调用`doBuild()`的地方,在`WebSecurityConfiguration`的`springSecurityFilterChain()`中:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
至此,我们分析完了`BrowserConfig`被 Spring Security 加载的过程。现在我们再来看看当我们自定义的配置被加载完后,`http`各属性的变化,在`BrowserConfig`的`configure()`末尾打上断点,当程序走到断点处时,查看`http`属性:
|
|
|
|
|
|
|
|
|
|

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

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

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

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

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

|
|
|
|
|
|
|
|
|
|
我们重点关注`initHandlerMappings(context);`这句,`initHandlerMappings()`用于初始化处理器映射器(处理器映射器可以根据请求找到对应的资源),我们来到`initHandlerMappings()`中:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
可以看到,当程序走到`initHandlerMappings()`中时,会从 bean 工厂中找出`HandlerMapping.class`类型的 bean,将其存储到`handlerMappings`属性中。这里看到一共找到 5 个,分别是:`requestMappingHandlerMapping`(将请求与标注了`@RequestMapping`的方法进行关联)、`weclomePageHandlerMapping`(将请求与主页进行关联)、`beanNameHandlerMapping`(将请求与同名的 bean 进行关联)、`routerFunctionMapping`(将请求与`RouterFunction`进行关联)、`resourceHandlerMapping`(将请求与静态资源进行关联),这 5 个 bean 是在`WebMvcAutoConfiguration$EnableWebMvcConfiguration`中配置的:
|
|
|
|
|
|
|
|
|
|
`requestMappingHandlerMapping:`
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
`weclomePageHandlerMapping:`
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
`beanNameHandlerMapping`、`routerFunctionMapping`、`resourceHandlerMapping`在`EnableWebMvcConfiguration`的父类(`WebMvcConfigurationSupport`)中配置:
|
|
|
|
|
|
|
|
|
|
`beanNameHandlerMapping:`
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
`routerFunctionMapping:`
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
`resourceHandlerMapping:`
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
我们将目光锁定在`resourceHandlerMapping`上,当`resourceHandlerMapping`被初始化时,会调用`addResourceHandlers()`为`registry`添加资源处理器,我们找到实际被调用的`addResourceHandlers()`,在`DelegatingWebMvcConfiguration`中:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
可以看到这里实际调用的是`configurers`属性的`addResourceHandlers()`,而`configurers`是一个 final 类型的成员变量,其值是`WebMvcConfigurerComposite`的实例,我们来到`WebMvcConfigurerComposite`中:
|
|
|
|
|
|
|
|
|
|

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

|
|
|
|
|
|
|
|
|
|
我们找到调用`addWebMvcConfigurers()`的地方,在`DelegatingWebMvcConfiguration`的`setConfigurers()`中:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
可以看到当`setConfigurers()`被初始化时,Spring 会往参数`configurers`中传入两个值,我们关注第一个值,是一个`WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter`的实例,注意它的属性`resourceProperties`,是一个`WebProperties$Resources`的实例,默认情况下,在实例化`WebMvcAutoConfigurationAdapter`时,由传入参数`webProperties`进行赋值:`webProperties.getResources()`:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
我们进入参数`webProperties`的类中,可以看到`getResources()`是直接实例化了一个`Resources`,其属性`staticLocations`是一个含有 4 个值的 final 类型的字符串数组,这 4 个值正是 Spring 寻找静态文件的地方:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
我们回到`WebMvcAutoConfiguration`的`addResourceHandlers()`中:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
在`addResourceHandlers()`中,会为`registry`添加两个资源处理器,当请求路径是“/webjars/”时,会在”classpath:/META-INF/resources/webjars/“路径下寻找对应的资源,当请求路径是“/\*\*”时,会在”classpath:/META-INF/resources/“、”classpath:/resources/“、”classpath:/static/“、”classpath:/public/“路径下寻找对应的资源。
|
|
|
|
|
|
|
|
|
|
现在我们通过访问`http://localhost:8080/login.html`来验证这个过程。
|
|
|
|
|
|
|
|
|
|
请求首先来到`DispatcherServlet`的`doDispatch()`中,由于是对静态资源的请求,当程序走到`mappedHandler = getHandler(processedRequest);`时,通过`getHandler()`返回`SimpleUrlHandlerMapping`(即`resourceHandlerMapping`的类型)的`HandlerExecutionChain`:
|
|
|
|
|
|
|
|
|
|

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

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

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

|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
`ClassPathResource`中:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
`ClassLoader`中:
|
|
|
|
|
|
|
|
|
|

|
|
|
|
|
|
|
|
|
|
`URLClassLoader`中:
|
|
|
|
|
|
|
|
|
|

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

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

|
|
|
|
|
|
|
|
|
|
在`authenticationManager()`中,**_AuthenticationManager_**转由**_AuthenticationConfiguration_**中获取,我们进入`getAuthenticationManager()`中:
|
|
|
|
|
|
|
|
|
|

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

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

|
|
|
|
|
|
|
|
|
|
我们来到**_InitializeUserDetailsBeanManagerConfigurer_**的`init()`中:
|
|
|
|
|
|
|
|
|
|

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

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

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

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

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

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

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

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

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

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