JWT过滤器:从单体应用到微服务架构

文章目录

        • [0. 模块划分](#0. 模块划分)
        • [1. 原有的认证鉴权都放到网关服务里吗?](#1. 原有的认证鉴权都放到网关服务里吗?)
        • [2. 为什么不能直接复用原来单体应用的JWT过滤器?](#2. 为什么不能直接复用原来单体应用的JWT过滤器?)
        • [3. 认证服务负责统一认证,网关向其他服务转发请求时怎么携带认证结果?](#3. 认证服务负责统一认证,网关向其他服务转发请求时怎么携带认证结果?)
        • [4. 网关服务下游服务如何获取userId?](#4. 网关服务下游服务如何获取userId?)
        • [5. 网关服务如何设置放行路径,允许跨域?](#5. 网关服务如何设置放行路径,允许跨域?)
        • [6. 核心对比表格](#6. 核心对比表格)

最近在做单体应用的微服务拆分,对网关这一块还不是很熟悉,写个笔记小小记录一下方便回看。


0. 模块划分

之前的单体应用多模块划分:

text 复制代码
demo(父模块,pom.xml)
├── **common**          -- 通用模块(核心工具类、常量、基础配置)
├── **framework**       -- 框架模块(安全、日志、异常处理等)
├── **system**          -- 系统模块(用户/角色/权限等核心业务)
├── **business**        -- 业务模块(可选,存放具体业务功能)
├── **api**             -- API模块(可选,存放对外接口DTO和Feign客户端)
└── **app**             -- 启动模块(主应用,依赖其他模块)

目前的微服务应用架构:

text 复制代码
demo(pom.xml仅做各服务依赖版本的统一管理)
├── common          	-- 通用模块
│	├── common-core     # 通用核心配置/工具模块
│	├── common-db       # 通用数据库配置模块
│	└── common-jwt      # 通用JWT配置/工具模块
│		├── JwtUtils			# JWT 解析工具类(可复用)
│		└── JwtProperties       # JWT 配置属性
│
├── auth-service		-- 认证服务
├── gateway-service		-- 网关服务(负责请求转发)
│	├── JwtAuthGlobalFilter				# 全局过滤器(核心逻辑)
│	├── JwtAuthenticationException 		# 自定义异常
│	├── SkipAuthProperties        		# 放行路径配置(从 yml 读取)
│	└── GatewayGlobalExceptionHandler 	# 统一处理 JWT 异常
│
├── log-service			-- 异步日志服务
├── ponit-service		-- 积分管理服务
├── x-service			-- 核心业务服务1
├── y-service			-- 核心业务服务2
├── statistics-service	-- 数据统计服务
└── user-service		-- 用户管理服务

1. 原有的认证鉴权都放到网关服务里吗?

不。

网关服务只做请求转发,认证服务做JWT认证,都不做RBAC鉴权 ,因为权限应该交由各服务自己 根据业务需求去做粒度更细的控制,如控制层接口上的 @PreAuthotize()

2. 为什么不能直接复用原来单体应用的JWT过滤器?

单体应用是传统的Servlet应用,虽然也能做多线程转发请求,但一对请求和响应由同一个线程全程负责,这期间会阻塞等待,效率不高。

微服务应用是WebFlux响应式编程模型,线程在转发请求后不会阻塞等待,而是继续转发其他请求或响应其他请求的返回结果,请求返回结果时也不一定会是之前负责转发的那个线程做响应,这样没有阻塞等待,效率更高,更适合高并发场景。

基于以上可知,原来单体项目的JWT过滤器是基于Servlet的,是传统的Spring Boot Web项目用的

而现在微服务是Spring Cloud项目,没有Servlet,是WebFlux响应式编程模型,所以原来的JWT过滤器不能用了,要按照WebFlux和Spring Cloud的规范重写一个功能类似但实现细节完全不一样的新的全局服务JWT过滤器。

3. 认证服务负责统一认证,网关向其他服务转发请求时怎么携带认证结果?

网关服务作为整个微服务架构应用的请求入口,前端请求网关服务的url,由网关服务解析token,提取userId放入请求头:Authorization: Bear {Token} -> X-User-Id: 123,最后根据url前缀和路由匹配规则转发请求到下游对应的服务。

其他服务不需要再次解析JWT,可以直接从请求头中获取当前用户信息。

比较一下JWT过滤器重写前后的实现细节:(源码在后面)

原有单体应用的JWT过滤器的业务逻辑:

text 复制代码
1. 从请求头中获取 JWT 令牌(Authorization Bear {token})
2. 解析 JWT 令牌获取用户信息
	2.1 从令牌中获取用户 ID
	2.2 从令牌中获取用户权限
3. 直接构造 UserDetails 对象(包括权限),本项目权限实时性要求不高故不查库加载用户详细信息。
4. 创建认证对象 UsernamePasswordAuthenticationToken
5. 设置认证详情(如 IP 地址、session ID 等),本项目 C 端小程序 + B 端管理员 都走 Token,没有基于 IP 或 session 的安全策略(如异地登录检测),故不需要设置认证详情
6. 将认证信息存入 Security 上下文,这样后续的过滤器/控制器可以直接获取当前用户信息
7. 捕获并处理 JWT 相关异常(这里抛出异常,后续会被全局异常处理器捕获并返回统一错误响应)
	7.1 专门处理 Token 过期异常
	7.2 专门处理 Token 格式错误或签名无效异常
	7.3 谨慎处理其他异常,最好只记录日志,不要中断流程,并清理 Security 上下文
8. 继续过滤器链的执行

目前微服务架构应用的JWT过滤器的业务逻辑:

text 复制代码
1. 放行不需要 token 的接口(如登录接口)
2. 从请求头获取 token
3. 解析 token 获取 userId
	3.1  捕获处理 Token 过期异常
	3.2  捕获处理其他异常
4. 将 userId 放入请求头,转发给下游服务

以下是单体vs微服务中JWT过滤器的源码:

java 复制代码
package org.example.framework.security.filter;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.common.constant.SecurityConstants;
import org.example.common.exception.JwtAuthenticationException;
import org.example.common.util.JwtUtils;
import org.example.framework.security.config.CustomUserDetails;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;

/**
 * JWT认证过滤器
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtils jwtUtils;

    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        // log.debug("JWT Filter processing: " + request.getRequestURI()); // 检查Spring Security配置的加载顺序
        try {
            // 1. 从请求头中获取JWT令牌
            String jwt = getJwtFromRequest(request);

            if (jwt != null && jwtUtils.validateToken(jwt)) {
                // 2. 解析JWT令牌获取用户信息
                // 2.1 从令牌中获取用户ID
                Long userId = jwtUtils.getUserIdFromToken(jwt); // sub

                // 2.2 从令牌中获取用户权限
                List<String> authorities = jwtUtils.getAuthoritiesFromToken(jwt);   // claims

                // 3. 直接构造UserDetails对象(包括权限),本项目权限实时性要求不高故不查库加载用户详细信息。
                // 当前设计:登录时查库 → 权限进 Token → 过滤器直接用
                /*String username = String.valueOf(userId);
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);*/
                UserDetails userDetails = new CustomUserDetails(
                        userId,                     // Long id
                        userId.toString(),          // username, 必须是 String
                        "",                         // JWT 无密码
                        authorities.stream()
                                .map(SimpleGrantedAuthority::new)
                                .collect(Collectors.toList())
                );

                // 4. 创建认证对象
                // 参数说明:
                // - principal: 用户身份(这里是UserDetails)
                // - credentials: 证书/密码(这里为null因为JWT已经验证)
                // - authorities: 用户的权限集合
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(
                                userDetails,
                                null,
                                userDetails.getAuthorities());

                // 5. 设置认证详情(如IP地址、session ID等)
                // 本项目 C 端小程序 + B 端管理员 都走 Token,没有基于 IP 或 session 的安全策略(如异地登录检测),故不需要设置认证详情
                // authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                // 6. 将认证信息存入Security上下文
                // 这样后续的过滤器/控制器可以直接获取当前用户信息
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        } /*catch (Exception e) {
            // 7. 捕获并处理JWT相关异常
            // 这里抛出异常,后续会被全局异常处理器捕获并返回统一错误响应
            throw new JwtAuthenticationException("JWT认证失败: " + e.getMessage(), e);
        }*/ catch (ExpiredJwtException e) {
            // 专门处理Token过期异常
            request.setAttribute("JWT_EXPIRED", e);
        } catch (MalformedJwtException | SignatureException e) {
            // 专门处理Token格式错误或签名无效异常
            request.setAttribute("JWT_INVALID", e);
        } catch (Exception e) {
            // 谨慎处理其他异常,最好只记录日志,不要中断流程
            // 例如:UserDetailsService 可能抛出的 UsernameNotFoundException
            logger.error("Could not set user authentication in security context", e);
            SecurityContextHolder.clearContext();   // 清理上下文
        }

        // 8. 继续过滤器链的执行
        filterChain.doFilter(request, response);
    }

    /**
     * 从请求中获取JWT token
     */
    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader(SecurityConstants.AUTH_HEADER);
        if (bearerToken != null && bearerToken.startsWith(SecurityConstants.TOKEN_PREFIX)) {
            return bearerToken.substring(SecurityConstants.TOKEN_PREFIX.length());
        }
        return null;
    }

}
java 复制代码
package org.example.gateway.filter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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;

import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

/**
 * Gateway 全局 JWT 认证过滤器
 * 职责:解析 JWT Token,提取 userId,放入请求头 X-User-Id 转发给下游服务
 *
 * 注意:本过滤器只做认证(解析 token),不做授权(权限判断由各业务服务自己负责)
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthGlobalFilter implements GlobalFilter, Ordered {

    private final JwtUtils jwtUtils;
    private final SkipAuthProperties skipAuthProperties;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();

        // 1. 放行登录接口(不需要 token)
        if (shouldSkipAuth(path)) {
            log.debug("放行登录接口: {}", path);
            return chain.filter(exchange);
        }

        // Authorization: Bearer <JWT>  →  解析  →  X-User-Id: 123 + X-User-Roles: ["ROLE_ADMIN", "ROLE_USER"]
        // 2. 从请求头获取 token
        String authHeader = request.getHeaders().getFirst("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            log.warn("缺少 Authorization 头或格式错误: {}", path);
            throw new JwtAuthenticationException(ResultCode.JWT_MISSING);
        }

        String token = authHeader.substring(7);

        // 3. 解析 token 获取 userId 和 roles
        Claims claims;
        Long userId;
        List<String> roles;
        try {
            claims = jwtUtils.parseToken(token);
            userId = Long.parseLong(claims.getSubject());
            roles = (List<String>) claims.get("authorities");
        } catch (SignatureException e) {
            log.warn("JWT 签名无效: {}", path);
            throw new JwtAuthenticationException(ResultCode.JWT_INVALID);
        } catch (ExpiredJwtException e) {
            log.warn("JWT 已过期: {}", path);
            throw new JwtAuthenticationException(ResultCode.JWT_EXPIRED);
        } catch (Exception e) {
            log.warn("JWT 解析失败: {}", path, e);
            throw new JwtAuthenticationException(ResultCode.JWT_INVALID);
        }

        // 4. 将 userId 放入请求头,转发给下游服务
        ServerHttpRequest mutatedRequest = request.mutate()
                .header("X-User-Id", String.valueOf(userId))
                .header("X-User-Roles", String.join(",", roles))    // 角色
                .headers(httpHeaders -> httpHeaders.remove("Authorization"))  // 去掉原始 token,下游不需要
                .build();

        return chain.filter(exchange.mutate().request(mutatedRequest).build());
    }

    /**
     * 判断是否需要跳过认证(放行接口)
     * Gateway 中没有 Spring Security,没有 SecurityFilterChain
     */
    private boolean shouldSkipAuth(String path) {
        return skipAuthProperties.getSkipAuthPaths().stream()
                .anyMatch(path::contains);
        /*return path.contains("/auth/login")
                || path.contains("/auth/wechat/login")
                || path.contains("/v3/api-docs")
                || path.contains("/swagger-ui")
                || path.contains("/doc.html");*/
    }

    @Override
    public int getOrder() {
        // 优先级最高,在路由转发之前执行
        return -100;
    }
}
4. 网关服务下游服务如何获取userId?
java 复制代码
// user-service 中获取当前用户
String userId = request.getHeader("X-User-Id");
5. 网关服务如何设置放行路径,允许跨域?

之前单体应用在 SecurityConfig 中设置:

java 复制代码
package org.example.framework.security.config;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.framework.security.filter.JwtAuthenticationFilter;
import org.example.framework.security.handler.AccessDeniedHandlerImpl;
import org.example.framework.security.handler.AuthenticationEntryPointImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Arrays;

/**
 * Spring Security配置
 */
@Slf4j
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        prePostEnabled = true,  // 启用@PreAuthorize
        securedEnabled = true,  // 启用@Secured
        jsr250Enabled = true    // 启用@RolesAllowed(JSR-250标准注解)
)
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Lazy  // 延迟加载,防止循环依赖
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    private final AuthenticationEntryPointImpl authenticationEntryPoint;
    private final AccessDeniedHandlerImpl accessDeniedHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        log.debug("SecurityConfig configuring HttpSecurity");   // 检查Spring Security配置的加载顺序
        http
                .csrf().disable() // 关闭CSRF(RESTful API无需)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态会话,禁用 Session,避免冲突和资源浪费
                .and()
                .authorizeRequests()
                // --- 放行所有 OPTIONS 预检请求 ---
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                // --- 放行SpringDoc的路径(相对context-path) ---
                .antMatchers(
                        "/v3/api-docs",
                        "/v3/api-docs/**",    // API描述文档的JSON数据
                        "/swagger-ui/**",     // Swagger UI的所有资源(HTML, JS, CSS)
                        "/swagger-ui.html",    // Swagger UI主页面,
                        "/webjars/**",           // Swagger 可能需要的静态资源
                        "/swagger-resources/**",    // Swagger 资源
                        "/doc.html"     // Knife4j 主页
                ).permitAll()
                // --- 放行自己的接口,例如登录 ---
                .antMatchers("/auth/login", "/auth/wechat/login").permitAll()
                // --- 指定路径鉴权 ---
                .antMatchers(
                        "/system/**",
                        "/admin/**").hasRole("ADMIN")   // 角色前缀会自动添加"ROLE_"
                // --- 其他安全规则 ---
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)   // 添加JWT过滤器
                .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)    // 添加401异常处理器
                .accessDeniedHandler(accessDeniedHandler)         // 添加403异常处理器
                .and()
                .cors().configurationSource(corsConfigurationSource()); // 启用 CORS,指定配置源
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 密码加密器
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean(); // 暴露AuthenticationManager供登录接口使用
    }

    /**
     * 提供全局 CORS 配置 Bean
     *
     * @return
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOriginPatterns(Arrays.asList("*")); // 允许所有源(开发用)
        configuration.setAllowedMethods(Arrays.asList("*"));        // 允许所有方法
        configuration.setAllowedHeaders(Arrays.asList("*"));        // 允许所有头
        configuration.setAllowCredentials(true);                    // 允许携带凭证(如 cookies)
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

微服务架构中,网关服务作为应用的统一入口,不基于Servlet,没有Spring Security。在配置文件 application.yml 中设置:

yml 复制代码
spring:
  application:
    name: gateway-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848   # Nacos 服务端的地址
        ip: 127.0.0.1                 # 当前服务的地址。自动探测 ip 错误时强制指定
        namespace: public
    gateway:
      routes:
        - id: auth-service-route
          uri: lb://auth-service	# lb:// 表示从 Nacos 负载均衡
          predicates:
            - Path=/auth/**			# 匹配 /api/auth/xxx
          filters:
            - StripPrefix=1			# 去掉第一段路径 /api,转发给 auth-service 时变成 /auth/xxx
      globalcors:                 	# 全局跨域配置
        cors-configurations:
          '[/**]':
            allowedOrigins: "*"     # 允许所有源(开发用)
            allowedMethods: "*"     # 允许所有方法
            allowedHeaders: "*"     # 允许所有头
            allowCredentials: true  # 允许携带凭证(如 cookies)


server:
  port: 8080
  servlet:
    context-path: /api

...

gateway:  # 自定义配置组
  skip-auth-paths:  # 不需要 token 直接放行的路径
    - /auth/login
    - /auth/wechat/login
    - /v3/api-docs
    - /swagger-ui
    - /doc.html

加载配置:

java 复制代码
package org.example.gateway.util;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

@Data
@Component
@ConfigurationProperties(prefix = "gateway")
public class SkipAuthProperties {
    private List<String> skipAuthPaths = new ArrayList<>();
}

6. 核心对比表格
对比维度 单体(Servlet) 微服务(Gateway/WebFlux)
过滤器类 OncePerRequestFilter GlobalFilter
请求对象 HttpServletRequest ServerHttpRequest
响应对象 HttpServletResponse ServerHttpResponse
过滤器链 FilterChain GatewayFilterChain
返回类型 void Mono
修改请求头 需要包装 HttpServletRequestWrapper mutate() 直接修改
异常处理 存入 request attribute,抛给全局异常处理器 直接抛自定义异常,由 ErrorWebExceptionHandler 处理
线程模型 阻塞 I/O(请求线程等待) 非阻塞 I/O(事件驱动)
相关推荐
notfound40432 小时前
解决SpringCloudGateway用户请求超时导致日志未记录情况
java·spring boot·spring·gateway·springcloud
进击切图仔2 小时前
多传感器数据采集系统技术架构
架构·机器人
踩着两条虫2 小时前
VTJ:架构设计模式
前端·架构·ai编程
小谢小哥2 小时前
49-缓存一致性详解
java·后端·架构
青槿吖2 小时前
Sentinel 进阶实战:Feign 整合 + 全局异常 + Nacos 持久化,生产环境直接用
java·开发语言·spring cloud·微服务·云原生·ribbon·sentinel
AI服务老曹2 小时前
【架构深评】打通 X86/ARM 异构屏障:基于 GB28181/RTSP 的企业级 AI 视频管理平台架构解析
arm开发·人工智能·架构
szxinmai主板定制专家2 小时前
基于ARM+FPGA高性能MPSOC 多轴伺服设计方案
arm开发·人工智能·嵌入式硬件·fpga开发·架构
禅思院3 小时前
中篇:构建弹性的异步组件
前端·架构·前端框架
企业架构师老王3 小时前
2026电网与发电企业巡检数据智能分析工具选型指南:从AI模型到实在Agent的架构实战
人工智能·ai·架构