作为 Java 生态中最主流的安全框架,Spring Security 凭借其强大的扩展性、与 Spring 生态的无缝集成,成为企业级应用认证(Authentication)与授权(Authorization)的首选方案。本文基于两天的系统学习笔记,从基础入门到高级实战,带你全面掌握 Spring Security 的核心用法,帮你解决项目中的安全需求。
目录
1.2 Spring Security vs Shiro:如何选型?
2.1 第一个 Spring Security 项目:5 分钟上手
2.2 核心接口:UserDetailsService 详解
2.3.3 BCryptPasswordEncoder 核心用法
步骤 1:请求进入 UsernamePasswordAuthenticationFilter
步骤 2:AuthenticationManager 分发认证任务
步骤 3:DaoAuthenticationProvider 执行认证
步骤 2:配置 PersistentTokenRepository
3.3.1 antMatchers ():Ant 风格表达式(推荐)
3.3.3 regexMatchers ():正则表达式匹配
3.5.1 hasAuthority ():判断是否具有指定权限
3.5.2 hasAnyAuthority ():判断是否具有指定权限中的任意一个
3.5.4 hasAnyRole ():判断是否具有指定角色中的任意一个
3.5.5 hasIpAddress ():判断是否来自指定 IP
3.8.2 @Secured:判断角色(需 ROLE_前缀)
3.8.3 @PreAuthorize:方法执行前判断权限(推荐)
3.8.4 @PostAuthorize:方法执行后判断权限
3.9 视图集成:Thymeleaf 中使用 Spring Security
步骤 3:获取认证信息(sec:authentication)
3.10.2 Spring Security 的 CSRF 防护机制
一、引言:为什么选择 Spring Security?

在开始之前,我们先明确两个核心问题:Spring Security 是什么? 以及什么时候该用它?
1.1 Spring Security 核心定位
Spring Security 是一个高度可定制的安全框架,基于 Spring IoC/DI 和 AOP,为应用提供声明式安全访问控制,核心解决两大问题:
- 认证(Authentication):验证 “你是谁”(比如用户登录时验证账号密码);
- 授权(Authorization):判断 “你能做什么”(比如普通用户不能访问管理员页面)。
它能替代传统重复的安全代码,支持表单登录、OAuth2.0、JWT、记住我等多种场景,且与 Spring Boot、Spring Cloud 无缝兼容。
1.2 Spring Security vs Shiro:如何选型?
很多人会纠结这两个框架,这里给出客观对比,帮你快速决策:
| 维度 | Spring Security | Shiro |
|---|---|---|
| 生态集成 | 与 Spring 生态深度绑定(Boot/Cloud) | 不依赖任何框架,可独立使用 |
| 社区支持 | Spring 官方维护,更新快、文档全 | Apache 项目,社区活跃但更新较慢 |
| 功能覆盖 | 功能全面(OAuth2.0/JWT/LDAP 等) | 核心功能齐全(认证 / 授权 / 记住我) |
| 上手难度 | 略复杂,需理解 Spring 生态 | 简单直观,API 友好,学习成本低 |
选型建议:
- 若项目基于 Spring Boot/Cloud,优先选 Spring Security(集成顺畅,无额外适配成本);
- 若项目不依赖 Spring,或追求快速上手、轻量,选 Shiro;
- 若团队熟悉 Spring,Spring Security 是长期更优解(功能扩展性更强)。
二、第一天:夯实认证基础
认证是安全的第一步,我们从最基础的项目搭建开始,逐步深入核心接口与自定义逻辑。
2.1 第一个 Spring Security 项目:5 分钟上手
Spring Boot 已将 Spring Security 封装为启动器,无需复杂配置,快速体验默认安全机制。
步骤 1:导入依赖
在 Spring Boot 项目的pom.xml中添加启动器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
步骤 2:体验默认行为
启动项目后,Spring Security 会自动生效:
- 拦截所有请求:未登录时,无论访问哪个 URL,都会跳转到内置登录页(
/login); - 默认账号密码:用户名固定为
user,密码会打印在控制台(格式如Using generated security password: 7c3018c2-1fba-4d18-bbf6-7d02f545cd91); - 登录后访问:输入账号密码,即可访问目标页面(如
http://localhost:8080/login.html)。

步骤 3:自定义默认账号密码
不想用随机密码?在application.yml(或application.properties)中配置:
spring:
security:
user:
name: bjsxt # 自定义用户名
password: bjsxt # 自定义密码
2.2 核心接口:UserDetailsService 详解
默认账号密码仅用于测试,实际项目中用户信息存储在数据库。UserDetailsService是 Spring Security 提供的用户信息查询接口,我们需实现它来从数据库获取用户。
2.2.1 接口作用
UserDetailsService只有一个核心方法:
public interface UserDetailsService {
// 根据用户名查询用户信息,返回UserDetails(用户详情)
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
2.2.2 关键参数与返回值
- 参数
username:客户端提交的用户名(默认表单参数名是username,可自定义); - 返回值
UserDetails:Spring Security 的用户详情接口,包含用户的账号、密码、权限等信息,常用实现类是org.springframework.security.core.userdetails.User; - 异常
UsernameNotFoundException:当用户名不存在时抛出,Spring Security 会自动识别为 “用户名不存在”。
2.2.3 UserDetails 核心方法
UserDetails接口定义了用户的核心属性,User实现类需满足这些要求:
public interface UserDetails extends Serializable {
// 获取用户权限(如"ROLE_ADMIN"、"menu:sys")
Collection<? extends GrantedAuthority> getAuthorities();
// 获取密码(数据库中存储的加密后密码)
String getPassword();
// 获取用户名(客户端提交的用户名)
String getUsername();
// 账号是否未过期
boolean isAccountNonExpired();
// 账号是否未锁定
boolean isAccountNonLocked();
// 密码是否未过期
boolean isCredentialsNonExpired();
// 账号是否可用
boolean isEnabled();
}
2.2.4 简单实现示例
@Service
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 模拟从数据库查询用户(实际项目中替换为DAO查询)
if (!"bjsxt".equals(username)) {
throw new UsernameNotFoundException("用户名不存在");
}
// 2. 构造用户权限(多个权限用逗号分隔,通过AuthorityUtils转换)
Collection<? extends GrantedAuthority> authorities =
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,menu:sys");
// 3. 返回UserDetails实现类(注意:密码需是加密后的,后续讲PasswordEncoder)
return new User(
username,
"$2a$10$EixZaYb4rVw54n11oM4b4.3G7Xf5pF1s5D5a5B5c5D5e5F5g5H5j5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5Y5Z5",
authorities
);
}
}
2.3 密码安全:PasswordEncoder 解析器
Spring Security 强制要求使用PasswordEncoder对密码进行加密存储,禁止明文存储(防止数据库泄露后密码直接被利用)。
2.3.1 为什么需要 PasswordEncoder?
- 明文密码风险:数据库泄露后,攻击者可直接登录;
- 加密算法要求:需支持单向加密(无法从密文反推明文)、盐值随机(相同明文加密后密文不同)。
2.3.2 内置解析器对比

