Spring Cloud Gateway之全局过滤器在工作中的使用场景
一、使用注意事项
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的请求拦截器在头中放入的数据,下游无法拿到。