SpringGateway配置使用WebClient编写全局拦截器鉴权

本文介绍了如何在SpringCloudAlibaba2021.0.5.0版本下,使用SpringGateway对非白名单请求进行拦截,通过访问unified-user-service进行权限验证,处理Token失效和有效情况,并利用WebClient实现负载均衡。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文基于spring-cloud-alibaba 2021.0.5.0SpringBoot 2.7.8版本开发,务必注意版本。

功能概括

  1. 使用SpringGateway多所有非白名单请求进行拦截,拦截后访问服务体系内unified-user-service服务进行权限验证,根据响应结果判断是否放行。
  2. Token失效返回403,有效则将返回的用户信息放入请求Header中传递给后端服务。
  3. unified-user-service服务有多个实例,要求使用负载均衡。

代码

注意高版本的SpringGateway是基于WebFlux框架的,非传统阻塞式模型,所以在这里不可以使用Fetgin,要使用WebFlux默认的http工具WebClient

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.inspur.gateway.rest.RestResponse;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
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.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class GlobalAuthFilter implements GlobalFilter, Ordered {

    @Value("${whiteList}")
    private List<String> whiteList;

    @Autowired
    private ReactorLoadBalancerExchangeFilterFunction lbFunction;

    private static final String AUTHORIZATION = "Authorization";
    private static final Logger logger = LoggerFactory.getLogger(GlobalAuthFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String method = exchange.getRequest().getMethodValue().toUpperCase();
        String reqUrl = exchange.getRequest().getURI().toString();
        logger.info("网关接收请求: " + method + " " + reqUrl);

        // 白名单过滤
        String reqPath = exchange.getRequest().getPath().toString();
        AntPathMatcher matcher = new AntPathMatcher();
        for (String whitePattern : whiteList) {
            if (matcher.match(whitePattern, reqPath)) {
                return chain.filter(exchange);
            }
        }

        // 缺失token直接返回
        List<String> authorizationHeader = exchange.getRequest().getHeaders().get(AUTHORIZATION);
        if (CollectionUtils.isEmpty(authorizationHeader) || StringUtils.isBlank(authorizationHeader.get(0))) {
            ServerHttpResponse interruptResponse = exchange.getResponse();
            interruptResponse.setStatusCode(HttpStatus.FORBIDDEN);
            interruptResponse.getHeaders().add("Content-Type", MediaType.APPLICATION_JSON_VALUE);

            RestResponse response = new RestResponse();
            response.setSuccess(false);
            response.setMsg("登录已失效,请重新登录");

            DataBuffer dataBuffer = interruptResponse.bufferFactory().wrap(JSON.toJSONBytes(response));
            return interruptResponse.writeWith(Mono.just(dataBuffer));
        }

        Map<String, String> paramMap = new HashMap<>(2);
        paramMap.put("apiMethod", method.toUpperCase());
        paramMap.put("apiUrl", reqPath);

        // 调用用户中心
        String token = authorizationHeader.get(0);
        return WebClient.builder()
        		// 这里注意和gateway路由配置不同,开启负载均衡也是http开头
                .baseUrl("http://unified-user-service/uus/token/check")
                .defaultUriVariables(paramMap)
                .defaultHeader(AUTHORIZATION, token)
                // 这里需要把负载均衡过滤器绑定到拦截器上,会自动把url里的服务名替换为实际ip
                .filter(lbFunction)
                .build()
                .get()
                .retrieve()
                .bodyToMono(RestResponse.class)
                .flatMap(res -> {
                    // 正常情况放行,并将用户信息放入header里面供后续服务使用
                    ServerHttpRequest modReq = exchange.getRequest().mutate().header("uInfo", JSONObject.toJSONString(res)).build();
                    ServerWebExchange modExc = exchange.mutate().request(modReq).build();
                    return chain.filter(modExc);
                })
                .onErrorResume(ex -> {
                    if (ex instanceof WebClientResponseException.Forbidden) {
                        logger.error("用户中心403,请求 {} {}, token {}", method, reqUrl, token);

                        // 组装异常消息
                        ServerHttpResponse interruptResponse = exchange.getResponse();
                        interruptResponse.setStatusCode(HttpStatus.FORBIDDEN);
                        interruptResponse.getHeaders().add("Content-Type", MediaType.APPLICATION_JSON_VALUE);

                        DataBuffer dataBuffer = interruptResponse.bufferFactory().wrap(((WebClientResponseException) ex).getResponseBodyAsByteArray());
                        return interruptResponse.writeWith(Mono.just(dataBuffer));
                    } else if (ex instanceof WebClientResponseException.InternalServerError) {
                        logger.error("用户中心500,请求 {} {}, token {}", method, reqUrl, token);

                        // 组装异常消息
                        ServerHttpResponse interruptResponse = exchange.getResponse();
                        interruptResponse.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
                        interruptResponse.getHeaders().add("Content-Type", MediaType.APPLICATION_JSON_VALUE);

                        RestResponse response = new RestResponse();
                        response.setSuccess(false);
                        response.setMsg("用户中心内部异常");

                        DataBuffer dataBuffer = interruptResponse.bufferFactory().wrap(JSONObject.toJSONString(response).getBytes(StandardCharsets.UTF_8));
                        return interruptResponse.writeWith(Mono.just(dataBuffer));
                    } else {
                        return Mono.error(ex);
                    }
                });
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

参考文档,强烈建议看官方文档

The ReactiveLoadBalancerClientFilter
Spring WebClient as a Load Balancer Client

在Spring MVC中,WebClient是一个强大的工具,用于RESTful API的客户端操作。以下是使用WebClient的一些最佳实践: 1. **依赖注入**:通常会在Spring配置文件中通过`@Autowired`将WebClient注入到需要它的组件(如Service或Repository),这样可以更好地管理生命周期。 ```java @Autowired private WebClient webClient; ``` 2. **工厂模式**:创建WebClient实例时,可以使用工厂模式封装配置细节,如超时、重试策略等,提高代码复用性和可维护性。 ```java public class WebClientFactory { public WebClient createWebClient() { return WebClient.builder().baseUrl("http://api.example.com").build(); } } ``` 3. **使用断言处理**:发送请求后,可以使用`ExchangeStrategies`和`ResponseSpec`来验证响应,如检查HTTP状态码、内容类型等。 ```java Mono<ServerResponse> response = webClient.get() .uri("/users/{id}", id) .headers(h -> h.accept(MediaType.APPLICATION_JSON)) .exchange(ClientResponse.BUILDER); response .expectStatus(isOk()) .expectBody(String.class).isEqualTo("expected result"); ``` 4. **错误处理和异常处理**:使用try-catch块处理可能出现的网络异常和API返回错误。 ```java try { ServerResponse userResponse = webClient.get().uri("/{username}") .retrieve().bodyToMono(User.class); } catch (HttpClientErrorException e) { // Handle error code and message } ``` 5. **批量请求和并发控制**:可以利用WebClient的流式API进行并行请求,使用`parallelStream()`或`collectList()`方法。 6. **线程安全**:如果在多线程环境中使用,确保WebClient实例是线程安全的,可以通过单例模式或使用WebClient.Builder的withSession方法。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值