返回介绍

Spring 系列

MyBatis

Netty

Dubbo

Tomcat

Redis

Nacos

Sentinel

RocketMQ

番外篇(JDK 1.8)

学习心得

SpringSecurity 自定义用户认证

发布于 2024-05-19 21:34:34 字数 40882 浏览 0 评论 0 收藏 0

Spring Security 自定义用户认证

Spring Boot 中开启 Spring Security一节中我们简单地搭建了一个 Spring Boot + Spring Security 的项目,其中登录页、用户名和密码都是由 Spring Security 自动生成的。Spring Security 支持我们自定义认证的过程,如使用自定义的登录页替换默认的登录页,用户信息的获取逻辑、登录成功或失败后的处理逻辑等。这里将在上一节的源码基础上进行改造。

配置自定义登录页

为了方便起见,我们直接在src/main/resources/resources</i>目录下创建一个login.html</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 跳转到我们自己定义的登录页面呢?很简单,只需要在BrowserSecurityConfig</i>的configure</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();
    }
}

上面代码中.loginPage("/login.html")</i>指定了跳转到登录页面的请求 URL,.loginProcessingUrl("/login")</i>对应登录页面 form 表单的action="/login"</i>,.antMatchers("/login.html", "/css/", "/error").permitAll()</i>表示跳转到登录页面的请求不被拦截。

这时候启动系统,访问http://localhost:8080/hello</i>,会看到页面已经被重定向到了http://localhost:8080/login.html</i>:

img

配置用户信息的获取逻辑

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</i>,只需要输入任意用户名以及 123456 作为密码即可登录系统。

源码解析

BrowserConfig 配置解析

我们首先来梳理下BrowserConfig</i>中的配置是如何被 Spring Security 所加载的。

首先找到调用BrowserConfig</i>的configure()</i>的地方,在其父类WebSecurityConfigurerAdapter</i>的getHttp()</i>中:

img

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

img

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

img

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

img

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

img

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

img

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

img

我们重点看第二句代码,可以看到这里会提取存储在 bean 工厂中类型为WebSecurityConfigurer.class</i>的 bean,而BrowserConfig</i>正是WebSecurityConfigurerAdapter</i>的实现类。

解决完configurers</i>的赋值问题,我们回到AbstractConfiguredSecurityBuilder</i>的init()</i>处,找到调用该方法的地方,在同个类的doBuild()</i>中:

img

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

img

往上一步找到调用doBuild()</i>的地方,在WebSecurityConfiguration</i>的springSecurityFilterChain()</i>中:

img

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

img

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

img

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

img

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

img

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

img

login.html 路径解析

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

我们首先来到DispatcherServlet</i>中,DispatcherServlet</i>是 Spring Web 处理请求的入口。当 Spring Web 项目启动后,第一次接收到请求时,会调用其initStrategies()</i>进行初始化:

img

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

img

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

requestMappingHandlerMapping:</i>

img

weclomePageHandlerMapping:</i>

img

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

beanNameHandlerMapping:</i>

img

routerFunctionMapping:</i>

img

resourceHandlerMapping:</i>

img

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

img

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

img

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

img

我们找到调用addWebMvcConfigurers()</i>的地方,在DelegatingWebMvcConfiguration</i>的setConfigurers()</i>中:

img

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

img

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

img

img

我们回到WebMvcAutoConfiguration</i>的addResourceHandlers()</i>中:img

img

addResourceHandlers()</i>中,会为registry</i>添加两个资源处理器,当请求路径是“/webjars/”时,会在”classpath:/META-INF/resources/webjars/“路径下寻找对应的资源,当请求路径是“/**”时,会在”classpath:/META-INF/resources/“、”classpath:/resources/“、”classpath:/static/“、”classpath:/public/“路径下寻找对应的资源。

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

请求首先来到DispatcherServlet</i>的doDispatch()</i>中,由于是对静态资源的请求,当程序走到mappedHandler = getHandler(processedRequest);</i>时,通过getHandler()</i>返回SimpleUrlHandlerMapping</i>(即resourceHandlerMapping</i>的类型)的HandlerExecutionChain</i>:

img

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

img

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

img

而在getResource()</i>中,实际是将请求路径(即login.html</i>)与前面配置的路径进行拼接(组合成/resources/login.html</i>这样的路径),再通过类加载器来寻找资源。

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

PathResourceResolver</i>中:

img

img

ClassPathResource</i>中:

img

ClassLoader</i>中:

img

URLClassLoader</i>中:

img

最终,类加载器会在如上两个路径下找到登录页并返回。

UserDetailService 配置解析

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

还记得前面我们讲BrowserConfig 配置被加载的过程吗?UserDetailService也是在这个过程中被一起加载完成的,回到BrowserConfig 配置解析的第一幅图中,如下:

img

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

img

authenticationManager()</i>中,AuthenticationManager转由AuthenticationConfiguration中获取,我们进入getAuthenticationManager()</i>中:

img

程序来到AuthenticationConfigurationgetAuthenticationManager()</i>中,AuthenticationManager转由AuthenticationManagerBuilder中获取,我们进入build()</i>中:

img

程序来到AbstractConfiguredSecurityBuilderdoBuild()</i>中,这里在构建AuthenticationManager实例时,需要初始化 3 个配置类,我们重点关注第 3 个配置类:org.springframework.security.config.annotation.authentication.configuration.InitializeUserDetailsBeanManagerConfigurer,这个配置类是在AuthenticationConfiguration中引入的:

img

我们来到InitializeUserDetailsBeanManagerConfigurerinit()</i>中:

img

这里会新建一个InitializeUserDetailsManagerConfigurer实例添加到AuthenticationManagerBuilder中。我们回到doBuild()</i>中:

img

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

img

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

img

InMemoryUserDetailsManager是在UserDetailsServiceAutoConfiguration类中配置的:

img

解决完UserDetailsService的加载问题,现在我们来看看 Spring Security 是如何通过UserDetailsService获取用户信息的。

通过Spring Boot 中开启 Spring Security一节的学习我们知道,登录判断的逻辑是在UsernamePasswordAuthenticationFilter中进行的,因此我们在UsernamePasswordAuthenticationFilterattemptAuthenticatio()</i>中打上断点,然后启动项目,访问登录页,输入用户名和密码点击登录后,程序来到UsernamePasswordAuthenticationFilter中:

img

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

img

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

img

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

img

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

img

程序来到DaoAuthenticationProviderretrieveUser()</i>中,可以看到正是在这里,会从UserDetailsServiceloadUserByUsername()</i>中寻找对应的用户信息。

参考

  1. Spring Security 自定义用户认证

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文