SpringSecurity
一、 Spring Security快速入门
官方文档:
https://docs.spring.io/spring-security/reference/index.html
功能:
身份认证
授权
防御常见攻击
- CSRF
- HTTP Headers
- HTTP Requests
1、身份认证(authentication)
官方代码示例:GitHub - spring-projects/spring-security-samples

1.1、创建Spring Boot项目
项目名:security-demo
JDK:17
SpringBoot:3.2.0(依赖了Spring Security 6.2.0)
Dependencies:Spring Web、Spring Security、Thymeleaf
1.2、创建IndexController
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index";
}
}
1.3、创建index.html
在路径resources/templates中创建index.html
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Hello Security!</title>
</head>
<body>
<h1>Hello Security</h1>
<!--通过使用@{/logout},Thymeleaf将自动处理生成正确的URL,以适应当前的上下文路径。
这样,无论应用程序部署在哪个上下文路径下,生成的URL都能正确地指向注销功能。-->
<a th:href="@{/logout}">Log Out 1</a>
<a href="/logout">Log Out 2</a>
</body>
</html>
1.4、启动项目测试Controller
浏览器中访问:http://localhost:8080/
**浏览器自动跳转到登录页面:**http://localhost:8080/login

输入用户名:user
输入密码:在控制台的启动日志中查找初始的默认密码
点击"Sign in"进行登录,浏览器就跳转到了index页面

1.5、注意事项
1.5.1、@{/logout}的作用
通过使用@{/logout},Thymeleaf将自动处理生成正确的URL,以适应当前的上下文路径。这样,无论应用程序部署在哪个上下文路径下,生成的URL都能正确地指向注销功能。
例如:如果我们在配置文件中添加如下内容
server.servlet.context-path=/demo
那么@{/logout}可以自动处理url为正确的相对路径
但是如果是普通的/logout,路径就会不正确
1.6、Spring Security默认做了什么
- 保护应用程序URL,要求对应用程序的任何交互进行身份验证。
- 程序启动时生成一个默认用户“user”。
- 生成一个默认的随机密码,并将此密码记录在控制台上。
- 生成默认的登录表单和注销页面。
- 提供基于表单的登录和注销流程。
- 对于Web请求,重定向到登录页面;
- 对于服务请求,返回401未经授权。
- 处理跨站请求伪造(CSRF)攻击。
- 处理会话劫持攻击。
- 写入Strict-Transport-Security以确保HTTPS。
- 写入X-Content-Type-Options以处理嗅探攻击。
- 写入Cache Control头来保护经过身份验证的资源。
- 写入X-Frame-Options以处理点击劫持攻击。
2、Spring Security 的底层原理
官方文档:Spring Security的底层原理
Spring Security之所以默认帮助我们做了那么多事情,它的底层原理是传统的Servlet过滤器
2.1、Filter
下图展示了处理一个Http请求时,过滤器和Servlet的工作流程:

因此我们可以在过滤器中对请求进行修改或增强。
你可以把过滤器想象成一个“检查站”,每个HTTP请求在到达你的应用之前,都会经过这些检查站。
过滤器链(FilterChain)
- 当客户端发送一个请求到你的应用时,服务器会创建一个过滤器链(FilterChain)。
- 这个链包含了一系列的过滤器(Filter),每个过滤器负责处理一些特定的任务,比如验证用户身份、检查权限等。
- 最后,请求会到达一个Servlet(比如Spring MVC中的
DispatcherServlet
),由它来处理实际的业务逻辑。
2.2、DelegatingFilterProxy(委托过滤器代理)
DelegatingFilterProxy 是 Spring Security 提供的一个 Filter 实现,可以在 Servlet 容器和 Spring 容器之间建立桥梁。通过使用 DelegatingFilterProxy,这样就可以将Servlet容器中的 Filter 实例打包放在 Spring 容器中管理。