Spring Security 提供多种解析器,其中BCryptPasswordEncoder是官方推荐(强哈希算法,支持盐值自动生成):
| 解析器 | 特点 | 状态 |
|---|---|---|
| BCryptPasswordEncoder | 基于 BCrypt 算法,盐值自动生成,强度可配置 | 推荐使用 |
| Md5PasswordEncoder | 基于 MD5 算法,无盐值,易破解 | 已弃用 |
| ShaPasswordEncoder | 基于 SHA 算法,无盐值,易破解 | 已弃用 |
| NoOpPasswordEncoder | 不加密,明文存储 | 仅测试用 |
2.3.3 BCryptPasswordEncoder 核心用法
// 1. 测试类中演示加密与匹配
@Test
public void testBCrypt() {
// 初始化解析器(强度默认10,范围4-31,值越大加密越慢)
PasswordEncoder encoder = new BCryptPasswordEncoder();
// 2. 加密密码(明文"123456")
String encodedPassword = encoder.encode("123456");
System.out.println("加密后密码:" + encodedPassword);
// 输出示例:$2a$10$EixZaYb4rVw54n11oM4b4.3G7Xf5pF1s5D5a5B5c5D5e5F5g5H5j5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5Y5Z5
// 3. 匹配密码(明文 vs 加密后密码)
boolean isMatch = encoder.matches("123456", encodedPassword);
System.out.println("密码是否匹配:" + isMatch); // 输出true
}
2.3.4 注入 PasswordEncoder Bean
必须将PasswordEncoder注入 Spring 容器,否则 Spring Security 会报错:
@Configuration
public class SecurityConfig {
// 注入BCryptPasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.4 自定义登录逻辑:从数据库获取用户
实际项目中,用户、角色、权限存储在数据库,需通过 MyBatis 查询。我们按 “表结构→依赖→配置→代码” 的顺序实现。
2.4.1 数据库表结构

需设计 5 张表(RBAC 权限模型):
-- 1. 用户表(存储账号密码)
CREATE TABLE `user` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`username` varchar(20) NOT NULL UNIQUE,
`password` varchar(64) NOT NULL -- 存储BCrypt加密后的密码
);
-- 2. 角色表
CREATE TABLE `role` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`name` varchar(20) NOT NULL -- 如"管理员"、"普通用户"
);
-- 3. 用户-角色关联表(多对多)
CREATE TABLE `role_user` (
`uid` bigint,
`rid` bigint,
FOREIGN KEY (`uid`) REFERENCES `user`(`id`),
FOREIGN KEY (`rid`) REFERENCES `role`(`id`)
);
-- 4. 菜单(权限)表
CREATE TABLE `menu` (
`id` bigint PRIMARY KEY AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`url` varchar(100),
`parent_id` bigint,
`permission` varchar(20) NOT NULL -- 如"menu:sys"、"user:list"
);
-- 5. 角色-菜单关联表(多对多)
CREATE TABLE `role_menu` (
`mid` bigint,
`rid` bigint,
FOREIGN KEY (`mid`) REFERENCES `menu`(`id`),
FOREIGN KEY (`rid`) REFERENCES `role`(`id`)
);
-- 插入测试数据
INSERT INTO `user` VALUES (1, '张三', '$2a$10$EixZaYb4rVw54n11oM4b4.3G7Xf5pF1s5D5a5B5c5D5e5F5g5H5j5K5L5M5N5O5P5Q5R5S5T5U5V5W5X5Y5Z5');
INSERT INTO `role` VALUES (1, '管理员'), (2, '普通用户');
INSERT INTO `role_user` VALUES (1, 1);
INSERT INTO `menu` VALUES (1, '系统管理', '', 0, 'menu:sys'), (2, '用户管理', '', 0, 'menu:user');
INSERT INTO `role_menu` VALUES (1, 1), (2, 1), (2, 2);
2.4.2 导入 MyBatis 依赖
<!-- MyBatis启动器 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
2.4.3 配置数据源与 MyBatis
在application.yml中配置:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8
username: root
password: 123456
mybatis:
type-aliases-package: com.bjsxt.pojo # 实体类别名包
mapper-locations: classpath:mybatis/*.xml # Mapper XML路径
2.4.4 编写 Mapper 接口与 XML
1. 实体类(User.java)
public class User {
private Long id;
private String username;
private String password;
// getter/setter
}
2. Mapper 接口(UserMapper.java)
@Mapper
public interface UserMapper {
// 根据用户名查询用户
User selectByUsername(String username);
// 根据用户名查询权限(menu.permission)
List<String> selectPermissionByUsername(String username);
// 根据用户名查询角色(role.name)
List<String> selectRoleByUsername(String username);
}
3. Mapper XML(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.bjsxt.mapper.UserMapper">
<!-- 根据用户名查询用户 -->
<select id="selectByUsername" parameterType="string" resultType="user">
SELECT id, username, password FROM user WHERE username = #{username}
</select>
<!-- 根据用户名查询权限 -->
<select id="selectPermissionByUsername" parameterType="string" resultType="string">
SELECT m.permission
FROM user u
JOIN role_user ru ON u.id = ru.uid
JOIN role r ON ru.rid = r.id
JOIN role_menu rm ON r.id = rm.rid
JOIN menu m ON rm.mid = m.id
WHERE u.username = #{username}
</select>
<!-- 根据用户名查询角色 -->
<select id="selectRoleByUsername" parameterType="string" resultType="string">
SELECT r.name
FROM user u
JOIN role_user ru ON u.id = ru.uid
JOIN role r ON ru.rid = r.id
WHERE u.username = #{username}
</select>
</mapper>
2.4.5 实现 UserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 查询用户
User user = userMapper.selectByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
// 2. 查询权限(menu.permission)
List<String> permissions = userMapper.selectPermissionByUsername(username);
// 3. 查询角色(需拼接"ROLE_"前缀,符合Spring Security规范)
List<String> roles = userMapper.selectRoleByUsername(username);
roles = roles.stream().map(role -> "ROLE_" + role).collect(Collectors.toList());
// 4. 合并权限与角色(Spring Security中角色也是一种权限)
List<String> allAuthorities = new ArrayList<>();
allAuthorities.addAll(permissions);
allAuthorities.addAll(roles);
// 5. 转换为GrantedAuthority集合
Collection<? extends GrantedAuthority> authorities =
AuthorityUtils.createAuthorityList(allAuthorities.toArray(new String[0]));
// 6. 返回UserDetails
return new User(
user.getUsername(),
user.getPassword(), // 数据库中已加密的密码
true, // 账号未过期
true, // 账号未锁定
true, // 密码未过期
true, // 账号可用
authorities
);
}
}
2.5 自定义登录页面:告别默认样式
默认登录页样式简陋,实际项目需替换为自定义页面。
2.5.1 编写登录页面(login.html)
放在src/main/resources/static目录下(静态资源目录):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>自定义登录页</title>
</head>
<body>
<!-- action需与配置的loginProcessingUrl一致 -->
<form action="/doLogin" method="post">
<div>
<label>用户名:</label>
<input type="text" name="username"> <!-- 参数名需与配置一致 -->
</div>
<div>
<label>密码:</label>
<input type="password" name="password"> <!-- 参数名需与配置一致 -->
</div>
<div>
<button type="submit">登录</button>
</div>
</form>
</body>
</html>
2.5.2 配置 SecurityConfig(关键)
注意:Spring Boot 2.7 后,WebSecurityConfigurerAdapter 已过时,需用SecurityFilterChain Bean 方式配置:
@Configuration
public class SecurityConfig {
@Autowired
private MyUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. 配置表单登录
.formLogin()
.loginPage("/login.html") // 自定义登录页面路径(未登录时跳转)
.loginProcessingUrl("/doLogin") // 登录请求处理路径(无需写Controller)
.successForwardUrl("/toMain") // 登录成功后转发路径(POST请求)
.failureForwardUrl("/toFail") // 登录失败后转发路径(POST请求)
.usernameParameter("username") // 自定义用户名参数名(默认username)
.passwordParameter("password") // 自定义密码参数名(默认password)
.and()
// 2. 配置URL访问控制
.authorizeRequests()
.antMatchers("/login.html", "/toFail").permitAll() // 放行登录页和失败页
.anyRequest().authenticated() // 其他所有请求需认证
.and()
// 3. 关闭CSRF防护(暂时关闭,后续详解)
.csrf().disable();
return http.build();
}
// 配置AuthenticationManager(指定UserDetailsService和PasswordEncoder)
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
2.5.3 编写控制器(处理成功 / 失败跳转)
@Controller
public class LoginController {
// 登录成功转发(POST请求)
@PostMapping("/toMain")
public String toMain() {
return "redirect:/main.html"; // 重定向到main.html(避免表单重复提交)
}
// 登录失败转发(POST请求)
@PostMapping("/toFail")
public String toFail() {
return "redirect:/fail.html"; // 重定向到fail.html
}
}
2.5.4 编写成功 / 失败页面
在static目录下新建main.html和fail.html:
<!-- main.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h1>登录成功!欢迎访问首页</h1>
</body>
</html>
<!-- fail.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录失败</title>
</head>
<body>
<h1>用户名或密码错误,请重新登录!</h1>
<a href="/login.html">返回登录页</a>
</body>
</html>
2.6 认证常用配置:自定义成功 / 失败处理器
successForwardUrl和failureForwardUrl仅支持转发(POST 请求),若需重定向到外部链接(如百度)或返回 JSON,需自定义处理器。
2.6.1 自定义登录成功处理器
实现AuthenticationSuccessHandler接口:
@Component
public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 1. 获取认证用户信息
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
System.out.println("登录成功,用户名:" + userDetails.getUsername());
System.out.println("用户权限:" + userDetails.getAuthorities());
// 2. 自定义响应(示例:重定向到百度)
response.sendRedirect("https://www.baidu.com");
// 若为前后端分离,可返回JSON:
// response.setContentType("application/json;charset=utf-8");
// PrintWriter out = response.getWriter();
// out.write("{\"code\":200,\"msg\":\"登录成功\"}");
// out.flush();
}
}
2.6.2 自定义登录失败处理器
实现AuthenticationFailureHandler接口:
@Component
public class MyLoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 1. 获取失败原因
String msg = "登录失败";
if (exception instanceof UsernameNotFoundException) {
msg = "用户名不存在";
} else if (exception instanceof BadCredentialsException) {
msg = "密码错误";
}
// 2. 自定义响应(示例:返回JSON)
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"code\":401,\"msg\":\"" + msg + "\"}");
out.flush();
}
}
2.6.3 配置处理器到 SecurityConfig
@Configuration
public class SecurityConfig {
@Autowired
private MyLoginSuccessHandler loginSuccessHandler;
@Autowired
private MyLoginFailureHandler loginFailureHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.successHandler(loginSuccessHandler) // 替换successForwardUrl
.failureHandler(loginFailureHandler) // 替换failureForwardUrl
.usernameParameter("username")
.passwordParameter("password")
// 其他配置不变...
.and()
.csrf().disable();
return http.build();
}
}
2.7 深入理解:完整认证流程(面试重点)

掌握认证流程是理解 Spring Security 的核心,也是面试高频考点。流程如下(从用户提交登录请求到认证成功):
步骤 1:请求进入 UsernamePasswordAuthenticationFilter
用户提交登录请求(如/doLogin),首先被UsernamePasswordAuthenticationFilter拦截:
- 从请求中提取用户名和密码(通过
usernameParameter和passwordParameter配置); - 构造
UsernamePasswordAuthenticationToken(未认证状态,仅包含用户名和密码); - 将
token交给AuthenticationManager处理。
步骤 2:AuthenticationManager 分发认证任务
AuthenticationManager本身不处理认证,而是管理多个AuthenticationProvider(认证提供者),通过supports()方法判断哪个Provider支持当前认证类型(如表单登录对应DaoAuthenticationProvider)。
步骤 3:DaoAuthenticationProvider 执行认证
DaoAuthenticationProvider是表单登录的核心认证提供者,执行以下操作:
- 查询用户:调用
UserDetailsService.loadUserByUsername(),从数据库获取UserDetails; - 密码匹配:通过
PasswordEncoder.matches(),对比客户端提交的密码与数据库中的加密密码; - 校验用户状态:检查
UserDetails的isAccountNonExpired()、isAccountNonLocked()等方法,确保用户状态正常; - 构造认证成功 token:创建
UsernamePasswordAuthenticationToken(已认证状态,包含用户信息和权限),返回给AuthenticationManager。
步骤 4:认证结果处理
- 认证成功:
AuthenticationManager将认证成功的token传递回UsernamePasswordAuthenticationFilter,过滤器调用loginSuccessHandler处理(如跳转、返回 JSON); - 认证失败:抛出
AuthenticationException(如UsernameNotFoundException、BadCredentialsException),过滤器调用loginFailureHandler处理。
流程总结图
用户提交登录请求 → UsernamePasswordAuthenticationFilter(提取账号密码)→
AuthenticationManager(分发任务)→ DaoAuthenticationProvider(执行认证)→
UserDetailsService(查用户)→ PasswordEncoder(验密码)→
认证成功/失败 → 调用对应处理器
三、第二天:精通授权与高级功能
认证解决 “你是谁”,授权解决 “你能做什么”。本节学习授权配置、记住我、退出登录、CSRF 防护等高级功能。
3.1 记住我:Remember Me 功能实现
“记住我” 功能允许用户下次访问时无需重新登录,Spring Security 通过 Cookie 存储用户信息到客户端,服务器端存储令牌到数据库。
步骤 1:导入依赖(MyBatis 已导入)
“记住我” 依赖 Spring JDBC 存储令牌,若已导入 MyBatis 启动器,无需额外导入(MyBatis 包含 Spring JDBC)。
步骤 2:配置 PersistentTokenRepository
用于存储 “记住我” 令牌到数据库(自动创建persistent_logins表):
@Configuration
public class RememberMeConfig {
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 第一次启动时自动创建表(创建后注释,避免重复创建)
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
}
步骤 3:配置 SecurityConfig
@Configuration
public class SecurityConfig {
@Autowired
private MyUserDetailsService userDetailsService;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 配置记住我
.rememberMe()
.userDetailsService(userDetailsService) // 登录逻辑
.tokenRepository(persistentTokenRepository) // 令牌存储
.tokenValiditySeconds(60 * 60 * 24 * 7) // 令牌有效期(7天,默认2周)
.rememberMeParameter("rememberMe") // 表单参数名(默认remember-me)
.and()
// 其他配置不变...
.csrf().disable();
return http.build();
}
}
步骤 4:修改登录页面
添加 “记住我” 复选框(参数名与rememberMeParameter一致):
<form action="/doLogin" method="post">
<div>
<label>用户名:</label>
<input type="text" name="username">
</div>
<div>
<label>密码:</label>
<input type="password" name="password">
</div>
<div>
<input type="checkbox" name="rememberMe" value="true"> 记住我
</div>
<div>
<button type="submit">登录</button>
</div>
</form>
3.2 安全退出:Logout 配置与扩展
Spring Security 默认支持退出登录,只需访问/logout即可,也可自定义配置。
3.2.1 默认退出行为
- 退出 URL:
/logout(POST 请求); - 退出成功后跳转:
/login?logout; - 退出操作:清除认证信息、销毁 Session、删除 “记住我” 令牌。
3.2.2 自定义退出配置
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.logout()
.logoutUrl("/doLogout") // 自定义退出URL
.logoutSuccessUrl("/login.html") // 退出成功后跳转路径
.deleteCookies("JSESSIONID", "remember-me") // 删除指定Cookie
.clearAuthentication(true) // 清除认证信息(默认true)
.invalidateHttpSession(true) // 销毁Session(默认true)
.and()
// 其他配置不变...
.csrf().disable();
return http.build();
}
3.2.3 自定义退出成功处理器
若需返回 JSON(前后端分离场景),实现LogoutSuccessHandler:
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"code\":200,\"msg\":\"退出登录成功\"}");
out.flush();
}
}
配置到 SecurityConfig:
@Autowired
private MyLogoutSuccessHandler logoutSuccessHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.logout()
.logoutUrl("/doLogout")
.logoutSuccessHandler(logoutSuccessHandler) // 替换logoutSuccessUrl
// 其他配置不变...
.csrf().disable();
return http.build();
}
3.3 访问控制:URL 匹配规则
授权的核心是 “哪些 URL 需要什么权限”,Spring Security 提供 3 种 URL 匹配方式:
3.3.1 antMatchers ():Ant 风格表达式(推荐)
支持?、*、**通配符,最常用:
?:匹配 1 个字符(如/user/?匹配/user/1,不匹配/user/12);*:匹配 0 个或多个字符(如/user/*匹配/user/1、/user/abc,不匹配/user/1/2);**:匹配 0 个或多个目录(如/user/**匹配/user/1、/user/1/2、/user/abc/def)。
示例:
http.authorizeRequests()
.antMatchers("/login.html", "/doLogin").permitAll() // 放行登录相关
.antMatchers("/js/**", "/css/**", "/images/**").permitAll() // 放行静态资源
.antMatchers("/admin/**").hasRole("管理员") // /admin/**需管理员角色
.anyRequest().authenticated(); // 其他请求需认证
3.3.2 anyRequest ():匹配所有请求
通常放在最后,作为 “兜底” 配置:
http.authorizeRequests()
.antMatchers("/login.html").permitAll()
.anyRequest().authenticated(); // 所有其他请求需认证
3.3.3 regexMatchers ():正则表达式匹配
适合复杂匹配场景(如匹配所有.js文件):
http.authorizeRequests()
.regexMatchers(".+[.]js").permitAll() // 放行所有.js文件
.anyRequest().authenticated();
注意:匹配顺序
具体的规则要放在前面,笼统的规则要放在后面,否则会被覆盖。例如:
// 错误:anyRequest()放在前面,后面的规则无效
http.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("/login.html").permitAll();
// 正确:具体规则在前,兜底规则在后
http.authorizeRequests()
.antMatchers("/login.html").permitAll()
.anyRequest().authenticated();
3.4 内置访问控制方法
Spring Security 提供 6 种常用的内置访问控制方法,底层均通过access()实现:
| 方法 | 作用 | 示例 |
|---|---|---|
permitAll() | 允许任何人访问 | .antMatchers("/login.html").permitAll() |
authenticated() | 需认证后访问 | .anyRequest().authenticated() |
anonymous() | 允许匿名访问(与permitAll()类似,但会执行匿名过滤器) | .antMatchers("/home").anonymous() |
denyAll() | 禁止任何人访问 | .antMatchers("/forbid").denyAll() |
rememberMe() | 仅 “记住我” 用户可访问 | .antMatchers("/remember").rememberMe() |
fullyAuthenticated() | 仅完全认证用户可访问(排除 “记住我” 用户) | .antMatchers("/admin").fullyAuthenticated() |
示例:
http.authorizeRequests()
.antMatchers("/home").anonymous() // 匿名用户可访问首页
.antMatchers("/remember").rememberMe() // “记住我”用户可访问
.antMatchers("/admin").fullyAuthenticated() // 完全认证用户可访问
.anyRequest().authenticated();
3.5 角色与权限判断
当用户已认证后,需进一步判断其角色或权限是否满足访问要求,Spring Security 提供 5 种方法:
3.5.1 hasAuthority ():判断是否具有指定权限
权限是UserDetails中getAuthorities()返回的字符串(如menu:sys、user:list)。
示例:
// /sys/**需"menu:sys"权限
http.authorizeRequests()
.antMatchers("/sys/**").hasAuthority("menu:sys")
.anyRequest().authenticated();
3.5.2 hasAnyAuthority ():判断是否具有指定权限中的任意一个
示例:
// /sys/**需"menu:sys"或"menu:user"权限
http.authorizeRequests()
.antMatchers("/sys/**").hasAnyAuthority("menu:sys", "menu:user")
.anyRequest().authenticated();
3.5.3 hasRole ():判断是否具有指定角色
角色需满足 “ROLE_前缀” 规范:
- 数据库中角色名是 “管理员”,则
UserDetails中需存储为 “ROLE_管理员”; - 使用
hasRole()时,参数无需加 “ROLE_”(底层自动拼接)。
示例:
// /admin/**需"管理员"角色(底层拼接为"ROLE_管理员")
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("管理员")
.anyRequest().authenticated();
3.5.4 hasAnyRole ():判断是否具有指定角色中的任意一个
示例:
// /admin/**需"管理员"或"操作员"角色
http.authorizeRequests()
.antMatchers("/admin/**").hasAnyRole("管理员", "操作员")
.anyRequest().authenticated();
3.5.5 hasIpAddress ():判断是否来自指定 IP
示例:
// 仅127.0.0.1可访问/admin/**
http.authorizeRequests()
.antMatchers("/admin/**").hasIpAddress("127.0.0.1")
.anyRequest().authenticated();
3.6 友好提示:自定义 403 无权限处理

当用户无权限访问时,默认返回 403 错误页,体验差。需自定义 403 响应(如返回 JSON 或跳转自定义页面)。
步骤 1:实现 AccessDeniedHandler
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException {
// 前后端分离场景:返回JSON
response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("{\"code\":403,\"msg\":\"权限不足,请联系管理员!\"}");
out.flush();
// 非前后端分离场景:跳转自定义403页
// response.sendRedirect("/403.html");
}
}
步骤 2:配置到 SecurityConfig
@Autowired
private MyAccessDeniedHandler accessDeniedHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 配置异常处理
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler) // 自定义403处理
.and()
// 其他配置不变...
.csrf().disable();
return http.build();
}
3.7 灵活控制:基于表达式的访问控制

access()方法支持 Spring EL 表达式,可实现更灵活的权限判断,内置表达式与之前的方法对应:
| 表达式 | 对应方法 | 示例 |
|---|---|---|
permitAll | permitAll() | .antMatchers("/login").access("permitAll") |
authenticated | authenticated() | .anyRequest().access("authenticated") |
hasAuthority('xxx') | hasAuthority("xxx") | .antMatchers("/sys").access("hasAuthority('menu:sys')") |
hasRole('xxx') | hasRole("xxx") | .antMatchers("/admin").access("hasRole('管理员')") |
isRememberMe() | rememberMe() | .antMatchers("/remember").access("isRememberMe()") |
自定义表达式逻辑
若内置表达式无法满足需求,可自定义 Service 方法,通过access()调用:
1. 自定义 Service
@Service
public class MyPermissionService {
// 参数固定:HttpServletRequest(请求)、Authentication(认证信息)
public boolean hasTwoPermissions(HttpServletRequest request, Authentication authentication) {
// 1. 获取当前用户权限
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
// 2. 判断是否同时具有"sys:save"和"user:save"权限
boolean hasSysSave = authorities.contains(new SimpleGrantedAuthority("sys:save"));
boolean hasUserSave = authorities.contains(new SimpleGrantedAuthority("user:save"));
return hasSysSave && hasUserSave;
}
}
2. 配置到 SecurityConfig
@Autowired
private MyPermissionService permissionService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 调用自定义Service方法:需同时具有两个权限才能访问/bjsxt
.antMatchers("/bjsxt").access("@permissionService.hasTwoPermissions(request, authentication)")
.anyRequest().authenticated();
return http.build();
}
3.8 注解驱动:基于注解的访问控制
除了 URL 配置,还可通过注解在 Controller/Service 方法上直接控制权限,需先开启注解支持。
3.8.1 开启注解支持
在启动类或配置类上添加@EnableGlobalMethodSecurity(Spring Boot 2.7 + 推荐@EnableMethodSecurity):
@SpringBootApplication
// 开启@Secured、@PreAuthorize、@PostAuthorize注解
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SpringSecurityDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityDemoApplication.class, args);
}
}
3.8.2 @Secured:判断角色(需 ROLE_前缀)
@Secured仅支持角色判断,参数需加 “ROLE_” 前缀:
@Controller
public class SysController {
// 需"ROLE_管理员"角色才能访问
@Secured("ROLE_管理员")
@RequestMapping("/admin/sys")
public String sysManage() {
return "sysManage";
}
}
3.8.3 @PreAuthorize:方法执行前判断权限(推荐)
支持 Spring EL 表达式,可判断权限、角色、IP 等,灵活性最高:
@Controller
public class SysController {
// 方法执行前判断:需"menu:sys"权限
@PreAuthorize("hasAuthority('menu:sys')")
@RequestMapping("/sys/list")
public String sysList() {
return "sysList";
}
// 方法执行前判断:需"管理员"角色或IP为127.0.0.1
@PreAuthorize("hasRole('管理员') or hasIpAddress('127.0.0.1')")
@RequestMapping("/sys/delete")
public String sysDelete() {
return "sysDelete";
}
}
3.8.4 @PostAuthorize:方法执行后判断权限
方法执行后才判断权限(适合需返回值参与判断的场景,极少用):
@Controller
public class SysController {
// 方法执行后判断:返回值的username等于当前用户名
@PostAuthorize("returnObject.username == authentication.principal.username")
@RequestMapping("/user/info")
public User getUserInfo(Long id) {
// 模拟从数据库查询用户
User user = userService.getById(id);
return user;
}
}
3.9 视图集成:Thymeleaf 中使用 Spring Security
非前后端分离项目中,常使用 Thymeleaf 渲染页面,可通过thymeleaf-extras-springsecurity5集成 Spring Security,实现 “根据权限动态显示内容”。
步骤 1:导入依赖
<!-- Thymeleaf启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Thymeleaf-Spring Security集成 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
步骤 2:页面引入命名空间
在 Thymeleaf 页面中添加sec命名空间:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>Thymeleaf集成Spring Security</title>
</head>
<body>
<!-- 内容 -->
</body>
</html>
步骤 3:获取认证信息(sec:authentication)
通过sec:authentication获取当前用户的认证信息:
<!-- 获取用户名 -->
<p>当前登录用户:<span sec:authentication="name"></span></p>
<!-- 获取用户权限 -->
<p>用户权限:<span sec:authentication="authorities"></span></p>
<!-- 获取用户详情(UserDetails) -->
<p>用户名(从principal获取):<span sec:authentication="principal.username"></span></p>
<!-- 获取客户端IP -->
<p>客户端IP:<span sec:authentication="details.remoteAddress"></span></p>
<!-- 获取SessionID -->
<p>SessionID:<span sec:authentication="details.sessionId"></span></p>
步骤 4:动态控制内容显示(sec:authorize)
通过sec:authorize根据权限判断是否显示元素:
<!-- 具有"menu:sys"权限才显示“系统管理”按钮 -->
<button sec:authorize="hasAuthority('menu:sys')">系统管理</button>
<!-- 具有"管理员"角色才显示“用户管理”按钮 -->
<button sec:authorize="hasRole('管理员')">用户管理</button>
<!-- 匿名用户显示“登录”按钮 -->
<a sec:authorize="isAnonymous()" href="/login.html">登录</a>
<!-- 已认证用户显示“退出登录”按钮 -->
<a sec:authorize="isAuthenticated()" href="/doLogout">退出登录</a>
3.10 安全防护:CSRF 原理与配置
前面的配置中我们一直关闭csrf(),本节详解 CSRF 攻击与防护。
3.10.1 什么是 CSRF?
CSRF(Cross-site Request Forgery,跨站请求伪造)是一种攻击方式:攻击者利用用户的登录状态(Cookie 中的 SessionID),伪造用户请求访问受信任网站,执行恶意操作(如转账、删除数据)。
攻击流程:

- 用户登录
www.xxx.com,服务器生成 SessionID 并存储到 Cookie; - 用户未退出
www.xxx.com,访问攻击者的www.attacker.com; www.attacker.com向www.xxx.com发起请求(如/transfer?to=attacker&money=1000);- 浏览器自动携带
www.xxx.com的 Cookie,服务器认为是用户本人操作,执行转账。
3.10.2 Spring Security 的 CSRF 防护机制
Spring Security 从 4.0 开始默认开启 CSRF 防护,核心原理是令牌验证:
- 服务器生成随机 CSRF 令牌(
_csrf),存储到 Session 和请求作用域; - 客户端提交请求(如登录、表单提交)时,需携带该令牌;
- 服务器验证请求中的令牌与 Session 中的令牌是否一致,一致则允许访问,否则拒绝。
3.10.3 开启 CSRF 防护(生产环境必须开启)
注释掉csrf().disable(),并在表单中携带 CSRF 令牌:
1. 修改 SecurityConfig
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 移除csrf().disable(),默认开启
.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/doLogin")
.successForwardUrl("/toMain")
.and()
.authorizeRequests()
.antMatchers("/login.html", "/toMain").permitAll()
.anyRequest().authenticated();
return http.build();
}
2. 表单中携带 CSRF 令牌
Thymeleaf 页面中通过${_csrf.token}获取令牌:
<form action="/doLogin" method="post">
<!-- 携带CSRF令牌(必须) -->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<div>
<label>用户名:</label>
<input type="text" name="username">
</div>
<div>
<label>密码:</label>
<input type="password" name="password">
</div>
<div>
<button type="submit">登录</button>
</div>
</form>
3. AJAX 请求携带 CSRF 令牌
前后端分离项目中,AJAX 请求需在 Header 中携带令牌:
// 获取CSRF令牌(可从页面元标签获取)
var csrfToken = $("meta[name='_csrf']").attr("content");
var csrfHeader = $("meta[name='_csrf_header']").attr("content");
// AJAX请求
$.ajax({
url: "/doLogin",
type: "post",
headers: {
[csrfHeader]: csrfToken // 在Header中携带令牌
},
data: {
username: "张三",
password: "123456"
},
success: function(res) {
console.log(res);
}
});
页面元标签配置:
<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}">
四、总结与后续学习方向
本文从基础到进阶,覆盖了 Spring Security 的核心功能:
- 认证:用户登录、自定义登录逻辑、密码加密、登录页面定制;
- 授权:URL 匹配、角色权限判断、表达式控制、注解控制;
- 高级功能:记住我、退出登录、自定义 403、Thymeleaf 集成、CSRF 防护。
后续学习方向
- OAuth2.0 与 JWT:解决分布式系统认证(如第三方登录、前后端分离 token 认证);
- LDAP 认证:集成 LDAP 服务器实现企业级用户认证;
- 动态权限:从数据库加载 URL - 权限映射,实现权限动态配置;
- 安全审计:记录用户操作日志,追踪安全事件。
Spring Security 的学习重点在于理解流程(如认证流程、授权流程)和灵活配置(根据项目需求定制安全规则)。建议结合实际项目多练手,才能真正掌握其核心用法。
2256





