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跳转到我们自己定义的登录页面呢?很简单,只需要在BrowserSecurityConfigconfigure中添加一些配置:

@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();
    }
}

上面代码中.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所加载的。

首先找到调用BrowserConfigconfigure()的地方,在其父类WebSecurityConfigurerAdaptergetHttp()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wie2hlUQ-1629341220257)(../../images/SpringSecurity/2b54af34-7d68-4f40-8726-d02d18e03dea.png)]
往上一步找到调用getHttp()的地方,在同个类的init()中:
在这里插入图片描述
往上一步找到调用init()的地方,在AbstractConfiguredSecurityBuilderinit()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4C7V6Gdi-1629341220259)(../../images/SpringSecurity/6e009bf1-aba3-4b89-8e86-d3d110e0f4a7.png)]
init()被调用时,它首先会遍历getConfigurers()返回的集合中的元素,调用其init(),点击getConfigurers()查看,发现其读取的是configurers属性的值,那么configurers是什么时候被赋值的呢?我们在同个类的add()中找到configurers被赋值的代码:
在这里插入图片描述
往上一步找到调用add()的地方,在同个类的apply()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pKYj5CNM-1629341220263)(../../images/SpringSecurity/1e929fec-d1ab-44b5-89bc-6e5ebcda1daf.png)]
往上一步找到调用apply()的地方,在WebSecurityConfigurationsetFilterChainProxySecurityConfigurer()中:
在这里插入图片描述
我们可以看到,在setFilterChainProxySecurityConfigurer()中,首先会实例化一个WebSecurityAbstractConfiguredSecurityBuilder的实现类)的实例,遍历参数webSecurityConfigurers,将存储在其中的元素作为参数传递给WebSecurityapply(),那么webSecurityConfigurers是什么时候被赋值的呢?我们根据@Value中的信息找到webSecurityConfigurers被赋值的地方,在AutowiredWebSecurityConfigurersIgnoreParentsgetWebSecurityConfigurers()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rsaol09X-1629341220266)(../../images/SpringSecurity/eb7a5916-4049-4682-9a11-10f1f1f94c74.png)]
我们重点看第二句代码,可以看到这里会提取存储在bean工厂中类型为WebSecurityConfigurer.class的bean,而BrowserConfig正是WebSecurityConfigurerAdapter的实现类。

解决完configurers的赋值问题,我们回到AbstractConfiguredSecurityBuilderinit()处,找到调用该方法的地方,在同个类的doBuild()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XMXr91VO-1629341220267)(../../images/SpringSecurity/522574e3-bacc-4794-a17e-492bc2b4457d.png)]
往上一步找到调用doBuild()的地方,在AbstractSecurityBuilderbuild()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JxSv9mfM-1629341220269)(../../images/SpringSecurity/fb22f2ca-3d9a-420f-b77a-f9c0f737d9ad.png)]
往上一步找到调用doBuild()的地方,在WebSecurityConfigurationspringSecurityFilterChain()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R7iPFeUe-1629341220270)(../../images/SpringSecurity/a5c61feb-ca72-4768-94bd-1b0a8cf8af70.png)]
至此,我们分析完了BrowserConfig被Spring Security加载的过程。现在我们再来看看当我们自定义的配置被加载完后,http各属性的变化,在BrowserConfigconfigure()末尾打上断点,当程序走到断点处时,查看http属性:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7wZWRovh-1629341220271)(../../images/SpringSecurity/6c72c09b-742c-4415-851a-8ca5292a4969.png)]
我们配置的.loginPage("/login.html").loginProcessingUrl("/login")FormLoginConfigurer中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2xqvi88h-1629341220272)(../../images/SpringSecurity/51ba02f0-bae6-4c08-9adf-7ee0f12b05d3.png)]
配置的.antMatchers("/login.html", “/css/”, “/error”).permitAll()ExpressionUrlAuthorizationConfigurer中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pfTLNBmk-1629341220273)(../../images/SpringSecurity/52390725-d87d-42b1-9071-ea21e445e1e6.png)]
这样,当我们访问除"/login.html", “/css/”, “/error"以外的路径时,在AbstractSecurityInterceptorFilterSecurityInterceptor的父类)的attemptAuthorization()中会抛出AccessDeniedException异常(最终由AuthenticationTrustResolverImplisAnonymous()进行判断)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dqbkJosJ-1629341220274)(../../images/SpringSecurity/558b8b1c-be32-44c4-8d8f-5f0d231741f8.png)]
当我们访问”/login.html", “/css/”, "/error"这几个路径时,在AbstractSecurityInterceptorFilterSecurityInterceptor的父类)的attemptAuthorization()中正常执行(最终由SecurityExpressionRootpermitAll()进行判断)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xBzMhYGv-1629341220275)(../../images/SpringSecurity/4612c27e-dd9f-4e60-92dc-fc9858496ec5.png)]