2.3、FilterChainProxy
复杂的业务中不可能只有一个过滤器。因此FilterChainProxy是Spring Security提供的一个特殊的Filter,它允许通过SecurityFilterChain将过滤器的工作委托给多个Bean Filter实例。
例子
- 检查流程管理器(FilterChainProxy):它负责根据包裹的目的地和类型,选择合适的检查流程。
- 检查流程(SecurityFilterChain):每个检查流程是一系列检查点的组合,比如:
- 流程1:检查电子产品是否符合安全标准 → 检查是否有损坏 → 打包。
- 流程2:检查食品是否过期 → 检查包装是否密封 → 打包。

2.4、SecurityFilterChain
SecurityFilterChain 被 FilterChainProxy 使用,负责查找当前的请求需要执行的Security Filter列表。

2.5、Multiple SecurityFilterChain
可以有多个SecurityFilterChain的配置,FilterChainProxy决定使用哪个SecurityFilterChain。如果请求的URL是/api/messages/,它首先匹配SecurityFilterChain0的模式/api/**,因此只调用SecurityFilterChain 0。假设没有其他SecurityFilterChain实例匹配,那么将调用SecurityFilterChain n。

其实很好理解,就是这些概念有点复杂,官网图画的也有点复杂了。
3、程序的启动和运行
3.1、DefaultSecurityFilterChain
SecurityFilterChain接口的实现,加载了默认的16个Filter(所以是个集合)
打个断点就可以知道这16个过滤器。

3.2、SecurityProperties
默认情况下Spring Security将初始的用户名和密码存在了SecurityProperties类中。这个类中有一个静态内部类User,配置了默认的用户名(name = "user")和密码(password = uuid)

我们也可以将用户名、密码配置在SpringBoot的配置文件中:在application.properties中配置自定义用户名和密码
spring.security.user.name=user
spring.security.user.password=123
二、 Spring Security自定义配置
1、基于内存的用户认证
1.1、创建自定义配置
实际开发的过程中,我们需要应用程序更加灵活,可以在SpringSecurity中创建自定义配置文件
官方文档:Java自定义配置
UserDetailsService用来管理用户信息,InMemoryUserDetailsManager是UserDetailsService的一个实现,用来管理基于内存的用户信息。
创建一个WebSecurityConfig文件:
定义一个@Bean,类型是UserDetailsService,实现是InMemoryUserDetailsManager
package com.atguigu.securitydemo.config;
@Configuration
@EnableWebSecurity//Spring项目总需要添加此注解,SpringBoot项目中不需要
public class WebSecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
//创建基于内存的用户信息管理器
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//使用nanager管理UserDeta1Ls对象
manager.createUser( //此行设置断点可以查看创建的user对象
//创建UserDeta1Ls对象,用于管理用户名、用户密码、用户角色:用户仅限等内容
User
.withDefaultPasswordEncoder()
.username("qin") //自定义用户名
.password("123.com") //自定义密码
.roles("USER") //自定义角色
.build()
);
return manager;
}
}
**测试:**使用用户名huan,密码password登录

基于springboot项目的所有第三方配置都会在这个autoconfigure中预先定义好,打开就会发现整合了非常多的预定义配置,然后打开security
这个文件夹

然后发现有这么一行注解

意思是,只要EnableWebSecurity.class
这个类被加载到了应用上下文中,那么@EnableWebSecurity这个注解就会生效;那这个类什么时候被加载呢:只要pom.xml中导入了security依赖,就会加载这个类。所以springboot项目中@EnableWebSecurity这个就不需要添加了。
还有,创建的UserDetailsService
并不是一个类,而是一个接口,他底下有很多实现类;

现在对第二个做一个测试,它的意思是,把用户信息管理在springsecurity应用程序的内存当中,
1.2、基于内存的用户认证流程
- 程序启动时:
- 创建
InMemoryUserDetailsManager
对象 - 创建
User
对象,封装用户名密码 - 使用InMemoryUserDetailsManager
将User存入内存
- 创建
- 校验用户时:
- SpringSecurity自动使用
InMemoryUserDetailsManager
的loadUserByUsername
方法从内存中
获取User对象 - 在
UsernamePasswordAuthenticationFilter
过滤器中的attemptAuthentication
方法中将用户输入的用户名密码和从内存中获取到的用户信息进行比较,进行用户认证
- SpringSecurity自动使用
2、基于数据库的数据源
2.1、SQL
创建三个数据库表并插入测试数据
-- 创建数据库
CREATE DATABASE `security-demo`;
USE `security-demo`;
-- 创建用户表
CREATE TABLE `user`(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) DEFAULT NULL ,
`password` VARCHAR(500) DEFAULT NULL,
`enabled` BOOLEAN NOT NULL
);
-- 唯一索引
CREATE UNIQUE INDEX `user_username_uindex` ON `user`(`username`);
-- 插入用户数据(密码是 "abc" )
INSERT INTO `user` (`username`, `password`, `enabled`) VALUES
('admin', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Helen', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Tom', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE);
2.2、引入依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.4.1</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2.3、配置数据源
#MySQL数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security-demo
spring.datasource.username=root
spring.datasource.password=123456
#SQL日志
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
2.4、实体类
package com.atguigu.securitydemo.entity;
@Data
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private Boolean enabled;
}
2.5、Mapper
package com.atguigu.securitydemo.mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
resources/mapper/UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.securitydemo.mapper.UserMapper">
</mapper>
2.6、Service
package com.atguigu.securitydemo.service;
public interface UserService extends IService<User> {
}
package com.atguigu.securitydemo.service.impl;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
2.7、Controller
package com.atguigu.securitydemo.controller;
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
public UserService userService;
@GetMapping("/list")
public List<User> getList(){
return userService.list();
}
}
测试:localhost:8080/demo/user/list
3、基于数据库的用户认证
3.1、基于数据库的用户认证流程
- 程序启动时:
- 创建
DBUserDetailsManager
类,实现接口 UserDetailsManager, UserDetailsPasswordService - 在应用程序中初始化这个类的对象
- 创建
- 校验用户时:
- SpringSecurity自动使用
DBUserDetailsManager
的loadUserByUsername
方法从数据库中
获取User对象 - 在
UsernamePasswordAuthenticationFilter
过滤器中的attemptAuthentication
方法中将用户输入的用户名密码和从数据库中获取到的用户信息进行比较,进行用户认证
- SpringSecurity自动使用
3.2、定义DBUserDetailsManager
package com.atguigu.securitydemo.config;
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
User user = userMapper.selectOne(queryWrapper);
if (user == null) {
throw new UsernameNotFoundException(username);
} else {
Collection<GrantedAuthority> authorities = new ArrayList<>();
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getEnabled(),
true, //用户账号是否未过期
true, //用户凭证是否未过期
true, //用户是否未被锁定
authorities); //权限列表
}
}
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
return null;
}
@Override
public void createUser(UserDetails user) {
}
@Override
public void updateUser(UserDetails user) {
}
@Override
public void deleteUser(String username) {
}
@Override
public void changePassword(String oldPassword, String newPassword) {
}
@Override
public boolean userExists(String username) {
return false;
}
}
3.3、初始化UserDetailsService
修改WebSecurityConfig中的userDetailsService方法如下
@Bean
public UserDetailsService userDetailsService() {
DBUserDetailsManager manager = new DBUserDetailsManager();
return manager;
}
或者直接在DBUserDetailsManager类上添加@Component注解
**测试:**使用数据库中配置的用户名和密码进行登录
4、SpringSecurity的默认配置
在WebSecurityConfig中添加如下配置(它是默认的行为,加与不加都行)
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//authorizeRequests():开启授权保护
//anyRequest():对所有请求开启授权保护
//authenticated():已认证(登录了的 )请求会自动被授权
http
.authorizeRequests(authorize -> authorize.anyRequest().authenticated())
.formLogin(withDefaults())//默认登录表单授权方式
.httpBasic(withDefaults());//使用基本授权方式
return http.build();
}
5、添加用户功能
5.1、Controller
UserController中添加方法
@PostMapping("/add")
public void add(@RequestBody User user){
userService.saveUserDetails(user);
}
5.2、Service
UserService接口中添加方法
void saveUserDetails(User user);
UserServiceImpl实现中添加方法
@Resource
private DBUserDetailsManager dbUserDetailsManager;
@Override
public void saveUserDetails(User user) {
UserDetails userDetails = org.springframework.security.core.userdetails.User
.withDefaultPasswordEncoder()
.username(user.getUsername()) //自定义用户名
.password(user.getPassword()) //自定义密码
.build();
dbUserDetailsManager.createUser(userDetails);
}
5.3、修改配置
DBUserDetailsManager中添加方法
@Override
public void createUser(UserDetails userDetails) {
User user = new User();
user.setUsername(userDetails.getUsername());
user.setPassword(userDetails.getPassword());
user.setEnabled(true);
userMapper.insert(user);
}
5.4、使用Swagger测试
pom中添加配置用于测试
<!--swagger测试-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
**Swagger测试地址:**http://localhost:8080/demo/doc.html

5.5、关闭csrf攻击防御
默认情况下SpringSecurity开启了csrf攻击防御的功能,这要求请求参数中必须有一个隐藏的**_csrf**字段,如下:

在filterChain方法中添加如下代码,关闭csrf攻击防御
//关闭csrf攻击防御
http.csrf((csrf) -> {
csrf.disable();
});
6、密码加密算法
参考文档:Password Storage :: Spring Security
6.1、密码加密方式
明文密码:
最初,密码以明文形式存储在数据库中。但是恶意用户可能会通过SQL注入等手段获取到明文密码,或者程序员将数据库数据泄露的情况也可能发生。
Hash算法:
Spring Security的PasswordEncoder
接口用于对密码进行单向转换
,从而将密码安全地存储。对密码单向转换需要用到哈希算法
,例如MD5、SHA-256、SHA-512等,哈希算法是单向的,只能加密,不能解密
。
因此,数据库中存储的是单向转换后的密码
,Spring Security在进行用户身份验证时需要将用户输入的密码进行单向转换,然后与数据库的密码进行比较。
因此,如果发生数据泄露,只有密码的单向哈希会被暴露。由于哈希是单向的,并且在给定哈希的情况下只能通过暴力破解的方式猜测密码
。
彩虹表:
恶意用户创建称为彩虹表
的查找表。
彩虹表就是一个庞大的、针对各种可能的字母组合预先生成的哈希值集合,有了它可以快速破解各类密码。越是复杂的密码,需要的彩虹表就越大,主流的彩虹表都是100G以上,目前主要的算法有LM, NTLM, MD5, SHA1, MYSQLSHA1, HALFLMCHALL, NTLMCHALL, ORACLE-SYSTEM, MD5-HALF。
加盐密码:
为了减轻彩虹表的效果,开发人员开始使用加盐密码。不再只使用密码作为哈希函数的输入,而是为每个用户的密码生成随机字节(称为盐)。盐和用户的密码将一起经过哈希函数运算,生成一个唯一的哈希。盐将以明文形式与用户的密码一起存储。然后,当用户尝试进行身份验证时,盐和用户输入的密码一起经过哈希函数运算,再与存储的密码进行比较。唯一的盐意味着彩虹表不再有效,因为对于每个盐和密码的组合,哈希都是不同的。
自适应单向函数:
随着硬件的不断发展,加盐哈希也不再安全。原因是,计算机可以每秒执行数十亿次哈希计算。这意味着我们可以轻松地破解每个密码。
现在,开发人员开始使用自适应单向函数来存储密码。使用自适应单向函数验证密码时,故意占用资源(故意使用大量的CPU、内存或其他资源)
。自适应单向函数允许配置一个“工作因子”
,随着硬件的改进而增加。我们建议将“工作因子”调整到系统中验证密码需要约一秒钟的时间。这种权衡是为了让攻击者难以破解密码
。
自适应单向函数包括bcrypt、PBKDF2、scrypt和argon2
。
6.2、PasswordEncoder
BCryptPasswordEncoder
使用广泛支持的bcrypt算法来对密码进行哈希。为了增加对密码破解的抵抗力,bcrypt故意设计得较慢。和其他自适应单向函数一样,应该调整其参数,使其在您的系统上验证一个密码大约需要1秒的时间。BCryptPasswordEncoder的默认实现使用强度10。建议您在自己的系统上调整和测试强度参数,以便验证密码时大约需要1秒的时间。
Argon2PasswordEncoder
使用Argon2算法对密码进行哈希处理。Argon2是密码哈希比赛的获胜者。为了防止在自定义硬件上进行密码破解,Argon2是一种故意缓慢的算法,需要大量内存。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。当前的Argon2PasswordEncoder实现需要使用BouncyCastle库。
Pbkdf2PasswordEncoder
使用PBKDF2算法对密码进行哈希处理。为了防止密码破解,PBKDF2是一种故意缓慢的算法。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。当需要FIPS认证时,这种算法是一个很好的选择。

SCryptPasswordEncoder
使用scrypt算法对密码进行哈希处理。为了防止在自定义硬件上进行密码破解,scrypt是一种故意缓慢的算法,需要大量内存。与其他自适应单向函数一样,它应该在您的系统上调整为大约1秒来验证一个密码。
6.3、密码加密测试
在测试类中编写一个测试方法
@Test
void testPassword() {
// 工作因子,默认值是10,最小值是4,最大值是31,值越大运算速度越慢
PasswordEncoder encoder = new BCryptPasswordEncoder(4);
//明文:"password"
//密文:result,即使明文密码相同,每次生成的密文也不一致
String result = encoder.encode("password");
System.out.println(result);
//密码校验
Assert.isTrue(encoder.matches("password", result), "密码不一致");
}
6.4、DelegatingPasswordEncoder
- 表中存储的密码形式:
{bcrypt}
$2aGRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW - 通过如下源码可以知道:可以通过
{bcrypt}
前缀动态获取和密码的形式类型一致的PasswordEncoder对象 - 目的:方便随时做密码策略的升级,兼容数据库中的老版本密码策略生成的密码

7、自定义登录页面
7.1、创建登录Controller
package com.atguigu.securitydemo.controller;
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
}
7.2、创建登录页面
resources/templates/login.html
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>登录</title>
</head>
<body>
<h1>登录</h1>
<div th:if="${param.error}">
错误的用户名和密码.</div>
<!--method必须为"post"-->
<!--th:action="@{/login}" ,
使用动态参数,表单中会自动生成_csrf隐藏字段,用于防止csrf攻击
login: 和登录页面保持一致即可,SpringSecurity自动进行登录认证-->
<form th:action="@{/login}" method="post">
<div>
<!--name必须为"username"-->
<input type="text" name="username" placeholder="用户名"/>
</div>
<div>
<!--name必须为"password"-->
<input type="password" name="password" placeholder="密码"/>
</div>
<input type="submit" value="登录" />
</form>
</body>
</html>
7.3、配置SecurityFilterChain
SecurityConfiguration:
.formLogin( form -> {
form
.loginPage("/login").permitAll() //登录页面无需授权即可访问
.usernameParameter("username") //自定义表单用户名参数,默认是username
.passwordParameter("password") //自定义表单密码参数,默认是password
.failureUrl("/login?error") //登录失败的返回地址
;
}); //使用表单授权方式