阶段二:夯实有状态认证底座(基于 Spring Security 6 + Redis Session 的前后端分离认证授权实践)
承接上一阶段传统 Session 表单登录基础实践,本阶段将深度拆解Session 有状态认证核心原理,完成 Spring Security 6 + Redis 分布式 Session 的企业级落地,夯实传统有状态认证技术底座;同时针对分布式、前后端分离架构下原生 Session 的适配痛点,给出标准化优化方案与工程化实现思路。
一、Session 有状态认证机制:
1、Session 本质与核心特征(有状态认证):
有状态认证是指服务端需要持久化维护用户的登录上下文信息,包括用户身份、登录时长、会话状态等,并通过唯一的 SessionID 与客户端建立绑定关系。服务端主动 “记住” 用户状态,客户端每次请求时,服务端均需根据 SessionID 检索内存中的会话数据,才能完成身份合法性校验。
2、传统 Session 标准工作流程:
在传统 Web 开发中,Session 是服务端存储用户登录身份、会话状态的核心组件,且强依赖 Cookie实现身份识别:
- 会话创建:用户首次访问,服务端创建 Session 并生成全局唯一 SESSIONID;
- 身份下发:服务端通过响应头将 SESSIONID 写入客户端 Cookie,由浏览器自动存储;
- 请求携带:客户端后续所有请求,自动在请求头携带包含 SESSIONID 的 Cookie;
- 身份校验:服务端通过 SESSIONID 匹配本地内存中的 Session 数据,完成用户身份识别与权限校验。
3、Session 机制的适用场景与原生缺陷:
(1)、适用场景:
Session 是传统单体架构的经典认证方案,在单实例、同域部署场景下具备实现简单、集成成本低的优势,适用于对会话管控、安全性、实时性要求较高的业务:
- 后台管理系统、企业内部系统;
- 需实时监控在线状态、支持主动登出 / 强制下线的应用;
- 严格权限校验、会话监听的电商 / 政务类 Web 应用。
(2)、原生 Session 核心缺陷:
在分布式集群、前后端分离等现代架构下,原生 Session 存在三大致命问题:
- 多服务器隔离,Session 存储在本地内存无法共享,导致集群部署下登录状态丢失;
- 服务重启后内存 Session 清空,会话失效;
- 前后端分离场景下,Cookie 无法跨域携带,认证失效。
4、Session 机制的优化方案:
在保留有状态认证核心的前提下,针对原生缺陷采用以下标准化优化方案,适配现代架构:

