一、使用注意事項
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的請求攔截器在頭中放入的數據,下游無法拿到。