掌握 Spring Security:认证、授权与高级功能全解析
作为 Java 生态中最主流的安全框架,Spring Security 凭借其强大的扩展性、与 Spring 生态的无缝集成,成为企业级应用认证(Authentication)与授权(Authorization)的首选方案。本文基于两天的系统学习笔记,从基础入门到高级实战,带你全面掌握 Spring Security 的核心用法,帮你解决项目中的安全需求。
目录
一、引言:为什么选择 Spring Security?
1.1 Spring Security 核心定位
1.2 Spring Security vs Shiro:如何选型?
二、第一天:夯实认证基础
2.1 第一个 Spring Security 项目:5 分钟上手
步骤 1:导入依赖
步骤 2:体验默认行为
步骤 3:自定义默认账号密码
2.2 核心接口:UserDetailsService 详解
2.2.1 接口作用
2.2.2 关键参数与返回值
2.2.3 UserDetails 核心方法
2.2.4 简单实现示例
2.3 密码安全:PasswordEncoder 解析器
2.3.1 为什么需要 PasswordEncoder?
2.3.2 内置解析器对比
2.3.3 BCryptPasswordEncoder 核心用法
2.3.4 注入 PasswordEncoder Bean
2.4 自定义登录逻辑:从数据库获取用户
2.4.1 数据库表结构
2.4.2 导入 MyBatis 依赖
2.4.3 配置数据源与 MyBatis
2.4.4 编写 Mapper 接口与 XML
1. 实体类(User.java)
2. Mapper 接口(UserMapper.java)
3. Mapper XML(UserMapper.xml)
2.4.5 实现 UserDetailsService
2.5 自定义登录页面:告别默认样式
2.5.1 编写登录页面(login.html)
2.5.2 配置 SecurityConfig(关键)
2.5.3 编写控制器(处理成功 / 失败跳转)
2.5.4 编写成功 / 失败页面
2.6 认证常用配置:自定义成功 / 失败处理器
2.6.1 自定义登录成功处理器
2.6.2 自定义登录失败处理器
2.6.3 配置处理器到 SecurityConfig
2.7 深入理解:完整认证流程(面试重点)
步骤 1:请求进入 UsernamePasswordAuthenticationFilter
步骤 2:AuthenticationManager 分发认证任务
步骤 3:DaoAuthenticationProvider 执行认证
步骤 4:认证结果处理
流程总结图
三、第二天:精通授权与高级功能
3.1 记住我:Remember Me 功能实现
步骤 1:导入依赖(MyBatis 已导入)
步骤 2:配置 PersistentTokenRepository
步骤 3:配置 SecurityConfig
步骤 4:修改登录页面
3.2 安全退出:Logout 配置与扩展
3.2.1 默认退出行为
3.2.2 自定义退出配置
3.2.3 自定义退出成功处理器
3.3 访问控制:URL 匹配规则
3.3.1 antMatchers ():Ant 风格表达式(推荐)
3.3.2 anyRequest ():匹配所有请求
3.3.3 regexMatchers ():正则表达式匹配
注意:匹配顺序
3.4 内置访问控制方法
3.5 角色与权限判断
3.5.1 hasAuthority ():判断是否具有指定权限
3.5.2 hasAnyAuthority ():判断是否具有指定权限中的任意一个
3.5.3 hasRole ():判断是否具有指定角色
3.5.4 hasAnyRole ():判断是否具有指定角色中的任意一个
3.5.5 hasIpAddress ():判断是否来自指定 IP
3.6 友好提示:自定义 403 无权限处理
步骤 1:实现 AccessDeniedHandler
步骤 2:配置到 SecurityConfig
3.7 灵活控制:基于表达式的访问控制
自定义表达式逻辑
1. 自定义 Service
2. 配置到 SecurityConfig
3.8 注解驱动:基于注解的访问控制
3.8.1 开启注解支持
3.8.2 @Secured:判断角色(需 ROLE_前缀)
3.8.3 @PreAuthorize:方法执行前判断权限(推荐)
3.8.4 @PostAuthorize:方法执行后判断权限
3.9 视图集成:Thymeleaf 中使用 Spring Security
步骤 1:导入依赖
步骤 2:页面引入命名空间
步骤 3:获取认证信息(sec:authentication)
3.10 安全防护:CSRF 原理与配置
3.10.1 什么是 CSRF?
3.10.2 Spring Security 的 CSRF 防护机制
3.10.3 开启 CSRF 防护(生产环境必须开启)
1. 修改 SecurityConfig
2. 表单中携带 CSRF 令牌
3. AJAX 请求携带 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 {@Overridepublic 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@Beanpublic 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.Driverurl: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8username: rootpassword: 123456mybatis: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 uJOIN role_user ru ON u.id = ru.uidJOIN role r ON ru.rid = r.idJOIN role_menu rm ON r.id = rm.ridJOIN menu m ON rm.mid = m.idWHERE u.username = #{username}</select><!-- 根据用户名查询角色 --><select id="selectRoleByUsername" parameterType="string" resultType="string">SELECT r.name FROM user uJOIN role_user ru ON u.id = ru.uidJOIN role r ON ru.rid = r.idWHERE u.username = #{username}</select></mapper>
2.4.5 实现 UserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic 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. 返回UserDetailsreturn 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 {@Autowiredprivate MyUserDetailsService userDetailsService;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic 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)@Beanpublic 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 {@Overridepublic 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 {@Overridepublic 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 {@Autowiredprivate MyLoginSuccessHandler loginSuccessHandler;@Autowiredprivate MyLoginFailureHandler loginFailureHandler;@Beanpublic 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 {@Autowiredprivate DataSource dataSource;@Beanpublic PersistentTokenRepository persistentTokenRepository() {JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();tokenRepository.setDataSource(dataSource);// 第一次启动时自动创建表(创建后注释,避免重复创建)// tokenRepository.setCreateTableOnStartup(true);return tokenRepository;}
}
步骤 3:配置 SecurityConfig
@Configuration
public class SecurityConfig {@Autowiredprivate MyUserDetailsService userDetailsService;@Autowiredprivate PersistentTokenRepository persistentTokenRepository;@Beanpublic 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 {@Overridepublic 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 {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException {// 前后端分离场景:返回JSONresponse.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403response.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 的学习重点在于理解流程(如认证流程、授权流程)和灵活配置(根据项目需求定制安全规则)。建议结合实际项目多练手,才能真正掌握其核心用法。