(1)、Redis 分布式 Session 共享:
将内存 Session 迁移至 Redis 集中式存储,多服务实例共享同一套会话数据,彻底解决集群状态不一致问题,是企业级单体集群的首选方案。
核心逻辑:登录生成 SessionID 存入 Redis,后续请求从 Redis 校验会话,实现跨实例共享。
(2)、Session 持久化与过期策略:
配置标准化 Session 过期时长,结合 Redis 持久化机制,避免服务重启导致会话失效,提升系统可用性。
(3)、跨域 Cookie 适配:
配置全局 CORS 跨域规则,开启凭证携带(withCredentials),优化 Cookie 安全属性(HttpOnly/Secure/SameSite),实现前后端分离场景下的跨域认证。
二、本阶段核心任务:
本阶段承接传统 Session 表单登录实践,以 Redis 分布式 Session 共享落地为核心,基于 Spring Security 6 完成有状态认证的企业级优化,解决原生 Session 的持久化、跨域痛点;同时沉淀数据库用户体系、动态权限、全局异常处理等可复用能力,全程不引入微服务、无状态相关组件,所有任务围绕单体有状态认证闭环设计,核心任务如下:
1、Redis 分布式 Session 基础配置:
集成 Spring Session + Redis,完成 Redis 连接配置、Session 序列化优化、Session Key 命名空间配置,将原生内存 Session 迁移至 Redis 集中式存储,实现 Session 跨服务实例共享。
2、Spring Security 6 核心配置优化:
基于 Spring Security 6 完善有状态认证基础配置,实现 BCrypt 密码加密、CustomUserDetails 自定义用户详情、UserDetailsService 业务落地,对接数据库用户 / 角色体系,替代内存用户,构建企业级用户认证基础能力。
3、跨域 Cookie 适配配置:
配置 CORS 跨域规则,实现前后端分离场景下 Cookie 的跨域携带,解决原生 Session 跨域认证失效问题;同时优化 Cookie 安全属性(HttpOnly、SameSite、过期时间),从基础层面规避 XSS、CSRF 安全风险。
4、会话管理企业级优化:
配置 Session 全局过期策略,实现会话并发控制、会话过期处理、主动登出 / 会话失效逻辑,结合 Redis Session 完成会话状态的实时管控,彻底解决服务重启会话丢失问题。
5、动态权限规则落地:
基于 Ant 路径匹配实现 URL - 角色动态权限配置,将权限规则抽离至配置文件,实现权限规则与业务代码解耦;开发动态授权管理器,完成请求 URI 与用户角色的自动校验,同时配置接口访问白名单。
6、全局异常与统一响应封装:
实现 Spring Security 全链路异常处理,统一返回 JSON 格式响应,替换框架默认的页面跳转方式;封装全局统一响应体,实现前端接口返回格式标准化,避免敏感异常信息暴露。
7、核心工具类与上下文封装:
开发用户上下文工具类,支持快速获取当前登录用户信息、角色、会话 ID;封装 JSON 响应工具类,适配过滤器、异常处理器等非 Controller 场景的 JSON 数据输出,减少代码冗余。
三、Session 有状态认证相关实践:
1、数据库设计:
采用 MySQL 数据库存储用户身份、角色及关联关系数据,基于用户 - 角色多对多关联模型设计数据表,通过用户-角色关联表实现用户与角色的动态绑定,支撑基于角色的权限校验体系。
本方案共设计 3 张核心数据表,分别为用户表、角色表、用户角色关联表,具体设计如下:
(1)、建表SQL:
-- 创建数据库 CREATE DATABASE IF NOT EXISTS security_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE security_db; -- 1. 用户表(sys_user):存储登录账号、密码等基础信息 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID(自增)', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录用户名(唯一)', password VARCHAR(100) NOT NULL COMMENT '密码(BCrypt加密存储)', nickname VARCHAR(50) DEFAULT '用户昵称' COMMENT '用户昵称', status TINYINT DEFAULT 1 COMMENT '账号状态:1-正常,0-禁用', create_by BIGINT DEFAULT 0 COMMENT '创建人ID(0表示系统)', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_by BIGINT DEFAULT 0 COMMENT '更新人ID', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', remark VARCHAR(200) DEFAULT '' COMMENT '备注' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '用户表'; -- 2. 角色表(sys_role):存储角色信息(如管理员、普通用户) CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID(自增)', role_code VARCHAR(50) NOT NULL UNIQUE COMMENT '角色编码(如ADMIN/USER,Spring Security识别时自动拼接ROLE_)', role_name VARCHAR(50) NOT NULL COMMENT '角色名称(如管理员/普通用户)', status TINYINT DEFAULT 1 COMMENT '1-启用,0-禁用', create_by BIGINT DEFAULT 0 COMMENT '创建人ID', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_by BIGINT DEFAULT 0 COMMENT '更新人ID', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', remark VARCHAR(200) DEFAULT '' COMMENT '备注' ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '角色表'; -- 3. 用户角色关联表(sys_user_role):用户-角色多对多关联 CREATE TABLE IF NOT EXISTS sys_user_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '关联ID(自增)', user_id BIGINT NOT NULL COMMENT '用户ID(关联sys_user.id)', role_id BIGINT NOT NULL COMMENT '角色ID(关联sys_role.id)', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', -- 外键约束:限制级联删除,需先删除关联数据 CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE RESTRICT, CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE RESTRICT, -- 唯一索引:防止同一用户重复绑定同一角色 CONSTRAINT uk_user_role UNIQUE (user_id, role_id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT = '用户角色关联表';
(2)、初始化数据:
-- 插入角色数据 INSERT INTO sys_role (role_code, role_name, remark) VALUES ('ADMIN', '管理员', '系统最高权限角色'), ('USER', '普通用户', '仅可访问普通业务接口'); -- 插入用户数据 -- 密码原文:123456,使用BCrypt加密 INSERT INTO sys_user (username, password, nickname) VALUES ('admin', '$2a$10$TOZxZHYaxfiWXM5doIZw7.9ZuyAwDXHmZssP5VBaPD46uMUg/AYSG', '系统管理员'), ('user', '$2a$10$TOZxZHYaxfiWXM5doIZw7.9ZuyAwDXHmZssP5VBaPD46uMUg/AYSG', '普通用户'); -- 用户与角色关联绑定 INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1), -- admin 用户绑定 管理员角色 (2, 2); -- user 用户绑定 普通用户角色
(3)、数据说明:
|
用户名 |
密码 |
角色 |
说明 |
|
admin |
123456 |
ADMIN |
管理员,可访问所有接口 |
|
user |
123456 |
USER |
普通用户,可访问用户级别接口 |
注意:密码已使用BCrypt加密,实际存储的是加密后的密文。
2、项目结构:

|
技术 |
版本 |
|
Java |
17 |
|
Spring Boot |
3.2.2 |
|
Spring Security |
6.2.1 |
|
MyBatis-Plus |
3.5.5 |
|
Redis |
- |
|
MySQL |
8.x |
|
Lombok |
1.18.30 |
|
FastJSON2 |
2.0.32 |
(1)、Redis-Key 结构说明表:
|
Key 前缀 / 完整格式 |
作用说明 |
过期时间 (TTL) |
|
spring:session:sessions:{sessionId} |
核心会话数据。存储具体的 Session 对象(包含用户认证信息 SecurityContext、创建时间、最后访问时间等),是实现分布式共享的关键。 |
与配置一致(默认 1800 秒),每次访问会自动刷新(滑动过期)。 |
|
spring:session:sessions:expires:{sessionId} |
过期触发键。这是一个 “空” 键,仅用于利用 Redis 的键空间通知感知 Session 何时过期,以便执行清理逻辑。 |
与配置一致(默认 1800 秒)。 |
|
spring:session:expirations:{timestamp} |
过期索引集。一个 Set 集合,存储了在该时间戳过期的所有 expires:{sessionId} 键,用于后台任务批量清理过期 Session。 |
动态变化,通常保留一段时间以确保清理任务能扫描到。 |
|
spring:session:index: org.springframework.session.FindByIndexNameSessionRepository .PRINCIPAL_NAME_INDEX_NAME:{username} |
用户索引键。一个 Set 集合,存储了该用户名下所有活跃的 sessionId,用于支持单点登录和按用户名查找 Session。 |
只要该用户有活跃 Session,此键就一直存在;当最后一个 Session 销毁时,此键删除。 |
3、用户登录认证流程:

4、请求访问控制流程:

5、单点登录控制流程:

6、登出流程:

