Spring Cloud Alibaba OpenFeign + Gateway 权限问题处理案例
在微服务架构中,权限管理是一个核心问题。本案例将详细介绍如何在Spring Cloud Alibaba环境中,结合Gateway网关和OpenFeign实现统一的权限控制和认证信息传递。
一、整体架构设计
客户端请求 → Gateway网关(鉴权) → 微服务A → OpenFeign调用 → 微服务B
二、网关层统一鉴权实现
1. 基于JWT的网关鉴权过滤器
@Component
public class JwtAuthGatewayFilterFactory extends AbstractGatewayFilterFactory<JwtAuthGatewayFilterFactory.Config> {
private final JwtTokenProvider tokenProvider;
public JwtAuthGatewayFilterFactory(JwtTokenProvider tokenProvider) {
super(Config.class);
this.tokenProvider = tokenProvider;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
// 1. 获取token
String token = resolveToken(request);
if (token == null || !tokenProvider.validateToken(token)) {
// 2. token无效,返回401
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 3. token有效,解析用户信息
Claims claims = tokenProvider.getClaimsFromToken(token);
String username = claims.getSubject();
String roles = claims.get("roles", String.class);
// 4. 将用户信息传递给下游服务
ServerHttpRequest.Builder requestBuilder = request.mutate()
.header("X-User-Name", username)
.header("X-User-Roles", roles);
// 5. 继续处理请求
return chain.filter(exchange.mutate().request(requestBuilder.build()).build());
};
}
private String resolveToken(ServerHttpRequest request) {
String bearerToken = request.getHeaders().getFirst("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
public static class Config {
// 配置项
private String headerName = "Authorization";
public String getHeaderName() {
return headerName;
}
public void setHeaderName(String headerName) {
this.headerName = headerName;
}
}
}
2. Gateway配置类
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
// 路由到用户服务
.route("user-service", r -> r.path("/api/users/**")
.filters(f -> f.stripPrefix(1)
.filter(new JwtAuthGatewayFilterFactory(null)) // 注入实际的tokenProvider
)
.uri("lb://user-service"))
// 路由到订单服务
.route("order-service", r -> r.path("/api/orders/**")
.filters(f -> f.stripPrefix(1)
.filter(new JwtAuthGatewayFilterFactory(null))
)
.uri("lb://order-service"))
// 无需鉴权的路由
.route("auth-service", r -> r.path("/api/auth/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://auth-service"))
.build();
}
}
3. 使用Sa-Token进行网关鉴权(替代方案)
@Configuration
public class SaTokenConfigure {
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截地址
.addInclude("/**")
// 排除地址
.addExclude("/api/auth/**")
.addExclude("/favicon.ico")
// 鉴权方法
.setAuth(obj -> {
// 检查是否登录
SaRouter.match("/api/**", r -> StpUtil.checkLogin());
// 权限校验示例
SaRouter.match("/api/admin/**", r -> StpUtil.checkRole("admin"));
})
// 异常处理
.setError(e -> {
ServerWebExchange exchange = WebUtils.getRequest();
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return SaResult.error("未授权访问:" + e.getMessage());
})
// 前置处理:将用户信息放入请求头
.setBeforeAuth(obj -> {
ServerWebExchange exchange = WebUtils.getRequest();
if (StpUtil.isLogin()) {
// 将用户信息传递给下游服务
exchange.mutate().request(builder -> {
builder.header("X-User-Id", StpUtil.getLoginIdAsString());
builder.header("X-User-Token", StpUtil.getTokenValue());
});
}
});
}
}
三、OpenFeign认证信息传递
1. 全局请求拦截器
@Component
public class FeignAuthInterceptor implements RequestInterceptor {
private final ServerWebExchange exchange;
// 使用Reactor的上下文获取当前请求上下文
public FeignAuthInterceptor(ServerWebExchange exchange) {
this.exchange = exchange;
}
@Override
public void apply(RequestTemplate template) {
if (exchange != null) {
// 从当前请求中获取用户信息头
HttpHeaders headers = exchange.getRequest().getHeaders();
// 传递用户身份信息
String username = headers.getFirst("X-User-Name");
String roles = headers.getFirst("X-User-Roles");
String token = headers.getFirst("X-User-Token");
if (StringUtils.hasText(username)) {
template.header("X-User-Name", username);
}
if (StringUtils.hasText(roles)) {
template.header("X-User-Roles", roles);
}
if (StringUtils.hasText(token)) {
template.header("X-User-Token", token);
}
// 如果使用JWT,也可以传递原始的Authorization头
String authorization = headers.getFirst("Authorization");
if (StringUtils.hasText(authorization)) {
template.header("Authorization", authorization);
}
}
}
}
2. 线程上下文传递(针对异步调用)
public class UserContext {
private static final ThreadLocal<String> USER_NAME = new ThreadLocal<>();
private static final ThreadLocal<String> USER_ROLES = new ThreadLocal<>();
private static final ThreadLocal<String> USER_TOKEN = new ThreadLocal<>();
// 工具方法
public static void setUserInfo(String username, String roles, String token) {
USER_NAME.set(username);
USER_ROLES.set(roles);
USER_TOKEN.set(token);
}
public static void clear() {
USER_NAME.remove();
USER_ROLES.remove();
USER_TOKEN.remove();
}
public static String getUsername() {
return USER_NAME.get();
}
public static String getRoles() {
return USER_ROLES.get();
}
public static String getToken() {
return USER_TOKEN.get();
}
}
// 修改拦截器使用线程上下文
@Component
public class FeignAuthInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 优先从线程上下文获取
String username = UserContext.getUsername();
String roles = UserContext.getRoles();
String token = UserContext.getToken();
if (StringUtils.hasText(username)) {
template.header("X-User-Name", username);
}
if (StringUtils.hasText(roles)) {
template.header("X-User-Roles", roles);
}
if (StringUtils.hasText(token)) {
template.header("X-User-Token", token);
}
}
}
// 自定义过滤器将用户信息存储到线程上下文
@Component
public class UserContextFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String username = request.getHeaders().getFirst("X-User-Name");
String roles = request.getHeaders().getFirst("X-User-Roles");
String token = request.getHeaders().getFirst("X-User-Token");
// 存储到线程上下文
UserContext.setUserInfo(username, roles, token);
return chain.filter(exchange).doFinally(signalType -> {
// 请求完成后清除上下文
UserContext.clear();
});
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1;
}
}
四、完整案例:订单服务调用用户服务
1. 订单服务的Feign接口
@FeignClient(value = "user-service", fallback = UserServiceFallback.class)
public interface UserServiceClient {
@GetMapping("/users/{id}")
UserDTO getUserById(@PathVariable("id") Long id);
@GetMapping("/users/checkPermission")
boolean checkUserPermission(@RequestParam("userId") Long userId,
@RequestParam("permission") String permission);
}
// 降级实现
@Component
public class UserServiceFallback implements UserServiceClient {
@Override
public UserDTO getUserById(Long id) {
return new UserDTO(); // 返回空对象或默认值
}
@Override
public boolean checkUserPermission(Long userId, String permission) {
return false; // 默认无权限
}
}
2. 订单服务的控制器
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private UserServiceClient userServiceClient;
@PostMapping
public ResponseEntity<OrderDTO> createOrder(@RequestBody OrderRequest request,
@RequestHeader("X-User-Id") String userIdStr) {
Long userId = Long.parseLong(userIdStr);
// 1. 权限校验:调用用户服务检查权限
boolean hasPermission = userServiceClient.checkUserPermission(userId, "order:create");
if (!hasPermission) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
// 2. 获取用户信息
UserDTO user = userServiceClient.getUserById(userId);
if (user == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
// 3. 创建订单
OrderDTO order = orderService.createOrder(request, user);
return ResponseEntity.ok(order);
}
}
3. 用户服务的控制器
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(@PathVariable Long id,
@RequestHeader("X-User-Name") String currentUsername) {
// 这里可以记录操作日志,包含当前操作用户
log.info("User {} is querying user info for ID: {}", currentUsername, id);
UserDTO user = userService.getUserById(id);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(user);
}
@GetMapping("/checkPermission")
public boolean checkUserPermission(@RequestParam Long userId,
@RequestParam String permission,
@RequestHeader("X-User-Roles") String roles) {
// 1. 管理员角色直接返回true
if (roles != null && roles.contains("ADMIN")) {
return true;
}
// 2. 检查用户是否有指定权限
return userService.checkPermission(userId, permission);
}
}
五、安全增强措施
1. 敏感信息加密传输
@Component
public class SensitiveDataFilter implements GlobalFilter, Ordered {
private final EncryptionService encryptionService;
public SensitiveDataFilter(EncryptionService encryptionService) {
this.encryptionService = encryptionService;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 加密响应中的敏感数据
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
// 解密响应数据
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
// 处理敏感信息加密
String responseBody = new String(content, StandardCharsets.UTF_8);
String encryptedBody = encryptionService.encryptSensitiveFields(responseBody);
return bufferFactory.wrap(encryptedBody.getBytes(StandardCharsets.UTF_8));
}));
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return -2; // 在JWT过滤器之后执行
}
}
2. 访问日志记录
@Component
public class AccessLogFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(AccessLogFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
long startTime = System.currentTimeMillis();
ServerHttpRequest request = exchange.getRequest();
// 记录请求信息,但不记录敏感信息
String path = request.getURI().getPath();
String method = request.getMethodValue();
String username = request.getHeaders().getFirst("X-User-Name") != null ?
request.getHeaders().getFirst("X-User-Name") : "anonymous";
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
long endTime = System.currentTimeMillis();
int statusCode = exchange.getResponse().getStatusCode().value();
// 记录访问日志
log.info("ACCESS_LOG: method={}, path={}, username={}, status={}, time={}ms",
method, path, username, statusCode, (endTime - startTime));
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
六、常见问题及解决方案
1. 认证信息丢失问题
问题:在异步调用时,认证信息无法正确传递
解决方案:使用Reactor上下文或ThreadLocal + 装饰器模式
// Reactor上下文方式
public class ReactorContextUtils {
public static final String USER_CONTEXT_KEY = "user_context";
public static Mono<ServerWebExchange> enrichContext(ServerWebExchange exchange) {
UserContext userContext = new UserContext();
userContext.setUsername(exchange.getRequest().getHeaders().getFirst("X-User-Name"));
// 设置其他属性...
return Mono.just(exchange).contextWrite(ctx -> ctx.put(USER_CONTEXT_KEY, userContext));
}
public static Mono<UserContext> getCurrentUserContext() {
return Mono.deferContextual(ctx -> {
if (ctx.hasKey(USER_CONTEXT_KEY)) {
return Mono.just(ctx.get(USER_CONTEXT_KEY));
}
return Mono.empty();
});
}
}
2. 跨服务权限校验性能问题
问题:频繁调用权限服务导致性能下降
解决方案:缓存权限信息 + 降级策略
@Service
public class CachedPermissionService {
@Autowired
private RedisTemplate<String, Boolean> redisTemplate;
@Autowired
private PermissionServiceClient permissionServiceClient;
// 缓存键前缀
private static final String PERMISSION_CACHE_KEY = "permission:";
public boolean checkPermission(Long userId, String permissionCode) {
String cacheKey = PERMISSION_CACHE_KEY + userId + ":" + permissionCode;
// 先查缓存
Boolean cachedResult = redisTemplate.opsForValue().get(cacheKey);
if (cachedResult != null) {
return cachedResult;
}
// 缓存未命中,调用权限服务
try {
boolean result = permissionServiceClient.checkPermission(userId, permissionCode);
// 缓存结果,设置较短的过期时间(如5分钟)
redisTemplate.opsForValue().set(cacheKey, result, 5, TimeUnit.MINUTES);
return result;
} catch (Exception e) {
// 服务调用失败,使用降级策略(默认拒绝)
log.error("Check permission failed", e);
return false;
}
}
// 用户权限变更时清除缓存
public void clearUserPermissionCache(Long userId) {
Set<String> keys = redisTemplate.keys(PERMISSION_CACHE_KEY + userId + ":*");
if (keys != null && !keys.isEmpty()) {
redisTemplate.delete(keys);
}
}
}
七、完整配置文件示例
application.yml
server:
port: 9000
spring:
application:
name: api-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- JwtAuth
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
- JwtAuth
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/api/auth/**
filters:
- StripPrefix=1
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: ["GET", "POST", "PUT", "DELETE"]
allowedHeaders: "*"
# JWT配置
jwt:
secret: your-secret-key
expiration: 3600000 # 1小时
# OpenFeign配置
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
sentinel:
enabled: true
# Sentinel配置
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8080
port: 8719
通过以上案例,我们实现了在Spring Cloud Alibaba环境中,Gateway网关的统一鉴权以及OpenFeign服务间调用时的认证信息传递,解决了微服务架构中的权限管理问题。这种方案不仅保证了系统的安全性,还提高了开发效率,避免了每个服务重复实现鉴权逻辑。