Spring Cloud Gateway之全局过滤器在工作中的使用场景
阅读原文时间:2021年11月25日阅读:1

一、使用注意事项

1、全局过滤器作用于所有的路由,不需要单独配置。

2、通过@Order来指定执行的顺序,数字越小,优先级越高。

二、默认全局拦截器的整体架构

三、实战场景,例如,校验token、记录请求参数(可参考这边https://www.cnblogs.com/hyf-huangyongfei/p/12849406.html)、替换负载均衡以后的路由等等

1、校验token

@Slf4j
public class AuthenFilter implements GlobalFilter, Ordered {

@Resource  
private IFeignClient feignClient;

private static final String GATEWAY\_ROUTE\_BEAN = "org.springframework.cloud.gateway.support" +  
        ".ServerWebExchangeUtils.gatewayRoute";

private static final String BEAR\_HEAD = "bear";

@Override  
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {  
    ServerHttpRequest request = exchange.getRequest();  
    String requestUrl = request.getPath().pathWithinApplication().value();  
    //判断过滤器是否执行  
    if (!RequestUtils.isFilter(requestUrl)) {  
        //该请求转发,因为访问/leap,需要展示登录页  
        if (requestUrl.equals("/leap/") || requestUrl.equals("/leap")) {  
            ServerHttpRequest authErrorReq = request.mutate()  
                    .path("/index.html")  
                    .build();  
            ServerWebExchange indexExchange = exchange.mutate().request(authErrorReq).build();  
            return chain.filter(indexExchange);  
        }  
        ResEntity res;  
        ServerHttpResponse response = exchange.getResponse();  
        Map<String, String> cookiesInfo = getCookiesInfo(request);  
        String account = cookiesInfo.get("account");  
        String token = cookiesInfo.get("token");  
        //校验token  
        res = feignClient.verifyToken(token);  
        log.info("校验token:{}", res.getMsg());  
        //如果token失效清除cookies ,让用户解锁或者重新登录  
        if (200 == res.getHttpStatus()) {  
            response.addCookie(ResponseCookie.from("token", token).path("/").build());  
            response.addCookie(ResponseCookie.from("userAccount", account).path("/").build());  
        } else {  
            log.error("网关过滤器AuthenFilter:{}", res.getMsg());  
            //token失效,通过cookies失效告知前端,重新解锁  
            response.addCookie(ResponseCookie.from("token", token).path("/").maxAge(Duration.ofSeconds(0L)).build());  
            response.addCookie(ResponseCookie.from("userAccount", account).path("/").maxAge(Duration.ofSeconds(0L)).build());  
            ServerHttpRequest authErrorReq = request.mutate()  
                    .path("/index.html")  
                    .build();  
            ServerWebExchange indexExchange = exchange.mutate().request(authErrorReq).build();  
            return chain.filter(indexExchange);  
        }

        final ResEntity resEntity = feignClient.findUserByAccount(account);  
        //判断用户是否存在  
        if (200 != resEntity.getHttpStatus() || null == resEntity.getData()) {  
            throw new BusinessException(ExceptionEnum.AUTH\_USER\_NOT\_FOUND, account);  
        }  
        //设置请求头信息  
        exchange = setHeader(exchange, resEntity);

    }

    return chain.filter(exchange);  
}

/\*\*  
 \* 获取cookies中的数据  
 \*  
 \* @param request 请求对象  
 \*/  
private Map<String, String> getCookiesInfo(ServerHttpRequest request) {  
    Map<String, String> map = new HashMap<>();  
    Set<Map.Entry<String, List<HttpCookie>>> cookies = request.getCookies().entrySet();  
    for (Map.Entry<String, List<HttpCookie>> entry : cookies) {  
        if ("userAccount".equals(entry.getKey())) {  
            map.put("account", entry.getValue().get(0).getValue());  
        }  
        if ("token".equals(entry.getKey())) {  
            map.put("token", entry.getValue().get(0).getValue());  
        }  
    }  
    return map;

}

/\*\*  
 \* 设置头信息  
 \* am exchange  
 \*  
 \* @param resEntity  
 \* @return  
 \* @throws UnsupportedEncodingException  
 \*/  
private ServerWebExchange setHeader(ServerWebExchange exchange, ResEntity resEntity) {  
    final HashMap<String, String> claims = Maps.newHashMap();  
    claims.put("jwt", UUID.randomUUID().toString().replaceAll("-", ""));  
    ServerHttpRequest userInfo = null;  
    try {  
        String user = URLEncoder.encode(JSON.toJSONString(resEntity.getData()), "UTF-8");  
        userInfo = exchange.getRequest().mutate()  
                .header(BEAR\_HEAD, JwtHelper.genToken(claims))  
                .header("userInfo", user)  
                .build();  
        exchange = exchange.mutate().request(userInfo).build();  
        //feign拦截器的线程局部变量  
        FeignRequestInterceptor.setContext(user);  
    } catch (UnsupportedEncodingException e) {  
        throw new BusinessException(ExceptionEnum.COMMON\_ENCODE\_EXCEPTION, e, "网关拦截器");  
    }

    return exchange;  
}

/\*\*  
 \* 过滤器的优先级  
 \*  
 \* @return  
 \*/  
@Override  
public int getOrder() {  
    return 4;  
}  

}

@Slf4j
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {

private static final String BEAR\_HEAD = "bear";

private static final String USER\_INFO\_HEAD = "hd-user";

private static final ThreadLocal<String> USER\_INFO = new ThreadLocal<>();

public static void setContext(String userInfo) {  
    USER\_INFO.set(userInfo);  
}

public static void clean() {  
    USER\_INFO.remove();  
}

@Override  
public void apply(RequestTemplate requestTemplate) {  
    final HashMap<String, String> claims = Maps.newHashMap();  
    claims.put("jwt", UUID.randomUUID().toString().replaceAll("-", ""));  
    requestTemplate.header(BEAR\_HEAD, JwtHelper.genToken(claims));  
    if (null != USER\_INFO.get()) {  
        requestTemplate.header(USER\_INFO\_HEAD, USER\_INFO.get());  
    }

}  

}

2、更改负载均衡后的url

@Slf4j
public class VersionControlFilter implements GlobalFilter, Ordered {

private static final int VERSION\_CONTROL\_FILTER\_ORDER = 101001;

private static final String HTTP\_PREFIX = "http://";

private static final String SLASH = "/";

private static final String STAR = "\*";

private static final String COLON = ":";

private final RedisUtil redisUtil;

private final ValueAnnotationUtils valueAnnotationUtils;

public VersionControlFilter(RedisUtil redisUtil, ValueAnnotationUtils valueAnnotationUtils) {  
    this.redisUtil = redisUtil;  
    this.valueAnnotationUtils = valueAnnotationUtils;  
}

@Override  
public int getOrder() {  
    return VERSION\_CONTROL\_FILTER\_ORDER;  
}

@Override  
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {  
    ServerHttpRequest request = exchange.getRequest();  
    //获取远程ip地址  
    InetSocketAddress inetSocketAddress = request.getRemoteAddress();  
    if (null == inetSocketAddress) {  
        return chain.filter(exchange);  
    }  
    String clientIp = inetSocketAddress.getAddress().getHostAddress();  
    //获取path  
    URI uri = request.getURI();  
    String path = uri.getPath();

    //只有非白名单路径才版本控住  
    String requestPath = RequestUtils.getCurrentRequest(request);  
    if (!RequestUtils.isFilter(requestPath)) {  
        //判断redis中是否存在key  
        boolean hasKey =  
                redisUtil.exists(valueAnnotationUtils.getVersionControl() + valueAnnotationUtils.getActiveEnv());  
        if (!hasKey) {  
            redisUtil.set(valueAnnotationUtils.getVersionControl() + valueAnnotationUtils.getActiveEnv(),  
                    JSON.toJSONString(new HashMap<>()));  
        }  
        //先取出原本的key  
        Map<String, String> preMap =  
                JSON.parseObject(redisUtil.get(valueAnnotationUtils.getVersionControl() + valueAnnotationUtils.getActiveEnv()),  
                        HashMap.class);  
        //正常url 例如 /platform/user/me  
        String clientAddress = clientIp + path;  
        String serviceIp = preMap.get(clientAddress);  
        //非正常,匹配正则表达式 例如 /platform/user/\* 或者 /platform/user/\*\*  
        URI requestUrl = exchange.getRequiredAttribute(GATEWAY\_REQUEST\_URL\_ATTR);  
        if (StringUtils.isBlank(serviceIp)) {  
            serviceIp = getRegx(clientIp, path, preMap);  
        }  
        if (StringUtils.isBlank(serviceIp)) {  
            return chain.filter(exchange);  
        }  
        //负载均衡以后的路由地址 例如:http://160.5.34.210:9772/platform/user/me  
        int port = requestUrl.getPort();

        //替换到灰度的版本中  
        StringBuilder forwardAddress = new StringBuilder(HTTP\_PREFIX);  
        forwardAddress.append(serviceIp)  
                .append(COLON)  
                .append(port)  
                .append(path);  
        //追加参数  
        if ("GET".equalsIgnoreCase(request.getMethodValue())) {  
            forwardAddress.append("?").append(uri.getQuery());  
        }  
        log.debug("VersionControlFilter 灰度转发的地址:{}", forwardAddress.toString());  
        try {  
            requestUrl = new URI(forwardAddress.toString());  
        } catch (URISyntaxException e) {  
            log.error("VersionControlFilter URI不合法:{}", requestUrl);  
        }

        exchange.getAttributes().put(GATEWAY\_REQUEST\_URL\_ATTR, requestUrl);  
    }  
    return chain.filter(exchange);  
}

/\*\*  
 \* 匹配正则规则  
 \*  
 \* @param clientIp 客户端ip  
 \* @param path     路径  
 \* @param map      redis中的数据  
 \* @return 服务器地址  
 \*/  
private String getRegx(String clientIp, String path, Map<String, String> map) {  
    String\[\] paths = path.split(SLASH);  
    if (1 > paths.length) {  
        log.error(" VersionControlFilter 请求路径:{}", path);  
        throw new BusinessException(" VersionControlFilter 请求路径不合法");  
    }  
    for (int i = 0; i < paths.length; i++) {  
        StringBuilder clientAddress = new StringBuilder(clientIp);  
        String item = paths\[i\];  
        if (StringUtils.isBlank(item)) {  
            continue;  
        }  
        for (int j = 0; j <= i; j++) {  
            if (StringUtils.isBlank(paths\[j\])) {  
                continue;  
            }  
            if (j == paths.length - 1) {  
                clientAddress.append(SLASH + STAR);  
            } else {  
                clientAddress.append(SLASH).append(paths\[j\]);  
            }  
        }  
        if (i != paths.length - 1) {  
            clientAddress.append(SLASH + STAR + STAR);  
        }

        String serverIp = map.get(clientAddress.toString());  
        if (StringUtils.isNotBlank(serverIp)) {  
            return serverIp;  
        }  
    }  
    return null;  
}

}

注意点:如果开启熔断,要注意熔断的线程隔离级别,否则Feign的请求拦截器在头中放入的数据,下游无法拿到。