Spring Cloud Alibaba OpenFeign + Gateway 权限问题处理案例

102 阅读6分钟

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服务间调用时的认证信息传递,解决了微服务架构中的权限管理问题。这种方案不仅保证了系统的安全性,还提高了开发效率,避免了每个服务重复实现鉴权逻辑。