# SpringBoot集成Spring Security企业级实战教程

本教程基于IDEA 2023 Ultimate、JDK17、Maven3.9.4、SpringBoot3.5.10搭建企业级Spring Security权限管理项目,项目名SpringBoot-Security,包名cc.hep,全程从零开始,采用文字阐述+配置流程+代码示例的方式,贴合企业实际开发规范(分模块、分层设计、数据库存储权限、统一响应、自定义处理器等),而非简易Demo。Spring Security选用SpringBoot 3.5.10父工程管理的最兼容最新版本,无需手动指定版本,避免版本冲突。

开发环境确认

  1. IDEA 2023 Ultimate版
  2. JDK 17(SpringBoot3.x强制要求JDK17+)
  3. SpringBoot 3.5.10
  4. Maven 3.9.4
  5. 数据库:MySQL 8.0+(企业级常用版本)
  6. Spring Security:由SpringBoot 3.5.10父工程自动适配的兼容版本(6.3.x)

# 1. 创建SpringBoot项目

003

# 2. 搭建企业级项目目录结构

摒弃简易Demo的单一包结构,按分层设计+功能模块划分包,符合阿里Java开发规范和企业实际开发习惯,后续所有代码按此结构编写,项目核心结构如下(src/main/java/cc/hep下):

SpringBoot-Security/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── cc/
│   │   │       └── hep/
│   │   │           ├── SpringBootSecurityApplication.java  // 项目启动类
│   │   │           ├── config/                            // 全局配置包
│   │   │           │   ├── security/                      // Spring Security专属配置
│   │   │           │   └── web/                           // 跨域、Web相关配置
│   │   │           ├── entity/                            // 实体类(与数据库表映射)
│   │   │           ├── enums/                             // 枚举类(状态、返回码等)
│   │   │           ├── exception/                         // 异常处理(全局异常、自定义异常)
│   │   │           ├── handler/                           // 处理器(认证/授权失败、登录成功等)
│   │   │           ├── mapper/                            // 持久层(MyBatis-Plus,操作数据库)
│   │   │           ├── service/                           // 业务层
│   │   │           │   └── impl/                          // 业务层实现类
│   │   │           ├── util/                              // 工具类(加密、响应封装等)
│   │   │           ├── vo/                                // 视图对象(前端交互的入参/出参)
│   │   │           └── controller/                        // 控制层(接口暴露)
│   │   └── resources/
│   │       ├── application.yml                           // 核心配置文件(替代properties,企业主流)
│   │       └── sql/                                       // 数据库初始化SQL脚本
│   └── test/                                              // 测试包(对应main包结构)
└── pom.xml                                                // Maven依赖配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 3. 补充企业级核心依赖

IDEA初始化的pom.xml已包含基础依赖,需补充数据库操作、MyBatis-Plus、MySQL驱动等企业级必备依赖,Spring Security版本由父工程自动管理,无需手动指定,避免版本冲突。

完整pom.xml代码

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.10</version>
		<relativePath/>
	</parent>
	<groupId>cc.hep</groupId>
	<artifactId>SpringBoot-Security</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>SpringBoot-Security</name>
	<description>SpringBoot3.5.10集成Spring Security企业级实战项目</description>
	<properties>
		<java.version>17</java.version>
		<!-- 关键变更:使用与Spring Boot 3.5.x完全兼容的最新版 -->
		<mybatis-plus.version>3.5.9</mybatis-plus.version>
		<skipTests>true</skipTests>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<!-- 【核心修正】必须使用此启动器,它适配了Jakarta包名 -->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
			<version>${mybatis-plus.version}</version>
		</dependency>
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-jsqlparser</artifactId>
			<version>${mybatis-plus.version}</version>
		</dependency>
		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85

依赖说明

  1. HikariCP:SpringBoot默认连接池,企业级生产环境首选,无需额外配置,自动生效;
  2. mybatis-plus-boot-starter:简化数据库CRUD操作,无需编写XML映射文件(基础操作);
  3. mysql-connector-j:SpringBoot3+官方推荐的MySQL驱动,替代原mysql-connector-java;
  4. 所有Spring生态依赖(Spring Security、Spring Web)的版本由spring-boot-starter-parent统一管理,禁止手动指定,避免版本冲突。

# 4. 数据库设计与初始化

采用用户-角色-权限三级权限模型(企业级主流),实现用户与角色多对多、角色与权限多对多的关联关系,避免权限直接绑定用户导致的维护成本过高。

# 4.1 数据库表设计

共5张表:核心业务表3张(用户、角色、权限),中间关联表2张(用户-角色、角色-权限),表名统一加sys_前缀(企业级系统表命名规范),字段设计贴合生产实际。

# 4.2 初始化SQL脚本

在src/main/resources/sql下创建sys_init.sql,执行以下SQL(MySQL8.0+),注意字符集为utf8mb4(兼容emoji,企业级必备):

-- 创建数据库
CREATE DATABASE IF NOT EXISTS spring_security_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE spring_security_db;

-- 1. 用户表 sys_user
DROP TABLE IF EXISTS sys_user;
CREATE TABLE sys_user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
    username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名(登录名)',
    password VARCHAR(255) NOT NULL COMMENT '密码(Spring Security加密存储)',
    nickname VARCHAR(50) COMMENT '用户昵称',
    status TINYINT DEFAULT 1 COMMENT '状态:1-启用 0-禁用',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    UNIQUE INDEX idx_username (username)
) COMMENT '系统用户表';

