Spring Security 完整使用指南

Spring Security 完整使用指南

本文档基于实际项目,全面讲解 Spring Security 的使用方法、工作原理和最佳实践


📚 目录

  1. [Spring Security 是什么](#Spring Security 是什么)
  2. 项目书写流程概览
  3. 详细开发步骤
  4. [Spring Security 工作原理深度解析](#Spring Security 工作原理深度解析)
  5. 完整的请求处理流程
  6. [Spring Security 核心功能详解](#Spring Security 核心功能详解)
  7. 常见问题与解答
  8. 下次开发时的快速上手指南

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 的约定(契约)

  1. Spring Security 需要知道如何从数据库加载用户信息
  2. 它定义了一个接口 UserDetailsService
  3. 您实现这个接口,告诉它"怎么查询用户"
  4. 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();
    }
}

🔑 关键理解点:

  1. @Configuration 注解:告诉 Spring 这是一个配置类
  2. 继承 WebSecurityConfigurerAdapter:获得配置 Security 的能力
  3. configure(HttpSecurity http) 方法:所有安全配置都在这里完成
  4. 链式调用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)

作用: 验证用户身份(用户名和密码是否正确)

实现方式:

  1. 用户提交表单到 /eLogin
  2. Spring Security 调用 EmpServiceImpl.loadUserByUsername()
  3. 从数据库查询用户信息
  4. 使用 BCryptPasswordEncoder 验证密码
  5. 验证成功 → 创建 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: 在以下场景会被调用:

  1. 用户登录时

    复制代码
    用户提交表单 → UsernamePasswordAuthenticationFilter 
    → AuthenticationManager → DaoAuthenticationProvider 
    → loadUserByUsername() ← 在这里调用
  2. Remember Me 自动登录时

    复制代码
    用户访问 → RememberMeAuthenticationFilter 
    → 从 Cookie 读取 token → 验证 token 
    → loadUserByUsername() ← 重新加载用户信息
  3. 自定义认证逻辑时

    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: 几种调试方法:

  1. 打印日志
java 复制代码
@Override
public UserDetails loadUserByUsername(String s) {
    System.out.println("Spring Security 调用了我!查询用户:" + s);
    // ...
}
  1. 启用 Spring Security 调试日志
yaml 复制代码
# application.yml
logging:
  level:
    org.springframework.security: DEBUG
  1. 在浏览器中查看
  • F12 → Network → 查看请求和响应
  • 查看 Cookie(remember-me、JSESSIONID)
  • 查看表单数据(_csrf、username、password)
  1. 使用断点调试
  • loadUserByUsername() 方法中打断点
  • MyConfig.configure() 方法中打断点

Q6:403 错误如何排查?

A: 按以下步骤排查:

  1. 确认用户已登录

    java 复制代码
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    System.out.println("当前用户:" + auth.getName());
    System.out.println("是否已认证:" + auth.isAuthenticated());
  2. 确认用户权限

    java 复制代码
    Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
    System.out.println("用户权限:" + authorities);
  3. 确认 URL 需要的权限

    java 复制代码
    // 检查配置
    http.authorizeRequests()
        .antMatchers("/show").hasAuthority("emp:findAll");  // 需要这个权限
  4. 确认权限名称一致

    java 复制代码
    // 数据库中的权限名称
    SELECT p.pname FROM power;
    // 结果:emp:findAll, emp:save, emp:edit, emp:remove
    
    // 配置中的权限名称
    .hasAuthority("emp:findAll")  // 必须完全一致(包括大小写、冒号)
  5. 检查 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配置

🎯 最佳实践

  1. 权限命名规范

    复制代码
    格式:资源:操作
    示例:
    - user:add    // 添加用户
    - user:edit   // 修改用户
    - user:delete // 删除用户
    - user:view   // 查看用户
    - order:*     // 订单的所有权限
  2. 密码加密

    java 复制代码
    // 注册时
    String encoded = passwordEncoder.encode(rawPassword);
    
    // 登录时
    // Spring Security 自动验证,不需要手动操作
  3. 多层防护

    复制代码
    ✅ 后端 URL 权限控制(必须)
    ✅ 后端方法权限控制(推荐)
    ✅ 前端页面元素控制(提升体验)
  4. 异常处理

    java 复制代码
    @ControllerAdvice
    public class SecurityExceptionHandler {
        
        @ExceptionHandler(AccessDeniedException.class)
        public String handleAccessDenied() {
            return "error/403";
        }
    }
  5. 日志记录

    java 复制代码
    @Component
    public class LoginSuccessHandler implements AuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(...) {
            logger.info("用户 {} 登录成功,IP: {}", 
                authentication.getName(), request.getRemoteAddr());
        }
    }

🎓 总结

Spring Security 的核心价值

  1. 简化开发

    • 不需要手写认证和授权代码
    • 声明式配置,简单易用
    • 与 Spring Boot 无缝集成
  2. 安全可靠

    • 防御常见安全攻击(CSRF、XSS、Session劫持等)
    • 密码加密存储
    • 成熟的企业级解决方案
  3. 功能强大

    • URL 级别权限控制
    • 方法级别权限控制
    • 页面元素权限控制
    • Remember Me 功能
    • Session 管理
    • 多种认证方式支持

下次开发时记住这些

  1. ✅ 实现 UserDetailsService 接口
  2. ✅ 配置 WebSecurityConfigurerAdapter
  3. ✅ 使用 BCryptPasswordEncoder 加密密码
  4. ✅ 设计 RBAC 权限模型(用户-角色-权限)
  5. ✅ 前后端同时进行权限控制

关键概念回顾

概念 说明
Authentication 认证对象,包含用户信息和权限
SecurityContext 安全上下文,存储 Authentication
UserDetailsService 用户查询服务(您需要实现)
UserDetails 用户详情对象(包含用户名、密码、权限)
GrantedAuthority 权限对象
Filter Chain 过滤器链(Spring Security 的核心机制)
CSRF Token 防止跨站请求伪造的令牌
Remember Me 记住我功能的令牌

相关推荐
玛卡巴卡012 分钟前
Maven 从入门到实战:搞定依赖管理与 Spring Boot 项目构建
java·spring boot·maven
vortex515 分钟前
用 Scoop 快速部署 JeecgBoot 开发环境:从依赖安装到服务管理
java·windows·springboot·web·开发·jeecg-boot
国服第二切图仔26 分钟前
Rust开发之使用panic!处理不可恢复错误
开发语言·后端·rust
جيون داد ناالام ميづ39 分钟前
Spring Boot 核心原理(一):基础认知篇
java·spring boot·后端
南囝coding1 小时前
现代Unix命令行工具革命:30个必备替代品完整指南
前端·后端
夏之小星星1 小时前
Springboot结合Vue实现分页功能
vue.js·spring boot·后端
唐僧洗头爱飘柔95272 小时前
【SpringCloud(8)】SpringCloud Stream消息驱动;Stream思想;生产者、消费者搭建
后端·spring·spring cloud·设计思想·stream消息驱动·重复消费问题
韩立学长2 小时前
【开题答辩实录分享】以《自动售货机刷脸支付系统的设计与实现》为例进行答辩实录分享
vue.js·spring boot·后端
fantasy5_52 小时前
手撕vector:从零实现一个C++动态数组
java·开发语言·c++
十八旬2 小时前
RuoYi-Vue3项目定制修改全攻略
java·windows