SpringSecurity的原理

  • 本质上是一个过滤器链,内部包含了提供各种功能的过滤器。

  • 常见的三种过滤器:

    • UsernamePasswordAuthenticationFilter:负责处理我们在登录页面填写了用户名密码后的登录请求
    • ExceptionTranslationFilter:处理过滤器链中抛出的任何 AccessDeniedExceptionAuthenticationException
    • FilterSecurityInterceptor:负责权限校验的过滤器
  • 通过 debug 可以查看当前系统过滤器链中有哪些过滤器以及它们的顺序

    • image-20240811174710000

各个 Filter 的作用浏览

  • WebAsyncManagerIntegrationFilter

    • 提供了对 SecurityContextWebAsyncManager 的集成,其会把 SecurityContext 设置到异步线程中,使其也能获取到用户上下文认证信息。
  • SecurityContextPersistenceFilter

    • 该类在所有的 Filter 之前,从 SecurityContextRepository 中取出用户认证信息,默认实现类为 HttpSessionSecurityContextRepository,其会从 Session 中取出已认证用户的信息,提高效率,避免每一次请求都要查询用户认证信息。
    • 取出之后会放入 SecurityContextHolder 中,以便其他 Filter 使用,SecurityContextHolder 使用 ThreadLocal 存储用户认证信息,保证了线程之间的信息隔离,最后在 finally 中清除该信息。
    • 可以配置 http 的 security-context-repository-ref 属性来自行控制获取到已认证用户信息的方式,比如使用 Redis 存储 session 等。当不使用 session 的话,最好配置为 NullSecurityContextRepository,避免占用服务器内存。
  • HeaderWriterFilter

    • 其会往该请求的 Header 中添加相应的信息,在 http 标签内部使用 security:headers 来控制。
  • CsrfFilter

    • 验证方式是通过客户端传来的 token 与服务端存储的 token 进行对比,来判断是否为伪造请求。
  • LogoutFilter

    • 匹配 URL,默认为 /logout,匹配成功后则用户退出,清除认证信息。如果有自己的退出逻辑,那么这个过滤器可以 disable。
  • UsernamePasswordAuthenticationFilter

    • 登录认证过滤器,默认是对 /login 的 POST 请求进行认证,首先该方法会先调用 attemptAuthentication 尝试认证获取一个 Authentication 的认证对象,然后通过 sessionStrategy.onAuthentication 执行持久化,也就是保存认证信息,转向下一个 Filter,最后调用 successfulAuthentication 执行认证后事件。
    • attemptAuthentication
      • 该方法是认证的主要方法,认证是委托配置的 authentication-manager -> authentication-provider 进行。
      • 比如对于该 Demo 配置的为如下,则默认使用的 manager 为 ProviderManager,使用的 provider 为 DaoAuthenticationProvideruserDetailServiceInMemoryUserDetailsManager,也就是从内存中获取用户认证信息,也就是下面 xml 配置的 user 与 admin 信息。
      • 认证基本流程为 UserDeatilService 根据用户名获取到认证用户的信息,然后通过 UserDetailsChecker.check 对用户进行状态校验,最后通过 additionalAuthenticationChecks 方法对用户进行密码校验成功后完成认证,返回一个认证对象。
  • DefaultLoginPageGeneratingFilter

    • 当请求为登录请求时,生成简单的登录页面返回,有自己的登录逻辑的话同样可以 disable。
  • BasicAuthenticationFilter

    • Http Basic 认证的支持,该认证会把用户名密码使用 base64 编码后放入 header 中传输,如下所示,认证成功后会把用户信息放入 SecurityContextHolder 中。
  • RequestCacheAwareFilter

    • 恢复被打断的请求,具体未研究。
  • SecurityContextHolderAwareRequestFilter

    • 针对 Servlet api 不同版本做的一些包装。
  • AnonymousAuthenticationFilter

    • SecurityContextHolder 中认证信息为空,则会创建一个匿名用户存入到 SecurityContextHolder 中。
  • SessionManagementFilter

    • 与登录认证拦截时作用一样,持久化用户登录信息,可以保存到 session 中,也可以保存到 cookie 或者 Redis 中。
  • ExceptionTranslationFilter

    • 异常拦截,其处在 Filter 链后部分,只能拦截其后面的节点并且着重处理 AuthenticationExceptionAccessDeniedException 两个异常。可以在此处定义一个 entryPoint,对错误请求返回 403。
  • FilterSecurityInterceptor

    • 主要是授权验证,方法为 beforeInvocation,在其中调用
      • image-20240811174723960
    • 获取到所配置资源访问的授权信息,对于上述配置,获取到的则为 hasRole('ROLE_USER'),然后根据 SecurityContextHolder 中存储的用户信息来决定其是否有权限,没权限则返回 403。具体想了解可以关注 HttpConfigurationBuilder.createFilterSecurityInterceptor() 方法,分析其创建流程加载了哪些数据,或者分析 SecurityExpressionOperations 的子类,其是权限鉴定的实现方法。

