文章目录
-
-
-
- [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(事件驱动) |