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 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
# 服务器端口配置
server:port: 8080# Spring 配置
spring:# 数据源配置datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/security?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueusername: rootpassword: 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 权限模型):
-- 用户表
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
);
示例数据:
-- 插入用户(密码都是 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
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; // 员工IDprivate String username; // 用户名private String password; // 密码(加密后)private Integer rid; // 角色ID
}
💡 为什么要用 Lombok?
- 不用手写 getter/setter(自动生成)
- 代码更简洁,可读性更好
@Data
一个注解搞定所有常用方法
第三步:Mapper 层(数据访问层)
3.1 EmpMapper.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 version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//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.pnameFROM emp eJOIN role_emp re ON e.eid = re.eidJOIN role r ON re.rid = r.ridJOIN role_power rp ON r.rid = rp.ridJOIN power p ON rp.pid = p.pidWHERE e.username = #{username}</select></mapper>
💡 为什么要多表联查?
- 实现了 RBAC(基于角色的权限控制)模型
- 一个用户可以有多个角色
- 一个角色可以有多个权限
- 灵活的权限管理,便于扩展
第四步:Service 层(业务层)⭐ 核心
4.1 EmpService.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(实现类)⭐ 核心
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 {@Autowiredprivate EmpMapper empMapper;/*** ⭐⭐⭐ 核心方法:Spring Security 认证时会自动调用* * 调用时机:* 1. 用户提交登录表单到 /eLogin* 2. Spring Security 的 UsernamePasswordAuthenticationFilter 拦截请求* 3. 调用 AuthenticationManager 进行认证* 4. AuthenticationManager 调用 DaoAuthenticationProvider* 5. DaoAuthenticationProvider 调用本方法加载用户信息* * @param s 用户输入的用户名* @return UserDetails 对象(包含用户名、密码、权限列表)* @throws UsernameNotFoundException 用户不存在时抛出*/@Overridepublic 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(登录失败页面)}@Overridepublic Emp selectByUsername(String username) {return empMapper.selectByEname(username);}@Overridepublic 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
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 配置)
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 {@Autowiredprivate DataSource dataSource; // Spring Boot 自动配置的数据源/*** 配置 token 存储方式* * 工作原理:* 1. 用户勾选"记住我"并登录成功* 2. 生成一个随机 token 存储到 persistent_logins 表* 3. 同时将 token 写入 Cookie(remember-me)* 4. 用户关闭浏览器后再访问* 5. Spring Security 从 Cookie 读取 token* 6. 从数据库验证 token 是否有效* 7. 有效则自动登录(无需输入密码)*/@Beanpublic PersistentTokenRepository getPersistentTokenRepository() {JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();jdbcTokenRepository.setDataSource(dataSource);// ⚠️ 第一次启动时开启,自动创建 persistent_logins 表// ⚠️ 第二次启动时务必注释掉,否则会报错(表已存在)// jdbcTokenRepository.setCreateTableOnStartup(true);return jdbcTokenRepository;}
}
💡 Remember Me 的价值:
- 提升用户体验(不用每次都登录)
- token 存储在数据库,比存在 Cookie 更安全
- 可以设置过期时间,到期自动失效
6.2 MyConfig.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 {@Autowiredprivate PersistentTokenRepository repository; // Remember Me 的 token 存储@Autowiredprivate EmpService empService; // 用户认证服务/*** 核心配置方法* * @param http HttpSecurity 对象,用于配置安全策略*/@Overrideprotected 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* (每次结果不同,但验证时都能通过)*/@Beanpublic BCryptPasswordEncoder getBCryptPasswordEncoder() {return new BCryptPasswordEncoder();}
}
🔑 关键理解点:
- @Configuration 注解:告诉 Spring 这是一个配置类
- 继承 WebSecurityConfigurerAdapter:获得配置 Security 的能力
- configure(HttpSecurity http) 方法:所有安全配置都在这里完成
- 链式调用:
http.formLogin().loginProcessingUrl(...).successForwardUrl(...)
第七步:启动类
SpringBootMain.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(登录页面)
<!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(登录成功页面)
<!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(查询页面示例)
<!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(权限不足错误页面)
<!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(安全上下文)
// 安全上下文的存储结构
SecurityContextHolder(线程级别的持有者)└─ SecurityContext(安全上下文)└─ Authentication(认证对象)├─ Principal(用户信息,如:zhangsan)├─ Credentials(凭证,如:加密后的密码)├─ Authorities(权限列表,如:[emp:save, emp:findAll])└─ isAuthenticated(是否已认证)
在代码中获取当前用户信息:
// 获取当前登录用户
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName(); // 用户名
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities(); // 权限列表
3. Authentication(认证对象)
// 认证对象的生命周期// 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 tokentoken 正确 → 继续↓
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 → ControllerEmpController.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 → ControllerEmpController.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)
作用: 控制用户访问权限(用户能访问哪些资源)
实现方式:
http.authorizeRequests().antMatchers("/show").hasAuthority("emp:findAll").antMatchers("/save").hasAuthority("emp:save");
权限判断逻辑:
// 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()
中查询并返回用户权限
高级用法:
// 方法级别的权限控制(需要启用 @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:密码加密
作用: 安全存储密码(不以明文存储)
实现方式:
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {return new BCryptPasswordEncoder();
}
工作原理:
// 注册时加密密码
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(记住我)
作用: 用户关闭浏览器后再次访问,无需重新登录
实现方式:
// 配置
http.rememberMe().userDetailsService(empService).tokenRepository(repository).tokenValiditySeconds(60 * 30);// 登录页面
<input type="checkbox" name="remember-me" value="true"/>
工作流程:
1. 用户勾选"记住我"并登录成功↓
2. RememberMeServices 生成 tokenseries: 随机字符串(系列号)token: 随机字符串(令牌)↓
3. 存储到数据库 persistent_logins 表username | series | token | last_usedzhangsan | 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 验证 tokenif (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 管理
作用: 管理用户登录状态
默认行为:
// 登录成功后
HttpSession session = request.getSession();
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);// 每次请求时
SecurityContext context = session.getAttribute("SPRING_SECURITY_CONTEXT");
SecurityContextHolder.setContext(context);
高级配置:
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 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>
工作原理:
// Thymeleaf 渲染时
1. 解析 sec:authorize 表达式
2. 从 SecurityContext 获取 Authentication 对象
3. 获取用户权限列表
4. 判断是否有指定权限
5. 有 → 渲染标签内容,没有 → 不渲染
与后端权限控制的关系:
- 页面控制:提升用户体验(看不到无权限的按钮)
- 后端控制:真正的安全保障(即使直接访问 URL 也会被拦截)
- 两者必须同时使用! (前端控制可以绕过,后端控制不可绕过)
常见问题与解答
Q1:为什么要实现 UserDetailsService 接口?
A: 这是 Spring Security 的约定(契约)。
// Spring Security 需要知道:
// 1. 如何从数据库查询用户?
// 2. 用户有哪些权限?// 它定义了一个接口:
public interface UserDetailsService {UserDetails loadUserByUsername(String username);
}// 您实现这个接口,告诉它"怎么查询"
@Service
public class EmpServiceImpl implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) {// 从数据库查询Emp emp = empMapper.selectByEname(username);// 查询权限List<String> permissions = empMapper.selectPnameByUsername(username);// 返回 UserDetailsreturn 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() ← 重新加载用户信息
-
自定义认证逻辑时
// 您可以手动调用 UserDetails user = empService.loadUserByUsername("zhangsan");
Q3:为什么 /eLogin 不需要写 Controller?
A: 因为 Spring Security 已经帮您处理了!
// 您的配置
http.formLogin().loginProcessingUrl("/eLogin"); // 告诉 Spring Security 拦截这个 URL// Spring Security 自动创建 UsernamePasswordAuthenticationFilter
// 这个过滤器会拦截 /eLogin 并处理登录逻辑// 请求处理流程:
POST /eLogin↓
UsernamePasswordAuthenticationFilter(拦截)↓ 在这里就处理完了,不会到达 Controller认证成功 → 转发到 /success认证失败 → 转发到 /empLogin// 只有 /empLogin(显示登录页)和 /success(登录成功)
// 才会到达 Controller
Q4:密码如何加密?
A: 使用 BCryptPasswordEncoder
// 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() 验证
// 您不需要手动验证
生成测试密码:
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: 几种调试方法:
- 打印日志
@Override
public UserDetails loadUserByUsername(String s) {System.out.println("Spring Security 调用了我!查询用户:" + s);// ...
}
- 启用 Spring Security 调试日志
# application.yml
logging:level:org.springframework.security: DEBUG
- 在浏览器中查看
- F12 → Network → 查看请求和响应
- 查看 Cookie(remember-me、JSESSIONID)
- 查看表单数据(_csrf、username、password)
- 使用断点调试
- 在
loadUserByUsername()
方法中打断点 - 在
MyConfig.configure()
方法中打断点
Q6:403 错误如何排查?
A: 按以下步骤排查:
-
确认用户已登录
Authentication auth = SecurityContextHolder.getContext().getAuthentication(); System.out.println("当前用户:" + auth.getName()); System.out.println("是否已认证:" + auth.isAuthenticated());
-
确认用户权限
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities(); System.out.println("用户权限:" + authorities);
-
确认 URL 需要的权限
// 检查配置 http.authorizeRequests().antMatchers("/show").hasAuthority("emp:findAll"); // 需要这个权限
-
确认权限名称一致
// 数据库中的权限名称 SELECT p.pname FROM power; // 结果:emp:findAll, emp:save, emp:edit, emp:remove// 配置中的权限名称 .hasAuthority("emp:findAll") // 必须完全一致(包括大小写、冒号)
-
检查 CSRF token
<!-- 表单中必须有 --> <input type="hidden" th:value="${_csrf.token}" name="_csrf"/>
Q7:如何实现"只能修改自己的数据"?
A: 使用 @PreAuthorize
和 SpEL 表达式
@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
// 配置
http.csrf().disable() // 关闭 CSRF(前后端分离不需要).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 不使用 Session.and().addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);// JWT Filter
public class JwtAuthenticationFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {// 1. 从 Header 中获取 JWT tokenString token = request.getHeader("Authorization");// 2. 验证 tokenif (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. 存入 SecurityContextSecurityContextHolder.getContext().setAuthentication(authentication);}// 7. 继续过滤器链chain.doFilter(request, response);}
}
下次开发时的快速上手指南
🚀 快速开发步骤(Spring Security 项目)
第一步:引入依赖
<!-- pom.xml -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
第二步:设计权限模型
-- 用户表
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
@Service
public class UserServiceImpl implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String username) {// 1. 查询用户User user = userMapper.selectByUsername(username);if (user == null) {throw new UsernameNotFoundException("用户不存在");}// 2. 查询权限List<String> permissions = userMapper.selectPermissions(username);// 3. 转换为 GrantedAuthorityList<GrantedAuthority> authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());// 4. 返回 UserDetailsreturn new org.springframework.security.core.userdetails.User(user.getUsername(),user.getPassword(),authorities);}
}
第四步:配置 Spring Security
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected 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 Mehttp.rememberMe().userDetailsService(userDetailsService).tokenValiditySeconds(3600 * 24 * 7); // 7天// 登出http.logout().logoutUrl("/logout").logoutSuccessUrl("/login");}@Beanpublic BCryptPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}
第五步:创建登录页面
<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 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:* // 订单的所有权限
-
密码加密
// 注册时 String encoded = passwordEncoder.encode(rawPassword);// 登录时 // Spring Security 自动验证,不需要手动操作
-
多层防护
✅ 后端 URL 权限控制(必须) ✅ 后端方法权限控制(推荐) ✅ 前端页面元素控制(提升体验)
-
异常处理
@ControllerAdvice public class SecurityExceptionHandler {@ExceptionHandler(AccessDeniedException.class)public String handleAccessDenied() {return "error/403";} }
-
日志记录
@Component public class LoginSuccessHandler implements AuthenticationSuccessHandler {@Overridepublic 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 | 记住我功能的令牌 |