# 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父工程管理的最兼容最新版本,无需手动指定版本,避免版本冲突。
开发环境确认
- IDEA 2023 Ultimate版
- JDK 17(SpringBoot3.x强制要求JDK17+)
- SpringBoot 3.5.10
- Maven 3.9.4
- 数据库:MySQL 8.0+(企业级常用版本)
- Spring Security:由SpringBoot 3.5.10父工程自动适配的兼容版本(6.3.x)
# 1. 创建SpringBoot项目

# 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依赖配置
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>
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
依赖说明
- HikariCP:SpringBoot默认连接池,企业级生产环境首选,无需额外配置,自动生效;
- mybatis-plus-boot-starter:简化数据库CRUD操作,无需编写XML映射文件(基础操作);
- mysql-connector-j:SpringBoot3+官方推荐的MySQL驱动,替代原
mysql-connector-java; - 所有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); -- 普通用户拥有查询用户列表权限
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' # 控制台日志格式
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
配置说明
- HikariCP连接池:
spring.datasource.hikari下的参数为企业级生产环境常用配置,可根据服务器性能调整; - MyBatis-Plus:
map-underscore-to-camel-case: true开启下划线转驼峰,避免数据库字段与实体类属性手动映射; - 日志配置:开发环境开启SQL日志和项目包debug日志,生产环境需将
cc.hep的日志级别改为info或warn,并关闭MyBatis-Plus的SQL日志; - 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;
}
}
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);
}
}
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;
}
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;
}
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;
}
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;
}
}
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
核心说明:
getAuthorities()是Spring Security权限判断的核心方法,必须返回用户的所有角色和权限,封装为SimpleGrantedAuthority;isEnabled()关联数据库的用户状态,实现禁用用户无法登录的企业级需求;- 其余方法可根据企业业务扩展(如账户过期、锁定逻辑)。
# 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);
}
}
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;
}
}
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);
}
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);
}
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
通用说明
- 基础CRUD方法(增删改查)由
BaseMapper自动提供,无需手动编写; - 自定义方法(如根据用户名查用户、根据用户ID查角色)为登录和权限查询的核心方法,后续需编写XML映射文件实现SQL查询。
# 7.3 编写Mapper XML映射文件
在src/main/resources下创建mapper目录,按Mapper包结构创建子目录,编写XML映射文件实现自定义SQL,企业级复杂查询推荐XML方式,可读性更高。
创建目录结构
resources/
└── mapper/
├── SysUserMapper.xml
├── SysRoleMapper.xml
└── SysPermissionMapper.xml
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>
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>
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>
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);
}
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;
}
}
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
核心说明:
@RequiredArgsConstructor:Lombok注解,通过构造器注入依赖,替代@Autowired,符合Spring官方推荐的依赖注入方式,避免空指针;loadUserByUsername:对接Spring Security的核心方法,完成用户+角色+权限的联合查询,封装为自定义用户详情类;- 对查询结果做非空处理,避免空指针异常(企业级代码健壮性要求)。
# 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();
}
}
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); // 输出加密后的字符串
}
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;
};
}
}
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
核心说明:
- 采用函数式编程实现
UserDetailsService接口,简化代码; - 用户名不存在时抛出
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();
}
}
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();
}
}
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();
}
}
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
企业级核心配置说明:
@EnableMethodSecurity:Spring Security 6.x替代@EnableGlobalMethodSecurity,开启方法级权限控制,支持@PreAuthorize(方法执行前鉴权)、@PostAuthorize(方法执行后鉴权)等注解;SessionCreationPolicy.STATELESS:无状态会话,前后端分离项目核心配置,服务器不存储会话,符合RESTful规范;csrf.disable():前后端分离项目无Cookie会话,CSRF攻击无意义,必须禁用;- 授权规则:
permitAll()指定匿名可访问的接口(如登录、登出),anyRequest().authenticated()表示所有其他接口必须登录后才能访问; - 异常处理器:将认证/授权失败的处理交给自定义处理器,返回统一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小时)
}
}
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权限");
}
}
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
权限注解说明:
@PreAuthorize("hasRole('ADMIN')"):判断用户是否拥有ROLE_ADMIN角色,框架自动拼接ROLE_前缀;@PreAuthorize("hasAuthority('sys:user:add')"):判断用户是否拥有指定的权限编码,直接匹配,无前缀;- 可组合使用:如
@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("登出成功");
}
}
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!"
}
2
3
4
5
未登录访问需认证接口
请求GET http://localhost:8080/api/user/info,返回(自定义认证失败响应):
{
"code": 401,
"msg": "未登录或登录已过期",
"data": null
}
2
3
4
5
管理员登录测试
- 发送
POST http://localhost:8080/api/auth/login,请求体为JSON:
{
"username": "admin",
"password": "123456"
}
2
3
4
返回{"code":200,"msg":"操作成功","data":"登录成功"};
2. 携带认证信息访问/api/sys/role/list(管理员专属),返回成功;
3. 携带认证信息访问/api/sys/user/add(拥有sys:user:add权限),返回成功。
普通用户权限不足测试
- 普通用户登录后访问
/api/sys/role/list(需要ROLE_ADMIN),返回(自定义授权失败响应):
{
"code": 403,
"msg": "权限不足,无法访问",
"data": null
}
2
3
4
5
- 普通用户访问
/api/sys/user/add(无该权限),同样返回403。
登出测试
发送POST http://localhost:8080/api/auth/logout,返回{"code":200,"msg":"操作成功","data":"登出成功"},登出后再次访问需认证接口会返回401。
# 13. 企业级功能扩展
本教程实现了Spring Security的核心集成,企业生产环境中还需扩展以下功能,贴合实际业务:
Token认证(替代基础认证)
前后端分离项目中,基础认证(用户名密码放在请求头)安全性低,需替换为JWT Token认证,步骤:
- 引入JWT依赖(jjwt);
- 编写JWT工具类(生成Token、解析Token、验证Token);
- 自定义过滤器(JwtAuthenticationFilter),在Security过滤链中添加,实现Token解析和认证;
- 禁用HTTP Basic,登录成功后生成Token返回给前端,前端后续请求携带Token在请求头。
动态权限配置
本教程为静态权限(代码/注解配置),企业生产环境中需实现动态权限(权限配置在数据库,支持后台配置,无需重启服务),步骤:
- 新增权限-接口映射表(配置接口URL与权限编码的关联);
- 自定义权限过滤器,每次请求时从数据库查询接口所需权限,与当前用户权限对比;
- 实现权限缓存(Redis),提高性能,避免每次请求查询数据库。
集成Redis缓存
企业级项目中需引入Redis,缓存用户信息、角色权限、接口权限,减少数据库查询,提高性能:
- 引入Spring Data Redis依赖;
- 配置Redis连接;
- 对
loadUserByUsername等核心方法添加缓存注解(@Cacheable); - 权限更新时清除对应缓存。
全局异常处理
在cc.hep.exception下创建全局异常处理器(@RestControllerAdvice),捕获项目中所有异常(包括业务异常、系统异常),返回统一JSON响应,替代零散的异常处理。
接口限流/防刷
生产环境中需对接口做限流/防刷,避免恶意请求,可通过Spring Cloud Gateway或自定义过滤器实现,结合Redis记录请求次数。
密码安全强化
- 增加密码复杂度校验(如长度、包含大小写/数字/特殊字符);
- 实现密码过期策略(如90天过期,强制修改);
- 实现连续输错密码锁定账户(如连续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的企业级权限项目,核心要点如下:
- 严格按企业级规范设计项目结构、数据库模型、分层代码,摒弃简易Demo的单一结构;
- 实现了用户-角色-权限三级企业级权限模型,数据库存储权限信息,替代框架默认的内存用户;
- 采用Spring Security6.x的组件式配置(替代废弃的WebSecurityConfigurerAdapter),包括密码编码器、用户详情服务、安全过滤链;
- 实现了统一JSON响应、自定义认证/授权失败处理器、跨域配置、方法级权限控制等企业级必备功能;
- 完成了接口测试,验证了匿名访问、认证、角色权限、授权失败等核心场景,确保集成效果;
- 给出了生产环境的功能扩展方案(JWT Token、动态权限、Redis缓存等)和常见问题解决方案,贴合企业实际开发。
本项目可直接作为企业级Spring Security权限管理的基础框架,在此基础上扩展JWT、动态权限、Redis等功能后,即可投入生产环境使用。