重点关注

  1. 登录验证 UsernamePasswordAuthenticationFilter
  2. 访问验证 BasicAuthenticationFilter
  3. 权限验证 FilterSecurityInterceptor

再导入 SpringSecurity 以来之后,访问接口会默认跳转到一个 SpringSecurity 自带的登录页面。

可以通过配置 SecurityConfig 来自定义的设置密码校验的过程。

通过配置 Bean UserDetailsService 来用户数据进行认证。

通过配置 Bean PasswordEncoder 来对密码进行加密。

image-20240811174734748

在实际开发中,可以自定义认证逻辑,自定义的逻辑需要实现 UserDetailsService 接口,在 SpringSecurity 中传入。

具体而言,自定义的 UserDetailsService 实现的是 loadUserByUsername(String username) 方法,我们需要根据传递的用户名查询到该用户(一般是从数据库查询,缓存也可以)并将查询到的用户封装为一个 UserDetails 对象,该对象由 SpringSecurity 提供,包含用户名、密码、权限。SpringSecurity 会根据 UserDetails 对象中的密码和客户端提供的密码进行比较。相同则认证通过。

image-20240811174808051

准备测试的数据库

CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255),
`password` varchar(255),
`phone` varchar(255),
PRIMARY KEY (`id`)
);

INSERT INTO `users` VALUES (1, 'root', 'abc123', '13812345678');
INSERT INTO `users` VALUES (2, 'Kin', 'abc123', '13812345678');

创建 UserDetailsService 的实现类,编写自定义认证逻辑

image-20240811174816618

在实际开发中,为了数据的安全性,一般不会在数据库中存放密码原文,而是会存放加密后的密码。用户传入的参数是明文密码。此时必须使用密码解析器才能将加密密码与明文密码做对比。Spring Security 中的密码解析器是 PasswordEncoder

SpringSecurity 要求 容器中必须有 PasswordEncoder 实例,之前使用的 NoOpPasswordEncoderPasswordEncoder 的实现类,意思是不解析密码,使用明文密码。

SpringSecurity 官方推荐的密码解析器是 BCryptPasswordEncoder

image-20240811174844558

在实际开发中,将 BCryptPasswordEncoder 的实例放入 Spring 容器即可,并且在用户注册完成后,将密码加密再保存到数据库。

如何自定义加密算法?

  • 继承 PasswordEncoder 接口
    • image-20240811174853758
  • 加解密本身在 SpringSecurity 中是高度独立的。
  • 可以在其他地方使用。
    • image-20240811174905094

SpringSecurity认证,自定义登陆界面

虽然 Spring Security 给我们提供了登录页面,但在实际项目中,更多的是使用自己的登录页面。Spring Security 也支持用户自定义登录页面。用法如下:

  1. 编写登录页面
  2. 在 Spring Security 配置类自定义登录页面
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

// Spring Security 配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义表单登录
http.formLogin()
.loginPage("/login.html") // 自定义登录页面
.usernameParameter("username") // 表单中的用户名项
.passwordParameter("password") // 表单中的密码项
.loginProcessingUrl("/login") // 登录路径,表单向该路径提交,提交后自动执行 UserDetailsService 的方法
.successForwardUrl("/main") // 登录成功后跳转的路径
.failureForwardUrl("/fail"); // 登录失败后跳转的路径

// 需要认证的资源
http.authorizeRequests().antMatchers("/login.html").permitAll() // 登录页不需要认证
.anyRequest().authenticated(); // 其余所有请求都需要认证

// 关闭 csrf 防护
http.csrf().disable();
}

@Override
public void configure(WebSecurity web) throws Exception {
// 静态资源放行
web.ignoring().antMatchers("/css/**");
}
}

CSRF 防护

CSRF:跨站请求伪造,通过伪造用户请求访问受信任的站点从而进行非法请求访问,是一种攻击手段。Spring Security 为了防止 CSRF 攻击,默认开启了 CSRF 防护,CSRF 防护限制了除了 GET 请求以外的大多数方法。我们要想正常使用 Spring Security 需要突破 CSRF 防护。

突破方法

  • 关闭 CSRF 防护
    • http.csrf().disable();
  • 突破 CSRF 防护
    • 请求在访问的时候需要携带参数名为 _csrf 值为令牌的请求头,令牌在服务器产生,如果携带的令牌和服务端的令牌匹配成功,则正常访问。

Spring Security 认证_会话管理

  • 用户认证通过后,我们可能需要获取用户信息,SpringSecurity 将用户信息保存再会话中,并提供会话管理,我们可以从 SecurityContext 对象中获取用户信息,SecurityContext 对象与当前线程绑定
  • image-20240811175036470

登录成功之后的处理方式

