上次我们进行了基础的配置并连接上了数据库,接下来咱就开始初步搭建rbac鉴权体系。
创建五张表:

-- AI 工作站基础鉴权表结构 (MySQL)
-- 采用 19位雪花算法(Snowflake) 生成主键,支持手机号/邮箱无密码验证码登录方式

-- 1. 用户表
CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL COMMENT '主键ID(19位雪花算法生成)',
  `phone` varchar(20) DEFAULT NULL COMMENT '手机号',
  `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
  `nickname` varchar(64) DEFAULT NULL COMMENT '展示昵称',
  `status` tinyint(2) DEFAULT '1' COMMENT '状态(0停用 1正常)',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_phone` (`phone`),
  UNIQUE KEY `uk_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户实体(无密码登录表)';

-- 2. 角色表
CREATE TABLE `sys_role` (
  `id` bigint(20) NOT NULL COMMENT '主键ID(19位雪花算法)',
  `role_name` varchar(64) NOT NULL COMMENT '角色名称(如: 管理员, 面试官)',
  `role_code` varchar(64) NOT NULL COMMENT '角色标识(如: ROLE_ADMIN, ROLE_GUEST)',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色信息表';

-- 3. 权限动作表 (基于 Action 的功能级鉴权核心)
CREATE TABLE `sys_permission` (
  `id` bigint(20) NOT NULL COMMENT '主键ID(19位雪花算法)',
  `action_code` varchar(100) NOT NULL COMMENT '动作标识(规范: [服务]:[资源]:[动作],如 ai:chat:send)',
  `action_name` varchar(64) NOT NULL COMMENT '动作描述(如: 发送AI对话, 上传知识库)',
  `module` varchar(32) NOT NULL COMMENT '所属模块(如: ai, rag, system)',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_action_code` (`action_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='功能操作权限策略表(Action)';

-- 4. 用户-角色关联表
CREATE TABLE `sys_user_role` (
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

-- 5. 角色-权限关联表
CREATE TABLE `sys_role_permission` (
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  `permission_id` bigint(20) NOT NULL COMMENT '权限ID',
  PRIMARY KEY (`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表';

然后创建对应的实体类,这里主要以SysUser为例讲解以下内容

@Data
@TableName("sys_user")
public class SysUser {
    
    // 使用分布式雪花算法自动生成 19 位 ID,并转为 String 传给前端防精度丢失
    @TableId(type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;

    private String phone;
    private String email;
    private String nickname;
    private Integer status;
    private LocalDateTime createTime;
}

Lombok 的 @Data 注解

Lombok 是一个编译期插件。当你在类上打上 @Data 后,它会在 Java 代码编译成 .class 字节码文件的时候,利用 AST (抽象语法树) 自动把这些 Getter/Setter/equals/hashCode/toString 方法给你“织入”(编织进去)。


MyBatis-Plus 的 @TableName
通常情况下,如果你的类名叫 SysUser,MyBatis-Plus 的驼峰映射策略会自动去数据库找叫做 sys_user 的表


@TableId(type = IdType.ASSIGN_ID)
这个枚举让 MyBatis-Plus 弃用了数据库自身的自增主键,转而使用其内置的雪花算法(Snowflake ID)
分布式会重复吗?

MyBatis-Plus 出厂自带的 DefaultIdentifierGenerator 会在你的服务每次启动时:
读取服务器MAC地址和JVM进程ID并算出机器码,再根据时间戳+毫秒内序列号算出雪花ID,因此不可能重复
但是要是服务部署在 Docker 容器或者 K8s 集群里,容器重启后,不仅 IP 和 MAC 地址是由虚拟网卡随机分配的,连 JVM 进程 ID 往往都统一是被虚拟化成了 1,那可能碰撞咋办?
我们可以手动实现一个 MyBatis-Plus 提供的 IdentifierGenerator 接口。在服务启动时,让每个节点去 Redis 分布式锁(或者 ZooKeeper)里去申请一个从 1 到 1024 的自增 workerId

去读取当前这台服务器的 网卡 MAC 地址。
去读取当前跑在这个服务器上的 JVM 进程 ID。
利用这两个独一无二的物理特征,通过哈希运算,算出一个 10 位的 workerId(机器码)。
在雪花算法的 64 位二进制中:时间戳 + 机器码 + 毫秒内序列号。因为这 3 台服务器的 MAC 地址或者进程 ID 肯定不同,所以生成的 机器码 一定不同,最终拼出来的这 19 位主键在整个分布式集群里绝对不可能重复。


@JsonSerialize(using = ToStringSerializer.class)
防精度丢失的 Jackson 序列化
前端的 JavaScript 没有专门的整数类型,其 Number 类型底层由 IEEE 754 双精度浮点数构成。这导致 JS 安全整数的极限是 2的53次方-1(最大 16 位数字)。
由于返回 JSON 是 Jackson 框架处理的,打上 @JsonSerialize(using = ToStringSerializer.class) 注解后。在序列化那一刻,Jackson 会聪明把原本的 Long 当成了 String 往外吐。

为啥不能直接存String当索引,而是bigInt?因为MySQL(InnoDB引擎)要在 B+ 树的节点上做大量的双分查找。如果 id 是 bigint 整数,CPU 只需要占用 1 个时钟周期,一条汇编指令就比较完了(它只要判断 A 和 B 谁大谁小即可)。
但是如果你是 VARCHAR 字符串,MySQL 必须拿着两个字符串**逐个字节(甚至考虑不同语种的排序规则 Collation)**进行遍历比较。


接下来是全局异常处理器


/**
 * 全局异常处理器
 * 拦截整个系统抛出的异常,转化成JSON
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 拦截自定义的业务异常 (GlobalException)
     */
    @ExceptionHandler(GlobalException.class)
    public Result<Void> handleGlobalException(GlobalException e) {
        // 打印警报日志 (通常不用 Error 级别,因为这是我们主动抛出的也是预期内的业务阻断)
        log.warn("业务拦截 - code: {}, message: {}", e.getResultCode().getCode(), e.getMessage());

        // 剥离出异常中的错误码和错误提示,原封不动地装进 Result 外壳发给前端
        return Result.error(e.getResultCode().getCode(), e.getMessage());
    }

    /**
     * 兜底大网:拦截系统中发生的所有其他未知异常 (NullPointerException, SQLSyntaxErrorException 等)
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        // 这种属于真出 Bug 了,必须打印出完整的错误堆栈 (Error级别) 供程序员排查
        log.error("系统兜底异常: ", e);

        // 给前端温柔地返回统一样式:500 服务器遇到未知异常,而不是抛出 Tomcat 那种带全部代码暴露的白板报错页
        return Result.error(ResultCode.INTERNAL_SERVER_ERROR);
    }
}

@RestControllerAdvice 本质是对 Spring 所有的 @RestController 做了一个切面拦截。它的视线笼罩着所有对外的接口,@ExceptionHandler(GlobalException.class)则是注解在方法上,表示抛出的这个异常由这个方法处理,
如果你的项目非常大,有多个 @RestControllerAdvice 类(比如一个是 CommonAdvice,一个是 SpecialAdvice),它们都定义了处理 GlobalException的方法,这时候你可以使用 @Order 注解 给这些 Advice 类排队。数字越小的类,优先级越高,它定义的拦截器就会先抢到异常的处理权。

Redis配置

data:
    redis:
      host: localhost
      port: 6379
      database: 0
      timeout: 5000  # 链接超时时间

database: 0 的意义:Redis 默认自带 16 个逻辑库(0-15)。为了防止与你电脑上运行的其他项目发生 Key 命名空间碰撞,在生产环境中,不同服务一定会被分配到指定的库。
timeout: 5000 (单位默认毫秒):如果没有配这一行,网络出现抖动时你的线程会死等,导致整个 Tomcat 连接池全部卡死。

然后我们来看redis。

RedisConfig 配置类

Spring Boot 出厂自带的 RedisTemplate<Object, Object>,如果直接使用,它采用的是源自远古的 JDK 原生序列化策略。如果你使用StringRedisTemplate ,这个 Spring 官方提供的一个快捷工具,它默认把 Key 和 Value 都设置成了字符串序列化,但是它只存取字符串,无法直接存取对象。比如

redisUtil.set("user:1", userEntity); 

因此,我们让 Key 的序列化强制改用 StringRedisSerializer,让 Value 的序列化强制改用 GenericJackson2JsonRedisSerializer,当我们直接丢给工具类一个复杂对象时(如包含 id 和权限列表的 LoginUser 实体),它会自动帮我们转换为紧凑好看的 JSON 字符串存入内存。

package com.huohuo.huohuospace.config;

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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis 全局配置类
 * 主要用于接管 Spring Boot 默认的 RedisTemplate 行为,处理序列化防坑
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // 1. 设置 Key 原生序列化,防止出现 \xac\xed\x00\x05t\x00 这种乱码前缀
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);

        // 2. 设置 Value 为 JSON 序列化,对象存进去自动变 JSON 字符串
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

RedisUtil工具类

package com.huohuo.huohuospace.common.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import org.springframework.beans.factory.annotation.Autowired;
import java.util.concurrent.TimeUnit;

/**
 * 分布式缓存操作工具类
 */
@Slf4j
@Component
public class RedisUtil {


    private final RedisTemplate<String, Object> redisTemplate;
    @Autowired
    public RedisUtil(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
    /**
     * 写入普通缓存
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            log.error("Redis 写入异常", e);
            return false;
        }
    }

    /**
     * 写入缓存并携带强硬的生命周期(核心用于验证码场景防刷)
     * @param time 过期时间,单位默认为秒
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                // 核心:使用 TimeUnit.SECONDS 精确控制存活阈值
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            log.error("Redis 带时效写入异常", e);
            return false;
        }
    }

    /**
     * 提取缓存数据
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }
    
    /**
     * 检查对应的 Key 是否存活在 Redis 中
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            log.error("Redis 嗅探异常", e);
            return false;
        }
    }

    /**
     * 删除指定的缓存数据(可批量)
     */
    public void delete(String... keys) {
        if (keys != null && keys.length > 0) {
            if (keys.length == 1) {
                redisTemplate.delete(keys[0]);
            } else {
                for (String key : keys) {
                    redisTemplate.delete(key);
                }
            }
        }
    }
}


工具类方法名 功能说明 底层 RedisTemplate 调用
set(String key, Object value) 写入普通缓存 (默认永久存活) opsForValue().set(key, value)
set(String key, Object value, long time) 写入带时效的缓存 (防刷/验证码等场景) opsForValue().set(key, value, time, TimeUnit.SECONDS)
get(String key) 提取缓存数据 opsForValue().get(key)
hasKey(String key) 嗅探 Key 是否存在 redisTemplate.hasKey(key)
delete(String... keys) 删除指定缓存 (支持单删与多删) redisTemplate.delete(key)

使用示例
假设有一个用户类 User:

// 必须实现序列化接口,建议加上,防止某些序列化器报错
public class User implements Serializable {
    private String name;
    private int age;
    // 构造函数、Getter、Setter 略
}

在 Service 业务层里,直接像这样调用即可:

@Autowired
private RedisUtil redisUtil;

public void demo() {
    // 1. 创建对象
    User user = new User("小火", 18);

    // 2. 存对象 (RedisUtil 内部会自动处理 Object 转换)
    redisUtil.set("user:1001", user, 3600); // 存入并设置 1 小时过期

    // 3. 取对象 (取出来是 Object,需要强转一下)
    User cachedUser = (User) redisUtil.get("user:1001");

    if (cachedUser != null) {
        System.out.println("从缓存拿到用户:" + cachedUser.getName());
    }
}

有了redis配置,我们接下来要开始鉴权认证部分了。
先来看几种认证模式对比:

1. 基于 Token 的认证(如 JWT)

原理:客户端登录成功后,服务端生成一个包含用户身份信息的加密令牌(Token),通常采用 JSON Web Token(JWT)格式。后续请求中,客户端在 HTTP Header(如 Authorization: Bearer )中携带该 Token,服务端验证其签名和有效期即可完成身份认证。

优点

  • 无状态:服务端无需存储会话信息,便于水平扩展。
  • 跨域友好:Token 可通过 Header 传递,天然支持跨域请求。
  • 自包含:JWT 可携带用户声明(claims),减少数据库查询。
  • 适用于移动端和第三方集成

缺点

  • 难以主动失效:需引入额外的 Token 黑名单机制
  • 安全性依赖密钥管理:若私钥泄露,所有 Token 都可能被伪造。

2. OAuth 2.0 / OpenID Connect(OIDC)

原理:OAuth 2.0 是一种授权框架,常用于第三方应用访问用户资源(如“使用 Google 登录”)。OpenID Connect 是基于 OAuth 2.0 的身份认证层,通过 ID Token(通常是 JWT)实现用户身份验证。

优点

  • 标准化、生态成熟:被 Google、GitHub、Microsoft 等广泛支持。
  • 支持委托授权:适合多租户、SaaS、第三方集成场景。
  • 细粒度权限控制:通过 Scope 和 Claims 实现权限最小化。

缺点

  • 复杂度高:涉及多个角色(Resource Owner、Client、Authorization Server、Resource Server),部署和调试成本较高。
  • 需维护授权服务器:小型项目可能过度设计。

3. API Key 认证

原理:为每个客户端分配一个唯一的密钥(API Key),在请求时通过 Header 或 Query Parameter 传递。

优点

  • 简单易用:适合机器对机器(M2M)通信或内部服务调用。
  • 易于限流与审计:可按 Key 统计调用量、设置配额。

缺点

  • 安全性较低:Key 易被泄露,且通常不具备时效性。
  • 缺乏用户上下文:一般用于服务级认证,而非用户级身份识别。
  • 不适合前端直接使用:暴露在前端代码中存在安全风险。

4. Session-Cookie(传统方式)

原理:用户登录后,服务端创建 Session 并将 Session ID 存入 Cookie,浏览器自动携带 Cookie 发起后续请求。

优点

  • 成熟稳定:长期被 Web 应用广泛使用。
  • 服务端可控:可随时销毁 Session 实现强制登出。

缺点

  • 有状态:Session 通常存储在内存或 Redis 中,影响横向扩展。
  • 跨域受限:Cookie 默认不跨域,需额外配置 CORS + credentials。
  • 移动端支持差:原生 App 不像浏览器那样自动管理 Cookie。
  • CSRF攻击

跨站请求伪造(CSRF) 是一种常见的网络攻击手段。在传统的架构中,用户登录后,服务器会下发带有登录状态的 Cookie,如果受害者在登录状态下误访了黑客搭建的恶意网站,黑客只需在网页内写下一段自动发起表单提交至原合法服务器的脚本即可将合法 Cookie 附加其中,诱导服务器以合法身份执行恶意的越权转账等操作。

5. 双重 Token 机制(Refresh Token + Access Token)

原理:结合短期有效的 Access Token(用于 API 认证)和长期有效的 Refresh Token(用于获取新 Access Token),兼顾安全与用户体验。

优点

  • 安全性高:Access Token 短期有效,即使泄露危害有限。
  • 体验好:用户无需频繁重新登录。
  • 支持主动登出:可通过撤销 Refresh Token 实现。

缺点

  • 实现复杂:需同时管理两种 Token 的存储、刷新和失效逻辑。
  • 仍需服务端状态:Refresh Token 通常需持久化存储以支持吊销。

总结对比

认证方式 无状态 跨域支持 安全性 适用场景
Session-Cookie ⚠️(需配置) 传统 Web 应用
JWT 高(需妥善管理密钥) 前后端分离、移动端
OAuth 2.0 / OIDC 第三方登录、企业级 SSO
API Key 内部服务、M2M 通信
双重 Token ⚠️(Refresh Token 有状态) 需要长期登录且高安全要求场景

JwtUtils 工具类实现

package com.huohuo.huohuospace.common.utils;

import com.huohuo.huohuospace.config.JwtProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

/**
 * JWT (JSON Web Token) 工具类
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtils {

    // 采用构造器注入我们刚才新建的属性配置类
    private final JwtProperties jwtProperties;

    /**
     * 生成安全签名 Key
     */
    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8));
    }

    /**
     * 生成 JWT Token
     * 
     * @param userId 用户的强一致性标示(19 位雪花 ID)
     * @param claims 自定义载荷 (Payload),默认 base64 编码,应存储非敏感信息
     */
    public String generateToken(String userId, Map<String, Object> claims) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtProperties.getExpiration() * 1000);

        return Jwts.builder()
                .claims(claims)
                .subject(userId)
                .issuedAt(now)
                .expiration(expiryDate)
                .signWith(getSigningKey())
                .compact(); 
    }

    /**
     * 验证并解析 Token
     */
    public Claims parseToken(String token) {
        return Jwts.parser()
                .verifyWith(getSigningKey())
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }

    /**
     * 提取 Token 中的用户 ID
     */
    public String getUserIdFromToken(String token) {
        return parseToken(token).getSubject();
    }

    /**
     * 校验 Token 是否合法且未过期
     */
    public boolean isTokenValid(String token) {
        try {
            parseToken(token);
            return true;
        } catch (Exception e) {
            log.warn("JWT Token 无效或已过期: {}", e.getMessage());
            return false;
        }
    }
}