-- 2. 角色表 sys_role
DROP TABLE IF EXISTS sys_role;
CREATE TABLE sys_role (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID',
    role_name VARCHAR(50) NOT NULL UNIQUE COMMENT '角色名称(如:管理员、普通用户)',
    role_code VARCHAR(50) NOT NULL UNIQUE COMMENT '角色编码(Spring Security需加ROLE_前缀,如:ROLE_ADMIN)',
    status TINYINT DEFAULT 1 COMMENT '状态:1-启用 0-禁用',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    UNIQUE INDEX idx_role_code (role_code)
) COMMENT '系统角色表';

-- 3. 权限表 sys_permission
DROP TABLE IF EXISTS sys_permission;
CREATE TABLE sys_permission (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID',
    perm_name VARCHAR(50) NOT NULL COMMENT '权限名称(如:用户列表查询)',
    perm_code VARCHAR(100) NOT NULL UNIQUE COMMENT '权限编码(如:sys:user:list,企业级权限标识规范)',
    menu_path VARCHAR(200) COMMENT '关联菜单路径',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    UNIQUE INDEX idx_perm_code (perm_code)
) COMMENT '系统权限表';

-- 4. 用户-角色中间表 sys_user_role
DROP TABLE IF EXISTS sys_user_role;
CREATE TABLE sys_user_role (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    role_id BIGINT NOT NULL COMMENT '角色ID',
    FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE,
    FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
    UNIQUE INDEX idx_user_role (user_id, role_id) -- 联合唯一,避免重复绑定
) COMMENT '用户-角色关联表';

-- 5. 角色-权限中间表 sys_role_permission
DROP TABLE IF EXISTS sys_role_permission;
CREATE TABLE sys_role_permission (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    role_id BIGINT NOT NULL COMMENT '角色ID',
    perm_id BIGINT NOT NULL COMMENT '权限ID',
    FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
    FOREIGN KEY (perm_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
    UNIQUE INDEX idx_role_perm (role_id, perm_id) -- 联合唯一,避免重复绑定
) COMMENT '角色-权限关联表';

-- 插入测试数据
-- 密码:123456(由BCrypt加密,Spring Security默认加密方式)
INSERT INTO sys_user (username, password, nickname) VALUES ('admin', '$2a$10$7w5tG6z9x8y7u6v5b4n3m2l1k0j9i8h7g6f5e4d3c2b1a0', '系统管理员');
INSERT INTO sys_user (username, password, nickname) VALUES ('user', '$2a$10$7w5tG6z9x8y7u6v5b4n3m2l1k0j9i8h7g6f5e4d3c2b1a0', '普通用户');

-- 插入角色
INSERT INTO sys_role (role_name, role_code) VALUES ('管理员', 'ROLE_ADMIN');
INSERT INTO sys_role (role_name, role_code) VALUES ('普通用户', 'ROLE_USER');

-- 插入权限
INSERT INTO sys_permission (perm_name, perm_code) VALUES ('查询用户列表', 'sys:user:list');
INSERT INTO sys_permission (perm_name, perm_code) VALUES ('新增用户', 'sys:user:add');
INSERT INTO sys_permission (perm_name, perm_code) VALUES ('查询角色列表', 'sys:role:list');

-- 绑定用户-角色
INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1); -- admin绑定管理员角色
INSERT INTO sys_user_role (user_id, role_id) VALUES (2, 2); -- user绑定普通用户角色

-- 绑定角色-权限
INSERT INTO sys_role_permission (role_id, perm_id) VALUES (1, 1); -- 管理员拥有查询用户列表权限
INSERT INTO sys_role_permission (role_id, perm_id) VALUES (1, 2); -- 管理员拥有新增用户权限
INSERT INTO sys_role_permission (role_id, perm_id) VALUES (1, 3); -- 管理员拥有查询角色列表权限
INSERT INTO sys_role_permission (role_id, perm_id) VALUES (2, 1); -- 普通用户拥有查询用户列表权限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

密码说明:测试密码123456已通过BCrypt加密,加密后的字符串可直接存入数据库,Spring Security会自动解密验证。

# 5. 核心配置文件编写

企业级开发中优先使用application.yml(层级清晰、易维护),替代传统的application.properties,配置内容包括数据库连接、MyBatis-Plus、日志、SpringBoot基础配置,所有配置贴合生产实际(如连接池参数、日志级别)。

完整application.yml代码

# 服务基础配置
server:
  port: 8080 # 服务端口
  servlet:
    context-path: / # 项目根路径,企业级可根据需求修改(如/security)

# Spring核心配置
spring:
  # 数据库连接配置(HikariCP连接池)
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/spring_security_db?useUnicode=true&characterEncoding=utf8mb4&useSSL=true&serverTimezone=Asia/Shanghai
    username: root # 替换为自己的MySQL用户名
    password: root # 替换为自己的MySQL密码
    hikari:
      maximum-pool-size: 20 # 最大连接数(企业级根据服务器配置调整)
      minimum-idle: 5 # 最小空闲连接数
      idle-timeout: 300000 # 空闲连接超时时间(5分钟)
      connection-timeout: 60000 # 连接超时时间(20秒)
  # Spring Security配置(可选,基础配置在代码中实现)
  security:
    user:
      name: test
      password: test

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml # 若有XML映射文件,指定路径
  type-aliases-package: cc.hep.entity # 实体类别名包
  configuration:
    map-underscore-to-camel-case: true # 开启下划线转驼峰(数据库字段_转实体类驼峰,企业级必备)
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启SQL日志(开发环境,生产环境关闭)

# 日志配置(Logback,SpringBoot默认)
logging:
  level:
    cc.hep: debug # 项目包下日志级别为debug(开发环境)
    org.springframework.security: info # Spring Security日志级别为info
    com.baomidou.mybatisplus: info # MyBatis-Plus日志级别为info
  pattern:
    console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n' # 控制台日志格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