登录成功后,如果除了跳转页面还需要执行一些自定义代码时,如:统计访问量,推送消息等操作时,可以自定义登录成功处理器。

  1. 自定义登录成功处理器
    1. image-20240811175044081
  2. 配置登录成功处理器
    1. image-20240811175059501
  3. image-20240811175106226

认证失败后的处理方式

登录失败后,如果除了跳转页面还需要执行一些自定义代码时,如:统计失败次数,记录日志等,可以自定义登录失败处理器。

  1. 自定义登录失败处理器
    1. image-20240811175114179
  2. 配置登录失败处理器
    1. image-20240811175120401

系统中一般都有退出登录的操作,退出登陆后,SpringSecurity 进行了以下操作:

  • 清除认证状态
  • 销毁 HttpSession 对象
  • 跳转到登录页面

在 Spring Security 中,退出登录的写法如下:

配置退出登录的路径和退出后跳转的路径

image-20240811175131860

在网页中添加退出登录超链接

img

SpringSecurity 中也可以配置退出成功处理器

处理器需要实现 LogoutSuccessHandler 接口,之后在 SecurityConfig 中添加。

SpringSecurity 认证 Remember me

image-20240811175144566

具体实现方式

  1. 编写“记住我”配置类
    • image-20240811175153640
  2. 修改 Security 配置类
    • image-20240811175201322
  3. 在登录页面添加“记住我”复选框
    • image-20240811175252668

资源的访问控制

在给用户授权后,我们就可以给系统中的资源设置访问控制,即拥有什么权限才能访问什么资源。

一般控制访问权限的方式

  1. 在配置类中设置
  2. 自定义访问控制逻辑
    • img
    • 在配置文件中使用自定义访问控制逻辑
    • image-20240811175306615

注解设置访问控制

除了配置类,在 SpringSecurity 中提供了一些访问控制的注解,这些注解默认都是不可用的,需要开启后使用。

  • @Secure

    :基于角色的权限控制,要求

    UserDetails

    中的权限名必须以

    ROLE_

    开头。

    • image-20240811175316616
    • image-20240811175323363
  • @PreAuthorize

    :可以在方法执行前判断用户是否具有权限。

    • image-20240811175329521

需要开启

image-20240811175336436

自定义403处理逻辑

  1. 制作权限不足处理类
    • image-20240811175343354
  2. 在 SpringSecurity 中添加
    • img

在前端实现访问控制

SpringSecurity 可以在一些视图技术中进行控制显示效果。例如在 Thymeleaf 中,只有登录用户拥有某些权限才会展示一些菜单。

pom.xml 中引入 Spring Security 和 Thymeleaf 的整合依赖

<!-- Spring Security 整合 Thymeleaf -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

在 Thymeleaf 中使用 Security 标签,控制前端的显示内容

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>主页面</title>
</head>
<body>
<h1>主页面</h1>
<ul>
<li sec:authorize="hasAnyAuthority('/reportform/find')">
<a href="/reportform/find">查询报表</a></li>
<li sec:authorize="hasAnyAuthority('/salary/find')">
<a href="/salary/find">查询工资</a></li>
<li sec:authorize="hasAnyAuthority('/staff/find')">
<a href="/staff/find">查询员工</a>
</li>
</ul>
<a href="/logout">退出登录</a>
</body>
</html>

pringSecurityFilter 运行总结

  • FilterComparator 比较器中初始化了 Spring Security 自带的 Filter 的顺序,即在创建时已经确定了默认 Filter 的顺序。并将所有过滤器保存在一个 filterToOrder Map 中。key 值是 Filter 的类名,value 是过滤器的顺序号。
  • 当我们调用 HttpSecurity#addFilterAt(A, B.class) 方法时(其中 B 一定是先于 A 添加,或者 B 本身就是默认的过滤器),它会将我们添加的过滤器 A 在 FilterComparator 中,并给给我们一个和 B 相同的序号(addFilterBefore(A, B.class) 给 A 的序号比 B 小1,addFilterAfter(A, B.class) 给 A 的序号比 B 大1)。同时,HttpSecurity#addFilter(Filter filter) 会将我们添加的过滤器添加在 filters List 集合中,而在 List 集合中我们手动添加的拦截器在除了 WebAsyncManagerIntegrationFilter 之外的所有系统默认的拦截器之前。
  • 最后 Spring Security 会调用 HttpSecurity#performBuild 方法,在这里会使用 FilterComparator 比较器对 filters 进行比较排序,序号小的在前,序号大的在后,序号相等则按照原先的 filters 中的顺序。
  • 由于在 filters List 集合中,我们自己添加的过滤器要在除了 WebAsyncManagerIntegrationFilter 之外的所有系统默认的拦截器之前。导致了当我们调用了 HttpSecurity#addFilterAt(A, B.class) 方法时,A 拦截器要先于 B 拦截器执行。