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

本文基于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

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值