7、Maven依赖:
<properties>
<java.version>17</java.version>
<spring-boot.version>3.2.2</spring-boot.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Redis + Spring Session -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
</dependencies>
8、YML配置:
server: port: 8080 spring: application: name: stage2-security6-demo # 数据源配置 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/security_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123 # Redis 连接核心配置 data: redis: database: 1 # Redis数据库索引(默认为0) host: 127.0.0.1 # Redis服务器地址 password: # Redis服务器连接密码 port: 6379 # Redis服务器连接端口 timeout: 300000 # 连接超时时间(毫秒),即最大等待时间 lettuce: pool: max-active: 8 # 连接池最大活跃连接数(默认8,高并发可调至20) max-idle: 5 # 连接池最大空闲连接数(默认5) max-wait: -1ms # 最大阻塞等待时间(-1ms=无限制,3.2.2需显式单位) min-idle: 0 # 连接池最小空闲连接数(默认0) shutdown-timeout: 100ms # 连接池关闭超时时间 # Session 分布式配置 session: # Session 存储类型(必填;取值:redis/ in-memory/ jdbc/ none;redis表示存储至Redis,实现跨实例共享) store-type: redis # Session 全局过期时间(单位s) timeout: 1800s redis: # Redis中Session Key前缀 namespace: spring:session # Session 同步至Redis的时机 # 取值:on_save/immediate;on_save=仅修改Session时同步(性能优先),immediate=每次请求都同步(一致性优先) flush-mode: on_save # cookie: # # 自定义Cookie 名称(避免暴露框架) # name: MY_SESSION # # Cookie 过期时间,与 Session 超时一致 # max-age: 1800s # # 禁止 JS 读取(防 XSS) # http-only: true # # 仅 HTTPS 传输(生产环境改为 true) # secure: false # # 防 CSRF 攻击 # same-site: lax # 权限规则配置 permission: # 白名单路径,无需登录即可访问 white-list: - /api/auth/login - /v1/hello # MyBatis-Plus配置 mybatis-plus: configuration: # 开启驼峰命名转换 map-underscore-to-camel-case: true # 打印SQL日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: # 主键自增策略 id-type: AUTO
9、核心实现流程:

(1)、数据库层实现-实体类与Mapper接口:
SysUserMapper.java -用户Mapper
@Mapper public interface SysUserMapper extends BaseMapper<SysUser> { /** * 根据用户名查询用户 * @param username 用户名 * @return 可用状态的用户信息 */ @Select("SELECT * FROM sys_user WHERE username = #{username} AND status = 1") SysUser selectByUsername(@Param("username") String username); }
SysRoleMapper.java -角色Mapper
@Mapper public interface SysRoleMapper extends BaseMapper<SysRole> { /** * 根据用户ID查询角色列表 * @param userId 用户ID * @return 角色列表 */ @Select("SELECT r.* FROM sys_role r JOIN sys_user_role ur ON r.id = ur.role_id WHERE ur.user_id = #{userId}") List<SysRole> selectByUserId(@Param("userId") Long userId); }
SysUserRoleMapper.java -用户角色关联Mapper
@Mapper public interface SysUserRoleMapper extends BaseMapper<SysUserRole> { }
(2)、公共类实现:
Response.java -统一响应类
/** * 统一响应类 * 用于封装API响应数据,统一返回格式 * */ @Data public class Response<T> { public static final int SUCCESS = 200; public static final int ERROR = 500; public static final int UNAUTHORIZED = 401; public static final int FORBIDDEN = 403; /** * 响应码 */ private Integer code; /** * 响应消息 */ private String message; /** * 响应数据 */ private T data; private Response() {} private static <T> Response<T> build(int code, String message, T data) { Response<T> response = new Response<>(); response.setCode(code); response.setMessage(message); response.setData(data); return response; } public static <T> Response<T> success() { return build(SUCCESS, "操作成功", null); } public static <T> Response<T> success(T data) { return build(SUCCESS, "操作成功", data); } public static <T> Response<T> success(String message, T data) { return build(SUCCESS, message, data); } public static <T> Response<T> error(String message) { return build(ERROR, message, null); } public static <T> Response<T> error(int code, String message) { return build(code, message, null); } }
(3)、配置类实现:
RedisConfig.java -Redis配置
配置 Redis 序列化方式,解决 Session 存储到 Redis 的序列化问题
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.*; /** * Redis 配置类 * <p> * 配置Spring Session使用Redis作为存储介质,包括: * 1. RedisTemplate序列化配置 * 2. Redis-Session序列化配置 * </p> */ @Configuration public class RedisConfig { /** * RedisTemplate 序列化配置 */ @Bean @SuppressWarnings("all") public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); jackson2JsonRedisSerializer.setObjectMapper(om); StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); template.setKeySerializer(stringRedisSerializer); template.setHashKeySerializer(stringRedisSerializer); template.setValueSerializer(jackson2JsonRedisSerializer); template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } /** * Redis-Session 序列化配置 */ @Bean RedisSerializer<Object> springSessionDefaultRedisSerializer() { return new JdkSerializationRedisSerializer(getClass().getClassLoader()); } }
GlobalCorsConfig.java -跨域配置
前后端分离场景下配置 CORS 允许跨域请求
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; /** * 跨域配置 * <p> * 配置Spring Boot应用的CORS(跨域资源共享)策略, * 允许前端应用从不同的域名访问后端API * </p> */ @Configuration public class GlobalCorsConfig { /** * 跨域配置源 * * 配置CORS策略,允许跨域请求,支持前端应用与后端API的安全通信 * * 生产环境注意事项: * - 应指定具体的前端域名,而不是使用通配符 * - 当setAllowCredentials(true)时,AllowedOrigins不能设为* * - 应根据实际需求限制允许的HTTP方法和请求头 * * @return CorsConfigurationSource实例 */ @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); // 允许的来源域名 // 注意:生产环境应指定具体的前端域名,而不是使用通配符 configuration.setAllowedOriginPatterns(Arrays.asList("*")); // 允许的HTTP方法 configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 允许的请求头 configuration.setAllowedHeaders(Arrays.asList("*")); // 允许携带Cookie(Session登录必须开启) configuration.setAllowCredentials(true); // 预检请求的缓存时间(秒) configuration.setMaxAge(3600L); // 创建CORS配置源 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // 对所有接口生效 source.registerCorsConfiguration("/**", configuration); return source; } }
WhitelistConfig.java -白名单配置
配置不需要认证即可访问的接口白名单
/** * 权限配置类 * <p> * 用于配置系统的白名单路径 * 通过@ConfigurationProperties注解从配置文件中加载权限配置 * </p> */ @Slf4j @Data @Component @ConfigurationProperties(prefix = "permission") public class WhitelistConfig { /** * 白名单路径(支持Ant风格:/** /api/auth/login 等) */ private List<String> whiteList; /** * 获取白名单路径数组(线程安全,判空保护) * @return 非空字符串数组 */ public String[] getWhitelistArray() { if (CollectionUtils.isEmpty(whiteList)) { log.warn("whitelist 为 null,将使用空数组"); return new String[0]; } return whiteList.toArray(new String[0]); } }
(4)、核心处理类实现:
CustomUserDetails.java -自定义用户详情类
Spring Security的认证流程需要UserDetails接口,但默认实现不满足业务需求,需要自定义
- 桥接业务数据与 Spring Security:将数据库中的用户信息转换为 Spring Security 可识别的格式
- 提供权限信息:getAuthorities() 方法返回用户的权限集合
- 支持角色前缀:自动为角色添加 ROLE_ 前缀,兼容 hasRole() 表达式
/** * 自定义用户详情类 * <p> * 实现Spring Security的UserDetails接口,封装业务用户数据 * 包含用户基本信息、角色代码和权限信息 * </p> */ public class CustomUserDetails implements UserDetails, Serializable { private static final long serialVersionUID = 1L; /** * 用户实体对象 */ private final SysUser sysUser; /** * 角色代码集合 */ private final Set<String> roleCodes; /** * 权限集合 */ private final Collection<? extends GrantedAuthority> authorities; /** * 构造函数 * 根据角色代码自动构建 Spring Security 所需的权限集合 * <p> * 注: * 在 Spring Security 体系内,getAuthorities() 所返回的 GrantedAuthority 权限标识无强制固定前缀约束, * ROLE_ 仅为框架默认约定,实际是否携带前缀,由权限校验注解 / 表达式决定。 * 1、基于 hasRole 表达式校验 * 执行 hasRole('ADMIN') 权限判定时,框架会自动在校验参数前拼接 ROLE_ 通用前缀,最终以 ROLE_ADMIN 作为匹配依据。 * 业务侧封装权限集合时,必须存入携带 ROLE_ 前缀的权限标识,才可通过角色鉴权。 * 2、基于 hasAuthority 表达式校验 * 执行 hasAuthority('ADMIN') 权限判定时,框架不做任何字符拼接与格式处理,采用原值精准匹配规则。 * 业务侧需保证权限集合内标识与校验参数完全一致,直接存入纯标识 ADMIN 即可,无需追加前缀。 * 3、自定义权限判定逻辑 * 若需脱离框架默认前缀规则,可通过实现 AccessDecisionVoter 投票器,或扩展 SecurityExpressionRoot 权限表达式根对象, * 自主定义权限比对规则,灵活定制权限前缀格式,亦可彻底摒弃前缀设计。 * </p> * * @param sysUser 用户实体对象 * @param roleCodes 角色代码集合(例如 ["ADMIN", "USER"]) */ public CustomUserDetails(SysUser sysUser, Set<String> roleCodes) { this.sysUser = sysUser; this.roleCodes = roleCodes; // 在构造时一次性将角色代码转换为 GrantedAuthority 对象 // 角色会加上 ROLE_ 前缀,以兼容 hasRole() 表达式 this.authorities = roleCodes.stream() .map(roleCode -> new SimpleGrantedAuthority("ROLE_" + roleCode)) .collect(Collectors.toUnmodifiableSet()); } /** * 获取用户实体对象 * @return 用户实体对象 */ public SysUser getSysUser() { return sysUser; } /** * 获取角色集合 * @return 角色集合 */ public Set<String> getRoleCodes() { return roleCodes; } /** * 获取权限集合 * @return 权限集合 */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } /** * 获取用户密码 * @return 用户密码 */ @Override public String getPassword() { return sysUser.getPassword(); } /** * 获取用户名 * @return 用户名 */ @Override public String getUsername() { return sysUser.getUsername(); } /** * 账户是否未过期 * @return true-未过期,false-已过期 */ @Override public boolean isAccountNonExpired() { return true; } /** * 账户是否未锁定 * @return true-未锁定,false-已锁定 */ @Override public boolean isAccountNonLocked() { return true; } /** * 凭证是否未过期 * @return true-未过期,false-已过期 */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 账户是否启用 * @return true-启用,false-禁用 */ @Override public boolean isEnabled() { return true; } }
UserDetailsServiceImpl.java -用户详情服务
Spring Security 认证流程的核心入口,实现UserDetailsService接口,负责加载用户信息与对应角色列表,并将业务层数据封装为 Spring Security 可识别的认证对象
/** * 用户详情服务实现类 * * 职责说明: * - 实现Spring Security的UserDetailsService接口 * - 用户登录时加载用户信息和角色权限 * - 为认证过程提供用户凭证验证数据 * * 工作流程: * 1. 用户发起登录请求 * 2. Spring Security调用loadUserByUsername方法 * 3. 根据用户名查询用户信息 * 4. 查询用户关联的角色列表 * 5. 构建CustomUserDetails对象返回 * 6. Spring Security验证密码并完成认证 */ @Service public class UserDetailsServiceImpl implements UserDetailsService { /** * 用户数据访问层 */ @Autowired private SysUserMapper sysUserMapper; /** * 角色数据访问层 */ @Autowired private SysRoleMapper sysRoleMapper; /** * 根据用户名加载用户详情 * * Spring Security认证流程的核心方法,负责: * * 认证流程: * 1. 根据用户名查询用户基本信息(用户名、密码、状态等) * 2. 验证用户是否存在,不存在则抛出UsernameNotFoundException * 3. 查询用户关联的角色列表 * 4. 验证用户是否有角色,无角色则抛出AuthenticationServiceException * 5. 提取角色代码集合(如ROLE_ADMIN、ROLE_USER) * 6. 构建CustomUserDetails对象并返回 * * @param username 用户名(登录时输入的用户名) * @return CustomUserDetails 用户详情对象,包含用户信息和角色权限 * @throws UsernameNotFoundException 用户不存在或账号已禁用时抛出 * @throws AuthenticationServiceException 用户未分配角色时抛出 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 1. 根据用户名查询用户信息 SysUser sysUser = sysUserMapper.selectByUsername(username); if (sysUser == null) { throw new UsernameNotFoundException("用户名不存在或账号已禁用"); } // 2. 查询用户角色列表 List<SysRole> roles = sysRoleMapper.selectByUserId(sysUser.getId()); if (CollectionUtils.isEmpty(roles)) { // 无角色用户不允许登录,增强安全性 throw new AuthenticationServiceException("用户未分配角色,无法登录"); } // 3. 提取角色代码集合 Set<String> roleCodes = roles.stream().map(SysRole::getRoleCode).collect(Collectors.toSet()); // 4. 构建并返回自定义UserDetails对象 return new CustomUserDetails(sysUser, roleCodes); } }
(5)、异常处理实现:
GlobalExceptionHandler.java -全局异常处理器
统一处理认证和授权异常,返回 JSON 格式错误响应
- 实现 AuthenticationEntryPoint:处理未认证异常(401)
- 实现 AccessDeniedHandler:处理权限不足异常(403)
- 实现 SessionInformationExpiredStrategy:处理会话过期异常
/** * 全局统一异常处理器 * * 职责说明: * - 捕获Controller、业务层抛出的各类业务异常 * - 处理Spring Security过滤器层异常(未登录、权限不足、会话过期) * - 统一返回JSON格式错误响应,避免浏览器显示空白错误页面 * * 实现的Security接口: * - AuthenticationEntryPoint - 处理未认证异常(HTTP 401) * - AccessDeniedHandler - 处理权限不足异常(HTTP 403) * - SessionInformationExpiredStrategy - 处理会话过期异常 */ @Slf4j @RestControllerAdvice public class GlobalExceptionHandler implements AuthenticationEntryPoint, AccessDeniedHandler, SessionInformationExpiredStrategy { // ======================== 业务层异常处理 ======================== /** * 处理方法级认证异常(HTTP 401) * * 处理 @PreAuthorize 等注解触发的认证失败。与下方 commence() 的区别: * - 本方法:处理 Controller 方法执行阶段的异常(AOP 切面层) * - commence():处理过滤器链阶段的异常(Filter 层,如未携带 Token) */ @ExceptionHandler(AuthenticationException.class) public Response handleAuthenticationException(AuthenticationException e) { log.warn("认证失败:{}", e.getMessage()); return Response.error(Response.UNAUTHORIZED, "认证失败,请重新登录"); } /** * 处理方法级授权异常(HTTP 403) * * 处理 @PreAuthorize 等注解触发的权限不足。与下方 handle() 的区别: * - 本方法:处理方法级权限校验失败(如角色不匹配) * - handle():处理 URL 级别权限拦截(SecurityConfig 中配置的规则) */ @ExceptionHandler(AccessDeniedException.class) public Response handleAccessDeniedException(AccessDeniedException e) { log.warn("权限不足:{}", e.getMessage()); return Response.error(Response.FORBIDDEN, "权限不足,无法访问"); } /** * 兜底异常处理 * <p> * 捕获所有未被明确处理的异常,作为全局异常兜底方案 * 记录异常堆栈信息便于排查问题,返回统一的错误响应 * </p> * @param e 异常对象 * @return 统一错误响应 */ @ExceptionHandler(Exception.class) public Response handleException(Exception e) { log.error("系统异常:", e); return Response.error("系统内部错误"); } // ======================== Security 过滤器层异常处理 ======================== /** * 处理过滤器层认证异常(HTTP 401) * <p> * 实现 {@link AuthenticationEntryPoint} 接口 * 当用户未登录或认证凭证无效时访问受保护资源时触发 * </p> * @param request HTTP请求对象 * @param response HTTP响应对象 * @param e 认证异常对象 */ @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { log.warn("未认证访问受保护接口: {}", request.getRequestURI()); writeJson(response, Response.UNAUTHORIZED, "请先登录"); } /** * 处理过滤器层授权异常(HTTP 403) * <p> * 实现 {@link AccessDeniedHandler} 接口 * 当用户已登录但缺少访问目标资源所需权限时触发 * </p> * @param request HTTP请求对象 * @param response HTTP响应对象 * @param e 权限拒绝异常对象 */ @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) { log.warn("权限不足访问接口: {}", request.getRequestURI()); writeJson(response, Response.FORBIDDEN, "权限不足,无法访问"); } /** * 会话过期异常处理 * <p> * 实现 {@link SessionInformationExpiredStrategy} 接口 * 当用户会话超时或被踢下线时触发 * </p> * @param event 会话过期事件对象,包含request、response等上下文信息 * @throws IOException 写入响应时可能抛出的IO异常 */ @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException { log.warn("会话已过期"); writeJson(event.getResponse(), Response.UNAUTHORIZED, "会话已过期,请重新登录"); } // ======================== 私有辅助方法 ======================== /** * 输出错误 JSON 响应 * @param response 响应对象 * @param code 错误码 * @param message 错误信息 */ private void writeJson(HttpServletResponse response, int code, String message) { try { response.setContentType("application/json; charset=utf-8"); response.setCharacterEncoding("UTF-8"); response.setStatus(code); response.getWriter().write(JSONObject.toJSONString(Response.error(code, message))); } catch (IOException e) { log.error("JSON 响应输出失败", e); } } }
(6)、工具类实现:
UserContextUtil.java -用户上下文工具类
基于 SecurityContextHolder 实现,支持跨线程访问认证信息
/** * 用户上下文工具类 * * 职责说明: * - 管理Spring Security安全上下文 * - 提供会话相关信息获取 * * 核心特性: * - 线程安全:每个线程(请求)有独立的SecurityContext副本 * - 基于Spring Security SecurityContextHolder实现 * - 集成Spring RequestContextHolder获取请求上下文 * */ @Slf4j public class UserContextUtil { /** * 获取当前登录用户实体 * * 从SecurityContext中提取当前认证用户的SysUser实体。 * 如果用户未登录或认证信息无效,返回null。 * * @return 当前登录用户实体,未登录返回null */ public static SysUser getCurrentUser() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.getPrincipal() instanceof CustomUserDetails) { return ((CustomUserDetails) auth.getPrincipal()).getSysUser(); } return null; } /** * 获取当前用户的角色代码集合 * * 从SecurityContext中提取当前用户的所有角色代码。 * 如果用户未登录,返回空集合。 * * @return 角色代码集合(如ROLE_ADMIN、ROLE_USER),未登录返回空集合 */ public static Set<String> getCurrentRoleCodes() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.getPrincipal() instanceof CustomUserDetails) { return ((CustomUserDetails) auth.getPrincipal()).getRoleCodes(); } return Collections.emptySet(); } }
(7)、控制器实现:
LoginController.java -登录/登出控制器
- 前后端分离场景下,提供自定义登录/登出接口,返回 JSON 格式响应
- 认证成功后需将 Authentication 存储到 SecurityContext,后续请求才能获取用户信息
/** * 登录控制器 * <p> * 处理用户登录和登出请求,实现自定义的登录和登出逻辑 * </p> */ @Slf4j @RestController @RequestMapping("/api/auth") public class LoginController { /** * 认证管理器,用于执行用户认证 */ @Autowired private AuthenticationManager authenticationManager; /** * 登录接口 * <p> * 处理用户登录请求,执行认证逻辑并返回登录结果 * </p> * @param loginData 登录数据,包含用户名和密码 * @param request 请求对象 * @return 登录结果,包含用户名和角色信息 */ @PostMapping("/login") public Response login(@RequestBody Map<String, String> loginData, HttpServletRequest request) { // 获取用户名和密码 String username = loginData.get("username"); String password = loginData.get("password"); // 验证用户名和密码是否为空 if(!StringUtils.hasText(username) || !StringUtils.hasText(password)){ return Response.error("用户名或密码不能为空"); } try { // 创建认证令牌 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); // 执行认证(自动触发 UserDetailsService 和密码校验) Authentication authentication = authenticationManager.authenticate(authenticationToken); // 认证成功后,进行以下操作: // 1、将认证信息存储到安全上下文中 SecurityContextHolder.getContext().setAuthentication(authentication); // 2、获取用户详情,从认证对象中提取用户信息 CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); // 3、构建角色代码字符串 String roleCodes = String.join(",", userDetails.getRoleCodes()); // 4、返回登录成功结果 log.info("登录成功:{}, 角色:{}", username, roleCodes); return Response.success(Map.of( "username", username, "roleCodes", roleCodes )); } catch (BadCredentialsException e) { // 密码错误异常 log.warn("登录失败:{}", username); return Response.error("用户名或密码错误"); } catch (LockedException e) { // 账号锁定异常 log.warn("账号已锁定:{}", username); return Response.error("账号已锁定,请联系管理员"); } catch (AuthenticationException e) { // 其他认证异常 log.warn("认证失败:{}", e.getMessage()); return Response.error("登录失败,请稍后重试"); } } /** * 登出接口 * <p> * 处理用户登出请求,清理会话和安全上下文 * </p> * @param request 请求对象 * @return 登出结果 */ @PostMapping("/logout") public Response logout(HttpServletRequest request) { // 获取当前会话(如果存在) HttpSession session = request.getSession(false); if (session != null) { // 使会话失效,Spring Session 会自动清理 Redis 中的会话数据 session.invalidate(); } // 清理安全上下文 SecurityContextHolder.clearContext(); // 返回登出成功结果 return Response.success("登出成功"); } }
OperationController.java -业务操作控制器
/** * 操作控制器 * <p> * 提供系统操作相关的接口,包括: * 1. 公共信息接口(无需认证) * 2. 用户信息接口(需要用户权限) * 3. 管理员信息接口(需要管理员权限) * </p> */ @RestController @RequestMapping("/v1") public class OperationController { /** * 公共信息接口 * <p> * 返回公共信息,无需认证即可访问 * </p> * @return 公共信息响应 */ @GetMapping("/hello") public Response getHelloInfo() { return Response.success(Map.of( "message", "公共信息" )); } /** * 获取普通用户信息 * <p> * 获取当前登录用户的信息和角色权限 * </p> * @return 用户信息响应 */ @GetMapping("/user/info") @PreAuthorize("hasAnyRole('USER', 'ADMIN')") public Response getUserInfo() { // 使用UserContextUtil获取用户信息 Object user = UserContextUtil.getCurrentUser(); Set<String> roleCodes = UserContextUtil.getCurrentRoleCodes(); return Response.success(Map.of( "message", "用户信息", "user", user, "roleCode", roleCodes )); } /** * 获取管理员信息 * <p> * 获取当前登录管理员的信息和角色权限 * </p> * @return 管理员信息响应 */ @GetMapping("/admin/info") @PreAuthorize("hasRole('ADMIN')") public Response getAdminInfo() { // 使用UserContextUtil获取用户信息 Object user = UserContextUtil.getCurrentUser(); Set<String> roleCodes = UserContextUtil.getCurrentRoleCodes(); return Response.success(Map.of( "message", "管理员信息", "user", user, "roleCode", roleCodes )); } }
(8)、安全配置实现:
SecurityConfig.java - Spring Security核心配置
Spring Security 的核心配置类,定义整个安全策略
|
配置项 |
说明 |
|
@EnableRedisIndexedHttpSession |
启用 Redis 分布式 Session |
|
@EnableMethodSecurity |
启用方法级安全注解(@PreAuthorize) |
/** * Spring Security 核心配置类 * * 职责说明: * - 配置认证机制:用户身份验证、密码加密 * - 配置授权规则:URL访问权限控制、白名单管理 * - 配置会话管理:Session策略、并发控制、Redis存储 * - 配置跨域处理:CORS配置 * - 配置异常处理:认证和授权异常统一处理 * * 关键特性: * - 基于Redis的分布式Session存储 * - 同一用户只能单点登录 * - 自定义异常处理器返回JSON格式错误 * - 白名单URL无需认证即可访问 */ @Configuration @EnableRedisIndexedHttpSession @EnableMethodSecurity public class SecurityConfig { /** * 全局异常处理器 * * 处理Spring Security过滤器层抛出的认证和授权异常: * - AuthenticationException(未认证):返回401状态码 * - AccessDeniedException(权限不足):返回403状态码 * - SessionInformationExpiredEvent(会话过期):返回401状态码 */ @Resource private GlobalExceptionHandler globalExceptionHandler; /** * 跨域配置源 * * 提供CORS(跨域资源共享)配置信息 */ @Resource private CorsConfigurationSource corsConfigurationSource; /** * 白名单配置 * * 管理无需认证即可访问的URL列表 */ @Resource private WhitelistConfig whitelistConfig; /** * Session仓库 * * 基于Redis的Session存储实现,支持: * - 分布式Session共享 * - 按用户名索引查询Session * - Session并发控制 */ @Resource private FindByIndexNameSessionRepository findByIndexNameSessionRepository; /** * 密码加密器 * * 使用BCrypt算法对密码进行加密存储 * * BCrypt特性: * - 自动加盐:每次加密都会生成随机盐值,防止彩虹表攻击 * - 单向哈希:不可逆,无法从密文还原明文 * - 可调强度:通过work factor参数调整计算复杂度 * - 安全性高:已被广泛验证,适合生产环境使用 * * @return BCryptPasswordEncoder实例 */ @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * 认证管理器 * * 负责处理用户认证请求,是Spring Security认证流程的核心组件 * * 认证流程: * 1. 接收认证请求(用户名、密码) * 2. 使用DaoAuthenticationProvider从数据库加载用户信息 * 3. 使用PasswordEncoder验证密码 * 4. 返回认证成功或失败的结果 * * @param userDetailsService 用户详情服务,用于加载用户信息 * @param passwordEncoder 密码加密器,用于验证密码 * @return AuthenticationManager实例 */ @Bean public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { // 匹配合适的AuthenticationProvider(DaoAuthenticationProvider) DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); provider.setPasswordEncoder(passwordEncoder); return new ProviderManager(provider); } /** * Session注册器 * * 用于管理用户Session的注册和查询,支持Session并发控制 * * 功能: * - 记录所有活跃Session * - 按用户名查询该用户的所有Session * - 配合maximumSessions实现单点登录 * - 配合maxSessionsPreventsLogin防止多点登录 * * @return SessionRegistry实例 */ @Bean public SessionRegistry sessionRegistry() { return new SpringSessionBackedSessionRegistry(findByIndexNameSessionRepository); } /** * 安全过滤器链配置 * * 配置Spring Security的核心安全规则,按执行顺序包括: * * 1. CSRF防护:禁用CSRF保护(前后端分离项目通常禁用) * 2. 跨域处理:配置CORS规则,允许跨域请求 * 3. 异常处理:自定义认证和授权异常处理器 * 4. 授权规则:配置URL访问权限,白名单放行 * 5. 会话管理:配置Session策略和并发控制 * 6. 登录登出:禁用默认表单登录和登出,使用自定义接口 * 7. HTTP Basic:禁用HTTP Basic认证 * * @param http HttpSecurity配置对象 * @return SecurityFilterChain实例 * @throws Exception 配置异常 */ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // ======================== CSRF防护 ======================== // 禁用CSRF保护 // 原因:前后端分离项目通常使用Token认证,CSRF防护意义不大 // 注意:生产环境如需启用,需在请求头携带CSRF Token .csrf(csrf -> csrf.disable()) // ======================== 跨域处理 ======================== // 配置CORS,使用自定义的CORS配置源 .cors(cors -> cors.configurationSource(corsConfigurationSource)) // ======================== 请求缓存 ======================== // 禁用 RequestCache(请求缓存机制)。 // 作用:Spring Security 默认会缓存未认证用户的请求,认证成功后自动重定向到该缓存请求。 // 在前后端分离项目中,API 接口不需要这种页面跳转行为,禁用后可避免意外重定向,让前端完全控制路由。 .requestCache(cache -> cache.disable()) // ======================== 异常处理 ======================== // 配置认证和授权异常的处理器 .exceptionHandling(exception -> exception // 认证异常处理:未登录或认证失败时触发(返回401) .authenticationEntryPoint(globalExceptionHandler) // 授权异常处理:已登录但权限不足时触发(返回403) .accessDeniedHandler(globalExceptionHandler) ) // ======================== 授权规则 ======================== // 配置URL访问权限 .authorizeHttpRequests(authorize -> authorize // 白名单URL:无需认证即可访问 // 包括登录、注册、静态资源等公开接口 .requestMatchers(whitelistConfig.getWhitelistArray()).permitAll() // 其他所有请求:必须认证后才能访问 .anyRequest().authenticated() ) // ======================== 会话管理 ======================== // 启用默认 SecurityContext 持久化 // SecurityContext 在每次请求结束后自动保存到 Session,无需手动调用 session.setAttribute() .securityContext(securityContext -> securityContext.requireExplicitSave(false)) // 配置Session策略和并发控制 .sessionManagement(session -> session // Session创建策略 // - ALWAYS:总是创建Session(即使不需要) // - NEVER:不主动创建Session,但可使用已存在的 // - IF_REQUIRED:需要时才创建(推荐) // - STATELESS:无状态,完全不使用Session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 会话并发控制:同一用户最多允许1个活跃Session .maximumSessions(1) // 会话注册器 // 1、不需要并发控制(即不配置 maximumSessions),那么不需要 SessionRegistry // 2、需要并发控制(配置maximumSessions时,即限制同一用户只能在一个地方登录),就必须提供 SessionRegistry 的实现 .sessionRegistry(sessionRegistry()) // 会话并发策略:禁止多点登录 // true:新登录请求被拒绝(推荐) // false:踢掉旧Session,允许新登录 .maxSessionsPreventsLogin(true) // 会话过期处理器:Session过期时触发 .expiredSessionStrategy(globalExceptionHandler) ) // ======================== 登录登出 ======================== // 禁用默认表单登录,使用自定义登录接口 .formLogin(form -> form.disable()) // 禁用默认登出,使用自定义登出接口 .logout(logout -> logout.disable()) // ======================== HTTP Basic ======================== // 禁用HTTP Basic认证 // 原因:前后端分离项目使用Token认证,不需要Basic认证 .httpBasic(basic -> basic.disable()); return http.build(); } }


四、跨域问题解决方案:
1、跨域问题:
跨域问题的核心根源是浏览器内置的同源安全策略,这是浏览器为防范 CSRF、XSS 等恶意攻击,保护用户 Cookie、Token 等隐私数据设立的底层安全规则,并非代码故障;该规则要求请求的协议、域名、端口三者完全一致才算同源,任意一项不同,浏览器就会拦截前端 AJAX、Fetch 发出的接口响应,且跨域限制仅针对浏览器端 JS 请求,服务器与服务器之间的通信不存在任何跨域约束。
2、业务中不依托前端处理跨域的根本原因:
前端所有跨域相关方案,包括 JSONP、关闭浏览器安全策略、前端代理打包上线,本质都是临时绕过的非标准手段且存在安全隐患,其中 JSONP 仅支持 GET 请求,无法传递 Token 与 Cookie,早已被行业淘汰,前端配置的代理仅作用于本地开发环境,项目打包部署后代理配置会直接失效,上线必然出现跨域故障;跨域管控源于浏览器底层的安全机制,仅靠前端修改代码无法突破原生校验,同时从合规与接口安全角度,携带 Cookie、Token 等登录态的接口,必须由后端维护合法域名白名单,以此防范恶意网站盗用接口发起攻击,这也是行业通用标准,前端代理只用于本地开发调试,生产环境的跨域问题统一交由 Java 后端或 Nginx 处理。
3、常用落地解决方案:
(1)、方案 1:SpringBoot 全局 CORS 统一配置,是企业常规首选方式,可对项目所有接口统一管控跨域白名单,支持登录态传递:
@Configuration public class GlobalCorsConfig implements WebMvcConfigurer { // 重写跨域映射配置方法,统一处理全局跨域规则 @Override public void addCorsMappings(CorsRegistry registry) { // 匹配所有接口路径,使跨域配置对整个项目生效 registry.addMapping("/**") // 设置允许跨域的前端域名(白名单) .allowedOrigins("https://official-front.com","http://localhost:8081") // 设置允许的请求方法类型 .allowedMethods("GET","POST","PUT","DELETE","OPTIONS") // 允许携带Cookie、Token等身份认证信息 .allowCredentials(true) // 设置预检请求的缓存有效期,提升请求效率 .maxAge(3600); } }
(2)、方案 2:@CrossOrigin 局部注解配置,适用于仅少数接口临时放开跨域的场景,可作用于单个接口或整个控制器:
@RestController @CrossOrigin(origins = "http://localhost:8081", allowCredentials = "true") public class UserController { @GetMapping("/user/info") public Object getUserInfo(){ return "用户业务数据"; } }
(3)、方案 3:Nginx 反向代理,无需修改业务代码,通过 Nginx 将前端静态资源与后端接口收敛到同一域名端口下,从架构层面彻底消除跨域,是线上生产环境的主流方案:
server { # 监听80端口(HTTP默认端口) listen 80; # 绑定前端访问的域名(生产环境填写真实域名) server_name xxxxx.com; # 匹配根路径请求,用于访问前端静态页面 location / { # 前端项目打包后的dist文件夹存放路径 root /data/front/dist; # 默认首页文件 index index.html; } # 匹配以/api/开头的接口请求,转发到Java后端 location /api/ { # 反向代理目标:Java后端服务地址+端口 proxy_pass http://127.0.0.1:8080/; # 传递原始请求的Host域名给后端,保证后端获取正确域名 proxy_set_header Host $host; # 传递客户端真实IP给后端,方便日志记录、IP校验 proxy_set_header X-Real-IP $remote_addr; } }
浙公网安备 33010602011771号