阶段二:夯实有状态认证底座(基于 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 机制的优化方案:

在保留有状态认证核心的前提下,针对原生缺陷采用以下标准化优化方案,适配现代架构:

8

(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、项目结构:

9

技术

版本

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、用户登录认证流程:

13

4、请求访问控制流程:

14

5、单点登录控制流程:

15

6、登出流程:

16

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、核心实现流程:

10

(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 -登录/登出控制器

  1. 前后端分离场景下,提供自定义登录/登出接口,返回 JSON 格式响应
  2. 认证成功后需将 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();
    }

}

11

12

四、跨域问题解决方案:

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;
    }
}

 

posted on 2026-06-24 00:18  爱文(Iven)  阅读(9)  评论(0)    收藏  举报

导航