login.html路径解析

当我们请求的资源需要经过认证时,Spring Security会将请求重定向到我们自定义的登录页,那么Spring又是如何找到我们自定义的登录页的呢?下面就让我们来解析一下:

我们首先来到DispatcherServlet中,DispatcherServlet是Spring Web处理请求的入口。当Spring Web项目启动后,第一次接收到请求时,会调用其initStrategies()进行初始化:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xZKvlKe2-1629341220276)(../../images/SpringSecurity/964ec4a4-6039-4205-8a87-ea2febcc00b6.png)]
我们重点关注initHandlerMappings(context);这句,initHandlerMappings()用于初始化处理器映射器(处理器映射器可以根据请求找到对应的资源),我们来到initHandlerMappings()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pvArzdhD-1629341220278)(../../images/SpringSecurity/0a97b011-34ed-4d57-945e-95c8e6bafc8e.png)]
可以看到,当程序走到initHandlerMappings()中时,会从bean工厂中找出HandlerMapping.class类型的bean,将其存储handlerMappings属性中。这里看到一共找到5个,分别是:requestMappingHandlerMapping(将请求与标注了@RequestMapping的方法进行关联)、weclomePageHandlerMapping(将请求与主页进行关联)、beanNameHandlerMapping(将请求与同名的bean进行关联)、routerFunctionMapping(将请求与RouterFunction进行关联)、resourceHandlerMapping(将请求与静态资源进行关联),这5个bean是在WebMvcAutoConfiguration$EnableWebMvcConfiguration中配置的:

requestMappingHandlerMapping:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wXcCcLB0-1629341220279)(../../images/SpringSecurity/cd7aee86-66f8-4197-99d1-1c9275e33bee.png)]
weclomePageHandlerMapping:
在这里插入图片描述
beanNameHandlerMappingrouterFunctionMappingresourceHandlerMappingEnableWebMvcConfiguration的父类(WebMvcConfigurationSupport)中配置:

