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 记住我功能的令牌

相关推荐
IT_陈寒2 小时前
Redis 高性能缓存设计:7个核心优化策略让你的QPS提升300%
前端·人工智能·后端
还是鼠鼠3 小时前
《黑马商城》微服务保护-详细介绍【简单易懂注释版】
java·spring boot·spring·spring cloud·sentinel·maven
brzhang3 小时前
高通把Arduino买了,你的“小破板”要变“AI核弹”了?
前端·后端·架构
她说..3 小时前
通过git拉取前端项目
java·前端·git·vscode·拉取代码
青衫码上行3 小时前
【从0开始学习Java | 第18篇】集合(下 - Map部分)
java·学习
我星期八休息3 小时前
C++异常处理全面解析:从基础到应用
java·开发语言·c++·人工智能·python·架构
江湖有缘4 小时前
【Docker项目实战】使用Docker部署ShowDoc文档管理工具
java·docker·容器
2401_841495644 小时前
【数据结构】汉诺塔问题
java·数据结构·c++·python·算法·递归·
程序猿阿越4 小时前
Kafka源码(六)消费者消费
java·后端·源码阅读