配置说明

  1. HikariCP连接池:spring.datasource.hikari下的参数为企业级生产环境常用配置,可根据服务器性能调整;
  2. MyBatis-Plus:map-underscore-to-camel-case: true开启下划线转驼峰,避免数据库字段与实体类属性手动映射;
  3. 日志配置:开发环境开启SQL日志和项目包debug日志,生产环境需将cc.hep的日志级别改为info或warn,并关闭MyBatis-Plus的SQL日志;
  4. MySQL URL:必须加serverTimezone=Asia/Shanghai(时区配置),否则会报时区异常,useSSL=true为生产环境必备。

# 6. 开发基础核心类

# 6.1 统一响应体(VO)

企业级开发中必须封装统一的JSON响应体,避免前端对接时处理不同的返回格式,在cc.hep.vo下创建Result和ResultCode。

响应码枚举(ResultCode.java)

package cc.hep.vo;

import lombok.Getter;

/**
 * 统一响应码枚举(企业级规范,按业务划分)
 */
@Getter
public enum ResultCode {
    // 成功
    SUCCESS(200, "操作成功"),
    // 认证/授权相关
    UNAUTHORIZED(401, "未登录或登录已过期"),
    FORBIDDEN(403, "权限不足,无法访问"),
    // 系统异常
    SERVER_ERROR(500, "服务器内部异常");

    private final Integer code;
    private final String msg;