beanNameHandlerMapping:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J3nHP0Kw-1629341220281)(../../images/SpringSecurity/8d05ac54-f034-47d4-b750-67b2e3b3cd14.png)]
routerFunctionMapping:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N9RVa4KD-1629341220282)(../../images/SpringSecurity/481b88aa-028d-4392-8c0a-365f1d0e2ae9.png)]
resourceHandlerMapping:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-og038TzL-1629341220283)(../../images/SpringSecurity/fcdab503-2735-46bd-a5b6-226dc348e78c.png)]
我们将目光锁定在resourceHandlerMapping上,当resourceHandlerMapping被初始化时,会调用addResourceHandlers()registry添加资源处理器,我们找到实际被调用的addResourceHandlers(),在DelegatingWebMvcConfiguration中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dYXf2Q4m-1629341220284)(../../images/SpringSecurity/6eca7b58-80f9-4e98-8483-62b4ef751854.png)]
可以看到这里实际调用的是configurers属性的addResourceHandlers(),而configurers是一个final类型的成员变量,其值是WebMvcConfigurerComposite的实例,我们来到WebMvcConfigurerComposite中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eK6GDyg6-1629341220285)(../../images/SpringSecurity/c365e5ab-a7a3-4ebf-8a25-09cd2e049f22.png)]
可以看到这里实际调用的是delegates属性的addResourceHandlers()delegates是一个final类型的集合,集合的元素由addWebMvcConfigurers()负责添加:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OeM3JMWb-1629341220286)(../../images/SpringSecurity/895afead-ea6b-4ac6-8138-fbe0a223daf9.png)]
我们找到调用addWebMvcConfigurers()的地方,在DelegatingWebMvcConfigurationsetConfigurers()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-25nIA55R-1629341220287)(../../images/SpringSecurity/a20e06a4-5a43-4edf-bea1-53fc9bc929e9.png)]
可以看到当setConfigurers()被初始化时,Spring会往参数configurers中传入两个值,我们关注第一个值,是一个WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter的实例,注意它的属性resourceProperties,是一个WebProperties$Resources的实例,默认情况下,在实例化WebMvcAutoConfigurationAdapter时,由传入参数webProperties进行赋值:webProperties.getResources()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TqbcjWMx-1629341220288)(../../images/SpringSecurity/97b6f22c-414d-4a16-aa8e-c3deece2f7cd.png)]
我们进入参数webProperties的类中,可以看到getResources()是直接实例化了一个Resources,其属性staticLocations是一个含有4个值的final类型的字符串数组,这4个值正是Spring寻找静态文件的地方:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3zZmyMbi-1629341220289)(../../images/SpringSecurity/4d84fe43-2646-4a6f-a580-f39f6416d02d.png)]
我们回到WebMvcAutoConfigurationaddResourceHandlers()中:
在这里插入图片描述
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GE2YwoTb-1629341220292)(../../images/SpringSecurity/ccd16b38-d724-4c03-949e-3b6ba03268a9.png)]
addResourceHandlers()中,会为registry添加两个资源处理器,当请求路径是“/webjars/”时,会在”classpath:/META-INF/resources/webjars/“路径下寻找对应的资源,当请求路径是“/**”时,会在”classpath:/META-INF/resources/“、”classpath:/resources/“、”classpath:/static/“、”classpath:/public/“路径下寻找对应的资源。

现在我们通过访问http://localhost:8080/login.html来验证这个过程。

请求首先来到DispatcherServletdoDispatch()中,由于是对静态资源的请求,当程序走到mappedHandler = getHandler(processedRequest);时,通过getHandler()返回SimpleUrlHandlerMapping(即resourceHandlerMapping的类型)的HandlerExecutionChain
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YXlrXUFw-1629341220293)(../../images/SpringSecurity/4c9c302d-2ce0-4b5b-beb6-c76d2e94038f.png)]
然后由实际的处理器进行处理:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MRZHSxxX-1629341220294)(../../images/SpringSecurity/1590bae4-2e3a-4f97-b02d-d918c49cac22.png)]
程序一路调试,来到ResourceHttpRequestHandlerhandleRequest()中,通过调用Resource resource = getResource(request);找到请求对应的资源:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-se3L5DmC-1629341220296)(../../images/SpringSecurity/0879e131-da5f-491e-a153-42770ce8b975.png)]
而在getResource()中,实际是将请求路径(即login.html)与前面配置的路径进行拼接(组合成/resources/login.html这样的路径),再通过类加载器来寻找资源。

后面源码深入过深,就不一一展开了,只截取其中比较重要的几段代码:

PathResourceResolver中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eMrFDWc8-1629341220297)(../../images/SpringSecurity/6c58e27d-dd29-48fc-b597-8067e1c97786.png)]在这里插入图片描述
ClassPathResource中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ho33kpsH-1629341220299)(../../images/SpringSecurity/42c5125e-0dc2-4c0c-9434-af4a9efd2d5d.png)]
ClassLoader中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ZMdRm2E-1629341220301)(../../images/SpringSecurity/7842f83d-5417-4cb2-bb30-d70f98c3053f.png)]
URLClassLoader中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yDJXC6Km-1629341220302)(../../images/SpringSecurity/870891e9-f8ea-4097-98c9-829b1cdcf145.png)]
最终,类加载器会在如上两个路径下找到登录页并返回。

UserDetailService配置解析

我们定义的用户信息的获取逻辑是如何被Spring Security应用的呢?让我们通过阅读源码来了解一下。

还记得前面我们讲BrowserConfig配置被加载的过程吗?UserDetailService也是在这个过程中被一起加载完成的,回到BrowserConfig配置解析的第一幅图中,如下:
在这里插入图片描述
在断点处位置,authenticationManager()会返回一个AuthenticationManager实例,我们进入authenticationManager()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u1mAtKaH-1629513864240)(D:\ronaldoc\workspace\source-code-hunter\images/SpringSecurity/e7a1a684-64db-41d0-a5c0-d9a841d86cc1.png)]
authenticationManager()中,AuthenticationManager转由AuthenticationConfiguration中获取,我们进入getAuthenticationManager()中:
在这里插入图片描述
程序来到AuthenticationConfigurationgetAuthenticationManager()中,AuthenticationManager转由AuthenticationManagerBuilder中获取,我们进入build()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GTzJPOk4-1629513864245)(D:\ronaldoc\workspace\source-code-hunter\images/SpringSecurity/19f71152-f456-4db7-a1d5-f79aaa37253b.png)]
程序来到AbstractConfiguredSecurityBuilderdoBuild()中,这里在构建AuthenticationManager实例时,需要初始化3个配置类,我们重点关注第3个配置类:org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer,这个配置类是在AuthenticationConfiguration中引入的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oeaKTOBs-1629513864246)(D:\ronaldoc\workspace\source-code-hunter\images/SpringSecurity/9f06c823-645d-413b-8bb6-1d81b8f329ea.png)]
我们来到InitializeUserDetailsBeanManagerConfigurerinit()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aHRNCZA6-1629513864249)(D:\ronaldoc\workspace\source-code-hunter\images/SpringSecurity/cd7d6cb9-c6e7-4570-aab8-309adcb15e16.png)]
这里会新建一个InitializeUserDetailsManagerConfigurer实例添加到AuthenticationManagerBuilder中。我们回到doBuild()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-89lWDVXi-1629513864250)(D:\ronaldoc\workspace\source-code-hunter\images/SpringSecurity/3ea76980-417d-4c0c-9330-e0bb241c6a47.png)]
可以看到配置类变成了5个,其中就有刚刚新建的InitializeUserDetailsManagerConfigurer,程序接下来会调用各个配置类的configure()进行配置,我们来到InitializeUserDetailsManagerConfigurerconfigure()中:
在这里插入图片描述
可以看到在configure()中,就会去bean工厂中寻找UserDetailsService类型的bean,若是我们没有自定义UserDetailsService的实现类的话,Spring Security默认会生成一个InMemoryUserDetailsManager的实例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hHkNidek-1629513864251)(D:\ronaldoc\workspace\source-code-hunter\images/SpringSecurity/c6a7370c-4afb-4c5d-aa35-ba9c3406b1ed.png)]
InMemoryUserDetailsManager是在UserDetailsServiceAutoConfiguration类中配置的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ObkQmvWr-1629513864253)(D:\ronaldoc\workspace\source-code-hunter\images/SpringSecurity/476f8954-abe3-4e26-bfe1-8c5b4abbf0e0.png)]
解决完UserDetailsService的加载问题,现在我们来看看Spring Security是如何通过UserDetailsService获取用户信息的。

通过Spring Boot中开启Spring Security一节的学习我们知道,登录判断的逻辑是在UsernamePasswordAuthenticationFilter中进行的,因此我们在UsernamePasswordAuthenticationFilterattemptAuthenticatio()中打上断点,然后启动项目,访问登录页,输入用户名和密码点击登录后,程序来到UsernamePasswordAuthenticationFilter中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CWXDSQVk-1629513864254)(D:\ronaldoc\workspace\source-code-hunter\images/SpringSecurity/1282014b-fc29-4c2b-9316-9fdd638653c9.png)]
这里将验证的逻辑交由AuthenticationManager进行,我们进入authenticate()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uS3kO7jP-1629513864255)(D:\ronaldoc\workspace\source-code-hunter\images/SpringSecurity/5d511c93-3614-40e0-b3c9-9673c573d60f.png)]
程序来到ProviderManagerauthenticate()中,这里将验证的逻辑委托给其父类进行,再次点击进入authenticate()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pGhHDVFz-1629513864256)(D:\ronaldoc\workspace\source-code-hunter\images/SpringSecurity/8dddd63e-c567-4b41-a9d9-8ef8aa6f2a92.png)]
这里将验证的逻辑交由AuthenticationProvider进行,我们进入authenticate()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DZhpwT45-1629513864256)(D:\ronaldoc\workspace\source-code-hunter\images/SpringSecurity/ec796a9b-7c65-49a2-9f9f-7685af7bd57b.png)]
程序来到AbstractUserDetailsAuthenticationProviderauthenticate()中,这里会根据用户名去寻找对应的用户实例,我们进入retrieveUser()中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FbM62G6y-1629513864257)(D:\ronaldoc\workspace\source-code-hunter\images/SpringSecurity/3980e264-c073-456a-b808-715edd85633a.png)]
程序来到DaoAuthenticationProviderretrieveUser()中,可以看到正是在这里,会从UserDetailsServiceloadUserByUsername()中寻找对应的用户信息。

参考

  1. Spring Security自定义用户认证
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