2026-04/20~26技术问题整理

2026-04/20~26技术问题整理

  • [一.MyBatis-Plus 分页查询](#一.MyBatis-Plus 分页查询)
    • 1.问题
    • 2.答案
      • [2.1 核心依赖](#2.1 核心依赖)
      • [2.2 Mapper 接口](#2.2 Mapper 接口)
      • [2.3 XML SQL](#2.3 XML SQL)
      • [2.4 分页插件配置(必须)](#2.4 分页插件配置(必须))
      • [2.5 关键知识点总结](#2.5 关键知识点总结)
        • [2.5.1 为什么用 {ew.sqlSegment}?](#2.5.1 为什么用 {ew.sqlSegment}?)
        • [2.5.2 @Param("page") 和 @Param("ew") 能改吗?](#2.5.2 @Param("page") 和 @Param("ew") 能改吗?)
  • [二. 模拟真实场景下请求的执行](#二. 模拟真实场景下请求的执行)
    • [1. 前置基础-生产级公共模块](#1. 前置基础-生产级公共模块)
      • [1.1 公共模块定位](#1.1 公共模块定位)
      • [1.2 公共模块结构](#1.2 公共模块结构)
      • [1.4 公共依赖 pom.xml](#1.4 公共依赖 pom.xml)
      • [1.5 全局常量类(AuthConstant)](#1.5 全局常量类(AuthConstant))
        • [1. 用途](#1. 用途)
        • [2. 设计原因](#2. 设计原因)
      • [1.6 公共实体:LoginUser(登录用户)](#1.6 公共实体:LoginUser(登录用户))
        • [1. 用途](#1. 用途)
        • [2. 设计原因](#2. 设计原因)
      • [1.7 用户上下文 UserContext](#1.7 用户上下文 UserContext)
        • [1. 用途](#1. 用途)
        • [2. 设计原因](#2. 设计原因)
      • [1.8 生产级 Redis 工具类(RedisUtil)](#1.8 生产级 Redis 工具类(RedisUtil))
        • [1. 用途](#1. 用途)
        • [2. 设计原因](#2. 设计原因)
      • [1.9 生产级 JWT 工具类(JwtUtil)](#1.9 生产级 JWT 工具类(JwtUtil))
        • [1. 用途](#1. 用途)
        • [2. 设计原因](#2. 设计原因)
      • [1.10 Feign 全局拦截器(核心:服务间透传)](#1.10 Feign 全局拦截器(核心:服务间透传))
        • [1. 用途](#1. 用途)
        • [2. 设计原因](#2. 设计原因)
      • [1.11 统一返回体(Result)](#1.11 统一返回体(Result))
        • [1. 用途](#1. 用途)
        • [2. 设计原因](#2. 设计原因)
    • [2. 网关 Gateway 模块-生产级详解](#2. 网关 Gateway 模块-生产级详解)
      • [1. 网关模块-核心定位 & 核心特点](#1. 网关模块-核心定位 & 核心特点)
        • [1.1 核心定位](#1.1 核心定位)
        • [1.2 关键生产特性(必懂)](#1.2 关键生产特性(必懂))
        • [1.3 网关模块目录结构](#1.3 网关模块目录结构)
      • [2. 网关模块-专属依赖 pom.xml](#2. 网关模块-专属依赖 pom.xml)
        • [1. 关键说明](#1. 关键说明)
      • [3. 网关模块-核心组件](#3. 网关模块-核心组件)
        • [1. 自定义配置类 JwtProperties](#1. 自定义配置类 JwtProperties)
          • [1. 作用](#1. 作用)
        • [2. 全局核心过滤器 GlobalAuthFilter](#2. 全局核心过滤器 GlobalAuthFilter)
          • [1. 核心作用](#1. 核心作用)
          • [2. 设计原理](#2. 设计原理)
        • [3. 网关全局配置 GatewayConfig](#3. 网关全局配置 GatewayConfig)
        • [1. 作用](#1. 作用)
        • [4. 网关 application.yml 配置文件](#4. 网关 application.yml 配置文件)
    • [2. 场景 1:用户登录认证(前端 → 网关 → user 服务)](#2. 场景 1:用户登录认证(前端 → 网关 → user 服务))
      • [1. 参与服务](#1. 参与服务)
      • [2. 全流程步骤](#2. 全流程步骤)
      • [3. 全流程逐步骤拆解](#3. 全流程逐步骤拆解)
        • [1. 模拟前端请求](#1. 模拟前端请求)
        • [2. Gateway 网关处理](#2. Gateway 网关处理)
        • [3. user 服务 → SpringSecurity 完整认证链路](#3. user 服务 → SpringSecurity 完整认证链路)
        • [4. 认证成功 → 生成 JWT 令牌](#4. 认证成功 → 生成 JWT 令牌)
        • [5. 生产核心:加载权限 + Redis 缓存](#5. 生产核心:加载权限 + Redis 缓存)
        • [6. 返回结果→前端存储](#6. 返回结果→前端存储)
      • [4. 核心代码实现](#4. 核心代码实现)
        • [1. gateway 全局过滤器(白名单放行)](#1. gateway 全局过滤器(白名单放行))
        • [2. User 服务 SpringSecurity 完整配置(SecurityConfig.java)](#2. User 服务 SpringSecurity 完整配置(SecurityConfig.java))
        • [3.User 服务 自定义用户认证类(UserDetailsServiceImpl.java)](#3.User 服务 自定义用户认证类(UserDetailsServiceImpl.java))
        • [4. User 服务 登录核心业务类(AuthService.java)](#4. User 服务 登录核心业务类(AuthService.java))
        • [5. User 服务 登录 Controller(完整)](#5. User 服务 登录 Controller(完整))
        • [6. Mapper 层 SQL](#6. Mapper 层 SQL)
    • [3. 场景2:登录后前端获取动态角色菜单](#3. 场景2:登录后前端获取动态角色菜单)
      • [1. 核心流程:](#1. 核心流程:)
      • [2. 全流程 极致分步拆解(10 步・无遗漏・带原理)](#2. 全流程 极致分步拆解(10 步・无遗漏・带原理))
        • [1. :前端发起菜单请求](#1. :前端发起菜单请求)
        • [2. Gateway 网关接收请求](#2. Gateway 网关接收请求)
        • [3. Portal 服务接收请求](#3. Portal 服务接收请求)
        • [4. Feign 拦截器自动透传请求头](#4. Feign 拦截器自动透传请求头)
        • [5. User 服务接收 Feign 请求](#5. User 服务接收 Feign 请求)
        • [6. Redis 缓存读取判断](#6. Redis 缓存读取判断)
        • [7. User 服务返回菜单数据](#7. User 服务返回菜单数据)
        • [8. Portal 服务转发结果](#8. Portal 服务转发结果)
      • [3. 全模块完整代码](#3. 全模块完整代码)
        • [1. 公共模块 Feign 拦截器](#1. 公共模块 Feign 拦截器)
        • [2. Portal 服务 Feign 客户端(UserFeign.java)](#2. Portal 服务 Feign 客户端(UserFeign.java))
        • [3. Portal 服务 菜单接口 Controller(PortalController.java)](#3. Portal 服务 菜单接口 Controller(PortalController.java))
        • [4. User 服务 菜单接口补充(UserController.java 新增方法)](#4. User 服务 菜单接口补充(UserController.java 新增方法))
    • [4. 场景3:外部接口调用A服务,A服务通过feign调用B服务,A的ThreadLocal竟然拿到了token信息](#4. 场景3:外部接口调用A服务,A服务通过feign调用B服务,A的ThreadLocal竟然拿到了token信息)
      • [1. 为什么会出现这个诡异现象?](#1. 为什么会出现这个诡异现象?)
        • [1. 根本原因:Tomcat 线程复用 + ThreadLocal 未主动清空](#1. 根本原因:Tomcat 线程复用 + ThreadLocal 未主动清空)
      • [2. 完整链路还原](#2. 完整链路还原)
      • [3. 高危影响](#3. 高危影响)
      • [4. 场景生活化理解](#4. 场景生活化理解)
      • [5. 结合前端发送请求到后端的真实场景,理解线程和 ThreadLocal](#5. 结合前端发送请求到后端的真实场景,理解线程和 ThreadLocal)
        • [1. 通俗解释](#1. 通俗解释)
          • [1. 线程(后端处理前端请求的核心)](#1. 线程(后端处理前端请求的核心))
          • [2. ThreadLocal(线程的私有小口袋)](#2. ThreadLocal(线程的私有小口袋))
        • [2. 真实业务场景(前端→后端请求必用)](#2. 真实业务场景(前端→后端请求必用))
        • [3. 核心铁律](#3. 核心铁律)
        • [4. 完整代码示例](#4. 完整代码示例)
          • [1. 核心工具类:ThreadLocal 存储用户](#1. 核心工具类:ThreadLocal 存储用户)
          • [2. 模拟后端三层架构(Controller→Service→Dao)](#2. 模拟后端三层架构(Controller→Service→Dao))
          • [3. 模拟多线程(前端并发请求)](#3. 模拟多线程(前端并发请求))
          • [4. 运行结果(数据完全隔离)](#4. 运行结果(数据完全隔离))
        • [5. 关键踩坑提醒](#5. 关键踩坑提醒)

一.MyBatis-Plus 分页查询

1.问题

手写分页查询 SQL,入参为 Page 对象 + QueryWrapper 对象,如何实现通用分页?

2.答案

2.1 核心依赖

java 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

2.2 Mapper 接口

java 复制代码
IPage<User> selectUserPage(@Param("page") IPage<User> page, @Param("ew") QueryWrapper<User> queryWrapper);

2.3 XML SQL

xml 复制代码
<select id="selectUserPage" resultType="com.xxx.entity.User">
    SELECT id,username FROM user
    <where>${ew.sqlSegment}</where>
</select>

2.4 分页插件配置(必须)

java 复制代码
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
    return interceptor;
}

2.5 关键知识点总结

2.5.1 为什么用 ${ew.sqlSegment}?

QueryWrapper 会把你写的条件(like/eq/in)自动拼接成 SQL 片段,${ew.sqlSegment} 直接把这段 SQL 插入到 中,MP 已处理 SQL 注入,安全可靠。

2.5.2 @Param("page") 和 @Param("ew") 能改吗?
  1. page:可以改,但不建议

  2. ew:必须叫 ew,这是 MP 解析条件构造器的固定别名

  3. 分页插件自动处理 LIMIT,无需手写

  4. 支持单表 / 连表分页查询

二. 模拟真实场景下请求的执行

1. 前置基础-生产级公共模块

所有服务依赖common公共模块,先写完整代码

1.1 公共模块定位

所有微服务的基础依赖,抽离通用代码、统一规范、解决微服务核心痛点:

复制代码
1. 上下文传递(Feign / 异步线程不丢失)
2. 统一返回格式
3. 统一缓存规范
4. 统一 JWT 鉴权
5. 统一异常处理
6. 服务间请求头透传

1.2 公共模块结构

java 复制代码
common/
├── pom.xml                # 公共依赖
├── constant/              # 全局常量
│   └── AuthConstant.java  # 认证/缓存/请求头常量
├── context/               # 用户上下文
│   └── UserContext.java   # 线程安全用户上下文(核心)
├── pojo/                  # 公共实体
│   ├── LoginUser.java     # 登录用户实体
│   └── Result.java        # 统一返回体
├── util/                  # 工具类
│   ├── RedisUtil.java     # 生产级Redis工具
│   ├── JwtUtil.java       # 生产级JWT工具
│   └── JsonUtil.java      # JSON序列化工具
├── interceptor/           # 拦截器
│   └── FeignHeaderInterceptor.java # Feign透传拦截器
└── exception/             # 全局异常
    └── GlobalExceptionHandler.java

1.4 公共依赖 pom.xml

xml 复制代码
<!-- Web -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <scope>provided</scope>
</dependency>
<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JWT -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<!-- 异步上下文(解决Feign/线程池丢失) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.14.3</version>
</dependency>
<!-- Feign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <scope>provided</scope>
</dependency>
<!-- 工具包 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.25</version>
</dependency>
<!-- Lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

1.5 全局常量类(AuthConstant)

1. 用途

统一管理请求头、缓存 Key、白名单,避免硬编码,生产必备规范

2. 设计原因

微服务多,统一常量防止拼写错误、方便维护

java 复制代码
/**
 * 认证相关常量(所有服务共用)
 * 生产规范:所有固定字符串统一维护,禁止硬编码
 */
public interface AuthConstant {

    // ====================== 网关透传请求头(核心!所有服务依赖) ======================
    /** 用户ID请求头 */
    String HEADER_USER_ID = "x-user-id";
    /** 角色编码请求头 */
    String HEADER_ROLE_CODE = "x-role-code";
    /** 用户名请求头 */
    String HEADER_USERNAME = "x-username";
    /** Token请求头 */
    String HEADER_TOKEN = "Authorization";

    // ====================== Redis缓存Key前缀(统一规范) ======================
    /** 用户菜单缓存Key */
    String REDIS_MENU_KEY = "user:menu:";
    /** 角色权限缓存Key */
    String REDIS_PERM_KEY = "role:permission:";
    /** 第三方鉴权缓存Key */
    String REDIS_THIRD_AUTH_KEY = "third:auth:";

    // ====================== 白名单(网关/业务服务共用) ======================
    List<String> WHITE_URL_LIST = Arrays.asList(
            "/user/login",        // 登录接口
            "/webservice/external",// 第三方公开接口
            "/actuator/**",       // 监控
            "/doc.html",          // 接口文档
            "/v3/api-docs/**"
    );
}

1.6 公共实体:LoginUser(登录用户)

1. 用途

存储当前登录用户信息,所有服务共享

2. 设计原因

微服务全链路传递用户信息,必须实现序列化(Redis/Feign 传输)

java 复制代码
import lombok.Data;
import java.io.Serializable;

/**
 * 全服务共享的登录用户实体
 * 1. 存储用户核心信息
 * 2. 支持序列化(Redis缓存/Feign传递)
 * 3. 所有业务服务从上下文获取该对象
 */
@Data
public class LoginUser implements Serializable {

    private static final long serialVersionUID = 1L;

    /** 用户ID(唯一标识) */
    private Long userId;

    /** 用户名 */
    private String username;

    /** 角色编码:admin-管理员 / user-普通用户 */
    private String roleCode;

    /** 租户ID(多租户系统必备) */
    private Long tenantId;
}

1.7 用户上下文 UserContext

1. 用途

线程安全存储当前用户信息,替代 Spring SecurityContext

2. 设计原因
  1. 普通ThreadLocal:Feign 调用、异步线程、线程池会丢失用户信息

  2. 生产必须用Alibaba TransmittableThreadLocal:支持父子线程 / 线程池 / Feign 传递

  3. 请求结束必须清空:防止内存泄漏、用户串号

java 复制代码
import com.alibaba.ttl.TransmittableThreadLocal;
import cn.hutool.core.util.StrUtil;

/**
 * 【微服务核心组件】全局用户上下文
 * 解决痛点:
 * 1. 普通ThreadLocal在Feign/异步线程中丢失用户信息
 * 2. 全服务统一获取当前登录用户
 * 3. 线程安全,无并发问题
 */
public class UserContext {

    /**
     * 生产专用:TransmittableThreadLocal
     * 支持:父子线程、线程池、Feign调用 上下文传递
     */
    private static final TransmittableThreadLocal<LoginUser> USER_THREAD_LOCAL = new TransmittableThreadLocal<>();

    /**
     * 存储用户信息
     */
    public static void set(LoginUser user) {
        if (user != null) {
            USER_THREAD_LOCAL.set(user);
        }
    }

    /**
     * 获取当前登录用户(所有业务代码通用)
     */
    public static LoginUser get() {
        return USER_THREAD_LOCAL.get();
    }

    /**
     * 获取当前用户ID(快捷方法)
     */
    public static Long getUserId() {
        LoginUser user = get();
        return user == null ? null : user.getUserId();
    }

    /**
     * 清空上下文
     * 【生产强制要求】:请求结束必须调用,防止内存泄漏/用户串号
     */
    public static void clear() {
        USER_THREAD_LOCAL.remove();
    }

    /**
     * 判断是否登录
     */
    public static boolean isLogin() {
        return get() != null && StrUtil.isNotBlank(get().getRoleCode());
    }
}

1.8 生产级 Redis 工具类(RedisUtil)

1. 用途

统一 Redis 操作,支持对象序列化、过期、泛型、异常捕获

2. 设计原因
  1. 原生 RedisTemplate 无泛型、序列化麻烦

  2. 生产必须捕获异常,防止 Redis 故障导致服务雪崩

java 复制代码
import cn.hutool.json.JSONUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
 * 生产级Redis工具类
 * 1. 自动序列化/反序列化对象
 * 2. 统一异常处理(Redis宕机不影响业务)
 * 3. 支持过期时间、泛型获取
 * 4. 所有服务缓存菜单/权限使用
 */
@Component
public class RedisUtil {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    // ====================== 存储对象 ======================
    public <T> void set(String key, T value) {
        try {
            String json = JSONUtil.toJsonStr(value);
            stringRedisTemplate.opsForValue().set(key, json);
        } catch (Exception e) {
            // 生产:Redis异常只打印日志,不抛错,保证服务可用
            e.printStackTrace();
        }
    }

    // ====================== 存储对象(带过期) ======================
    public <T> void set(String key, T value, long timeout, TimeUnit unit) {
        try {
            String json = JSONUtil.toJsonStr(value);
            stringRedisTemplate.opsForValue().set(key, json, timeout, unit);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // ====================== 获取对象(泛型) ======================
    public <T> T get(String key, Class<T> clazz) {
        try {
            String json = stringRedisTemplate.opsForValue().get(key);
            return JSONUtil.toBean(json, clazz);
        } catch (Exception e) {
            return null;
        }
    }

    // ====================== 删除缓存 ======================
    public void delete(String key) {
        try {
            stringRedisTemplate.delete(key);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // ====================== 判断Key是否存在 ======================
    public Boolean hasKey(String key) {
        try {
            return stringRedisTemplate.hasKey(key);
        } catch (Exception e) {
            return false;
        }
    }
}

1.9 生产级 JWT 工具类(JwtUtil)

1. 用途

user 服务生成 Token + gateway 校验 Token,双服务共用

2. 设计原因
  1. JWT 是微服务无状态鉴权标准

  2. 生产必须:验签、过期、异常处理、配置化密钥

  3. 网关只解析不生成,用户服务只生成不解析

java 复制代码
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;

/**
 * 生产级JWT工具类
 * 职责分工:
 * 1. user服务:生成Token(登录成功后)
 * 2. gateway服务:解析/校验Token(所有请求入口)
 * 3. 其他服务:不操作JWT,只拿网关透传的请求头
 */
@Component
public class JwtUtil {

    /**
     * JWT密钥(生产从nacos配置中心读取,禁止硬编码)
     */
    @Value("${jwt.secret}")
    private String secret;

    /**
     * Token过期时间(默认2小时)
     */
    @Value("${jwt.expire:7200}")
    private Long expire;

    // ====================== 生成Token(user服务专用) ======================
    public String generateToken(LoginUser user) {
        return Jwts.builder()
                // 主题:用户ID
                .setSubject(user.getUserId().toString())
                // 自定义载荷:存储用户核心信息(网关解析用)
                .claim("username", user.getUsername())
                .claim("roleCode", user.getRoleCode())
                .claim("tenantId", user.getTenantId())
                // 过期时间
                .setExpiration(new Date(System.currentTimeMillis() + expire * 1000))
                // 签名算法+密钥
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    // ====================== 解析Token(gateway服务专用) ======================
    public Claims parseToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token.replace("Bearer ", ""))
                    .getBody();
        } catch (ExpiredJwtException e) {
            throw new RuntimeException("Token已过期");
        } catch (SignatureException e) {
            throw new RuntimeException("Token签名非法");
        } catch (Exception e) {
            throw new RuntimeException("Token解析失败");
        }
    }
}

1.10 Feign 全局拦截器(核心:服务间透传)

1. 用途

自动把网关的用户请求头传递给下游服务

2. 设计原因

微服务 A 调用微服务 B,请求头会丢失,必须手动透传

java 复制代码
import cn.hutool.core.util.StrUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import static com.common.constant.AuthConstant.*;

/**
 * 【微服务核心】Feign全局请求头拦截器
 * 解决痛点:
 * 服务间Feign调用时,网关传递的 x-user-id 等请求头会丢失
 * 作用:自动把上游请求头,透传给下游所有服务
 * 生效范围:product/portal/inspect 所有Feign调用
 */
@Component
public class FeignHeaderInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 获取当前请求的上下文
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return;
        }

        HttpServletRequest request = attributes.getRequest();

        // 从原请求中,获取网关透传的用户信息
        String userId = request.getHeader(HEADER_USER_ID);
        String roleCode = request.getHeader(HEADER_ROLE_CODE);
        String username = request.getHeader(HEADER_USERNAME);

        // 透传给下游服务(Feign请求头)
        if (StrUtil.isNotBlank(userId)) {
            template.header(HEADER_USER_ID, userId);
        }
        if (StrUtil.isNotBlank(roleCode)) {
            template.header(HEADER_ROLE_CODE, roleCode);
        }
        if (StrUtil.isNotBlank(username)) {
            template.header(HEADER_USERNAME, username);
        }
    }
}

1.11 统一返回体(Result)

1. 用途

所有服务统一返回格式,前端统一解析

2. 设计原因

微服务多接口,返回格式必须一致

java 复制代码
import lombok.Data;
import java.io.Serializable;

/**
 * 全局统一返回结果
 * 所有Controller/Feign接口必须返回该对象
 */
@Data
public class Result<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /** 成功状态码 */
    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 int code;
    /** 提示信息 */
    private String msg;
    /** 数据 */
    private T data;

    // 成功
    public static <T> Result<T> success() {
        return success(null);
    }

    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(SUCCESS);
        result.setMsg("操作成功");
        result.setData(data);
        return result;
    }

    // 失败
    public static <T> Result<T> error(String msg) {
        Result<T> result = new Result<>();
        result.setCode(ERROR);
        result.setMsg(msg);
        return result;
    }

    // 未授权
    public static <T> Result<T> unauthorized() {
        Result<T> result = new Result<>();
        result.setCode(UNAUTHORIZED);
        result.setMsg("未登录或Token过期");
        return result;
    }
}

2. 网关 Gateway 模块-生产级详解

1. 网关模块-核心定位 & 核心特点

1.1 核心定位
  1. 微服务唯一统一入口,所有前端、第三方、服务请求必须经过网关;

  2. 只负责:路由转发、JWT 鉴权、请求头清洗、白名单放行、跨域、限流;

  3. 不处理任何业务逻辑、不访问数据库、不使用 SpringSecurity、不使用 Servlet。

1.2 关键生产特性(必懂)
  1. 架构:Spring Cloud Gateway → WebFlux 响应式

  2. 无 Servlet 容器:没有 HttpServletRequest、没有 ServletContext

  3. 无 ThreadLocal 原生支持:不能用普通 ThreadLocal

  4. 不依赖 SpringSecurity:网关不做认证授权,只做Token 合法性校验

  5. 只依赖公共模块 common 工具(JwtUtil、常量),不写冗余代码

1.3 网关模块目录结构
java 复制代码
gateway/
├── pom.xml                    # 网关专属依赖
├── src/main/java/com/gateway/
│   ├── config/
│   │   ├── GatewayConfig.java   # 全局路由、跨域、网关配置
│   │   └── JwtProperties.java   # JWT 配置参数(配置化)
│   ├── filter/
│   │   └── GlobalAuthFilter.java # 【核心】全局JWT鉴权过滤器
│   ├── util/                    # 复用common工具,无重复代码
│   └── GatewayApplication.java   # 启动类
└── src/main/resources/
    └── application.yml          # 路由、JWT、网关配置

2. 网关模块-专属依赖 pom.xml

1. 关键说明
  1. 禁用 spring-web(Servlet),必须用 spring-webflux

  2. 必须引入 common 公共模块(复用常量、JwtUtil)

  3. 禁用 SpringSecurity 依赖(网关不需要)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>

    <groupId>com.gateway</groupId>
    <artifactId>gateway</artifactId>
    <version>1.0.0</version>

    <dependencies>
        <!-- 核心:Spring Cloud Gateway 网关 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!-- 关键:WebFlux 响应式(网关专属,替代Servlet) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

        <!-- 引入公共模块(复用:常量、JwtUtil、通用工具) -->
        <dependency>
            <groupId>com.common</groupId>
            <artifactId>common</artifactId>
            <version>1.0.0</version>
        </dependency>

        <!-- 注册中心(Nacos/Eureka 按需) -->
        <dependency>
            <groupId>com.al.cloud</groupId>
            <artifactId>nacos-gateway-starter</artifactId>
        </dependency>

        <!-- 工具、Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependency>
</project>

3. 网关模块-核心组件

1. 自定义配置类 JwtProperties
1. 作用
  1. 网关 JWT 密钥、过期时间配置化,不写死代码

  2. 生产环境统一由 Nacos 配置中心管理

  3. 与 common 模块 JwtUtil 配置统一

java 复制代码
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * 网关JWT配置类
 * 生产用途:
 * 1. 配置绑定,避免硬编码密钥
 * 2. 与公共模块JwtUtil参数统一
 * 3. 支持Nacos动态配置
 */
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
    /**
     * JWT 加密密钥
     */
    private String secret;

    /**
     * Token 过期时间(秒)
     */
    private Long expire;
}
2. 全局核心过滤器 GlobalAuthFilter
1. 核心作用
  1. 拦截所有请求

  2. 白名单接口(登录、第三方)直接放行

  3. 校验 JWT 有效性:过期、篡改、非法

  4. 解析 JWT 载荷:userId、roleCode、username

  5. 重构请求头,向下游所有服务透传统一用户头

  6. 拦截非法请求,返回 401 未授权

2. 设计原理
  1. 网关是 WebFlux,使用 GlobalFilter 响应式过滤器

  2. 不使用 Servlet 接口,使用 ServerWebExchange

  3. 不修改源请求,使用 mutate() 重构请求头

  4. 异常统一捕获,防止网关崩溃

java 复制代码
import com.common.constant.AuthConstant;
import com.common.util.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.gateway.GlobalFilter;
import reactor.core.publisher.Mono;

/**
 * 【网关核心】全局JWT鉴权过滤器
 * 作用:
 * 1. 全局拦截所有请求
 * 2. 白名单接口放行
 * 3. 校验JWT合法性
 * 4. 解析用户信息,透传下游服务统一请求头
 * 5. 拦截非法、过期、篡改Token
 *
 * 设计优先级:Order=-100 最先执行
 */
@Configuration
@RequiredArgsConstructor
@Order(-100)
public class GlobalAuthFilter implements GlobalFilter {

    private final JwtUtil jwtUtil;
    private final JwtProperties jwtProperties;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 获取当前请求路径
        String requestPath = exchange.getRequest().getPath().value();

        // 2. 核心:白名单接口直接放行(登录、第三方、监控)
        if (isWhitePath(requestPath)) {
            return chain.filter(exchange);
        }

        // 3. 获取请求头中的Token
        ServerHttpRequest request = exchange.getRequest();
        String token = request.getHeaders().getFirst(AuthConstant.HEADER_TOKEN);

        // 4. 无Token → 直接401
        if (token == null || token.isBlank()) {
            return handleUnauthorized(exchange);
        }

        try {
            // 5. 去除Bearer前缀,解析JWT
            String realToken = token.replace("Bearer ", "");
            Claims claims = jwtUtil.parseToken(realToken);

            // 6. 解析核心用户信息
            String userId = claims.getSubject();
            String roleCode = claims.get("roleCode", String.class);
            String username = claims.get("username", String.class);

            // 7. 【核心】重构请求头,向下游所有服务透传统一标识
            ServerHttpRequest newRequest = request.mutate()
                    .header(AuthConstant.HEADER_USER_ID, userId)
                    .header(AuthConstant.HEADER_ROLE_CODE, roleCode)
                    .header(AuthConstant.HEADER_USERNAME, username)
                    .build();

            // 8. 转发修改后的请求到下游服务
            return chain.filter(exchange.mutate().request(newRequest).build());

        } catch (Exception e) {
            // 9. Token过期、篡改、无效 → 401
            return handleUnauthorized(exchange);
        }
    }

    /**
     * 判断是否为白名单路径
     */
    private boolean isWhitePath(String path) {
        return AuthConstant.WHITE_URL_LIST.stream()
                .anyMatch(white -> path.startsWith(white.replace("**", "")));
    }

    /**
     * 统一返回:未授权401
     */
    private Mono<Void> handleUnauthorized(ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        return response.setComplete();
    }
}
3. 网关全局配置 GatewayConfig
1. 作用
  1. 全局跨域配置(网关统一处理,下游服务无需配置)

  2. 路由规则自动加载

  3. 网关全局参数配置

java 复制代码
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.CompleteCorsConfigurationSource;

/**
 * 网关全局配置
 * 1. 全局跨域(WebFlux响应式)
 * 2. 网关通用配置
 */
@Configuration
public class GatewayConfig {

    /**
     * 网关统一跨域配置
     * 生产关键点:
     * 1. 网关统一处理跨域,下游所有服务关闭跨域
     * 2. 解决前端跨域冲突
     */
    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration corsConfig = new CorsConfiguration();
        corsConfig.setAllowAllOrigin(true);
        corsConfig.setAllowAllMethod(true);
        corsConfig.setAllowAllHeader(true);
        corsConfig.setAllowCredentials(true);

        CompleteCorsConfigurationSource source = new CompleteCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfig);
        return new CorsWebFilter(source);
    }
}
4. 网关 application.yml 配置文件
yaml 复制代码
# Spring 核心配置节点
spring:
  cloud:
    gateway:
      # ===================== 全局跨域配置(网关层统一处理) =====================
      globalcors:
        # 跨域规则配置集合(可配置多套规则,此处匹配所有路径)
        cors-configurations:
          # 匹配所有请求路径:/** 表示任意层级的URL(如 /user/1、/order/list 等)
          '[/**]':
            # 允许的跨域源地址:* 表示允许所有域名(生产环境建议指定具体域名,如https://www.xxx.com)
            allowed-origins: "*"
            # 允许的HTTP请求方法:* 覆盖GET/POST/PUT/DELETE/OPTIONS等所有方法
            allowed-methods: "*"
            # 允许的请求头:* 允许前端携带任意自定义头(如Token、Content-Type、Authorization等)
            allowed-headers: "*"
            # 是否允许跨域请求携带Cookie/Token等凭证信息(true为允许,前端传凭证时必须开启)
            allow-credentials: true

      # ===================== 路由规则配置(核心转发逻辑) =====================
      routes:
        # 路由唯一ID:自定义,建议与微服务名一致(仅用于标识,无业务逻辑)
        - id: user-service
          # 转发目标地址:lb:// 表示启用Spring Cloud负载均衡(Load Balancer)
          # user-service 是注册中心(如Nacos/Eureka)中的微服务名称
          uri: lb://user-service
          # 路由断言(Predicate):判断请求是否匹配当前路由的条件
          predicates:
            # 路径断言:匹配所有以 /user/ 开头的请求(如 /user/login、/user/info/123)被转发到user-service服务.
            - Path=/user/**
          # 路由过滤器(Filter):对请求/响应进行预处理/后处理
          filters:
            # 1. 基于Redis令牌桶的限流过滤器
            - name: RequestRateLimiter
              args:
                # 令牌桶每秒填充速率:允许的平均QPS(每秒处理10个请求)
                redis-rate-limiter.replenishRate: 10
                # 令牌桶最大容量:允许的峰值QPS(突发请求最多处理20个)
                # 说明:桶内令牌用完后,请求会被限流,直到令牌补充
                redis-rate-limiter.burstCapacity: 20
                # 限流键解析器:引用自定义的ipKeyResolver Bean(基于IP维度限流)
                # 需自行实现该Bean(示例见下文)
                key-resolver: "#{@ipKeyResolver}"

2. 场景 1:用户登录认证(前端 → 网关 → user 服务)

1. 参与服务

前端 → Gateway 网关 → User 用户服务

2. 全流程步骤

  1. 前端发送登录请求到网关

  2. gateway:白名单放行,路由转发到user-service

  3. user 服务通过SpringSecurity 完整认证链校验身份

  4. 校验通过:封装用户信息→生成 JWT 令牌

  5. 读取用户角色、菜单、接口权限→永久缓存 Redis(低频数据)

  6. 返回 JWT 给前端,前端本地存储,后续所有请求携带 Token

  7. 异常拦截:账号错误、账号禁用、权限缺失统一报错

3. 全流程逐步骤拆解

1. 模拟前端请求

请求地址:http://localhost:8080/user/login

请求方式:POST

请求体:

bash 复制代码
{"username":"admin","password":"123456"}
2. Gateway 网关处理
  1. 网关是WebFlux 响应式,非 Servlet,无 Security;

  2. 读取全局白名单列表 AuthConstant.WHITE_URL_LIST;

  3. 匹配 /user/login → 直接放行,不校验 JWT、不解析令牌;

  4. 读取路由配置,负载转发到 user-service;

  5. 网关不处理任何账号密码校验,不生成任何用户信息;

  6. 网关不创建任何上下文(无 ThreadLocal)。

  7. 生产原理:登录阶段用户无 Token,网关一旦校验 Token,直接 401 报错,所以登录接口必须全局放行

3. user 服务 → SpringSecurity 完整认证链路
  1. 请求进入 user 服务 SpringSecurity 过滤器链;

  2. 触发自定义 UserDetailsService;

  3. 数据库查询用户信息:账号、密码、状态、角色;

  4. 校验:账号是否存在、账号是否禁用、密码是否匹配;

  5. 校验成功:构建 Authentication 对象,存入SecurityContext;

  6. 校验失败:直接抛出异常,返回前端错误提示

4. 认证成功 → 生成 JWT 令牌
  1. 读取当前用户 ID、用户名、角色编码;

  2. 调用common模块JwtUtil,使用统一密钥生成 JWT;

  3. JWT 载荷存储核心必要信息:userId、roleCode、username;

  4. 不存储敏感数据,保证无状态。

5. 生产核心:加载权限 + Redis 缓存

关键点:菜单、角色权限是低频数据,绝不每次请求查库

  1. 根据roleCode查询数据库→用户菜单列表、角色接口权限列表;

  2. 调用common模块RedisUtil;

  3. 写入永久缓存(无过期,权限修改后手动清空):

    复制代码
     1. 校验 Redis 缓存是否存在:
     
     	.缓存 Key:role:menu:{roleCode}、role:permission:{roleCode}
    
     2. 无缓存:查询数据库 → 写入 Redis永久缓存
    
     3. 存在缓存:直接复用,不查询数据库
    
     4. 生产设计逻辑:
    
     	. 菜单、角色权限为低频修改数据
    
     	. 同角色所有用户共享一份缓存,极致优化性能
    
     	. 权限修改后,人工清空对应角色缓存即可
    
     	. 下次登录 / 访问自动重建缓存。
6. 返回结果→前端存储
  1. 返回统一Result格式;

  2. 携带JWT 令牌;

  3. 前端接收后,存入localStorage/sessionStorage;

  4. 后续所有请求,自动携带 Authorization 头。

4. 核心代码实现

1. gateway 全局过滤器(白名单放行)
java 复制代码
package com.gateway.filter;

import com.common.constant.AuthConstant;
import com.common.util.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 网关全局过滤器 【完整无删减】
 * 核心:登录白名单放行、JWT校验、下游请求头透传
 * 网关:WebFlux架构、无Servlet、无Security
 */
@Component
@Order(-100)
@RequiredArgsConstructor
public class GlobalAuthFilter implements GlobalFilter {

    private final JwtUtil jwtUtil;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 获取请求路径
        String requestPath = exchange.getRequest().getPath().value();

        // 2. 核心:登录、白名单接口 直接放行 【登录核心逻辑】
        boolean isWhite = AuthConstant.WHITE_URL_LIST.stream()
                .anyMatch(pattern -> requestPath.startsWith(pattern.replace("**", "")));
        if (isWhite) {
            return chain.filter(exchange);
        }

        // 3. 非白名单:获取Token
        ServerHttpRequest request = exchange.getRequest();
        String token = request.getHeaders().getFirst(AuthConstant.HEADER_TOKEN);
        if (token == null || token.isBlank()) {
            return reject(exchange);
        }

        // 4. JWT校验 解析
        try {
            String realToken = token.replace("Bearer ", "");
            Claims claims = jwtUtil.parseToken(realToken);

            // 5. 解析用户核心信息
            String userId = claims.getSubject();
            String roleCode = claims.get("roleCode", String.class);
            String username = claims.get("username", String.class);

            // 6. 重构请求头 向下游所有服务透传
            ServerHttpRequest newRequest = request.mutate()
                    .header(AuthConstant.HEADER_USER_ID, userId)
                    .header(AuthConstant.HEADER_ROLE_CODE, roleCode)
                    .header(AuthConstant.HEADER_USERNAME, username)
                    .build();

            // 7. 放行请求
            return chain.filter(exchange.mutate().request(newRequest).build());
        } catch (Exception e) {
            // 8. Token过期、篡改、非法 统一拦截
            return reject(exchange);
        }
    }

    /**
     * 统一返回401 未授权
     */
    private Mono<Void> reject(ServerWebExchange exchange) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        return response.setComplete();
    }
}
2. User 服务 SpringSecurity 完整配置(SecurityConfig.java)
java 复制代码
package com.user.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

/**
 * User服务 专属Security配置
 * 仅登录认证使用 业务服务禁用
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    private final UserDetailsService userDetailsService;

    public SecurityConfig(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 关闭CSRF 微服务无状态必备
                .csrf().disable()
                // 关闭表单登录 无需页面登录
                .formLogin().disable()
                // 权限配置
                .authorizeRequests()
                // 登录接口 永久放行
                .antMatchers("/user/login").permitAll()
                // 其余所有接口 必须认证
                .anyRequest().authenticated();

        // 绑定自定义用户认证逻辑
        http.userDetailsService(userDetailsService);
        return http.build();
    }

    /**
     * 密码加密算法 BCrypt 生产标配
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证管理器  登录认证核心
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}
3.User 服务 自定义用户认证类(UserDetailsServiceImpl.java)
java 复制代码
package com.user.service.impl;

import com.user.mapper.SysUserMapper;
import com.user.entity.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;

/**
 * SpringSecurity 认证核心类 【完整版】
 * 校验账号、状态、角色
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final SysUserMapper sysUserMapper;

    public UserDetailsServiceImpl(SysUserMapper sysUserMapper) {
        this.sysUserMapper = null;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 1. 根据用户名查询数据库
        SysUser user = sysUserMapper.selectByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }

        // 2. 生产必备:校验账号状态 0=禁用
        if (user.getStatus() == 0) {
            throw new RuntimeException("账号已被禁用,请联系管理员");
        }

        // 3. 封装角色信息 前缀ROLE_
        List<GrantedAuthority> authorityList = new ArrayList<>();
        authorityList.add(new SimpleGrantedAuthority("ROLE_" + user.getRoleCode()));

        // 4. 返回Security认证实体
        return new User(
                user.getUsername(),
                user.getPassword(),
                true,
                true,
                true,
                true,
                authorityList
        );
    }
}
4. User 服务 登录核心业务类(AuthService.java)
java 复制代码
package com.user.service;

import com.common.constant.AuthConstant;
import com.common.entity.LoginUser;
import com.common.util.JwtUtil;
import com.common.util.RedisUtil;
import com.user.entity.SysUser;
import com.user.mapper.SysMenuMapper;
import com.user.mapper.SysUserMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import java.util.List;

/**
 * 登录业务层 【完整·修正RBAC·无简化】
 * 核心:角色维度缓存  杜绝用户绑定菜单
 */
@Service
public class AuthService {

    private final AuthenticationManager authenticationManager;
    private final SysUserMapper sysUserMapper;
    private final SysMenuMapper sysMenuMapper;
    private final JwtUtil jwtUtil;
    private final RedisUtil redisUtil;

    public AuthService(AuthenticationManager authenticationManager,
                       SysUserMapper sysUserMapper,
                       SysMenuMapper sysMenuMapper,
                       JwtUtil jwtUtil,
                       RedisUtil redisUtil) {
        this.authenticationManager = authenticationManager;
        this.sysUserMapper = sysUserMapper;
        this.sysMenuMapper = sysMenuMapper;
        this.jwtUtil = jwtUtil;
        this.redisUtil = redisUtil;
    }

    /**
     * 生产级完整登录逻辑
     */
    public LoginUser login(String username, String password) {
        // 1. 构建认证令牌
        UsernamePasswordAuthenticationToken authToken =
                new UsernamePasswordAuthenticationToken(username, password);

        // 2. 启动SpringSecurity认证链路  核心校验账号密码
        Authentication authentication = authenticationManager.authenticate(authToken);

        // 3. 查询完整用户信息
        SysUser sysUser = sysUserMapper.selectByUsername(username);

        // 4. 提取核心角色编码 【核心】
        String roleCode = sysUser.getRoleCode();

        // 5. 读取角色菜单+权限  【角色维度 非用户维度】
        String menuKey = AuthConstant.ROLE_MENU_KEY + roleCode;
        String permKey = AuthConstant.ROLE_PERMISSION_KEY + roleCode;

        // 6. 读取Redis缓存  优先缓存 减少数据库
        List<?> menuList = redisUtil.get(menuKey, List.class);
        List<String> permList = redisUtil.get(permKey, List.class);

        // 7. 缓存不存在  查询数据库并写入永久缓存
        if (menuList == null) {
            menuList = sysMenuMapper.getMenuByRoleCode(roleCode);
            redisUtil.set(menuKey, menuList);
        }
        if (permList == null) {
            permList = sysMenuMapper.getPermByRoleCode(roleCode);
            redisUtil.set(permKey, permList);
        }

        // 8. 封装全局登录实体
        LoginUser loginUser = new LoginUser();
        loginUser.setUserId(sysUser.getId());
        loginUser.setUsername(sysUser.getUsername());
        loginUser.setRoleCode(roleCode);

        return loginUser;
    }
}
5. User 服务 登录 Controller(完整)
java 复制代码
package com.user.controller;

import com.common.entity.Result;
import com.common.entity.LoginUser;
import com.common.util.JwtUtil;
import com.user.dto.LoginDTO;
import com.user.service.AuthService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 登录接口 完整版
 */
@RestController
@RequestMapping("/user")
public class UserController {

    private final AuthService authService;
    private final JwtUtil jwtUtil;

    public UserController(AuthService authService, JwtUtil jwtUtil) {
        this.authService = authService;
    }

    @PostMapping("/login")
    public Result<String> login(@RequestBody LoginDTO dto) {
        try {
            // 1. Security认证+角色权限缓存
            LoginUser loginUser = authService.login(dto.getUsername(), dto.getPassword());
            // 2. 生成JWT令牌
            String token = jwtUtil.generateToken(loginUser);
            // 3. 结果返回
            return Result.success(token);
        } catch (Exception e) {
            // 4. 全局异常捕获
            return Result.error(e.getMessage());
        }
    }
}
6. Mapper 层 SQL
xml 复制代码
<!-- 根据角色编码 查询菜单 -->
<select id="getMenuByRoleCode" resultType="com.user.entity.SysMenu">
    SELECT DISTINCT m.id,m.menu_name,m.parent_id,m.path
    FROM sys_role r
    LEFT JOIN sys_role_menu rm ON r.id = rm.role_id
    LEFT JOIN sys_menu m ON rm.menu_id = m.id
    WHERE r.role_code = #{roleCode}
</select>

<!-- 根据角色编码 查询接口权限 -->
<select id="getPermByRoleCode" resultType="java.lang.String">
    SELECT DISTINCT m.path
    FROM sys_role r
    LEFT JOIN sys_role_menu rm ON r.id = rm.role_id
    LEFT JOIN sys_menu m ON rm.menu_id = m.id
    WHERE r.role_code = #{roleCode}
</select>

3. 场景2:登录后前端获取动态角色菜单

1. 核心流程:

  1. 前端携带 JWT Token,请求门户服务获取菜单

  2. 网关校验 Token 合法性,解析用户角色编码、用户 ID,透传请求头

  3. Portal 服务接收请求,通过 Feign 调用 User 服务

  4. Feign 拦截器自动透传网关传递的角色编码请求头

  5. User 服务读取 Redis「角色维度菜单缓存」,无需查询数据库

  6. 菜单数据经 Portal 服务原路返回前端

  7. 前端根据菜单数据,动态渲染侧边栏(不同角色显示不同菜单)

2. 全流程 极致分步拆解(10 步・无遗漏・带原理)

1. :前端发起菜单请求
  1. 请求地址:http://gateway:8080/portal/getMenu

  2. 请求方式:GET

  3. 请求头:Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...(登录返回的JWT Token)

  4. 无请求体

  5. 原理:登录后前端已存储 Token,所有后续请求必须携带 Token,证明身份

2. Gateway 网关接收请求

1.1 提取请求头中的Authorization Token,去除Bearer 前缀

1.2 调用公共模块JwtUtil,校验 Token 合法性(签名、过期时间)

1.3 校验通过:解析 JWT 载荷中的userId、roleCode、username

1.4 重构请求头,向下游 Portal 服务透传 3 个核心信息:

x-user-id:用户 ID(备用,非菜单查询用)

复制代码
	x-role-code:角色编码(核心,菜单查询唯一依据)
	
	x-username:用户名(备用)

1.5 基于路由配置,负载转发请求至portal-service

1.6 核心原理:网关仅校验 Token、透传信息,不处理菜单业务、不查缓存 / 数据库

3. Portal 服务接收请求
  1. Portal 服务接收网关透传的请求头,提取x-role-code(核心参数)

  2. 调用 Feign 客户端,发起对 User 服务的请求,获取角色菜单

  3. 核心原理:Portal 服务是门户层,仅做请求转发、数据透传,不存储菜单、不查库

4. Feign 拦截器自动透传请求头
  1. 公共模块FeignHeaderInterceptor触发(所有 Feign 调用自动生效)

  2. 读取当前请求(Portal 服务接收的请求)中的x-user-id、x-role-code请求头

  3. 自动将这两个请求头,添加到 Feign 调用的请求中,传递给 User 服务

  4. 核心原理:无需手动传递请求头,Feign 拦截器全局生效,避免服务间调用丢失用户信息

5. User 服务接收 Feign 请求
  1. User 服务接收 Feign 调用,提取请求头中的x-role-code(角色编码)

  2. 拼接 Redis 缓存 Key:role:menu:{roleCode}(角色维度,与场景一缓存 Key 一致)

  3. 调用公共模块RedisUtil,读取 Redis 缓存中的菜单列表

  4. 核心原理:User 服务是唯一维护菜单缓存的服务,其他服务不直接操作菜单缓存

6. Redis 缓存读取判断
  1. 若 Redis 中存在该角色的菜单缓存(role:menu:admin),直接读取缓存数据,不查库

  2. 若缓存不存在(如角色权限刚修改、缓存被清空),则调用SysMenuMapper,根据角色编码查询数据库菜单列表,写入 Redis 永久缓存后,再返回数据

  3. 核心原理:菜单是低频修改数据,永久缓存减少数据库压力,同角色所有用户共享 1 份缓存

7. User 服务返回菜单数据
  1. 封装公共模块Result统一返回体,携带菜单列表数据

  2. 将结果返回给 Portal 服务的 Feign 客户端

8. Portal 服务转发结果
  1. Portal 服务接收 User 服务返回的菜单数据,不做任何修改

  2. 封装统一Result格式,原路返回给前端

3. 全模块完整代码

1. 公共模块 Feign 拦截器
java 复制代码
package com.common.interceptor;

import com.common.constant.AuthConstant;
import cn.hutool.core.util.StrUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 公共Feign全局拦截器 【完整版】
 * 核心:服务间Feign调用时,自动透传网关传递的用户请求头
 * 生效范围:Portal、Product、Inspect所有Feign调用
 */
@Component
public class FeignHeaderInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 1. 获取当前请求上下文(仅MVC服务有效,网关WebFlux无此上下文)
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return; // 非MVC请求(如定时任务),无需透传
        }

        HttpServletRequest request = attributes.getRequest();

        // 2. 提取网关透传的核心请求头,自动透传给下游服务
        String userId = request.getHeader(AuthConstant.HEADER_USER_ID);
        String roleCode = request.getHeader(AuthConstant.HEADER_ROLE_CODE);
        String username = request.getHeader(AuthConstant.HEADER_USERNAME);

        // 3. 透传请求头(非空才透传,避免空值)
        if (StrUtil.isNotBlank(userId)) {
            template.header(AuthConstant.HEADER_USER_ID, userId);
        }
        if (StrUtil.isNotBlank(roleCode)) {
            template.header(AuthConstant.HEADER_ROLE_CODE, roleCode);
        }
        if (StrUtil.isNotBlank(username)) {
            template.header(AuthConstant.HEADER_USERNAME, username);
        }
    }
}
2. Portal 服务 Feign 客户端(UserFeign.java)
java 复制代码
package com.portal.feign;

import com.common.constant.AuthConstant;
import com.common.entity.Result;
import com.user.entity.SysMenu;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;

import java.util.List;

/**
 * Portal服务 调用User服务的Feign客户端 【完整版】
 * 核心:获取角色菜单
 */
@FeignClient(name = "user-service") // 对应User服务注册名
public interface UserFeign {

    /**
     * 调用User服务,获取角色对应的菜单(只读Redis缓存)
     * @param roleCode 网关透传的角色编码(核心参数)
     * @return 角色菜单列表
     */
    @GetMapping("/user/getRoleMenu")
    Result<List<SysMenu>> getRoleMenu(@RequestHeader(AuthConstant.HEADER_ROLE_CODE) String roleCode);
}
3. Portal 服务 菜单接口 Controller(PortalController.java)
java 复制代码
package com.portal.controller;

import com.common.constant.AuthConstant;
import com.common.entity.Result;
import com.portal.feign.UserFeign;
import com.user.entity.SysMenu;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * Portal门户服务 Controller 【完整版】
 * 核心:接收前端菜单请求,Feign调用User服务获取菜单
 */
@RestController
@RequestMapping("/portal")
public class PortalController {

    private final UserFeign userFeign;

    // 构造注入(生产禁用@Autowired,推荐构造注入)
    public PortalController(UserFeign userFeign) {
        this.userFeign = userFeign;
    }

    /**
     * 前端获取动态角色菜单
     * @param roleCode 网关透传的角色编码(核心,用于查询角色菜单)
     * @return 菜单列表,用于前端渲染侧边栏
     */
    @GetMapping("/getMenu")
    public Result<List<SysMenu>> getMenu(@RequestHeader(AuthConstant.HEADER_ROLE_CODE) String roleCode) {
        try {
            // 1. Feign调用User服务,获取角色菜单(自动透传请求头)
            Result<List<SysMenu>> menuResult = userFeign.getRoleMenu(roleCode);
            // 2. 直接返回结果(Portal不处理业务,仅透传)
            return menuResult;
        } catch (Exception e) {
            // 3. 生产级异常处理:捕获Feign调用异常、业务异常
            return Result.error("获取菜单失败:" + e.getMessage());
        }
    }
}
4. User 服务 菜单接口补充(UserController.java 新增方法)
java 复制代码
package com.user.controller;

import com.common.constant.AuthConstant;
import com.common.entity.Result;
import com.common.util.RedisUtil;
import com.user.entity.SysMenu;
import com.user.mapper.SysMenuMapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * User服务 Controller 【新增菜单接口,完整版】
 */
@RestController
@RequestMapping("/user")
public class UserController {

    // 省略场景一中已有的注入(AuthService、JwtUtil)
    private final RedisUtil redisUtil;
    private final SysMenuMapper sysMenuMapper;

    public UserController(AuthService authService, JwtUtil jwtUtil, RedisUtil redisUtil, SysMenuMapper sysMenuMapper) {
        this.authService = authService;
        this.jwtUtil = jwtUtil;
        this.redisUtil = redisUtil;
        this.sysMenuMapper = sysMenuMapper;
    }

    // 省略场景一中已有的login方法...

    /**
     * 供Portal服务Feign调用,获取角色菜单(只读Redis,优先缓存)
     * @param roleCode 角色编码(Feign透传,来自网关)
     * @return 角色菜单列表
     */
    @GetMapping("/getRoleMenu")
    public Result<List<SysMenu>> getRoleMenu(@RequestHeader(AuthConstant.HEADER_ROLE_CODE) String roleCode) {
        try {
            // 1. 拼接角色菜单缓存Key(角色维度,生产标准)
            String menuCacheKey = AuthConstant.ROLE_MENU_KEY + roleCode;

            // 2. 优先读取Redis缓存
            List<SysMenu> menuList = redisUtil.get(menuCacheKey, List.class);

            // 3. 缓存不存在:查询数据库,写入缓存后返回
            if (menuList == null || menuList.isEmpty()) {
                menuList = sysMenuMapper.getMenuByRoleCode(roleCode);
                redisUtil.set(menuCacheKey, menuList); // 永久缓存
            }

            // 4. 返回结果
            return Result.success(menuList);
        } catch (Exception e) {
            return Result.error("获取角色菜单失败:" + e.getMessage());
        }
    }
}

4. 场景3:外部接口调用A服务,A服务通过feign调用B服务,A的ThreadLocal竟然拿到了token信息

1. 为什么会出现这个诡异现象?

1. 根本原因:Tomcat 线程复用 + ThreadLocal 未主动清空
  1. Web 容器(Tomcat、Undertow)是线程池模型

    复制代码
     处理完前端带 Token 的登录用户请求后,线程不会销毁,放回线程池复用。
  2. 你的用户上下文(UserContext)基于 普通 ThreadLocal 存储

    复制代码
     请求结束时,没有执行 remove () 清理
  3. 下一次

    复制代码
     .外部无 Token 的第三方接口请求,刚好命中同一个复用线程
    
     .ThreadLocal 还残留上一个用户的 Token / 租户 / 用户 ID
    
     .导致:无登录请求 → 拿到旧用户上下文 → Feign 自动携带旧 Token 调 B 服务

2. 完整链路还原

  1. 用户登录请求(有 Token)

    复制代码
     → 进入服务 A
     → 拦截器解析 Token,存入 ThreadLocal<UserContext>
     → 接口执行完毕
     ❌ 未清理 ThreadLocal,数据残留在当前工作线程
  2. 线程放回线程池

  3. 外部第三方无 Token 请求访问服务 A

    复制代码
     → Tomcat 分配到同一个旧线程
     → UserContext.get () 读到上一个用户的残留 Token
     → Feign 拦截器自动把旧 Token 塞进请求头
     → 服务 B 错误识别为登录用户

3. 高危影响

  1. 权限错乱:第三方匿名请求,拿着内部用户身份操作数据
  2. 数据越权:跨用户、跨租户数据泄露 / 篡改
  3. 线上严重 Bug:偶现、难以复现(因为依赖线程池随机复用)
  4. 排查极难:时好时坏,并发量越大越容易出现

4. 场景生活化理解

  1. 线程 = 杯子

  2. 线程池=柜子

  3. ThreadLocal 数据 = 杯子里的水

  4. 请求 = 喝水的人

    第一个人喝完水没倒水,杯子放回柜子;

    第二个人来,随手拿了同一个杯子,直接喝到上一个人的剩水。

5. 结合前端发送请求到后端的真实场景,理解线程和 ThreadLocal

1. 通俗解释
1. 线程(后端处理前端请求的核心)
  1. 把后端服务器(Tomcat) 比作一家餐厅:

    .前端发送 HTTP 请求 = 顾客点单

    .线程 = 餐厅的服务员

    .线程池 = 固定的服务员队伍(不用反复招聘)

  2. 处理流程

    前端发请求 → 服务器从线程池分配一个空闲服务员(线程) → 这个服务员全程专属处理你的请求(Controller→Service→Dao)→ 返回结果给前端 → 服务员回到线程池待命。

  3. 关键:一个前端请求,对应一个独立线程,多请求并发就是多线程同时干活,互不干扰。

2. ThreadLocal(线程的私有小口袋)

问题:服务员处理订单时,需要记住用户 ID、登录信息,如果从 Controller 到 Service 到 Dao 都手动传参,代码会非常冗余。

ThreadLocal = 线程的【私有口袋】

  1. 每个线程有自己的独立口袋,别的线程绝对拿不到
  2. 处理请求时,把用户信息放进口袋,全链路随便取,不用传参
  3. 请求处理完,清空口袋,等待下一个任务
  4. 核心作用:多线程下数据隔离、无锁安全、全链路无参传递数据。
2. 真实业务场景(前端→后端请求必用)

这是 ThreadLocal 最经典的使用场景:

  1. 前端登录后,请求携带 Token 发送到后端
  2. 后端拦截器解析出用户 ID / 登录信息
  3. 存入 当前请求对应的线程 的 ThreadLocal
  4. Controller/Service/Dao 直接获取数据,不用在所有方法里传用户 ID
  5. 请求结束,清空 ThreadLocal
3. 核心铁律
  1. 线程隔离:A 线程的数据,B 线程访问不到(绝对安全)
  2. 非共享:不用加锁,性能极高
  3. 必须手动移除:线程池会复用线程,不删除会导致数据污染 / 内存泄漏!
4. 完整代码示例

我们模拟:3 个前端并发请求 → 后端多线程处理 → ThreadLocal 存储用户信息 → 全链路获取数据。

1. 核心工具类:ThreadLocal 存储用户
java 复制代码
/**
 * ThreadLocal 工具类:存放当前请求的用户ID
 * 全链路可用,线程私有
 */
public class UserContext {
    // 初始化 ThreadLocal:存储字符串类型的用户ID
    private static final ThreadLocal<String> USER_LOCAL = new ThreadLocal<>();

    // 存入用户ID
    public static void set(String userId) {
        USER_LOCAL.set(userId);
    }

    // 获取用户ID
    public static String get() {
        return USER_LOCAL.get();
    }

    // 清空数据:必须调用!!!
    public static void remove() {
        USER_LOCAL.remove();
    }
}
2. 模拟后端三层架构(Controller→Service→Dao)
java 复制代码
// 模拟 Dao 层:直接从 ThreadLocal 取用户
class UserDao {
    public void query() {
        String userId = UserContext.get();
        System.out.println("Dao层查询用户:" + userId + " | 线程:" + Thread.currentThread().getName());
    }
}

// 模拟 Service 层
class UserService {
    private final UserDao dao = new UserDao();
    public void doService() {
        String userId = UserContext.get();
        System.out.println("Service层处理请求:" + userId + " | 线程:" + Thread.currentThread().getName());
        dao.query();
    }
}

// 模拟 Controller:接收前端请求
class UserController {
    private final UserService service = new UserService();

    // 接收前端传入的用户ID
    public void handleRequest(String userId) {
        try {
            // 1. 存入ThreadLocal
            UserContext.set(userId);
            System.out.println("Controller接收请求:" + userId + " | 线程:" + Thread.currentThread().getName());
            // 2. 调用Service,无需传参!
            service.doService();
        } finally {
            // 3. 必须清空!防止线程复用导致数据污染
            UserContext.remove();
        }
    }
}
3. 模拟多线程(前端并发请求)
java 复制代码
public class ThreadLocalDemo {
    public static void main(String[] args) {
        UserController controller = new UserController();

        // 模拟3个前端并发请求,用3个独立线程处理
        new Thread(() -> controller.handleRequest("用户1001"), "请求线程-1").start();
        new Thread(() -> controller.handleRequest("用户1002"), "请求线程-2").start();
        new Thread(() -> controller.handleRequest("用户1003"), "请求线程-3").start();
    }
}
4. 运行结果(数据完全隔离)
java 复制代码
Controller接收请求:用户1001 | 线程:请求线程-1
Controller接收请求:用户1002 | 线程:请求线程-2
Service层处理请求:用户1001 | 线程:请求线程-1
Service层处理请求:用户1002 | 线程:请求线程-2
Dao层查询用户:用户1001 | 线程:请求线程-1
Dao层查询用户:用户1002 | 线程:请求线程-2
Controller接收请求:用户1003 | 线程:请求线程-3
Service层处理请求:用户1003 | 线程:请求线程-3
Dao层查询用户:用户1003 | 线程:请求线程-3
5. 关键踩坑提醒

后端使用线程池,线程会被反复复用!

如果不调用 UserContext.remove():

  1. 线程 1 处理完用户 1001,没有清空数据
  2. 线程池复用线程 1 处理新请求(用户 1004)
  3. 新请求会读到旧的用户 1001 → 业务 bug(数据污染)
  4. ✅ 规范:必须在 finally 中调用 remove ()
相关推荐
杜子不疼.2 小时前
【C++ 在线五子棋对战】 - 项目介绍与环境搭建
开发语言·c++
50万马克的面包2 小时前
C 语言第18讲:预处理详解
c语言·开发语言·windows
APIshop2 小时前
Java 调用阿里巴巴商品详情接口实战指南:完整流程与代码实现
java·开发语言
努力努力再努力wz2 小时前
【Qt 入门系列】从应用场景到开发环境:建立对 Qt 的第一层认知
c语言·开发语言·数据库·c++·b树·qt·缓存
无限进步_2 小时前
【C++】红黑树完全解析:从概念到插入与平衡维护
java·c语言·开发语言·数据结构·c++·后端·算法
加勒比海带662 小时前
人工智能前沿——「试问当前国外AI大模型哪家强?」
大数据·开发语言·图像处理·人工智能
雪度娃娃3 小时前
Effective Modern C++——auto
开发语言·c++
无限进步_3 小时前
简单聊聊 C++ 中的 unordered_map 和 unordered_set
c语言·开发语言·数据结构·c++·windows·哈希算法·散列表
LNN20223 小时前
半导体设备 UI 开发工程师:完整工作执行手册
开发语言·python·ui