22 KiB
Spring Security 自定义用户认证
在Spring Boot 中开启 Spring Security一节中我们简单地搭建了一个 Spring Boot + Spring Security 的项目,其中登录页、用户名和密码都是由 Spring Security 自动生成的。Spring Security 支持我们自定义认证的过程,如使用自定义的登录页替换默认的登录页,用户信息的获取逻辑、登录成功或失败后的处理逻辑等。这里将在上一节的源码基础上进行改造。
配置自定义登录页
为了方便起见,我们直接在src/main/resources/resources
目录下创建一个login.html
(不需要 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 跳转到我们自己定义的登录页面呢?很简单,只需要在 BrowserSecurityConfig
的 configure
中添加一些配置:
@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**,具体实现如下:
@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
的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()
中寻找对应的用户信息。