配置类:

package com.huohuo.huohuospace.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * JWT 配置属性映射类
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
    
    /**
     * 安全秘钥(HS256 算法要求至少 32 字符/256位)
     */
    private String secret = "huohuospace_matrix_super_secret_key_2024_03_23_0123456789";

    /**
     * 过期时间 (单位:秒) 604800 = 7天
     */
    private Long expiration = 604800L;
}

JwtAuthenticationFilter

为什么需要 JwtAuthenticationFilter?
Spring Security 默认并不认识 JWT。它只处理如表单登录、HTTP Basic、OAuth2 等内置认证方式。
而 JWT 是一种自包含令牌,通常通过 Authorization: Bearer Header 传递。
因此,我们必须:

  • 拦截每个请求;
  • 解析并验证 JWT;
  • 构建 Authentication 对象;
  • 将其设置到 SecurityContextHolder 中,以便后续授权逻辑(如 @PreAuthorize)能正常工作。

这就需要一个自定义过滤器插入到 Spring Security 的过滤器链中。该拦截器继承自 OncePerRequestFilter,以硬性保证当前链路下的每一个 HTTP Request 被执行切面校验一次。

package com.huohuo.huohuospace.config.security;

import com.huohuo.huohuospace.common.utils.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Collections;

/**
 * JWT 前置认证过滤器
 * 用于截获 HTTP 请求报文、解析合法身份凭证并显式注入全局安全上下文
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtils jwtUtils;

    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws ServletException, IOException {

        // 1. 从 HTTP Head 取出 Authorization 认证信息
        String authHeader = request.getHeader("Authorization");
        String jwt = null;
        String userId = null;

        // 2. 根据 JWT RFC 国际规范提取有效载荷
        if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
            jwt = authHeader.substring(7);
            try {
                // 利用工具类防空指针与篡改验证逻辑
                if (jwtUtils.isTokenValid(jwt)) {
                    userId = jwtUtils.getUserIdFromToken(jwt);
                }
            } catch (Exception e) {
                log.warn("Invalid JWT Authorization: {}", e.getMessage());
            }
        }

        // 3. 将 JWT 负载认证体系转译为 Spring Security 官方身份凭据对象
        if (userId != null && SecurityContextHolder.getContext().getAuthentication() == null) {

            // 构建基础抽象用户对象实体
            UserDetails userDetails = User.withUsername(userId)
                    .password("") // 屏蔽原生地态密码校验环节
                    .authorities(Collections.emptyList()) // 后续迭代将对接 DB 获取 RBAC 配置
                    .build();

            // 生成被 Security 支持的原生令牌结构
            UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                    userDetails,
                    null,
                    userDetails.getAuthorities()
            );

            // 补充注入 IP / Source 网络详细参数便于后续日志系统归纳溯源
            authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

            // 将此令牌托管至最高上下文
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }

        // 4. 将控制权归还给 Tomcat 的后续核心级过滤器链
        filterChain.doFilter(request, response);
    }
}

SecurityConfig 配置层

集成 JWT 时,SecurityConfig 的职责尤为关键——它需要禁用传统机制、启用无状态模式、注册自定义 JWT 过滤器,并明确划定哪些路径公开、哪些需要认证。

package com.huohuo.huohuospace.config;

import com.huohuo.huohuospace.config.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * Spring Boot Web Security 顶级装配类
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 停用 CSRF 验证过滤器:架构中已不存在能够被默认浏览器窃取利用的自动携带类凭证机制。
            .csrf(AbstractHttpConfigurer::disable)
            
            // 设定无状态模型:废除内存池 Session 托管能力。
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            
            // 路由权限管控规则统筹
            .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/api/auth/**").permitAll() // 白名单放权网关暴露接口
                    .requestMatchers("/error").permitAll()      // 关闭特定容灾框架底座阻滞
                    .anyRequest().authenticated()               // 对网关外流量严格拉起身份强验
            )
            
            // 将高度自定制化的 JWT 获取校验链并网并强制优先级最高化
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}
  • csrf(AbstractHttpConfigurer::disable)
    关闭自带的 CSRF 工具。前文的理论论证已陈述:基于手工附加至 Request Header 的 JWT Token 的自洽防御能力本身就高过笨重庞大的全局过滤防御器。关闭它是对机器算力的极致释放。
  • sessionManagement(SessionCreationPolicy.STATELESS)
    废弃基于内存和 Cookie 机制建立会话联系的传统模式,保证内存使用不受单体架构请求积压的影响。这也是为什么横向扩容分布式部署必须实施该策略的根本主因。
  • addFilterBefore(...) 的插入点设计策略
    由于系统根本摒弃了 Spring 预设的 HTTP Post 表单认证流机制,如果我们将解析挂载于请求管道后端,会导致早期框架触发未经授权异常并挂起流程。因此采用前置策略注入至 UsernamePasswordAuthenticationFilter 之前(甚至之前更远)。一旦通过即完成全局 AuthToken 染色投递,框架默认身份判断组件检查时,流程自然完美通过。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