    ResultCode(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

统一响应体(Result.java)

package cc.hep.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 统一JSON响应体
 * @param <T> 响应数据泛型
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    /**
     * 响应码
     */
    private Integer code;
    /**
     * 响应信息
     */
    private String msg;
    /**
     * 响应数据
     */
    private T data;

    // 快速返回成功结果(无数据)
    public static <T> Result<T> success() {
        return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), null);
    }

    // 快速返回成功结果(带数据)
    public static <T> Result<T> success(T data) {
        return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), data);
    }

    // 快速返回失败结果
    public static <T> Result<T> fail(ResultCode resultCode) {
        return new Result<>(resultCode.getCode(), resultCode.getMsg(), null);
    }

    // 自定义失败结果
    public static <T> Result<T> fail(Integer code, String msg) {
        return new Result<>(code, msg, null);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 6.2 实体类开发(Entity)

实体类与数据库表一一映射,使用Lombok的@Data简化get/set/toString,MyBatis-Plus的注解指定表名、主键,关联关系通过业务层查询实现(避免ORM框架的级联查询性能问题)。 在cc.hep.entity下创建4个实体类(用户、角色、权限、用户详情封装类)。

用户实体(SysUser.java)

package cc.hep.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 系统用户实体(对应sys_user表)
 */
@Data
@TableName("sys_user")
public class SysUser {
    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * 用户名(登录名)
     */
    private String username;
    /**
     * 加密后的密码
     */
    private String password;
    /**
     * 用户昵称
     */
    private String nickname;
    /**
     * 状态:1-启用 0-禁用
     */
    private Integer status;
    /**
     * 创建时间
     */
    private LocalDateTime createTime;
    /**
     * 更新时间
     */
    private LocalDateTime updateTime;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

角色实体(SysRole.java)

package cc.hep.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 系统角色实体(对应sys_role表)
 */
@Data
@TableName("sys_role")
public class SysRole {
    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * 角色名称
     */
    private String roleName;
    /**
     * 角色编码(ROLE_前缀)
     */
    private String roleCode;
    /**
     * 状态:1-启用 0-禁用
     */
    private Integer status;
    /**
     * 创建时间
     */
    private LocalDateTime createTime;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

权限实体(SysPermission.java)

package cc.hep.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

/**
 * 系统权限实体(对应sys_permission表)
 */
@Data
@TableName("sys_permission")
public class SysPermission {
    /**
     * 主键ID
     */
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * 权限名称
     */
    private String permName;
    /**
     * 权限编码(如sys:user:list)
     */
    private String permCode;
    /**
     * 关联菜单路径
     */
    private String menuPath;
    /**
     * 创建时间
     */
    private LocalDateTime createTime;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

用户详情封装类(SysUserDetails.java)

核心类:实现Spring Security的UserDetails接口,封装用户的基础信息+角色+权限,作为Spring Security的用户身份载体,替代框架默认的User类(更贴合企业实际业务)。

package cc.hep.entity;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 自定义用户详情类(实现UserDetails,对接Spring Security)
 */
@Data
public class SysUserDetails implements UserDetails {
    /**
     * 数据库中的用户实体
     */
    private SysUser sysUser;
    /**
     * 用户拥有的角色编码(如ROLE_ADMIN)
     */
    private List<String> roleCodes;
    /**
     * 用户拥有的权限编码(如sys:user:list)
     */
    private List<String> permCodes;

    /**
     * 核心方法:返回用户的所有权限(包括角色+权限,Spring Security的权限判断依据)
     * 注意:角色需作为权限的一种,直接返回即可,框架会自动处理ROLE_前缀的角色判断
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 合并角色和权限,封装为SimpleGrantedAuthority
        List<SimpleGrantedAuthority> authorities = roleCodes.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        authorities.addAll(permCodes.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList()));
        return authorities;
    }

    /**
     * 核心方法:返回用户密码(Spring Security用于密码验证)
     */
    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }

    /**
     * 核心方法:返回用户名(Spring Security用于用户名匹配)
     */
    @Override
    public String getUsername() {
        return sysUser.getUsername();
    }

    /**
     * 核心方法:账户是否未过期(企业级可扩展,此处默认true)
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 核心方法:账户是否未被锁定(企业级可扩展,此处默认true)
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 核心方法:凭证是否未过期(企业级可扩展,此处默认true)
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 核心方法:账户是否启用(关联数据库的status字段:1-启用 0-禁用)
     */
    @Override
    public boolean isEnabled() {
        return sysUser.getStatus() == 1;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

核心说明:

  1. getAuthorities()是Spring Security权限判断的核心方法,必须返回用户的所有角色和权限,封装为SimpleGrantedAuthority;
  2. isEnabled()关联数据库的用户状态,实现禁用用户无法登录的企业级需求;
  3. 其余方法可根据企业业务扩展(如账户过期、锁定逻辑)。

# 7. 持久层开发(Mapper)

基于MyBatis-Plus实现,无需编写XML映射文件,只需继承BaseMapper接口,添加@Mapper注解(或在启动类加@MapperScan,企业级推荐后者,减少注解冗余)。

# 7.1 启动类和MybatisPlus配置

package cc.hep;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootSecurityApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootSecurityApplication.class, args);
	}
}
1
2
3
4
5
6
7
8
9
10
11
12

创建一个MybatisPlusConfig配置类,内容如下。

package cc.hep.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 你可以继续在此添加其他插件,例如:防全表更新插件
        // interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 7.2 编写Mapper接口

在cc.hep.mapper下创建4个Mapper接口,分别操作用户、角色、权限、中间表,继承BaseMapper即可实现基础CRUD,企业级复杂查询可通过MyBatis-Plus的条件构造器实现。

用户Mapper(SysUserMapper.java)

package cc.hep.mapper;

import cc.hep.entity.SysUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

/**
 * 用户持久层
 */
public interface SysUserMapper extends BaseMapper<SysUser> {
    /**
     * 根据用户名查询用户(登录核心方法)
     */
    SysUser selectByUsername(@Param("username") String username);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

角色Mapper(SysRoleMapper.java)

package cc.hep.mapper;

import cc.hep.entity.SysRole;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 角色持久层
 */
public interface SysRoleMapper extends BaseMapper<SysRole> {
    /**
     * 根据用户ID查询角色编码
     */
    List<String> selectRoleCodesByUserId(@Param("userId") Long userId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

权限Mapper(SysPermissionMapper.java)

package cc.hep.mapper;

import cc.hep.entity.SysPermission;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 权限持久层
 */
public interface SysPermissionMapper extends BaseMapper<SysPermission> {
    /**
     * 根据用户ID查询权限编码
     */
    List<String> selectPermCodesByUserId(@Param("userId") Long userId);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

通用说明

  1. 基础CRUD方法(增删改查)由BaseMapper自动提供,无需手动编写;
  2. 自定义方法(如根据用户名查用户、根据用户ID查角色)为登录和权限查询的核心方法,后续需编写XML映射文件实现SQL查询。

# 7.3 编写Mapper XML映射文件

在src/main/resources下创建mapper目录,按Mapper包结构创建子目录,编写XML映射文件实现自定义SQL,企业级复杂查询推荐XML方式,可读性更高。

创建目录结构

resources/
└── mapper/
    ├── SysUserMapper.xml
    ├── SysRoleMapper.xml
    └── SysPermissionMapper.xml
1
2
3
4
5

SysUserMapper.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="cc.hep.mapper.SysUserMapper">
    <!-- 根据用户名查询用户 -->
    <select id="selectByUsername" resultType="cc.hep.entity.SysUser">
        SELECT id, username, password, nickname, status, create_time, update_time
        FROM sys_user
        WHERE username = #{username}
    </select>
</mapper>
1
2
3
4
5
6
7
8
9
10

SysRoleMapper.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="cc.hep.mapper.SysRoleMapper">
    <!-- 根据用户ID查询角色编码 -->
    <select id="selectRoleCodesByUserId" resultType="java.lang.String">
        SELECT r.role_code
        FROM sys_user_role ur
        LEFT JOIN sys_role r ON ur.role_id = r.id
        WHERE ur.user_id = #{userId} AND r.status = 1
    </select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11

SysPermissionMapper.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="cc.hep.mapper.SysPermissionMapper">
    <!-- 根据用户ID查询权限编码 -->
    <select id="selectPermCodesByUserId" resultType="java.lang.String">
        SELECT p.perm_code
        FROM sys_user_role ur
        LEFT JOIN sys_role_permission rp ON ur.role_id = rp.role_id
        LEFT JOIN sys_permission p ON rp.perm_id = p.id
        WHERE ur.user_id = #{userId}
    </select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12

# 8. 业务层开发(Service)

企业级开发中业务层必须遵循接口+实现类的设计模式,分离业务定义和实现,便于后续扩展和测试,核心业务为用户信息查询、角色查询、权限查询(支撑Spring Security的认证和授权)。

# 8.1 用户业务层

在cc.hep.service下创建接口SysUserService,在cc.hep.service.impl下创建实现类SysUserServiceImpl。

接口(SysUserService.java)

package cc.hep.service;

import cc.hep.entity.SysUser;
import cc.hep.entity.SysUserDetails;
import com.baomidou.mybatisplus.extension.service.IService;

/**
 * 用户业务层接口(继承IService,MyBatis-Plus提供基础业务方法)
 */
public interface SysUserService extends IService<SysUser> {
    /**
     * 根据用户名查询用户详情(包含角色+权限,Spring Security登录核心方法)
     */
    SysUserDetails loadUserByUsername(String username);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

实现类(SysUserServiceImpl.java)

package cc.hep.service.impl;

import cc.hep.entity.SysUser;
import cc.hep.entity.SysUserDetails;
import cc.hep.mapper.SysPermissionMapper;
import cc.hep.mapper.SysRoleMapper;
import cc.hep.mapper.SysUserMapper;
import cc.hep.service.SysUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;

/**
 * 用户业务层实现类
 * 1. 继承ServiceImpl:MyBatis-Plus提供基础业务实现
 * 2. @RequiredArgsConstructor:Lombok注入依赖(替代@Autowired,企业级推荐)
 */
@Service
@RequiredArgsConstructor
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

    // 注入Mapper(Lombok的@RequiredArgsConstructor需变量为final)
    private final SysUserMapper sysUserMapper;
    private final SysRoleMapper sysRoleMapper;
    private final SysPermissionMapper sysPermissionMapper;

    /**
     * 根据用户名查询用户详情(核心方法,对接Spring Security)
     */
    @Override
    public SysUserDetails loadUserByUsername(String username) {
        // 1. 根据用户名查询用户基础信息
        SysUser sysUser = sysUserMapper.selectByUsername(username);
        if (Objects.isNull(sysUser)) {
            return null; // 用户名不存在,后续Spring Security会抛出认证异常
        }
        // 2. 根据用户ID查询角色编码
        List<String> roleCodes = sysRoleMapper.selectRoleCodesByUserId(sysUser.getId());
        // 3. 根据用户ID查询权限编码
        List<String> permCodes = sysPermissionMapper.selectPermCodesByUserId(sysUser.getId());
        // 4. 封装为自定义用户详情类
        SysUserDetails userDetails = new SysUserDetails();
        userDetails.setSysUser(sysUser);
        userDetails.setRoleCodes(roleCodes == null ? List.of() : roleCodes);
        userDetails.setPermCodes(permCodes == null ? List.of() : permCodes);
        return userDetails;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

核心说明:

  1. @RequiredArgsConstructor:Lombok注解,通过构造器注入依赖,替代@Autowired,符合Spring官方推荐的依赖注入方式,避免空指针;
  2. loadUserByUsername:对接Spring Security的核心方法,完成用户+角色+权限的联合查询,封装为自定义用户详情类;
  3. 对查询结果做非空处理,避免空指针异常(企业级代码健壮性要求)。

# 8.2 角色/权限业务层

若后续需要角色、权限的管理功能(如新增角色、分配权限),可按上述方式编写SysRoleService、SysPermissionService,本教程聚焦Spring Security集成,暂不实现具体管理功能。

# 9. Spring Security核心配置

Spring Security 6.x(SpringBoot3.5.10适配)废弃了传统的WebSecurityConfigurerAdapter,采用组件式配置(通过@Bean注入核心组件),核心配置包括:密码编码器、用户详情服务、安全过滤链、方法级权限开启,所有配置类放在cc.hep.config.security下。

# 9.1 密码编码器配置

Spring Security 6.x强制要求配置密码编码器,否则启动报错,框架提供多种加密方式,企业级生产环境首选BCryptPasswordEncoder(不可逆加密,支持盐值自动生成)。

package cc.hep.config.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * 密码编码器配置(Spring Security核心组件,必须配置)
 */
@Configuration
public class PasswordEncoderConfig {

    /**
     * 注入BCrypt密码编码器
     * 特点:不可逆加密、自动生成盐值、可配置加密强度(默认10)
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

说明:测试数据中的密码123456就是通过该编码器加密的,加密代码可通过测试类生成:

// 生成BCrypt加密密码
public static void main(String[] args) {
    PasswordEncoder encoder = new BCryptPasswordEncoder();
    String encode = encoder.encode("123456");
    System.out.println(encode); // 输出加密后的字符串
}
1
2
3
4
5
6

# 9.2 用户详情服务配置

实现Spring Security的UserDetailsService接口,对接自定义的SysUserService,完成数据库用户信息与Spring Security的对接,替代框架默认的内存用户。

package cc.hep.config.security;

import cc.hep.entity.SysUserDetails;
import cc.hep.service.SysUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

/**
 * 用户详情服务配置(对接数据库,替代Spring Security默认的内存用户)
 */
@Configuration
@RequiredArgsConstructor
public class UserDetailsServiceConfig {

    private final SysUserService sysUserService;

    /**
     * 注入自定义UserDetailsService
     */
    @Bean
    public UserDetailsService userDetailsService() {
        // 函数式编程实现接口,根据用户名查询用户详情
        return username -> {
            SysUserDetails userDetails = sysUserService.loadUserByUsername(username);
            if (userDetails == null) {
                // 用户名不存在,抛出Spring Security标准异常,后续由异常处理器处理
                throw new UsernameNotFoundException("用户名或密码错误");
            }
            return userDetails;
        };
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

核心说明:

  1. 采用函数式编程实现UserDetailsService接口,简化代码;
  2. 用户名不存在时抛出UsernameNotFoundException(Spring Security标准异常),后续通过自定义异常处理器返回统一JSON响应。

# 9.3 自定义认证/授权失败处理器

企业级前后端分离项目中,禁止返回框架默认的页面/字符串,需自定义处理器,将认证失败(如用户名密码错误)、授权失败(如权限不足)的结果封装为统一的JSON响应,放在cc.hep.handler下。

认证失败处理器(AuthFailureHandler.java)

package cc.hep.handler;

import cc.hep.vo.Result;
import cc.hep.vo.ResultCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.PrintWriter;

/**
 * 自定义未认证处理器(双接口实现)
 * 1. AuthenticationEntryPoint:处理未登录/令牌过期的未认证请求(核心,解决类型不匹配)
 * 2. AuthenticationFailureHandler:处理登录时的账号密码错误/账号禁用等认证失败
 */
@Component
@RequiredArgsConstructor
public class AuthFailureHandler implements AuthenticationEntryPoint, AuthenticationFailureHandler {

    private final ObjectMapper objectMapper; // Spring默认的JSON序列化工具

    /**
     * 实现AuthenticationEntryPoint:处理未登录/未携带令牌的请求(核心重写方法)
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        this.writeErrorResponse(response, ResultCode.UNAUTHORIZED);
    }

    /**
     * 实现AuthenticationFailureHandler:处理登录时的认证失败(如账号密码错、账号禁用)
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        this.writeErrorResponse(response, ResultCode.UNAUTHORIZED);
    }

    /**
     * 抽离公共的错误响应写入方法,避免代码冗余
     */
    private void writeErrorResponse(HttpServletResponse response, ResultCode resultCode) throws IOException {
        // 1. 设置响应头(JSON格式、UTF-8、跨域)
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        // 2. 封装失败结果
        Result<Void> fail = Result.fail(resultCode);
        // 3. 将JSON结果写入响应体
        PrintWriter writer = response.getWriter();
        writer.write(objectMapper.writeValueAsString(fail));
        writer.flush();
        writer.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

授权失败处理器(AuthAccessDeniedHandler.java)

package cc.hep.handler;

import cc.hep.vo.Result;
import cc.hep.vo.ResultCode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.PrintWriter;

/**
 * 自定义授权失败处理器(权限不足)
 */
@Component
@RequiredArgsConstructor
public class AuthAccessDeniedHandler implements AccessDeniedHandler {

    private final ObjectMapper objectMapper;

    /**
     * 授权失败时执行
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 1. 设置响应头
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        response.setHeader("Access-Control-Allow-Origin", "*");
        // 2. 封装失败结果
        Result<Void> fail = Result.fail(ResultCode.FORBIDDEN);
        // 3. 写入响应体
        PrintWriter writer = response.getWriter();
        writer.write(objectMapper.writeValueAsString(fail));
        writer.flush();
        writer.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 9.4 安全过滤链配置

核心配置类,替代传统的WebSecurityConfigurerAdapter,通过SecurityFilterChain配置认证规则、授权规则、登录/登出、异常处理器、跨域、CSRF等企业级必备功能,前后端分离项目需禁用表单登录、开启HTTP Basic、禁用CSRF。

package cc.hep.config.security;

import cc.hep.handler.AuthAccessDeniedHandler;
import cc.hep.handler.AuthFailureHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

/**
 * Spring Security核心过滤链配置(6.x修正版)
 * 解决:1. CORS enable()方法废弃 2. 认证入口点类型不匹配
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启方法级权限,支持@PreAuthorize/@PostAuthorize等注解
@RequiredArgsConstructor
public class SecurityFilterChainConfig {

    // 注入自定义组件
    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;
    private final AuthFailureHandler authFailureHandler; // 已实现双接口,可直接注入
    private final AuthAccessDeniedHandler authAccessDeniedHandler; // 授权失败处理器

    /**
     * 配置认证提供者(对接UserDetailsService和PasswordEncoder)
     */
    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService); // 自定义用户详情服务
        provider.setPasswordEncoder(passwordEncoder); // 密码编码器
        return provider;
    }

    /**
     * 配置认证管理器(Spring Security 6.x必须手动注入)
     */
    @Bean
    public AuthenticationManager authenticationManager() {
        return new ProviderManager(authenticationProvider());
    }

    /**
     * 【新增核心Bean】跨域配置源(解决6.x cors.enable()废弃问题)
     * 与全局CorsConfig规则保持一致,无需重复配置
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        // 允许的跨域源(生产环境指定具体域名,如http://localhost:8081)
        config.setAllowedOriginPatterns(List.of("*"));
        // 允许的请求方法
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        // 允许的请求头
        config.setAllowedHeaders(List.of("*"));
        // 允许携带Cookie/令牌
        config.setAllowCredentials(true);
        // 预检请求缓存时间(1小时,避免频繁OPTIONS请求)
        config.setMaxAge(3600L);
        // 应用到所有接口
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }

    /**
     * 核心:配置Security过滤链(6.x修正版)
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                // 1. 禁用CSRF(前后端分离项目,无会话,CSRF无意义)
                .csrf(csrf -> csrf.disable())
                // 2. 配置CORS(6.x新写法:指定跨域配置源,替代原enable())
                .cors(cors -> cors.configurationSource(corsConfigurationSource()))
                // 3. 禁用表单登录(前后端分离,使用JSON接口登录)
                .formLogin(form -> form.disable())
                // 4. 配置异常处理器(已修正,类型匹配)
                .exceptionHandling(ex -> {
                    // 未认证处理器(实现了AuthenticationEntryPoint接口,解决类型不匹配)
                    ex.authenticationEntryPoint(authFailureHandler);
                    // 授权失败处理器(权限不足)
                    ex.accessDeniedHandler(authAccessDeniedHandler);
                })
                // 5. 无状态会话(前后端分离企业级必备,服务器不存储会话)
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 6. 配置授权规则(URL级别的权限控制)
                .authorizeHttpRequests(auth -> auth
                        // 匿名可访问的接口(登录、登出、测试接口)
                        .requestMatchers("/hello", "/api/auth/**").permitAll()
                        // 所有其他接口必须认证后才能访问
                        .anyRequest().authenticated()
                )
                // 7. 自定义登出配置
                .logout(logout -> logout
                        .logoutUrl("/api/auth/logout") // 自定义登出接口
                        .invalidateHttpSession(true) // 销毁会话
                        .clearAuthentication(true) // 清除认证信息
                        .permitAll() // 登出接口匿名可访问
                )
                .build();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119

企业级核心配置说明:

  1. @EnableMethodSecurity:Spring Security 6.x替代@EnableGlobalMethodSecurity,开启方法级权限控制,支持@PreAuthorize(方法执行前鉴权)、@PostAuthorize(方法执行后鉴权)等注解;
  2. SessionCreationPolicy.STATELESS:无状态会话,前后端分离项目核心配置,服务器不存储会话,符合RESTful规范;
  3. csrf.disable():前后端分离项目无Cookie会话,CSRF攻击无意义,必须禁用;
  4. 授权规则:permitAll()指定匿名可访问的接口(如登录、登出),anyRequest().authenticated()表示所有其他接口必须登录后才能访问;
  5. 异常处理器:将认证/授权失败的处理交给自定义处理器,返回统一JSON响应。

# 10. 跨域配置

SpringBoot+Spring Security的跨域需要双重配置:SpringMVC的跨域配置+Spring Security的跨域开启(已在SecurityFilterChain中配置cors.enable()),在cc.hep.config.web下创建CorsConfig.java:

package cc.hep.config.web;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 全局跨域配置(SpringMVC)
 * 配合Spring Security的cors.enable(),实现完整跨域支持
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    /**
     * 配置跨域规则
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") // 所有接口允许跨域
                .allowedOriginPatterns("*") // 允许所有源(生产环境指定具体域名,如http://localhost:8081)
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的请求方法
                .allowedHeaders("*") // 允许的请求头
                .allowCredentials(true) // 允许携带Cookie
                .maxAge(3600); // 预检请求缓存时间(1小时)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

生产环境注意:allowedOriginPatterns需指定具体的前端域名(如http://www.hep.cc),禁止使用*,避免跨域安全风险。

# 11. 开发测试接口与自定义登录/登出接口

基础测试接口(HelloController.java)

在cc.hep.controller下创建基础接口,测试匿名访问、认证访问、权限控制:

package cc.hep.controller;

import cc.hep.vo.Result;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 基础测试接口
 */
@RestController
public class HelloController {

    /**
     * 匿名可访问(配置在SecurityFilterChain中)
     */
    @GetMapping("/hello")
    public Result<String> hello() {
        return Result.success("Hello Spring Security!");
    }

    /**
     * 需认证后访问(无权限限制,只要登录即可)
     */
    @GetMapping("/api/user/info")
    public Result<String> userInfo() {
        return Result.success("这是用户基础信息,需登录后访问");
    }

    /**
     * 方法级权限控制:需要ROLE_ADMIN角色
     * @PreAuthorize:方法执行前鉴权,hasRole自动拼接ROLE_前缀
     */
    @GetMapping("/api/sys/role/list")
    @PreAuthorize("hasRole('ADMIN')")
    public Result<String> roleList() {
        return Result.success("这是角色列表,仅管理员可访问");
    }

    /**
     * 方法级权限控制:需要sys:user:add权限
     * hasAuthority:直接匹配权限编码,不拼接前缀
     */
    @GetMapping("/api/sys/user/add")
    @PreAuthorize("hasAuthority('sys:user:add')")
    public Result<String> addUser() {
        return Result.success("新增用户,需sys:user:add权限");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

权限注解说明:

  1. @PreAuthorize("hasRole('ADMIN')"):判断用户是否拥有ROLE_ADMIN角色,框架自动拼接ROLE_前缀;
  2. @PreAuthorize("hasAuthority('sys:user:add')"):判断用户是否拥有指定的权限编码,直接匹配,无前缀;
  3. 可组合使用:如@PreAuthorize("hasRole('ADMIN') and hasAuthority('sys:user:add')")。

自定义登录/登出接口(AuthController.java)

前后端分离项目中,使用JSON接口替代框架默认的表单登录,在cc.hep.controller下创建AuthController,实现基于用户名密码的登录、登出:

package cc.hep.controller;

import cc.hep.vo.Result;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 自定义登录/登出接口(前后端分离)
 */
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

    /**
     * 自定义登录接口(Spring Security会自动拦截处理用户名密码验证)
     * 前端传参:username/password(JSON格式)
     */
    @PostMapping("/login")
    public Result<String> login() {
        // 若执行到此处,说明认证成功
        return Result.success("登录成功");
    }

    /**
     * 自定义登出接口
     */
    @PostMapping("/logout")
    public Result<String> logout(HttpServletRequest request, HttpServletResponse response) {
        // 获取当前认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            // 清除认证信息、销毁会话
            new SecurityContextLogoutHandler().logout(request, response, authentication);
        }
        return Result.success("登出成功");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

说明:Spring Security会自动拦截/api/auth/login接口,验证请求体中的username和password,验证成功后执行接口逻辑,失败则触发自定义的AuthFailureHandler。

# 12. 项目启动与测试

# 12.1 启动项目

直接运行启动类SpringBootSecurityApplication.java的main方法,若配置无误,控制台会输出启动成功日志,服务运行在8080端口。

# 12.2 接口测试

使用PostMan或Curl工具测试接口,验证匿名访问、认证、角色权限、授权失败等场景,测试数据:

  • 管理员账号:admin/123456(拥有ROLE_ADMIN和所有权限)
  • 普通用户账号:user/123456(拥有ROLE_USER和sys:user:list权限)

匿名访问测试

请求GET http://localhost:8080/hello,返回:

{
    "code": 200,
    "msg": "操作成功",
    "data": "Hello Spring Security!"
}
1
2
3
4
5

未登录访问需认证接口

请求GET http://localhost:8080/api/user/info,返回(自定义认证失败响应):

{
    "code": 401,
    "msg": "未登录或登录已过期",
    "data": null
}
1
2
3
4
5

管理员登录测试

  1. 发送POST http://localhost:8080/api/auth/login,请求体为JSON:
{
    "username": "admin",
    "password": "123456"
}
1
2
3
4

返回{"code":200,"msg":"操作成功","data":"登录成功"}; 2. 携带认证信息访问/api/sys/role/list(管理员专属),返回成功; 3. 携带认证信息访问/api/sys/user/add(拥有sys:user:add权限),返回成功。

普通用户权限不足测试

  1. 普通用户登录后访问/api/sys/role/list(需要ROLE_ADMIN),返回(自定义授权失败响应):
{
    "code": 403,
    "msg": "权限不足,无法访问",
    "data": null
}
1
2
3
4
5
  1. 普通用户访问/api/sys/user/add(无该权限),同样返回403。

登出测试

发送POST http://localhost:8080/api/auth/logout,返回{"code":200,"msg":"操作成功","data":"登出成功"},登出后再次访问需认证接口会返回401。

# 13. 企业级功能扩展

本教程实现了Spring Security的核心集成,企业生产环境中还需扩展以下功能,贴合实际业务:

Token认证(替代基础认证)

前后端分离项目中,基础认证(用户名密码放在请求头)安全性低,需替换为JWT Token认证,步骤:

  1. 引入JWT依赖(jjwt);
  2. 编写JWT工具类(生成Token、解析Token、验证Token);
  3. 自定义过滤器(JwtAuthenticationFilter),在Security过滤链中添加,实现Token解析和认证;
  4. 禁用HTTP Basic,登录成功后生成Token返回给前端,前端后续请求携带Token在请求头。

动态权限配置

本教程为静态权限(代码/注解配置),企业生产环境中需实现动态权限(权限配置在数据库,支持后台配置,无需重启服务),步骤:

  1. 新增权限-接口映射表(配置接口URL与权限编码的关联);
  2. 自定义权限过滤器,每次请求时从数据库查询接口所需权限,与当前用户权限对比;
  3. 实现权限缓存(Redis),提高性能,避免每次请求查询数据库。

集成Redis缓存

企业级项目中需引入Redis,缓存用户信息、角色权限、接口权限,减少数据库查询,提高性能:

  1. 引入Spring Data Redis依赖;
  2. 配置Redis连接;
  3. 对loadUserByUsername等核心方法添加缓存注解(@Cacheable);
  4. 权限更新时清除对应缓存。

全局异常处理

在cc.hep.exception下创建全局异常处理器(@RestControllerAdvice),捕获项目中所有异常(包括业务异常、系统异常),返回统一JSON响应,替代零散的异常处理。

接口限流/防刷

生产环境中需对接口做限流/防刷,避免恶意请求,可通过Spring Cloud Gateway或自定义过滤器实现,结合Redis记录请求次数。

密码安全强化

  1. 增加密码复杂度校验(如长度、包含大小写/数字/特殊字符);
  2. 实现密码过期策略(如90天过期,强制修改);
  3. 实现连续输错密码锁定账户(如连续5次输错,锁定10分钟)。

# 14. 常见问题与解决方案

项目启动报错:No PasswordEncoder found for id "null"

原因:未配置密码编码器或Spring Security未找到密码编码器; 解决方案:确保配置了PasswordEncoder的@Bean,且在DaoAuthenticationProvider中设置了passwordEncoder。

用户名密码正确但提示认证失败

原因:1. 密码未加密或加密方式与密码编码器不一致;2. 用户状态为禁用(status=0);3. Mapper查询方法返回null; 解决方案:1. 确保数据库密码为BCrypt加密后的字符串;2. 检查用户状态是否为1;3. 调试loadUserByUsername方法,确认查询结果非空。

跨域请求失败

原因:1. 未配置SpringMVC的跨域配置;2. Spring Security未开启cors;3. 前端请求头未配置; 解决方案:确保配置了CorsConfig,且在SecurityFilterChain中开启cors.enable()。

方法级权限注解@PreAuthorize无效

原因:未开启@EnableMethodSecurity注解; 解决方案:在Security配置类上添加@EnableMethodSecurity(Spring Security 6.x)。

数据库查询返回null,报空指针

原因:未对Mapper查询结果做非空处理; 解决方案:在Service层对所有查询结果做非空判断,如roleCodes == null ? List.of() : roleCodes。

# 总结

本教程从零开始基于IDEA搭建了SpringBoot3.5.10+Spring Security6.x的企业级权限项目,核心要点如下:

  1. 严格按企业级规范设计项目结构、数据库模型、分层代码,摒弃简易Demo的单一结构;
  2. 实现了用户-角色-权限三级企业级权限模型,数据库存储权限信息,替代框架默认的内存用户;
  3. 采用Spring Security6.x的组件式配置(替代废弃的WebSecurityConfigurerAdapter),包括密码编码器、用户详情服务、安全过滤链;
  4. 实现了统一JSON响应、自定义认证/授权失败处理器、跨域配置、方法级权限控制等企业级必备功能;
  5. 完成了接口测试,验证了匿名访问、认证、角色权限、授权失败等核心场景,确保集成效果;
  6. 给出了生产环境的功能扩展方案(JWT Token、动态权限、Redis缓存等)和常见问题解决方案,贴合企业实际开发。

本项目可直接作为企业级Spring Security权限管理的基础框架,在此基础上扩展JWT、动态权限、Redis等功能后,即可投入生产环境使用。

最近更新: 2026/2/4
 |  Copyright © -Present Kelsen All Rights Reserved. | Author KelsenJing