Spring Security 完整使用指南
本文档基于实际项目,全面讲解 Spring Security 的使用方法、工作原理和最佳实践
📚 目录
- [Spring Security 是什么](#Spring Security 是什么)
- 项目书写流程概览
- 详细开发步骤
- [Spring Security 工作原理深度解析](#Spring Security 工作原理深度解析)
- 完整的请求处理流程
- [Spring Security 核心功能详解](#Spring Security 核心功能详解)
- 常见问题与解答
- 下次开发时的快速上手指南
Spring Security 是什么
🎯 核心定位
Spring Security 是一个企业级安全框架,提供了完整的安全解决方案,包括:
- ✅ 认证(Authentication) - 验证"你是谁"(登录验证)
- ✅ 授权(Authorization) - 控制"你能做什么"(权限控制)
- ✅ 防护(Protection) - 抵御各种安全攻击(CSRF、XSS、Session劫持等)
- ✅ 会话管理(Session Management) - 管理用户登录状态
- ✅ 密码加密(Password Encoding) - 安全存储密码
🤔 为什么需要 Spring Security?
不使用 Spring Security 的后果:
- ❌ 需要手写几千行安全相关代码
- ❌ 在每个接口手动检查权限
- ❌ 容易出现安全漏洞
- ❌ Session 管理混乱
- ❌ 密码明文存储,极不安全
使用 Spring Security 的好处:
- ✅ 声明式配置,简单易用
- ✅ 自动处理认证和授权
- ✅ 内置多种安全防护机制
- ✅ 成熟的企业级解决方案
- ✅ 与 Spring Boot 无缝集成
项目书写流程概览
📋 开发步骤清单
第一步:基础配置
├─ pom.xml(Maven 依赖)
├─ application.yml(数据库配置)
└─ 数据库表结构设计(用户表、角色表、权限表)
第二步:数据层开发
├─ 实体类(Emp.java)
├─ Mapper 接口(EmpMapper.java)
└─ Mapper XML(EmpMapper.xml - 多表联查权限)
第三步:业务层开发 ⭐ 核心
├─ Service 接口(EmpService.java - 继承 UserDetailsService)
└─ Service 实现(EmpServiceImpl.java - 实现认证逻辑)
第四步:控制层开发
└─ Controller(EmpController.java - 页面路由)
第五步:Security 配置 ⭐ 核心
├─ RememberMeConfig.java(记住我功能配置)
└─ MyConfig.java(核心安全配置)
第六步:启动类
└─ SpringBootMain.java
第七步:前端页面
├─ empLogin.html(登录页面)
├─ success.html(登录成功页面 - 带权限控制的按钮)
├─ show.html、save.html、edit.html、remove.html(功能页面)
└─ error/403.html(权限不足错误页面)
详细开发步骤
第一步:基础配置
1.1 创建 Maven 项目
为什么使用 Maven?
- 依赖管理自动化(不需要手动下载 jar 包)
- 统一的项目结构
- 方便的版本管理
- 简化项目打包和发布
项目结构:
springsecurity01/
├─ src/
│ ├─ main/
│ │ ├─ java/
│ │ │ └─ com/jr/
│ │ │ ├─ config/ # 配置类
│ │ │ ├─ controller/ # 控制器
│ │ │ ├─ mapper/ # 数据访问层
│ │ │ ├─ pojo/ # 实体类
│ │ │ ├─ service/ # 业务层
│ │ │ └─ SpringBootMain.java # 启动类
│ │ └─ resources/
│ │ ├─ application.yml # 配置文件
│ │ ├─ com/jr/mapper/ # MyBatis XML
│ │ ├─ templates/ # Thymeleaf 模板
│ │ └─ static/ # 静态资源
│ └─ test/ # 测试代码
└─ pom.xml # Maven 配置
1.2 配置 pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jr.dz18</groupId>
<artifactId>springsecurity01</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 继承 Spring Boot 父项目,用于版本管理 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.10.RELEASE</version>
</parent>
<dependencies>
<!-- Spring MVC 启动器:提供 Web 功能 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis 启动器:持久层框架 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<!-- MySQL 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!-- Thymeleaf 模板引擎:用于渲染 HTML 页面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Lombok:简化实体类代码(自动生成 getter/setter) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- ⭐ Spring Security 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.1.10.RELEASE</version>
</dependency>
<!-- Thymeleaf 和 Spring Security 集成:页面权限控制标签 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
<!-- JUnit 测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<!-- 资源拷贝插件:确保 XML、HTML 等文件被正确打包 -->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.xml</include>
<include>**/*.html</include>
<include>**/*.js</include>
<include>**/*.png</include>
<include>**/*.properties</include>
</includes>
</resource>
</resources>
</build>
</project>
1.3 配置 application.yml
yaml
# 服务器端口配置
server:
port: 8080
# Spring 配置
spring:
# 数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
# MyBatis 配置
mybatis:
# 实体类包路径(MyBatis 会自动扫描)
type-aliases-package: com.jr.pojo
# Mapper XML 文件位置
mapper-locations: classpath:com/jr/mapper/*.xml
# 配置
configuration:
# 控制台输出 SQL 语句(开发时方便调试)
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
1.4 数据库表结构设计
核心表结构(RBAC 权限模型):
sql
-- 用户表
CREATE TABLE `emp` (
`eid` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL UNIQUE,
`password` VARCHAR(100) NOT NULL, -- 存储 BCrypt 加密后的密码
`rid` INT -- 角色ID(简化版,实际应该用中间表)
);
-- 角色表
CREATE TABLE `role` (
`rid` INT PRIMARY KEY AUTO_INCREMENT,
`rname` VARCHAR(50) NOT NULL
);
-- 权限表
CREATE TABLE `power` (
`pid` INT PRIMARY KEY AUTO_INCREMENT,
`pname` VARCHAR(50) NOT NULL -- 如:emp:save, emp:findAll
);
-- 用户-角色关联表(多对多)
CREATE TABLE `role_emp` (
`eid` INT,
`rid` INT,
PRIMARY KEY (`eid`, `rid`)
);
-- 角色-权限关联表(多对多)
CREATE TABLE `role_power` (
`rid` INT,
`pid` INT,
PRIMARY KEY (`rid`, `pid`)
);
-- Remember Me 功能需要的表(Spring Security 自动创建)
CREATE TABLE `persistent_logins` (
`username` VARCHAR(64) NOT NULL,
`series` VARCHAR(64) PRIMARY KEY,
`token` VARCHAR(64) NOT NULL,
`last_used` TIMESTAMP NOT NULL
);
示例数据:
sql
-- 插入用户(密码都是 123456,已用 BCrypt 加密)
INSERT INTO emp VALUES (1, 'zhangsan', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7.QNcbJSu', 1);
INSERT INTO emp VALUES (2, 'lisi', '$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7.QNcbJSu', 2);
-- 插入角色
INSERT INTO role VALUES (1, '管理员');
INSERT INTO role VALUES (2, '普通用户');
-- 插入权限
INSERT INTO power VALUES (1, 'emp:save');
INSERT INTO power VALUES (2, 'emp:findAll');
INSERT INTO power VALUES (3, 'emp:edit');
INSERT INTO power VALUES (4, 'emp:remove');
-- 用户-角色关联
INSERT INTO role_emp VALUES (1, 1); -- zhangsan 是管理员
INSERT INTO role_emp VALUES (2, 2); -- lisi 是普通用户
-- 角色-权限关联
INSERT INTO role_power VALUES (1, 1); -- 管理员有 save 权限
INSERT INTO role_power VALUES (1, 2); -- 管理员有 findAll 权限
INSERT INTO role_power VALUES (1, 3); -- 管理员有 edit 权限
INSERT INTO role_power VALUES (1, 4); -- 管理员有 remove 权限
INSERT INTO role_power VALUES (2, 2); -- 普通用户只有 findAll 权限
第二步:实体类
Emp.java
java
package com.jr.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 员工实体类
* 对应数据库的 emp 表
*/
@Component // 注册为 Spring Bean
@AllArgsConstructor // Lombok:自动生成全参构造器
@NoArgsConstructor // Lombok:自动生成无参构造器
@Data // Lombok:自动生成 getter/setter/toString/equals/hashCode
public class Emp {
private Integer eid; // 员工ID
private String username; // 用户名
private String password; // 密码(加密后)
private Integer rid; // 角色ID
}
💡 为什么要用 Lombok?
- 不用手写 getter/setter(自动生成)
- 代码更简洁,可读性更好
@Data
一个注解搞定所有常用方法
第三步:Mapper 层(数据访问层)
3.1 EmpMapper.java
java
package com.jr.mapper;
import com.jr.pojo.Emp;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 员工数据访问层
*/
@Component
@Mapper // MyBatis 注解,标记为 Mapper 接口
public interface EmpMapper {
/**
* 根据用户名查询用户
* 用于登录时验证用户是否存在
*/
@Select("select * from emp where username=#{username}")
Emp selectByEname(String username);
/**
* 根据用户名查询用户权限列表
* 通过多表联查获取用户的所有权限
*
* 查询逻辑:emp → role_emp → role → role_power → power
*
* 返回示例:["emp:save", "emp:findAll", "emp:edit"]
*/
List<String> selectPnameByUsername(String username);
}
3.2 EmpMapper.xml
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.jr.mapper.EmpMapper">
<!--
根据用户名查询权限列表
执行流程:
1. 从 emp 表找到用户
2. 通过 role_emp 表找到用户的角色
3. 通过 role 表获取角色信息
4. 通过 role_power 表找到角色对应的权限
5. 从 power 表获取权限名称
示例:
输入:username = "zhangsan"
输出:["emp:save", "emp:findAll", "emp:edit", "emp:remove"]
-->
<select id="selectPnameByUsername" resultType="string" parameterType="string">
SELECT p.pname
FROM emp e
JOIN role_emp re ON e.eid = re.eid
JOIN role r ON re.rid = r.rid
JOIN role_power rp ON r.rid = rp.rid
JOIN power p ON rp.pid = p.pid
WHERE e.username = #{username}
</select>
</mapper>
💡 为什么要多表联查?
- 实现了 RBAC(基于角色的权限控制)模型
- 一个用户可以有多个角色
- 一个角色可以有多个权限
- 灵活的权限管理,便于扩展
第四步:Service 层(业务层)⭐ 核心
4.1 EmpService.java(接口)
java
package com.jr.service;
import com.jr.pojo.Emp;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* 员工业务接口
*
* ⭐ 关键点:继承了 UserDetailsService 接口
*
* UserDetailsService 是 Spring Security 提供的接口,
* 包含一个方法:loadUserByUsername(String username)
*
* 当用户登录时,Spring Security 会自动调用这个方法来加载用户信息
*/
public interface EmpService extends UserDetailsService {
/**
* 根据用户名查询用户信息
* @param username 用户名
* @return 用户信息
*/
Emp selectByUsername(String username);
/**
* 根据用户名查询用户权限
* @param username 用户名
* @return 权限列表
*/
java.util.List<String> selectPermissionsByUsername(String username);
}
🔑 为什么要继承 UserDetailsService?
这是 Spring Security 的约定(契约):
- Spring Security 需要知道如何从数据库加载用户信息
- 它定义了一个接口
UserDetailsService
- 您实现这个接口,告诉它"怎么查询用户"
- Spring Security 在需要的时候会自动调用您的实现
类比:
- 就像您实现
Serializable
接口,JVM 就知道如何序列化您的对象 - 您实现
UserDetailsService
,Spring Security 就知道如何加载用户信息
4.2 EmpServiceImpl.java(实现类)⭐ 核心
java
package com.jr.service.Impl;
import com.jr.mapper.EmpMapper;
import com.jr.pojo.Emp;
import com.jr.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 员工业务实现类
*
* ⭐ 这是整个 Spring Security 认证的核心类
*/
@Service // 注册为 Spring Bean(非常重要!Spring Security 会通过依赖注入找到它)
public class EmpServiceImpl implements EmpService {
@Autowired
private EmpMapper empMapper;
/**
* ⭐⭐⭐ 核心方法:Spring Security 认证时会自动调用
*
* 调用时机:
* 1. 用户提交登录表单到 /eLogin
* 2. Spring Security 的 UsernamePasswordAuthenticationFilter 拦截请求
* 3. 调用 AuthenticationManager 进行认证
* 4. AuthenticationManager 调用 DaoAuthenticationProvider
* 5. DaoAuthenticationProvider 调用本方法加载用户信息
*
* @param s 用户输入的用户名
* @return UserDetails 对象(包含用户名、密码、权限列表)
* @throws UsernameNotFoundException 用户不存在时抛出
*/
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
System.out.println("==========================================");
System.out.println("⭐ Spring Security 调用了 loadUserByUsername");
System.out.println("⭐ 查询用户:" + s);
System.out.println("==========================================");
// 第一步:从数据库查询用户
Emp emp = selectByUsername(s);
if (emp == null) {
// 用户不存在,抛出异常(Spring Security 会捕获并返回"用户名或密码错误")
throw new UsernameNotFoundException("用户名不存在,登录失败");
}
// 第二步:查询用户的所有权限
// 返回示例:["emp:save", "emp:findAll", "emp:edit"]
List<String> listPermission = selectPermissionsByUsername(s);
// 第三步:将权限字符串转换为 Spring Security 的权限对象
List<SimpleGrantedAuthority> listAuthority = new ArrayList<>();
for (String permission : listPermission) {
listAuthority.add(new SimpleGrantedAuthority(permission));
}
// 第四步:构造并返回 UserDetails 对象
// Spring Security 会用这个对象进行密码验证和权限检查
return new User(
emp.getUsername(), // 用户名
emp.getPassword(), // 密码(加密后的)
listAuthority // 权限列表
);
// ⭐ 返回后,Spring Security 会:
// 1. 用 BCryptPasswordEncoder 验证密码
// 2. 验证成功 → 创建 Authentication 对象 → 存入 SecurityContext
// 3. 验证失败 → 跳转到 /empLogin(登录失败页面)
}
@Override
public Emp selectByUsername(String username) {
return empMapper.selectByEname(username);
}
@Override
public List<String> selectPermissionsByUsername(String username) {
return empMapper.selectPnameByUsername(username);
}
}
🔍 深度解析:为什么 Spring Security 会调用这个方法?
Spring Boot 启动时:
1. 扫描到 @Service 注解的 EmpServiceImpl
2. 发现它实现了 UserDetailsService 接口
3. 自动注册到 Spring Security 的 AuthenticationManager 中
用户登录时:
POST /eLogin (username=zhangsan, password=123456)
↓
UsernamePasswordAuthenticationFilter(Spring Security 提供)
↓
AuthenticationManager(Spring Security 提供)
↓
DaoAuthenticationProvider(Spring Security 提供)
↓ 需要从数据库加载用户信息,怎么查?
↓ 调用 UserDetailsService.loadUserByUsername()
↓ 这个 UserDetailsService 是谁?
↓ 就是您的 EmpServiceImpl!(通过依赖注入找到的)
↓
EmpServiceImpl.loadUserByUsername() ← 您的代码在这里执行
↓ 返回 UserDetails 对象
↓
DaoAuthenticationProvider 验证密码
↓ 密码正确
↓
创建 Authentication 对象并存入 SecurityContext
↓
登录成功,跳转到 /success
第五步:Controller 层
EmpController.java
java
package com.jr.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 员工控制器
*
* 作用:处理页面路由
*
* 注意:
* - /eLogin 不会经过这里(被 Spring Security 拦截处理)
* - /empLogin 会经过这里(显示登录页面)
* - /success 会经过这里(显示登录成功页面)
* - /show, /save, /edit, /remove 都会经过这里
*/
@Controller
public class EmpController {
/**
* 动态路由处理
*
* 示例:
* - 访问 /empLogin → 返回 "empLogin" → 渲染 empLogin.html
* - 访问 /success → 返回 "success" → 渲染 success.html
* - 访问 /show → 返回 "show" → 渲染 show.html
*
* @param url 路径变量
* @return 视图名称
*/
@RequestMapping("/{url}")
public String show(@PathVariable String url) {
System.out.println("Controller 处理路径:" + url);
return url; // 返回视图名称,Thymeleaf 会渲染对应的 HTML 文件
}
}
💡 Controller 的作用:
- 处理 URL 路由,返回视图名称
- Spring Security 会在 Controller 之前进行权限检查
- 权限不足会直接返回 403,不会到达 Controller
第六步:Spring Security 配置 ⭐⭐⭐ 核心
6.1 RememberMeConfig.java(Remember Me 配置)
java
package com.jr.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* Remember Me 功能配置
*
* 作用:配置"记住我"功能,将 token 存储到数据库
*/
@Configuration
public class RememberMeConfig {
@Autowired
private DataSource dataSource; // Spring Boot 自动配置的数据源
/**
* 配置 token 存储方式
*
* 工作原理:
* 1. 用户勾选"记住我"并登录成功
* 2. 生成一个随机 token 存储到 persistent_logins 表
* 3. 同时将 token 写入 Cookie(remember-me)
* 4. 用户关闭浏览器后再访问
* 5. Spring Security 从 Cookie 读取 token
* 6. 从数据库验证 token 是否有效
* 7. 有效则自动登录(无需输入密码)
*/
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// ⚠️ 第一次启动时开启,自动创建 persistent_logins 表
// ⚠️ 第二次启动时务必注释掉,否则会报错(表已存在)
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
💡 Remember Me 的价值:
- 提升用户体验(不用每次都登录)
- token 存储在数据库,比存在 Cookie 更安全
- 可以设置过期时间,到期自动失效
6.2 MyConfig.java(核心安全配置)⭐⭐⭐
java
package com.jr.config;
import com.jr.service.EmpService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
/**
* Spring Security 核心配置类
*
* ⭐⭐⭐ 这是整个安全框架的配置中心
*
* 作用:
* 1. 配置登录认证(表单登录)
* 2. 配置权限控制(URL 权限)
* 3. 配置 Remember Me 功能
* 4. 配置密码加密算法
* 5. 配置 CSRF 防护
*/
@Configuration
public class MyConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PersistentTokenRepository repository; // Remember Me 的 token 存储
@Autowired
private EmpService empService; // 用户认证服务
/**
* 核心配置方法
*
* @param http HttpSecurity 对象,用于配置安全策略
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// ========== 1. 表单认证配置 ==========
http.formLogin()
// 登录表单提交地址(Spring Security 会拦截这个 URL)
.loginProcessingUrl("/eLogin")
// 登录成功后跳转的地址(POST 请求,服务器内部转发)
.successForwardUrl("/success")
// 登录失败后跳转的地址(POST 请求,服务器内部转发)
.failureForwardUrl("/empLogin")
// 自定义登录页面的地址
// 当用户未登录访问受保护资源时,会重定向到这个页面
.loginPage("/empLogin")
// 可选配置:
// .usernameParameter("uname") // 自定义用户名参数名(默认是 username)
// .passwordParameter("pwd") // 自定义密码参数名(默认是 password)
;
// ========== 2. 权限配置(URL 访问控制)==========
http.authorizeRequests()
// permitAll():允许所有人访问(包括未登录用户)
.antMatchers("/empLogin").permitAll()
// hasAuthority():需要指定权限才能访问
.antMatchers("/show").hasAuthority("emp:findAll") // 查询功能需要 emp:findAll 权限
.antMatchers("/save").hasAuthority("emp:save") // 添加功能需要 emp:save 权限
.antMatchers("/remove").hasAuthority("emp:remove") // 删除功能需要 emp:remove 权限
.antMatchers("/edit").hasAuthority("emp:edit") // 修改功能需要 emp:edit 权限
// authenticated():需要认证(登录)才能访问
.anyRequest().authenticated() // 其他所有请求都需要登录
// 权限检查流程:
// 1. 用户访问 /show
// 2. FilterSecurityInterceptor 拦截请求
// 3. 从配置中查到需要 emp:findAll 权限
// 4. 从 SecurityContext 中获取用户的权限列表
// 5. 判断用户是否有 emp:findAll 权限
// 6. 有 → 放行,没有 → 返回 403 错误
;
// ========== 3. Remember Me 功能配置 ==========
http.rememberMe()
// 指定用户认证服务(用于 Remember Me 自动登录时加载用户信息)
.userDetailsService(empService)
// 指定 token 存储方式(存储到数据库)
.tokenRepository(repository)
// token 有效期(秒):30 分钟
.tokenValiditySeconds(60 * 30)
// 工作流程:
// 1. 用户勾选"记住我"并登录成功
// 2. 生成 token 并存储到数据库的 persistent_logins 表
// 3. 将 token 写入 Cookie(名称:remember-me)
// 4. 用户关闭浏览器
// 5. 再次访问时,RememberMeAuthenticationFilter 从 Cookie 读取 token
// 6. 从数据库验证 token
// 7. 验证成功 → 调用 empService.loadUserByUsername() 加载用户信息
// 8. 自动创建 Authentication 对象并存入 SecurityContext
// 9. 用户无需重新登录即可访问受保护资源
;
// ========== 4. CSRF 防护 ==========
// 默认开启(建议保持开启)
// 作用:防止跨站请求伪造攻击
// 原理:表单提交时必须携带 CSRF token,token 不匹配则拒绝请求
// 如果需要关闭(不推荐):
// http.csrf().disable();
// CSRF 防护的工作流程:
// 1. 用户访问登录页面 /empLogin
// 2. Spring Security 生成一个随机 CSRF token
// 3. 将 token 存入 Session
// 4. 在页面中添加隐藏字段:<input type="hidden" name="_csrf" value="xxx">
// 5. 用户提交表单
// 6. Spring Security 验证表单中的 token 是否与 Session 中的一致
// 7. 一致 → 继续处理,不一致 → 拒绝请求(返回 403)
}
/**
* 配置密码加密器
*
* ⭐ BCryptPasswordEncoder 是 Spring Security 推荐的加密算法
*
* 特点:
* 1. 单向加密(不可逆)
* 2. 自动加盐(每次加密结果都不同)
* 3. 防止彩虹表攻击
* 4. 验证时会自动提取盐值进行比对
*
* 示例:
* 原始密码:123456
* 第一次加密:$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7.QNcbJSu
* 第二次加密:$2a$10$GhQ8T3K1jR5YfN8qL9xZfuKM3pW7vZ8dX2rY4sQ1nH3kL5mJ6tP9e
* (每次结果不同,但验证时都能通过)
*/
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
🔑 关键理解点:
- @Configuration 注解:告诉 Spring 这是一个配置类
- 继承 WebSecurityConfigurerAdapter:获得配置 Security 的能力
- configure(HttpSecurity http) 方法:所有安全配置都在这里完成
- 链式调用 :
http.formLogin().loginProcessingUrl(...).successForwardUrl(...)
第七步:启动类
SpringBootMain.java
java
package com.jr;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Spring Boot 启动类
*/
@SpringBootApplication // 标记为 Spring Boot 应用
@MapperScan("com.jr.mapper") // 扫描 Mapper 接口
public class SpringBootMain {
public static void main(String[] args) {
SpringApplication.run(SpringBootMain.class, args);
System.out.println("========================================");
System.out.println("⭐ 应用启动成功!");
System.out.println("⭐ 访问地址:http://localhost:8080");
System.out.println("========================================");
}
}
启动时会发生什么?
1. Spring Boot 启动
2. 扫描所有带 @Component, @Service, @Controller, @Configuration 的类
3. 创建 Bean 并注入依赖关系
4. Spring Security 自动配置:
- 创建过滤器链(UsernamePasswordAuthenticationFilter 等)
- 将 EmpServiceImpl 注册到 AuthenticationManager
- 应用 MyConfig 中的所有配置
5. MyBatis 扫描 Mapper 接口
6. Thymeleaf 配置模板路径
7. 启动内置 Tomcat,监听 8080 端口
8. 应用就绪,可以接受请求
第八步:前端页面
8.1 empLogin.html(登录页面)
html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>员工登录</title>
</head>
<body>
<h2>员工管理系统 - 登录</h2>
<hr>
<!--
表单提交说明:
- action="/eLogin":提交到 /eLogin(被 Spring Security 拦截处理)
- method="post":必须是 POST 请求
- name="username":用户名字段(默认参数名,可以在配置中修改)
- name="password":密码字段(默认参数名,可以在配置中修改)
- name="remember-me":记住我字段(固定参数名)
- name="_csrf":CSRF token(必须携带,否则请求被拒绝)
-->
<form action="/eLogin" method="post">
员工姓名: <input type="text" name="username" required/><br><br>
员工密码: <input type="password" name="password" required/><br><br>
<!-- Remember Me 复选框 -->
<input type="checkbox" name="remember-me" value="true"/> 记住我(30分钟内免登录)<br><br>
<!-- CSRF token(Spring Security 自动生成) -->
<input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>
<input type="submit" value="登录">
</form>
<p style="color: red;">
测试账号:<br>
用户名:zhangsan 密码:123456(管理员,拥有所有权限)<br>
用户名:lisi 密码:123456(普通用户,只有查询权限)
</p>
</body>
</html>
8.2 success.html(登录成功页面)
html
<!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>登录成功</title>
<style>
.menu { margin: 20px; }
.menu a {
display: inline-block;
padding: 10px 20px;
margin: 5px;
background-color: #4CAF50;
color: white;
text-decoration: none;
border-radius: 5px;
}
.menu a:hover { background-color: #45a049; }
</style>
</head>
<body>
<h2>欢迎来到员工管理系统</h2>
<p>登录成功!请选择要执行的操作:</p>
<hr>
<div class="menu">
<!--
sec:authorize="hasAuthority('权限名')"
作用:根据用户权限动态显示/隐藏按钮
工作原理:
1. Thymeleaf 渲染页面时执行这个表达式
2. 从 SecurityContext 中获取用户的权限列表
3. 判断用户是否有指定权限
4. 有 → 渲染这个标签,没有 → 不渲染(用户看不到)
示例:
- zhangsan(管理员):能看到所有按钮
- lisi(普通用户):只能看到"查询"按钮
-->
<a href="/show" sec:authorize="hasAuthority('emp:findAll')">📋 查询员工</a>
<a href="/save" sec:authorize="hasAuthority('emp:save')">➕ 添加员工</a>
<a href="/edit" sec:authorize="hasAuthority('emp:edit')">✏️ 修改员工</a>
<a href="/remove" sec:authorize="hasAuthority('emp:remove')">🗑️ 删除员工</a>
<!-- 退出登录(任何人都能看到) -->
<a href="/logout" style="background-color: #f44336;">🚪 退出登录</a>
</div>
<hr>
<p style="color: gray;">
💡 提示:根据您的权限,您只能看到部分按钮。<br>
尝试直接访问无权限的 URL(如:/remove),会返回 403 错误。
</p>
</body>
</html>
8.3 show.html(查询页面示例)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>查询员工</title>
</head>
<body>
<h2>员工列表</h2>
<p>这是查询员工页面(需要 emp:findAll 权限)</p>
<hr>
<p>此处可以显示员工列表...</p>
<br>
<a href="/success">返回首页</a>
</body>
</html>
8.4 error/403.html(权限不足错误页面)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>403 - 权限不足</title>
<style>
body {
text-align: center;
padding-top: 50px;
font-family: Arial, sans-serif;
}
h1 { color: #f44336; font-size: 72px; }
p { font-size: 18px; color: #666; }
a {
display: inline-block;
margin-top: 20px;
padding: 10px 20px;
background-color: #2196F3;
color: white;
text-decoration: none;
border-radius: 5px;
}
</style>
</head>
<body>
<h1>403</h1>
<h2>权限不足</h2>
<p>抱歉,您没有权限访问此页面。</p>
<p>请联系管理员获取相应权限。</p>
<a href="/success">返回首页</a>
</body>
</html>
Spring Security 工作原理深度解析
🔍 核心概念
1. 过滤器链(Filter Chain)
Spring Security 的核心是一系列过滤器,它们在 DispatcherServlet 之前执行:
HTTP 请求
↓
┌─────────────────────────────────────────┐
│ Spring Security 过滤器链 │
│ (在 Controller 之前执行) │
├─────────────────────────────────────────┤
│ SecurityContextPersistenceFilter │ ← 从 Session 加载安全上下文
│ LogoutFilter │ ← 处理登出
│ UsernamePasswordAuthenticationFilter │ ← 处理 /eLogin 登录请求
│ RequestCacheAwareFilter │ ← 记住登录前的请求
│ SecurityContextHolderAwareRequestFilter │ ← 包装 request 对象
│ RememberMeAuthenticationFilter │ ← 处理 Remember Me
│ AnonymousAuthenticationFilter │ ← 匿名用户处理
│ SessionManagementFilter │ ← Session 管理
│ ExceptionTranslationFilter │ ← 异常处理(401、403)
│ FilterSecurityInterceptor │ ← 权限检查
└────────────────┬────────────────────────┘
↓
DispatcherServlet
↓
Controller
2. SecurityContext(安全上下文)
java
// 安全上下文的存储结构
SecurityContextHolder(线程级别的持有者)
└─ SecurityContext(安全上下文)
└─ Authentication(认证对象)
├─ Principal(用户信息,如:zhangsan)
├─ Credentials(凭证,如:加密后的密码)
├─ Authorities(权限列表,如:[emp:save, emp:findAll])
└─ isAuthenticated(是否已认证)
在代码中获取当前用户信息:
java
// 获取当前登录用户
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName(); // 用户名
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities(); // 权限列表
3. Authentication(认证对象)
java
// 认证对象的生命周期
// 1. 用户提交登录表单(未认证)
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken("zhangsan", "123456");
token.setAuthenticated(false); // 未认证
// 2. 调用 loadUserByUsername() 从数据库加载用户信息
UserDetails userDetails = empService.loadUserByUsername("zhangsan");
// userDetails = User[username=zhangsan, password=$2a$10$..., authorities=[emp:save, emp:findAll]]
// 3. 验证密码
boolean matches = passwordEncoder.matches("123456", userDetails.getPassword());
// 4. 验证成功,创建已认证的 Authentication 对象
UsernamePasswordAuthenticationToken authenticated =
new UsernamePasswordAuthenticationToken(
userDetails.getUsername(),
userDetails.getPassword(),
userDetails.getAuthorities()
);
authenticated.setAuthenticated(true); // 已认证
// 5. 存入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authenticated);
// 6. 将 SecurityContext 存入 Session
session.setAttribute("SPRING_SECURITY_CONTEXT", context);
完整的请求处理流程
场景 1:首次访问受保护资源
用户访问:http://localhost:8080/show(未登录)
↓
1. SecurityContextPersistenceFilter
从 Session 中加载 SecurityContext
结果:Session 中没有 → 创建空的 SecurityContext
↓
2. AnonymousAuthenticationFilter
发现用户未登录 → 创建匿名用户的 Authentication
↓
3. FilterSecurityInterceptor
检查 /show 需要的权限:emp:findAll
当前用户:匿名用户(没有任何权限)
结果:权限不足
↓
4. ExceptionTranslationFilter(捕获权限不足异常)
判断:用户未登录(匿名用户)
动作:重定向到 /empLogin(登录页面)
↓
5. 用户看到登录页面
场景 2:用户登录
用户在登录页输入:username=zhangsan, password=123456
点击"登录"按钮
↓
POST /eLogin(username=zhangsan, password=123456, _csrf=xxx)
↓
1. CsrfFilter
验证 CSRF token
token 正确 → 继续
↓
2. UsernamePasswordAuthenticationFilter(拦截 /eLogin)
从请求中提取用户名和密码
创建未认证的 Authentication 对象
↓
3. AuthenticationManager
调用 DaoAuthenticationProvider 进行认证
↓
4. DaoAuthenticationProvider
步骤 4.1:调用 EmpServiceImpl.loadUserByUsername("zhangsan")
步骤 4.2:从数据库查询用户和权限
步骤 4.3:返回 UserDetails 对象
步骤 4.4:验证密码(BCryptPasswordEncoder)
步骤 4.5:密码正确 → 创建已认证的 Authentication 对象
↓
5. UsernamePasswordAuthenticationFilter
将 Authentication 存入 SecurityContext
↓
6. RememberMeServices(如果勾选了"记住我")
生成 token 并存储到数据库
将 token 写入 Cookie
↓
7. SecurityContextPersistenceFilter
将 SecurityContext 存入 Session
↓
8. 认证成功
内部转发到 /success
↓
9. 再次经过过滤器链
FilterSecurityInterceptor 检查 /success 的权限
anyRequest().authenticated() → 只需要登录即可
当前用户已登录 → 放行
↓
10. DispatcherServlet → Controller
EmpController.show("success")
返回 "success" 视图
↓
11. Thymeleaf 渲染 success.html
执行 sec:authorize 表达式,根据权限显示按钮
↓
12. 用户看到登录成功页面(带权限控制的按钮)
场景 3:已登录用户访问有权限的资源
用户(zhangsan,权限:emp:save, emp:findAll, emp:edit, emp:remove)
访问:http://localhost:8080/show
↓
1. SecurityContextPersistenceFilter
从 Session 中加载 SecurityContext
结果:找到了 Authentication 对象
放入 SecurityContextHolder
↓
2. FilterSecurityInterceptor
步骤 2.1:从配置中查询 /show 需要的权限
结果:hasAuthority("emp:findAll")
步骤 2.2:从 SecurityContext 获取用户权限
结果:[emp:save, emp:findAll, emp:edit, emp:remove]
步骤 2.3:判断用户是否有 emp:findAll 权限
结果:有 ✅
步骤 2.4:放行
↓
3. DispatcherServlet → Controller
EmpController.show("show")
返回 "show" 视图
↓
4. Thymeleaf 渲染 show.html
↓
5. 用户看到查询页面
场景 4:已登录用户访问无权限的资源
用户(lisi,权限:emp:findAll)
访问:http://localhost:8080/remove
↓
1. SecurityContextPersistenceFilter
从 Session 中加载 SecurityContext
结果:找到了 Authentication 对象(lisi 已登录)
↓
2. FilterSecurityInterceptor
步骤 2.1:从配置中查询 /remove 需要的权限
结果:hasAuthority("emp:remove")
步骤 2.2:从 SecurityContext 获取用户权限
结果:[emp:findAll]
步骤 2.3:判断用户是否有 emp:remove 权限
结果:没有 ❌
步骤 2.4:抛出 AccessDeniedException
↓
3. ExceptionTranslationFilter(捕获异常)
判断:用户已登录,但权限不足
动作:返回 403 错误
↓
4. Spring Boot 默认错误处理
查找 error/403.html
↓
5. 用户看到 403 错误页面
场景 5:Remember Me 自动登录
用户(zhangsan)上次登录时勾选了"记住我"
现在关闭浏览器后重新打开,访问:http://localhost:8080/show
↓
1. SecurityContextPersistenceFilter
从 Session 中加载 SecurityContext
结果:Session 已失效,没有找到
↓
2. RememberMeAuthenticationFilter
步骤 2.1:从 Cookie 中读取 remember-me token
结果:找到 token
步骤 2.2:从数据库的 persistent_logins 表查询 token
结果:token 有效(未过期)
步骤 2.3:调用 EmpServiceImpl.loadUserByUsername("zhangsan")
步骤 2.4:从数据库加载用户信息和权限
步骤 2.5:创建 Authentication 对象
步骤 2.6:存入 SecurityContext
↓
3. FilterSecurityInterceptor
检查权限 → 通过
↓
4. 用户成功访问 /show(无需重新登录)
Spring Security 核心功能详解
功能 1:认证(Authentication)
作用: 验证用户身份(用户名和密码是否正确)
实现方式:
- 用户提交表单到
/eLogin
- Spring Security 调用
EmpServiceImpl.loadUserByUsername()
- 从数据库查询用户信息
- 使用 BCryptPasswordEncoder 验证密码
- 验证成功 → 创建 Authentication 对象 → 存入 SecurityContext
您需要做的:
- ✅ 实现
UserDetailsService
接口 - ✅ 在
loadUserByUsername()
方法中从数据库查询用户 - ✅ 返回
UserDetails
对象(包含用户名、密码、权限)
功能 2:授权(Authorization)
作用: 控制用户访问权限(用户能访问哪些资源)
实现方式:
java
http.authorizeRequests()
.antMatchers("/show").hasAuthority("emp:findAll")
.antMatchers("/save").hasAuthority("emp:save");
权限判断逻辑:
java
// 1. 从配置中获取 URL 需要的权限
String requiredAuthority = "emp:findAll";
// 2. 从 SecurityContext 获取用户的权限列表
List<String> userAuthorities = ["emp:save", "emp:findAll"];
// 3. 判断
if (userAuthorities.contains(requiredAuthority)) {
// 放行
} else {
// 返回 403 错误
}
您需要做的:
- ✅ 在
MyConfig
中配置 URL 权限规则 - ✅ 在数据库中维护用户-角色-权限关系
- ✅ 在
loadUserByUsername()
中查询并返回用户权限
高级用法:
java
// 方法级别的权限控制(需要启用 @EnableGlobalMethodSecurity)
@PreAuthorize("hasAuthority('emp:save')")
public void save(Emp emp) {
// ...
}
// 支持 SpEL 表达式
@PreAuthorize("hasRole('ADMIN') or authentication.name == #username")
public void update(String username, Emp emp) {
// ...
}
功能 3:密码加密
作用: 安全存储密码(不以明文存储)
实现方式:
java
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
工作原理:
java
// 注册时加密密码
String rawPassword = "123456";
String encodedPassword = bCryptPasswordEncoder.encode(rawPassword);
// 结果:$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7.QNcbJSu
// 登录时验证密码
boolean matches = bCryptPasswordEncoder.matches("123456", encodedPassword);
// 结果:true
BCrypt 的特点:
- ✅ 单向加密(不可逆)
- ✅ 自动加盐(每次加密结果不同)
- ✅ 防止彩虹表攻击
- ✅ 计算成本可调(增加暴力破解难度)
您需要做的:
- ✅ 在配置类中声明
BCryptPasswordEncoder
Bean - ✅ 在用户注册时使用它加密密码
- ✅ 数据库存储加密后的密码(长度至少 60 字符)
功能 4:Remember Me(记住我)
作用: 用户关闭浏览器后再次访问,无需重新登录
实现方式:
java
// 配置
http.rememberMe()
.userDetailsService(empService)
.tokenRepository(repository)
.tokenValiditySeconds(60 * 30);
// 登录页面
<input type="checkbox" name="remember-me" value="true"/>
工作流程:
1. 用户勾选"记住我"并登录成功
↓
2. RememberMeServices 生成 token
series: 随机字符串(系列号)
token: 随机字符串(令牌)
↓
3. 存储到数据库 persistent_logins 表
username | series | token | last_used
zhangsan | abc123 | xyz789 | 2023-10-05 10:00:00
↓
4. 将 series 和 token 写入 Cookie(remember-me)
Cookie: remember-me=base64(username:series:token)
↓
5. 用户关闭浏览器
Session 失效,Authentication 丢失
↓
6. 用户再次访问
RememberMeAuthenticationFilter 从 Cookie 读取 token
↓
7. 从数据库验证 token
↓
8. token 有效 → 调用 loadUserByUsername() 加载用户信息
↓
9. 自动创建 Authentication 对象并存入 SecurityContext
↓
10. 用户无需重新登录即可访问
安全机制:
- ✅ token 存储在数据库(服务器端验证)
- ✅ 每次使用后更新 token(防止 token 被盗用)
- ✅ 可设置过期时间
- ✅ 可手动清除(用户登出时删除数据库记录)
功能 5:CSRF 防护
作用: 防止跨站请求伪造攻击
攻击场景示例:
1. 用户登录了银行网站 bank.com
2. 浏览器保存了 bank.com 的 Session Cookie
3. 用户访问恶意网站 evil.com
4. evil.com 的页面包含:
<form action="https://bank.com/transfer" method="POST">
<input name="to" value="hacker">
<input name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>
5. 表单自动提交,浏览器携带 bank.com 的 Cookie
6. bank.com 收到请求,以为是用户本人操作
7. 转账成功,用户损失 10000 元
Spring Security 的 CSRF 防护:
1. 用户访问登录页面
Spring Security 生成随机 CSRF token
存入 Session
↓
2. 页面中添加隐藏字段
<input type="hidden" name="_csrf" value="abc123xyz">
↓
3. 用户提交表单
携带 CSRF token
↓
4. Spring Security 验证 token
if (request.getParameter("_csrf").equals(session.getAttribute("_csrf"))) {
// 继续处理
} else {
// 拒绝请求(返回 403)
}
恶意网站无法伪造的原因:
- ❌ 恶意网站无法读取 bank.com 页面的内容(同源策略)
- ❌ 因此无法获取 CSRF token
- ❌ 提交的表单没有 token → 请求被拒绝
您需要做的:
- ✅ 保持 CSRF 防护开启(默认)
- ✅ 在所有 POST 表单中添加 CSRF token
- ✅ Thymeleaf 模板:
<input type="hidden" th:value="${_csrf.token}" name="_csrf"/>
何时可以关闭 CSRF?
- 纯 API 项目(前后端分离,使用 JWT token)
- 前端使用 AJAX 且在 HTTP Header 中传递 CSRF token
功能 6:Session 管理
作用: 管理用户登录状态
默认行为:
java
// 登录成功后
HttpSession session = request.getSession();
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
// 每次请求时
SecurityContext context = session.getAttribute("SPRING_SECURITY_CONTEXT");
SecurityContextHolder.setContext(context);
高级配置:
java
http.sessionManagement()
// 并发 Session 控制(同一用户最多登录数)
.maximumSessions(1) // 只允许一个地方登录
.maxSessionsPreventsLogin(true) // 达到上限后拒绝新登录(false 为踢掉旧 Session)
.expiredUrl("/session-expired") // Session 过期跳转页面
// Session 固定攻击防护
.sessionFixation().migrateSession() // 登录成功后更换 Session ID
// Session 创建策略
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED); // 需要时才创建
功能 7:页面元素级权限控制
作用: 根据用户权限动态显示/隐藏页面元素
实现方式:
html
<!-- 引入命名空间 -->
<html xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<!-- 根据权限显示/隐藏 -->
<a href="/save" sec:authorize="hasAuthority('emp:save')">添加</a>
<!-- 根据角色显示/隐藏 -->
<a href="/admin" sec:authorize="hasRole('ADMIN')">管理员面板</a>
<!-- 获取当前用户名 -->
<p>欢迎,<span sec:authentication="name">用户</span></p>
<!-- 复杂表达式 -->
<div sec:authorize="isAuthenticated() and hasAuthority('emp:edit')">
编辑功能区
</div>
工作原理:
java
// Thymeleaf 渲染时
1. 解析 sec:authorize 表达式
2. 从 SecurityContext 获取 Authentication 对象
3. 获取用户权限列表
4. 判断是否有指定权限
5. 有 → 渲染标签内容,没有 → 不渲染
与后端权限控制的关系:
- 页面控制:提升用户体验(看不到无权限的按钮)
- 后端控制:真正的安全保障(即使直接访问 URL 也会被拦截)
- 两者必须同时使用! (前端控制可以绕过,后端控制不可绕过)
常见问题与解答
Q1:为什么要实现 UserDetailsService 接口?
A: 这是 Spring Security 的约定(契约)。
java
// Spring Security 需要知道:
// 1. 如何从数据库查询用户?
// 2. 用户有哪些权限?
// 它定义了一个接口:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username);
}
// 您实现这个接口,告诉它"怎么查询"
@Service
public class EmpServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) {
// 从数据库查询
Emp emp = empMapper.selectByEname(username);
// 查询权限
List<String> permissions = empMapper.selectPnameByUsername(username);
// 返回 UserDetails
return new User(emp.getUsername(), emp.getPassword(), authorities);
}
}
// Spring Security 在需要的时候会自动调用您的方法
类比:
- 就像插座(接口)和插头(实现)
- Spring Security 提供插座(UserDetailsService)
- 您提供插头(EmpServiceImpl)
- 插上后就能工作
Q2:loadUserByUsername() 什么时候被调用?
A: 在以下场景会被调用:
-
用户登录时
用户提交表单 → UsernamePasswordAuthenticationFilter → AuthenticationManager → DaoAuthenticationProvider → loadUserByUsername() ← 在这里调用
-
Remember Me 自动登录时
用户访问 → RememberMeAuthenticationFilter → 从 Cookie 读取 token → 验证 token → loadUserByUsername() ← 重新加载用户信息
-
自定义认证逻辑时
java// 您可以手动调用 UserDetails user = empService.loadUserByUsername("zhangsan");
Q3:为什么 /eLogin 不需要写 Controller?
A: 因为 Spring Security 已经帮您处理了!
java
// 您的配置
http.formLogin()
.loginProcessingUrl("/eLogin"); // 告诉 Spring Security 拦截这个 URL
// Spring Security 自动创建 UsernamePasswordAuthenticationFilter
// 这个过滤器会拦截 /eLogin 并处理登录逻辑
// 请求处理流程:
POST /eLogin
↓
UsernamePasswordAuthenticationFilter(拦截)
↓ 在这里就处理完了,不会到达 Controller
认证成功 → 转发到 /success
认证失败 → 转发到 /empLogin
// 只有 /empLogin(显示登录页)和 /success(登录成功)
// 才会到达 Controller
Q4:密码如何加密?
A: 使用 BCryptPasswordEncoder
java
// 1. 注册 Bean
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// 2. 在用户注册时加密密码
@Autowired
private BCryptPasswordEncoder passwordEncoder;
public void register(Emp emp) {
// 加密密码
String encodedPassword = passwordEncoder.encode(emp.getPassword());
emp.setPassword(encodedPassword);
// 存入数据库
empMapper.insert(emp);
}
// 3. 登录时验证
// Spring Security 会自动调用 passwordEncoder.matches() 验证
// 您不需要手动验证
生成测试密码:
java
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encoded = encoder.encode("123456");
System.out.println(encoded);
// 输出:$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7.QNcbJSu
// 将这个值存入数据库
}
Q5:如何调试 Spring Security?
A: 几种调试方法:
- 打印日志
java
@Override
public UserDetails loadUserByUsername(String s) {
System.out.println("Spring Security 调用了我!查询用户:" + s);
// ...
}
- 启用 Spring Security 调试日志
yaml
# application.yml
logging:
level:
org.springframework.security: DEBUG
- 在浏览器中查看
- F12 → Network → 查看请求和响应
- 查看 Cookie(remember-me、JSESSIONID)
- 查看表单数据(_csrf、username、password)
- 使用断点调试
- 在
loadUserByUsername()
方法中打断点 - 在
MyConfig.configure()
方法中打断点
Q6:403 错误如何排查?
A: 按以下步骤排查:
-
确认用户已登录
javaAuthentication auth = SecurityContextHolder.getContext().getAuthentication(); System.out.println("当前用户:" + auth.getName()); System.out.println("是否已认证:" + auth.isAuthenticated());
-
确认用户权限
javaCollection<? extends GrantedAuthority> authorities = auth.getAuthorities(); System.out.println("用户权限:" + authorities);
-
确认 URL 需要的权限
java// 检查配置 http.authorizeRequests() .antMatchers("/show").hasAuthority("emp:findAll"); // 需要这个权限
-
确认权限名称一致
java// 数据库中的权限名称 SELECT p.pname FROM power; // 结果:emp:findAll, emp:save, emp:edit, emp:remove // 配置中的权限名称 .hasAuthority("emp:findAll") // 必须完全一致(包括大小写、冒号)
-
检查 CSRF token
html<!-- 表单中必须有 --> <input type="hidden" th:value="${_csrf.token}" name="_csrf"/>
Q7:如何实现"只能修改自己的数据"?
A: 使用 @PreAuthorize
和 SpEL 表达式
java
@Service
public class EmpServiceImpl implements EmpService {
/**
* 只能修改自己的信息
* authentication.name 是当前登录用户名
* #emp.username 是方法参数的 username
*/
@PreAuthorize("authentication.name == #emp.username")
public void update(Emp emp) {
empMapper.update(emp);
}
/**
* 管理员可以修改任何人,普通用户只能修改自己
*/
@PreAuthorize("hasAuthority('ADMIN') or authentication.name == #emp.username")
public void adminUpdate(Emp emp) {
empMapper.update(emp);
}
}
// 需要启用方法级别的安全控制
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyConfig extends WebSecurityConfigurerAdapter {
// ...
}
Q8:前后端分离项目如何使用 Spring Security?
A: 使用 JWT token 代替 Session
java
// 配置
http
.csrf().disable() // 关闭 CSRF(前后端分离不需要)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 不使用 Session
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
// JWT Filter
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
// 1. 从 Header 中获取 JWT token
String token = request.getHeader("Authorization");
// 2. 验证 token
if (jwtUtil.validate(token)) {
// 3. 从 token 中解析用户信息
String username = jwtUtil.getUsernameFromToken(token);
// 4. 加载用户信息和权限
UserDetails userDetails = empService.loadUserByUsername(username);
// 5. 创建 Authentication 对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
// 6. 存入 SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// 7. 继续过滤器链
chain.doFilter(request, response);
}
}
下次开发时的快速上手指南
🚀 快速开发步骤(Spring Security 项目)
第一步:引入依赖
xml
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
第二步:设计权限模型
sql
-- 用户表
CREATE TABLE user (id, username, password, ...);
-- 角色表
CREATE TABLE role (id, name, ...);
-- 权限表
CREATE TABLE permission (id, name, ...);
-- 用户-角色关联
CREATE TABLE user_role (user_id, role_id);
-- 角色-权限关联
CREATE TABLE role_permission (role_id, permission_id);
第三步:实现 UserDetailsService
java
@Service
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) {
// 1. 查询用户
User user = userMapper.selectByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 2. 查询权限
List<String> permissions = userMapper.selectPermissions(username);
// 3. 转换为 GrantedAuthority
List<GrantedAuthority> authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 4. 返回 UserDetails
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
authorities
);
}
}
第四步:配置 Spring Security
java
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单登录
http.formLogin()
.loginProcessingUrl("/login")
.successForwardUrl("/index")
.failureForwardUrl("/login?error")
.loginPage("/login");
// 权限控制
http.authorizeRequests()
.antMatchers("/login", "/register").permitAll()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated();
// Remember Me
http.rememberMe()
.userDetailsService(userDetailsService)
.tokenValiditySeconds(3600 * 24 * 7); // 7天
// 登出
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login");
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
第五步:创建登录页面
html
<form action="/login" method="post">
用户名: <input type="text" name="username"/>
密码: <input type="password" name="password"/>
<input type="checkbox" name="remember-me"/> 记住我
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
<button type="submit">登录</button>
</form>
第六步:页面权限控制
html
<html xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<div sec:authorize="hasAuthority('user:add')">
<button>添加用户</button>
</div>
</html>
📋 核心配置清单
功能 | 配置 | 说明 |
---|---|---|
登录页面 | .loginPage("/login") |
自定义登录页面 |
登录处理 | .loginProcessingUrl("/login") |
表单提交地址 |
登录成功 | .successForwardUrl("/index") |
成功跳转页面 |
登录失败 | .failureForwardUrl("/login?error") |
失败跳转页面 |
URL放行 | .antMatchers("/login").permitAll() |
任何人都能访问 |
权限控制 | .hasAuthority("user:add") |
需要指定权限 |
角色控制 | .hasRole("ADMIN") |
需要指定角色 |
登录即可 | .authenticated() |
只需登录 |
Remember Me | .rememberMe() |
记住我功能 |
登出 | .logout() |
登出配置 |
CSRF | .csrf().disable() |
关闭CSRF(慎用) |
Session | .sessionManagement() |
Session配置 |
🎯 最佳实践
-
权限命名规范
格式:资源:操作 示例: - user:add // 添加用户 - user:edit // 修改用户 - user:delete // 删除用户 - user:view // 查看用户 - order:* // 订单的所有权限
-
密码加密
java// 注册时 String encoded = passwordEncoder.encode(rawPassword); // 登录时 // Spring Security 自动验证,不需要手动操作
-
多层防护
✅ 后端 URL 权限控制(必须) ✅ 后端方法权限控制(推荐) ✅ 前端页面元素控制(提升体验)
-
异常处理
java@ControllerAdvice public class SecurityExceptionHandler { @ExceptionHandler(AccessDeniedException.class) public String handleAccessDenied() { return "error/403"; } }
-
日志记录
java@Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(...) { logger.info("用户 {} 登录成功,IP: {}", authentication.getName(), request.getRemoteAddr()); } }
🎓 总结
Spring Security 的核心价值
-
简化开发
- 不需要手写认证和授权代码
- 声明式配置,简单易用
- 与 Spring Boot 无缝集成
-
安全可靠
- 防御常见安全攻击(CSRF、XSS、Session劫持等)
- 密码加密存储
- 成熟的企业级解决方案
-
功能强大
- URL 级别权限控制
- 方法级别权限控制
- 页面元素权限控制
- Remember Me 功能
- Session 管理
- 多种认证方式支持
下次开发时记住这些
- ✅ 实现
UserDetailsService
接口 - ✅ 配置
WebSecurityConfigurerAdapter
- ✅ 使用
BCryptPasswordEncoder
加密密码 - ✅ 设计 RBAC 权限模型(用户-角色-权限)
- ✅ 前后端同时进行权限控制
关键概念回顾
概念 | 说明 |
---|---|
Authentication | 认证对象,包含用户信息和权限 |
SecurityContext | 安全上下文,存储 Authentication |
UserDetailsService | 用户查询服务(您需要实现) |
UserDetails | 用户详情对象(包含用户名、密码、权限) |
GrantedAuthority | 权限对象 |
Filter Chain | 过滤器链(Spring Security 的核心机制) |
CSRF Token | 防止跨站请求伪造的令牌 |
Remember Me | 记住我功能的令牌 |