自定义 Spring Gateway GlobalFilter 实现 黑名单、请求鉴权、请求加解密、请求签名验证
通过自定义 Spring Gateway GlobalFilter 实现 黑名单、请求鉴权、请求加解密、请求签名验证
自定义拦截器配置
定义 GatewayFilterProperties
@RefreshScope
@ConfigurationProperties(GatewayFilterProperties.PREFIX)
@Data
public class GatewayFilterProperties {
public static final String PREFIX = "spring.cloud.gateway";
/**
* Specifies the name of the header on where to find the token (i.e.X-Authorization).
*/
private String authorizationHeaderName = RequestAuthorizationFilter.AUTHORIZATION_HEADER;
private String authorizationParamName = RequestAuthorizationFilter.AUTHORIZATION_PARAM;
private String authorizationCookieName = RequestAuthorizationFilter.AUTHORIZATION_PARAM;
/**
* 类似Shiro的过滤链定义,用于初始化默认的过滤规则
*/
private Map<String , String> filterMap = new LinkedHashMap<>();
private RequestCrypto crypto = new RequestCrypto();
private RequestSignature signature = new RequestSignature();
@Data
public static class RequestCrypto {
/**
* 是否启用传输机密性和传输完整性验过滤器
*/
private boolean enabled;
/**
* 白名单,不需要验签,以逗号分割
*/
private String whiteList;
/**
* 加密解密方式,default:默认的行为,internal:内部加密,flksec:弗兰科信息
*/
private CryptoType type = CryptoType.NOOP;
}
@Data
public static class RequestSignature {
/**
* 是否启用请求签名验证过滤器
*/
private boolean enabled;
/**
* 白名单,不需要验签,以逗号分割
*/
private String whiteList;
}
}
定义 GatewayFilterConfiguration 用于初始化配置对象
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties({GatewayFilterProperties.class})
public class GatewayFilterConfiguration {
}
后续逻辑依赖的工具和对象
1、XHeaders 定义了系统额外增加的 Header 名称
public class XHeaders {
/**
* 客户端唯一请求ID
*/
public static final String X_REQUEST_ID = "X-Request-Id";
/**
* JWT Token
*/
public static final String X_AUTHORIZATION = "X-Authorization";
/**
* 经度
*/
public static final String X_LONGITUDE = "X-Longitude";
/**
* 纬度
*/
public static final String X_LATITUDE = "X-Latitude";
/**
* 国家地区编码:http://doc.chacuo.net/iso-3166-1
*/
public static final String X_REGION = "X-Region";
/**
* 最新定位省份
*/
public static final String X_PROVINCE = "X-Province";
/**
* 最新定位城市
*/
public static final String X_CITY = "X-City";
/**
* 最新定位区
*/
public static final String X_AREA = "X-Area";
/**
* 国际化(zh_CN:简体中文、zh_TW:繁体中文、en_US:英语)
*/
public static final String X_LANGUAGE = "X-Language";
/**
* 客户端时区
*/
public static final String X_TIMEZONE = "X-TimeZone";
/**
* 参数签名
*/
public static final String X_SIGN = "X-Sign";
/**
* 请求加密方式,eg: "X-Encrypt": AES
*/
public static final String X_ENCRYPT = "X-Encrypt";
/**
* 客户端应用ID
*/
public static final String X_APP_ID = "X-App-Id";
/**
* 客户端应用渠道
*/
public static final String X_APP_CHANNEL = "X-App-Channel";
/**
* 客户端应用版本号
*/
public static final String X_APP_VERSION = "X-App-Version";
/**
* 客户端设备唯一标识
*/
public static final String X_DEVICE_IMEI = "X-Device-IMEI";
/**
* IOS 6+的设备唯一标识
*/
public static final String X_DEVICE_IDFA = "X-Device-IDFA";
/**
* IOS 设备识别码
*/
public static final String X_DEVICE_OPENUDID = "X-Device-OPENUDID";
/**
* Android id原值
*/
public static final String X_DEVICE_ANDROIDID = "X-Device-ANDROIDID";
/**
* Android Q及更高版本的设备号
*/
public static final String X_DEVICE_OAID = "X-Device-OAID";
/**
* 客户端设备Mac地址
*/
public static final String X_DEVICE_MAC = "X-Device-MAC";
/**
* 客户端设备型号
*/
public static final String X_DEVICE_MODEL = "X-Device-Model";
/**
* 客户端网络类型( 2G、3G、4G、5G、Unknown)
*/
public static final String X_DEVICE_NET_TYPE = "X-Device-NetType";
/**
* 客户端设备系统版本
*/
public static final String X_DEVICE_OS = "X-OS";
/**
* 接口请求来源(用于行为统计)
*/
public static final String X_REFERRER = "X-Referrer";
/**
* 调用此接口的菜单编码(用于PV统计)
*/
public static final String X_FEATURE_CODE = "X-Feature-Code";
/**
* 调用此接口的功能操作编码(用于PV统计)
*/
public static final String X_FEATURE_OPT_CODE = "X-Feature-Opt-Code";
/**
* 账户UID
*/
public static final String X_UID = "X-Uid";
/**
* 用户名
*/
public static final String X_UNAME = "X-Uname";
/**
* 用户Key:用户业务表中的唯一ID
*/
public static final String X_UKEY = "X-Ukey";
/**
* 用户Code:用户业务表中的唯一编码
*/
public static final String X_UCODE = "X-Ucode";
/**
* 角色ID(角色表Id)
*/
public static final String X_RID = "X-Rid";
/**
* 角色Key:角色业务表中的唯一ID
*/
public static final String X_RKEY = "X-Rkey";
/**
* 角色Code:角色业务表中的唯一编码
*/
public static final String X_RCODE = "X-Rcode";
}
GatewayUtils 定义响应处理方法
@Slf4j
public class GatewayUtils {
public static Mono<Void> returnMono(ServerHttpResponse response, ApiRestResponse<String> apiRestResponse) {
byte[] bits = JSONObject.toJSONString(apiRestResponse).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); // 指定编码,否则在浏览器中会中文乱码
return response.writeWith(Mono.just(buffer));
}
public static Mono<Void> returnMono(ServerWebExchange exchange, GatewayFilterChain chain){
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute("startTime");
if (startTime != null){
long executeTime = (System.currentTimeMillis() - startTime);
log.info("耗时:{}ms" , executeTime);
log.info("状态码:{}" , Objects.requireNonNull(exchange.getResponse().getStatusCode()).value());
}
}));
}
}
SymmetricCryptoUtil 定义使用内部加解密模式时使用的加解密工具
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.digest.HMac;
import cn.hutool.crypto.digest.HmacAlgorithm;
import cn.hutool.crypto.symmetric.AES;
import cn.hutool.crypto.symmetric.SM4;
import cn.hutool.crypto.symmetric.SymmetricAlgorithm;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.apache.commons.lang3.StringUtils;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.concurrent.TimeUnit;
/**
* 对称加密工具类
*/
public class SymmetricCryptoUtil {
private static Cache<String, SymmetricCrypto> SYMMETRIC_CRYPTO_CACHE = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterAccess(60, TimeUnit.SECONDS)
.build();
private static Cache<String, HMac> HMAC_CACHE = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterAccess(60, TimeUnit.SECONDS)
.build();
/**
* 获取 SymmetricCrypto
* @param key 密钥
* @param iv 偏移向量,加盐
* @return SymmetricCrypto
*/
public static SymmetricCrypto getSymmetricCrypto(String algorithmType, String mode, String padding, String key, String iv) {
StringJoiner keyJoiner = new StringJoiner("_").add(algorithmType).add(mode).add(padding).add(key).add(iv);
// 构造对称加密器
return SYMMETRIC_CRYPTO_CACHE.get(keyJoiner.toString(), join -> {
String[] keyArr = join.split("_");
String algorithmTypeStr = Objects.toString(keyArr[0], SM4.ALGORITHM_NAME);
String modeStr = Objects.toString(keyArr[1], Mode.ECB.name());
String paddingStr = Objects.toString(keyArr[2], Padding.PKCS5Padding.name());
byte[] keyBytes = StringUtils.isBlank(keyArr[3]) ? null : keyArr[3].getBytes(CharsetUtil.CHARSET_UTF_8);
byte[] ivBytes = StringUtils.isBlank(keyArr[4]) ? null : keyArr[4].getBytes(CharsetUtil.CHARSET_UTF_8);
// 构造SM4加密器
if(SM4.ALGORITHM_NAME.equalsIgnoreCase(algorithmType)){
return new SM4(Mode.valueOf(algorithmTypeStr), Padding.valueOf(keyArr[2]), keyBytes, ivBytes);
}
// 构造AES加密器
if(SymmetricAlgorithm.AES.name().equalsIgnoreCase(algorithmType)){
return new AES(Mode.valueOf(modeStr), Padding.valueOf(paddingStr), keyBytes, ivBytes);
}
return new AES(Mode.valueOf(algorithmTypeStr), Padding.valueOf(keyArr[2]), keyBytes, ivBytes);
});
}
/**
* 获取 SymmetricCrypto
* @param key 密钥
* @param iv 偏移向量,加盐
* @return SymmetricCrypto
*/
public static SymmetricCrypto getSm4(String mode, String padding, String key, String iv) {
return getSymmetricCrypto(SM4.ALGORITHM_NAME, mode, padding, key, iv);
}
/**
* 获取aes
* @param key 密钥,支持三种密钥长度:128、192、256位
* @param iv 偏移向量,加盐
* @return AES
*/
public static SymmetricCrypto getAes(String mode, String padding, String key, String iv) {
return getSymmetricCrypto(SymmetricAlgorithm.AES.name(), mode, padding, key, iv);
}
/**
* 获取aes
* @param hmacAlgorithm Hmac算法
* @param key 密钥,支持三种密钥长度:128、192、256位
* @return AES
*/
public static HMac getHmac(HmacAlgorithm hmacAlgorithm, String key) {
StringJoiner keyJoiner = new StringJoiner("_").add(hmacAlgorithm.getValue()).add(key);
// 构造对称加密器
return HMAC_CACHE.get(keyJoiner.toString(), join -> {
String[] keyArr = join.split("_");
String hmacAlgorithmStr = Objects.toString(keyArr[0], HmacAlgorithm.HmacSM3.getValue());
byte[] keyBytes = keyArr[1].getBytes(CharsetUtil.CHARSET_UTF_8);
return new HMac(hmacAlgorithmStr, keyBytes);
});
}
}
定义响应对象 ApiRestResponse
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.ToString;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* model for interacting with client.
*/
@ApiModel(value = "ApiRestResponse", description = "接口响应对象")
@ToString
@JsonIgnoreProperties(ignoreUnknown = true)
public class ApiRestResponse<T extends Object> {
@ApiModelProperty(name = "code", dataType = "String", value = "成功或异常编码")
private final int code;
@ApiModelProperty(name = "status", dataType = "String", value = "旧接口成功、失败或异常辅助判断标记:success、fail、error", allowableValues = "success,fail,error")
private final String status;
@ApiModelProperty(name = "message", dataType = "String", value = "成功或异常消息")
private final String message;
@ApiModelProperty(name = "data", dataType = "java.lang.Object", value = "成功或异常数据")
private T data;
@ApiModelProperty(name = "error", dataType = "java.util.List<Map<String, String>>", value = "校验失败信息")
private List<Map<String,String>> error;
public ApiRestResponse() {
this.code = ApiCode.SC_SUCCESS.getCode();
this.status = Constants.RT_SUCCESS;
this.message = ApiCode.SC_SUCCESS.getReason();
}
protected ApiRestResponse(final ApiCode code) {
this.code = code.getCode();;
this.status = code.getStatus();
this.message = code.getReason();
}
protected ApiRestResponse(final ApiCode code, final T data) {
this.code = code.getCode();;
this.status = code.getStatus();
this.message = code.getReason();
this.data = data;
}
protected ApiRestResponse(final CustomApiCode code) {
this.code = code.getCode();;
this.status = code.getStatus();
this.message = code.getReason();
}
protected ApiRestResponse(final CustomApiCode code, final T data) {
this.code = code.getCode();;
this.status = code.getStatus();
this.message = code.getReason();
this.data = data;
}
protected ApiRestResponse(final ApiCode code, final String message, final T data) {
this.code = code.getCode();;
this.status = code.getStatus();
this.message = message;
this.data = data;
}
protected ApiRestResponse(final ApiCode code, final String message, final T data, List<Map<String,String>> error) {
this.code = code.getCode();;
this.status = code.getStatus();
this.message = message;
this.data = data;
this.error = error;
}
protected ApiRestResponse(final int code, final String message) {
this(code, Constants.RT_SUCCESS, message);
}
protected ApiRestResponse(final int code, final String status, final String message) {
this.code = code;
this.status = status;
this.message = message;
}
protected ApiRestResponse(final int code, final String message, final T data) {
this(code, Constants.RT_SUCCESS, message, data);
}
protected ApiRestResponse(final int code, final String status, final String message, final T data) {
this.code = code;
this.status = status;
this.message = message;
this.data = data;
}
// success -----------------------------------------------------------------
public static <T> ApiRestResponse<T> success(final String message) {
return of(ApiCode.SC_SUCCESS, message, null);
}
public static <T> ApiRestResponse<T> success(final T data) {
return of(ApiCode.SC_SUCCESS, data);
}
public static <T> ApiRestResponse<T> success(final ApiCode code,final T data) {
return of(code, data);
}
public static <T> ApiRestResponse<T> success(final int code, final String message) {
return of(code, Constants.RT_SUCCESS, message);
}
public static <T> ApiRestResponse<T> success(final ApiCode code, final String message) {
return of(code, message, null);
}
// fail -----------------------------------------------------------------
public static <T> ApiRestResponse<T> fail(final String message) {
return of(ApiCode.SC_FAIL, message, null);
}
public static <T> ApiRestResponse<T> fail(final T data) {
return of(ApiCode.SC_FAIL, data);
}
public static <T> ApiRestResponse<T> fail(final ApiCode code, final T data) {
return of(code, data);
}
public static <T> ApiRestResponse<T> fail(final int code, final String message) {
return of(code, Constants.RT_FAIL, message);
}
public static <T> ApiRestResponse<T> fail(final ApiCode code, final String message) {
return of(code, message, null);
}
// error -----------------------------------------------------------------
public static <T> ApiRestResponse<T> error(final String message) {
return of(ApiCode.SC_INTERNAL_SERVER_ERROR, message, null);
}
public static <T> ApiRestResponse<T> error(final T data) {
return of(ApiCode.SC_INTERNAL_SERVER_ERROR, data);
}
public static <T> ApiRestResponse<T> error(final ApiCode code, final T data) {
return of(code, data);
}
public static <T> ApiRestResponse<T> error(final int code, final String message) {
return of(code, Constants.RT_ERROR, message);
}
public static <T> ApiRestResponse<T> error(final ApiCode code, final String message) {
return of(code, message, null);
}
public static <T> ApiRestResponse<T> error(final ApiCode code, final String message, List<Map<String,String>> error) {
return of(code, message, null, error);
}
// -----------------------------------------------------------------
public static <T> ApiRestResponse<T> of(final ApiCode code) {
return new ApiRestResponse<T>(code);
}
public static <T> ApiRestResponse<T> of(final ApiCode code, final T data) {
return new ApiRestResponse<T>(code, data);
}
public static <T> ApiRestResponse<T> of(final ApiCode code, final String message, final T data) {
return new ApiRestResponse<T>(code, message, data);
}
public static <T> ApiRestResponse<T> of(final ApiCode code, final String message, final T data, List<Map<String,String>> error) {
return new ApiRestResponse<T>(code, message, data, error);
}
public static <T> ApiRestResponse<T> of(final String code, final String message) {
return new ApiRestResponse<T>(Integer.parseInt(code), message);
}
public static <T> ApiRestResponse<T> of(final int code, final String message) {
return new ApiRestResponse<T>(code, message);
}
public static <T> ApiRestResponse<T> of(final String code, final String status, final String message) {
return of(Integer.parseInt(code), status, message, null);
}
public static <T> ApiRestResponse<T> of(final int code, final String status, final String message) {
return of(code, status, message, null);
}
public static <T> ApiRestResponse<T> of(final int code, final String status, final String message, final T data) {
return new ApiRestResponse<T>(code, status, message, data);
}
public static <T> ApiRestResponse<T> of(final CustomApiCode code) {
return new ApiRestResponse<T>(code);
}
public static <T> ApiRestResponse<T> of(final CustomApiCode code, final T data) {
return new ApiRestResponse<T>(code, data);
}
public int getCode() {
return code;
}
public String getStatus() {
return status;
}
public String getMessage() {
return message;
}
public T getData() {
return data;
}
@JsonIgnore
public List<Map<String, String>> getError() {
return error;
}
@JsonIgnore
public boolean isSuccess() {
return status == Constants.RT_SUCCESS || code == ApiCodeValue.SC_SUCCESS;
}
public Map<String, Object> toMap(){
Map<String, Object> rtMap = new HashMap<String, Object>();
rtMap.put("code", code);
rtMap.put("status", status);
rtMap.put("message", message);
rtMap.put("data", data);
return rtMap;
}
}
WebFluxUtils
import lombok.extern.slf4j.Slf4j;
import org.springframework.biz.utils.StringUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequest;
import reactor.core.publisher.Flux;
import javax.servlet.http.HttpServletRequest;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
@Slf4j
public class WebFluxUtils {
private static final String XML_HTTP_REQUEST = "XMLHttpRequest";
private static final String X_REQUESTED_WITH = "X-Requested-With";
private static final String CONTENT_TYPE_JSON = "application/json";
private static final String[] xheaders = new String[]{"X-Forwarded-For", "x-forwarded-for"};
private static final String[] headers = new String[]{"Cdn-Src-Ip", "Proxy-Client-IP", "WL-Proxy-Client-IP", "X-Real-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
private static final String LOCAL_HOST = "localhost";
private static final String LOCAL_IP6 = "0:0:0:0:0:0:0:1";
private static final String LOCAL_IP = "127.0.0.1";
private static final String UNKNOWN = "unknown";
public static boolean isAjaxResponse(ServerHttpRequest request ) {
return isAjaxRequest(request) || isContentTypeJson(request) || isPostRequest(request);
}
public static boolean isObjectRequest(ServerHttpRequest request ) {
return isPostRequest(request) && isContentTypeJson(request);
}
public static boolean isObjectRequest(HttpRequest request ) {
return isPostRequest(request) && isContentTypeJson(request);
}
public static boolean isAjaxRequest(ServerHttpRequest request ) {
return XML_HTTP_REQUEST.equals(request.getHeaders().getFirst(X_REQUESTED_WITH));
}
public static boolean isAjaxRequest(HttpRequest request ) {
return request.getHeaders().get(X_REQUESTED_WITH).contains(XML_HTTP_REQUEST);
}
public static boolean isContentTypeJson(ServerHttpRequest request ) {
return request.getHeaders().get(HttpHeaders.CONTENT_TYPE).contains(CONTENT_TYPE_JSON);
}
public static boolean isContentTypeJson(HttpRequest request ) {
return request.getHeaders().get(HttpHeaders.CONTENT_TYPE).contains(CONTENT_TYPE_JSON);
}
public static boolean isPostRequest(ServerHttpRequest request ) {
return HttpMethod.POST.compareTo(request.getMethod()) == 0;
}
public static boolean isPostRequest(HttpRequest request ) {
return HttpMethod.POST.compareTo(request.getMethod()) == 0;
}
/**
* 2、从Flux<DataBuffer>中获取字符串的方法
* @return 请求体
*/
public static String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest) {
if(serverHttpRequest.getHeaders().getContentLength() == 0) {
return org.apache.commons.lang3.StringUtils.EMPTY;
}
// 获取请求体
Flux<DataBuffer> body = serverHttpRequest.getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
body.subscribe(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
DataBufferUtils.release(buffer);
bodyRef.set(charBuffer.toString());
});
// 获取request body
return bodyRef.get();
}
/**
* 获取请求客户端IP地址,支持代理服务器
* @param request {@link HttpServletRequest} 对象
* @return IP地址
*/
public static String getRemoteAddr(ServerHttpRequest request) {
// 1、获取客户端IP地址,支持代理服务器
String remoteAddr = UNKNOWN;
for (String xheader : xheaders) {
remoteAddr = request.getHeaders().getFirst(xheader);
log.debug(" {} : {} " , xheader, remoteAddr);
if (StringUtils.hasText(remoteAddr) && !UNKNOWN.equalsIgnoreCase(remoteAddr)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if( remoteAddr.indexOf(",") !=-1 ){
remoteAddr = remoteAddr.split(",")[0];
}
break;
}
}
if (!StringUtils.hasText(remoteAddr) || UNKNOWN.equalsIgnoreCase(remoteAddr)) {
for (String header : headers) {
remoteAddr = request.getHeaders().getFirst(header);
log.debug(" {} : {} " , header, remoteAddr);
if(StringUtils.hasText(remoteAddr) && !UNKNOWN.equalsIgnoreCase(remoteAddr)){
break;
}
}
}
// 2、没有取得特定标记的值
if (!StringUtils.hasText(remoteAddr) || UNKNOWN.equalsIgnoreCase(remoteAddr)) {
remoteAddr = request.getRemoteAddress().getAddress().getHostAddress();
}
// 3、判断是否localhost访问
if( LOCAL_HOST.equals(remoteAddr) || LOCAL_IP6.equals(remoteAddr)){
remoteAddr = LOCAL_IP;
}
return remoteAddr;
}
public static boolean isSameSegment(ServerHttpRequest request) {
String localIp = request.getLocalAddress().getAddress().getHostAddress();
String remoteIp = getRemoteAddr(request);
log.info("localIp:{},remoteIp:{} url:{}", localIp, remoteIp, request.getPath().value());
int mask = getIpV4Value("255.255.255.0");
boolean flag = (mask & getIpV4Value(localIp)) == (mask & getIpV4Value(remoteIp));
return flag;
}
public static int getIpV4Value(String ipOrMask) {
byte[] addr = getIpV4Bytes(ipOrMask);
int address1 = addr[3] & 0xFF;
address1 |= ((addr[2] << 8) & 0xFF00);
address1 |= ((addr[1] << 16) & 0xFF0000);
address1 |= ((addr[0] << 24) & 0xFF000000);
return address1;
}
public static byte[] getIpV4Bytes(String ipOrMask) {
try {
String[] addrs = ipOrMask.split("\\.");
int length = addrs.length;
byte[] addr = new byte[length];
for (int index = 0; index < length; index++) {
addr[index] = (byte) (Integer.parseInt(addrs[index]) & 0xff);
}
return addr;
} catch (Exception e) {
}
return new byte[4];
}
public static String getDeviceId(ServerHttpRequest request ) {
HttpHeaders headers = request.getHeaders();
// 1、判断是否 Apple 设备
String deviceId = headers.getFirst(XHeaders.X_DEVICE_IDFA);
if(!StringUtils.hasText(deviceId)) {
deviceId = headers.getFirst(XHeaders.X_DEVICE_OAID);
}
if(!StringUtils.hasText(deviceId)) {
deviceId = headers.getFirst(XHeaders.X_DEVICE_OPENUDID);
}
// 2、判断是否 Android 设备
if(!StringUtils.hasText(deviceId)) {
deviceId = headers.getFirst(XHeaders.X_DEVICE_IMEI);
}
if(!StringUtils.hasText(deviceId)) {
deviceId = headers.getFirst(XHeaders.X_DEVICE_ANDROIDID);
}
if(!StringUtils.hasText(deviceId)) {
deviceId = headers.getFirst(XHeaders.X_DEVICE_OAID);
}
return deviceId;
}
}
/**
* 加密解密方式
*/
public enum CryptoType {
/**
* 默认的行为
*/
NOOP,
/**
* 系统内部加解密
*/
INTERNAL,
/**
* 弗兰科信息
*/
FLKSEC
}
黑名单 GlobalFilter
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.biz.utils.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisOperationTemplate;
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.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.stream.Collectors;
@Component
public class RequestBlacklistFilter implements GlobalFilter, Ordered, InitializingBean {
private static final AntPathMatcher pathMathcer = new AntPathMatcher();
private List<String> ignorePatterns;
@Autowired
private GatewayFilterProperties gatewayProperties;
@Autowired
private RedisOperationTemplate redisOperationTemplate;
@Autowired
private MessageSource messageSource;
@Override
public void afterPropertiesSet() throws Exception {
List<Entry<String, String>> noneEntries = gatewayProperties.getFilterMap().entrySet().stream()
.filter((entry) -> {
String[] array = StringUtils.tokenizeToStringArray(entry.getValue().toString());
return ArrayUtils.contains(array, "anon");
})
.collect(Collectors.toList());
List<String> ignorePatterns = new ArrayList<String>();
if (!CollectionUtils.isEmpty(noneEntries)) {
ignorePatterns = noneEntries.stream().map(mapper -> {
return mapper.getKey();
}).collect(Collectors.toList());
}
this.setIgnorePatterns(ignorePatterns);
}
@Override
public int getOrder() {
// RequestBlacklistFilter > RequestAuthorizationFilter > RequestCryptoFilter > RequestSignOnlyFilter
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 忽略部分请求
if(!CollectionUtils.isEmpty(ignorePatterns)) {
for (String pattern : ignorePatterns) {
if(pathMathcer.match(pattern, exchange.getRequest().getPath().value())) {
return chain.filter(exchange);
}
}
}
ServerHttpRequest serverHttpRequest = exchange.getRequest();
ServerHttpResponse originalResponse = exchange.getResponse();
Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext());
// 1、判断是否 Apple 设备
String deviceId = WebFluxUtils.getDeviceId(serverHttpRequest);
// 3、判断设备ID是否取到
if (!StringUtils.hasText(deviceId)) {
String message = getMessageSource().getMessage("app.blacklist.device.required", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(20017, message));
}
// 4、设备黑名单检查
String appBlacklistKey = BlacklistRedisKey.APP_BLACKLIST.getFunction().apply("device", null);
boolean isMemeber = getRedisOperationTemplate().sHasKey(appBlacklistKey, deviceId);
if (isMemeber){
String message = getMessageSource().getMessage("app.blacklist.device.banned", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(20017, message));
}
return chain.filter(exchange);
}
public RedisOperationTemplate getRedisOperationTemplate() {
return redisOperationTemplate;
}
public MessageSource getMessageSource() {
return messageSource;
}
public void setIgnorePatterns(List<String> ignorePatterns) {
this.ignorePatterns = ignorePatterns;
}
}
请求鉴权 GlobalFilter
/**
* Authenticate requests with header 'X-Authorization: Bearer jwt-token'.
*/
@Component
@Slf4j
public class RequestAuthorizationFilter implements GlobalFilter, Ordered, InitializingBean {
private static final AntPathMatcher pathMathcer = new AntPathMatcher();
/**
* HTTP Authorization header, equal to <code>X-Authorization</code>
*/
public static final String AUTHORIZATION_HEADER = "X-Authorization";
public static final String AUTHORIZATION_PARAM = "token";
private String authorizationHeaderName = AUTHORIZATION_HEADER;
private String authorizationParamName = AUTHORIZATION_PARAM;
private String authorizationCookieName = AUTHORIZATION_PARAM;
private List<String> ignorePatterns;
@Autowired
private GatewayFilterProperties gatewayProperties;
@Autowired
private MessageSource messageSource;
@Override
public void afterPropertiesSet() throws Exception {
this.setAuthorizationHeaderName(gatewayProperties.getAuthorizationHeaderName());
this.setAuthorizationParamName(gatewayProperties.getAuthorizationParamName());
this.setAuthorizationCookieName(gatewayProperties.getAuthorizationCookieName());
List<Entry<String, String>> noneEntries = gatewayProperties.getFilterMap().entrySet().stream()
.filter((entry) -> {
String[] array = StringUtils.tokenizeToStringArray(entry.getValue().toString());
return ArrayUtils.contains(array, "anon");
})
.collect(Collectors.toList());
List<String> ignorePatterns = new ArrayList<String>();
if (!CollectionUtils.isEmpty(noneEntries)) {
ignorePatterns = noneEntries.stream().map(mapper -> {
return mapper.getKey();
}).collect(Collectors.toList());
}
this.setIgnorePatterns(ignorePatterns);
}
@Override
public int getOrder() {
// RequestBlacklistFilter > RequestAuthorizationFilter > RequestCryptoFilter > RequestSignOnlyFilter
return Ordered.HIGHEST_PRECEDENCE + 100;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 忽略部分请求
if(!CollectionUtils.isEmpty(ignorePatterns)) {
for (String pattern : ignorePatterns) {
if(pathMathcer.match(pattern, exchange.getRequest().getPath().value())) {
return chain.filter(exchange);
}
}
}
// 提取 Token
String token = obtainToken(exchange.getRequest());;
// 返回10022状态码和提示信息
if (!StringUtils.hasText(token)) {
ServerHttpResponse response = exchange.getResponse();
Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext());
String message = getMessageSource().getMessage("request.authorization.required", null, locale);
return GatewayUtils.returnMono(response, ApiRestResponse.fail(10022, message));
}
return chain.filter(exchange);
}
protected String obtainToken(ServerHttpRequest request) {
// 从header中获取token
String token = request.getHeaders().getFirst(getAuthorizationHeaderName());
if(StringUtils.hasText(token)){
log.debug("obtain token from header : {}, token : {} ", getAuthorizationHeaderName(), token);
} else {
log.debug("obtain token from header : {}, token is null.", getAuthorizationHeaderName());
}
// 如果header中不存在token,则从参数中获取token
if (StringUtils.isEmpty(token)) {
token = request.getQueryParams().getFirst(getAuthorizationParamName());
log.debug("obtain token from param : {}, token : {} ", getAuthorizationParamName(), token);
}
// 从 cookie 获取 token
if (StringUtils.isEmpty(token)) {
HttpCookie httpCookie = request.getCookies().getFirst(getAuthorizationCookieName());
if (Objects.nonNull(httpCookie)){
token = httpCookie.getValue();
log.debug("obtain token from cookie : {}, token : {} ", getAuthorizationCookieName(), token);
}
}
if(StringUtils.isEmpty(token)) {
log.error("obtain token by header {}, param {}, cookie {} is null.", getAuthorizationHeaderName(), getAuthorizationParamName(), getAuthorizationCookieName());
}
return token;
}
public MessageSource getMessageSource() {
return messageSource;
}
public void setIgnorePatterns(List<String> ignorePatterns) {
this.ignorePatterns = ignorePatterns;
}
public String getAuthorizationHeaderName() {
return authorizationHeaderName;
}
public void setAuthorizationHeaderName(String authorizationHeaderName) {
this.authorizationHeaderName = authorizationHeaderName;
}
public String getAuthorizationParamName() {
return authorizationParamName;
}
public void setAuthorizationParamName(String authorizationParamName) {
this.authorizationParamName = authorizationParamName;
}
public String getAuthorizationCookieName() {
return authorizationCookieName;
}
public void setAuthorizationCookieName(String authorizationCookieName) {
this.authorizationCookieName = authorizationCookieName;
}
}
请求加解密、请求签名验证 GlobalFilter
定义抽象的 GlobalFilter 实现 RequestSafetyFilter
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
public abstract class RequestSafetyFilter implements GlobalFilter, Ordered {
/**
* HTTP Authorization header, equal to <code>X-Authorization</code>
*/
public static final String AUTHORIZATION_HEADER = "X-Authorization";
public static final String AUTHORIZATION_PARAM = "token";
private String authorizationHeaderName = AUTHORIZATION_HEADER;
private String authorizationParamName = AUTHORIZATION_PARAM;
private String authorizationCookieName = AUTHORIZATION_PARAM;
/**
* HTTP Sign header, equal to <code>X-Sign</code>
*/
public static final String SIGN_HEADER = "X-Sign";
public static final String SIGN_PARAM = "sign";
private String signHeaderName = SIGN_HEADER;
private String signParamName = SIGN_PARAM;
private String signCookieName = SIGN_PARAM;
protected String obtainToken(ServerHttpRequest request) {
// 从header中获取token
String token = request.getHeaders().getFirst(getAuthorizationHeaderName());
// 如果header中不存在token,则从参数中获取token
if (!StringUtils.hasText(token)) {
return request.getQueryParams().getFirst(getAuthorizationParamName());
}
// 从 cookie 获取 token
if (!StringUtils.hasText(token)) {
return request.getCookies().getFirst(getAuthorizationCookieName()).getValue();
}
return token;
}
protected String obtainSign(ServerHttpRequest request) {
// 从header中获取sign
String token = request.getHeaders().getFirst(getSignHeaderName());
// 如果header中不存在sign,则从参数中获取sign
if (!StringUtils.hasText(token)) {
return request.getQueryParams().getFirst(getSignParamName());
}
return token;
}
public String getAuthorizationHeaderName() {
return authorizationHeaderName;
}
public void setAuthorizationHeaderName(String authorizationHeaderName) {
this.authorizationHeaderName = authorizationHeaderName;
}
public String getAuthorizationParamName() {
return authorizationParamName;
}
public void setAuthorizationParamName(String authorizationParamName) {
this.authorizationParamName = authorizationParamName;
}
public String getAuthorizationCookieName() {
return authorizationCookieName;
}
public void setAuthorizationCookieName(String authorizationCookieName) {
this.authorizationCookieName = authorizationCookieName;
}
public String getSignHeaderName() {
return signHeaderName;
}
public void setSignHeaderName(String signHeaderName) {
this.signHeaderName = signHeaderName;
}
public String getSignParamName() {
return signParamName;
}
public void setSignParamName(String signParamName) {
this.signParamName = signParamName;
}
public String getSignCookieName() {
return signCookieName;
}
public void setSignCookieName(String signCookieName) {
this.signCookieName = signCookieName;
}
}
RequestCryptoFilter
/**
* 传输机密性和传输完整性验过滤器,确保信息在传输过程中不被未授权方获取或泄露。
* - 传输机密性:SM4(国密对称加密算法)
* - 传输完整性:SM3(国密摘要签名算法)
*/
@Component
@Slf4j
public class RequestCryptoFilter extends RequestSafetyFilter implements InitializingBean {
private static final AntPathMatcher PATH_MATHCER = new AntPathMatcher();
@Setter
private List<String> ignorePatterns;
private final EnumMap<CryptoType, RequestCryptoStrategy> enumMap = new EnumMap<>(CryptoType.class);
@Autowired
private GatewayFilterProperties gatewayProperties;
@Getter
@Autowired
private MessageSource messageSource;
@Autowired
private List<RequestCryptoStrategy> cryptoStrategies;
@Override
public int getOrder() {
// RequestBlacklistFilter > RequestAuthorizationFilter > RequestCryptoFilter > RequestSignOnlyFilter
return Ordered.HIGHEST_PRECEDENCE + 200;
}
@Override
public void afterPropertiesSet() throws Exception {
List<Map.Entry<String, String>> noneEntries = gatewayProperties.getFilterMap().entrySet().stream()
.filter((entry) -> {
String[] array = StringUtils.tokenizeToStringArray(entry.getValue());
// 指定 no-crypto 与指定anon但是没指定crypto的都是要忽略掉的请求
return ArrayUtils.contains(array, "no-crypto") || (ArrayUtils.contains(array, "anon") && !ArrayUtils.contains(array, "crypto"));
})
.collect(Collectors.toList());
List<String> ignorePatterns = new ArrayList<String>();
if (!CollectionUtils.isEmpty(noneEntries)) {
ignorePatterns = noneEntries.stream().map(Map.Entry::getKey).collect(Collectors.toList());
}
this.setIgnorePatterns(ignorePatterns);
enumMap.putAll(cryptoStrategies.stream().collect(Collectors.toMap(RequestCryptoStrategy::getType, strategy -> strategy)));
log.debug("CryptoStrategyProvider:{}", enumMap);
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1、判断 Crypto 是否启用,未启用则直接跳过
if (Objects.isNull(gatewayProperties.getCrypto()) || !gatewayProperties.getCrypto().isEnabled() || Objects.isNull(gatewayProperties.getCrypto().getType())) {
return chain.filter(exchange);
}
// 2、获取 Request 和 Response 对象
ServerHttpRequest originalRequest = exchange.getRequest();
ServerHttpResponse originalResponse = exchange.getResponse();
// 3、忽略部分请求
String path = originalRequest.getPath().value();
log.info("Path:{}", path);
log.info("WhiteList:{}", gatewayProperties.getCrypto().getWhiteList());
if (StringUtils.hasText(gatewayProperties.getCrypto().getWhiteList())) {
for (String pattern : StringUtils.tokenizeToStringArray(gatewayProperties.getCrypto().getWhiteList())) {
if (PATH_MATHCER.match(pattern, path)) {
return chain.filter(exchange);
}
}
}
log.info("IgnorePatterns:{}", ignorePatterns);
if (!CollectionUtils.isEmpty(ignorePatterns)) {
for (String pattern : ignorePatterns) {
if (PATH_MATHCER.match(pattern, path)) {
return chain.filter(exchange);
}
}
}
// 4、获取 Locale
Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext());
exchange.getAttributes().put("startTime", System.currentTimeMillis());
// 5、获取 Sign 并检查是否存在
String sign = obtainSign(exchange.getRequest());
// 5.1、返回1403状态码和提示信息
if (!StringUtils.hasText(sign)) {
String message = getMessageSource().getMessage("request.signature.required", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
}
// 6、获取 Token
String token = obtainToken(exchange.getRequest());
// 7、根据 Provider 进行解密
CryptoType provider = Optional.ofNullable(gatewayProperties.getCrypto().getType()).orElse(CryptoType.NOOP);
RequestCryptoStrategy cryptoStrategy = enumMap.get(provider);
log.debug("CryptoType:{}, cryptoStrategy : {}", provider, cryptoStrategy);
return cryptoStrategy.doExchange(exchange, chain, sign, token);
}
}
RequestCryptoStrategy
import com.tianyin.edu.gateway.setup.crypto.CryptoType;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 请求内容的传输机密性和完整性所需的加解密策略
*/
public interface RequestCryptoStrategy {
/**
* 获取加解密方式
* @return 加解密方式
*/
CryptoType getType();
/**
* 对请求数据进行解密
* @param exchange 请求上下文
* @param chain 过滤器链
* @param sign 签名
* @param token 令牌
* @return 解密后的数据
*/
Mono<Void> doExchange(ServerWebExchange exchange, GatewayFilterChain chain, String sign, String token);
}
AbstractRequestCryptoStrategy
/**
* 加解密抽象实现
*/
@Slf4j
public abstract class AbstractRequestCryptoStrategy implements RequestCryptoStrategy {
private static final Set<HttpMethod> NEED_FILTER_METHOD_SET = Sets.newHashSet(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT);
private static final Set<MediaType> NEED_FILTER_MEDIA_TYPE_SET = Sets.newHashSet(MediaType.APPLICATION_JSON);
@Getter
private MessageSource messageSource;
public AbstractRequestCryptoStrategy(MessageSource messageSource) {
this.messageSource = messageSource;
}
@Override
public Mono<Void> doExchange(ServerWebExchange exchange, GatewayFilterChain chain, String sign, String token) {
log.debug("Start to decrypt request data ...");
// 获取请求体
ServerHttpRequest originalRequest = exchange.getRequest();
// 获取响应体
ServerHttpResponse originalResponse = exchange.getResponse();
// 请求头
HttpHeaders headers = originalRequest.getHeaders();
// 请求方法
HttpMethod method = originalRequest.getMethod();
// Locale
Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext());
// 满足条件,进行过滤
if (isNeedFilterMethod(method) && isNeedFilterContentType(headers.getContentType())) {
// 2、获取查询参数
MultiValueMap<String, String> queryParams = originalRequest.getQueryParams();
log.debug("Encrypted Request Query Params : {}", queryParams);
// 2.1、加密算法类型,系统支持 SM4【必须】
String algorithmType = queryParams.getFirst("algorithmType");
// 2.2、采用的加密模式,系统支持 ecb,cbc,cfb,ofb,推荐 cbc,国密不允许使用 ecb 模式【必须】
String encMode = StringUtils.defaultIfBlank(queryParams.getFirst("encMode"), "ecb").toUpperCase();
// 2.3、加密运算所采用的填充模式,系统支持 PKCS5Padding 和 NoPadding,推荐 PKCS5Padding【必须】
String padMode = queryParams.getFirst("padMode");
// 2.4、Base64 格式的密钥字符串【必须】
String key = queryParams.getFirst("key");
key = StringUtils.isNoneBlank(key) ? Base64.decodeStr(key) : key;
// 2.5、Base64 格式的初始向量,加密模式为 cbc,cfb,ofb 时该参数不能为空,解码后长度 为 16 位,可自定义【非必须】
String iv = StringUtils.defaultIfBlank(queryParams.getFirst("iv"), null);
iv = StringUtils.isNoneBlank(iv) ? Base64.decodeStr(iv) : iv;
// 2.6、明文是否进行了 base64 编码 true/false 【非必须】
boolean plainIsEncode = Optional.ofNullable(queryParams.getFirst("plainIsEncode")).map(Boolean::parseBoolean).orElse(false);
// 2.7、进行参数校验
if (StringUtils.isEmpty(algorithmType)) {
String message = getMessageSource().getMessage("request.crypto.algorithmType.required", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
}
if (StringUtils.isEmpty(encMode)) {
String message = getMessageSource().getMessage("request.crypto.encMode.required", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
}
if (StringUtils.isEmpty(padMode)) {
String message = getMessageSource().getMessage("request.crypto.padMode.required", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
}
if (StringUtils.isEmpty(key)) {
String message = getMessageSource().getMessage("request.crypto.key.required", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
}
// 处理没有请求体的情况
if (originalRequest.getHeaders().getContentLength() == 0) {
// 处理没有请求体的情况
return handleRequestWithoutBody(exchange, chain, locale, token, sign, algorithmType, encMode, padMode, key, iv, plainIsEncode);
}
// 处理有请求体的情况
return handleRequestWithBody(exchange, chain, locale, token, sign, algorithmType, encMode, padMode, key, iv, plainIsEncode);
}
// cryptoSwitch = 0
// cryptoType = NOOP、INTERNAL、FLKSEC
// 不满足条件,直接跳过
return chain.filter(exchange);
}
private Mono<Void> handleRequestWithoutBody(ServerWebExchange exchange, GatewayFilterChain chain, Locale locale, String token, String sign,
String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode) {
try {
// 获取请求体
ServerHttpRequest originalRequest = exchange.getRequest();
// 获取响应体
ServerHttpResponse originalResponse = exchange.getResponse();
// 解密查询参数
MultiValueMap<String, String> decryptQueryParams = decryptQueryParams(originalRequest.getQueryParams(), algorithmType, encMode, padMode, key, iv, plainIsEncode);
// 5、进行签名逻辑处理
try {
// 5.1、提取请求头信息
String appId = originalRequest.getHeaders().getFirst(XHeaders.X_APP_ID);
String appChannel = originalRequest.getHeaders().getFirst(XHeaders.X_APP_CHANNEL);
String appVersion = originalRequest.getHeaders().getFirst(XHeaders.X_APP_VERSION);
// 5.2、构建要签名的原文
String signContent = SignUtil.buildSignContent(appId, appVersion, appChannel, token, decryptQueryParams, null);
// 5.2、对参数进行签名验证
String hash = this.doSign(signContent, key, iv, plainIsEncode);
if(!hash.equals(sign)) {
String message = getMessageSource().getMessage("request.signature.mismatch", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
}
} catch (Exception e){
log.error(e.getMessage());
String message = getMessageSource().getMessage("request.signature.incorrect", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
}
// 装饰新的请求体
ServerHttpRequestDecorator requestDecorator = serverHttpRequestDecorator(originalRequest, originalResponse, decryptQueryParams, null);
// 装饰新的响应体
ServerHttpResponseDecorator responseDecorator = serverHttpResponseDecorator(originalResponse, algorithmType, encMode, padMode, key, iv, plainIsEncode);
// 使用新的请求和响应转发
ServerWebExchange serverWebExchange = exchange.mutate().request(requestDecorator).response(responseDecorator).build();
// 放行拦截
return chain.filter(serverWebExchange);
} catch (Exception e) {
log.error("密文过滤器加解密错误", e);
String message = getMessageSource().getMessage("request.crypto.error", null, locale);
return GatewayUtils.returnMono(exchange.getResponse(), ApiRestResponse.fail(1403, message));
}
}
private Mono<Void> handleRequestWithBody(ServerWebExchange exchange, GatewayFilterChain chain, Locale locale, String token, String sign,
String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode){
return DataBufferUtils.join(exchange.getRequest().getBody()).flatMap(dataBuffer -> {
// 获取请求体
ServerHttpRequest originalRequest = exchange.getRequest();
// 获取响应体
ServerHttpResponse originalResponse = exchange.getResponse();
try {
// 获取解密后的Query参数
MultiValueMap<String, String> decryptQueryParams = decryptQueryParams(originalRequest.getQueryParams(), algorithmType, encMode, padMode, key, iv, plainIsEncode);
// 获取请求Body
String originalRequestBody = getOriginalRequestBody(dataBuffer);
// 解密请求Body
log.info("请求Body解密,原文:{}", originalRequestBody);
// 1、解密请求体
byte[] decryptBodyBytes = this.decryptBodyString(originalRequestBody, algorithmType, encMode, padMode, key, iv, plainIsEncode);
// 2、创建新缓冲区并写入数据
// String bodyString = new String(decryptBodyBytes, StandardCharsets.UTF_8);
DataBuffer decryptBodyDataBuffer = originalResponse.bufferFactory().wrap(decryptBodyBytes);
DataBufferUtils.retain(decryptBodyDataBuffer);
String decryptRequestBody = StandardCharsets.UTF_8.decode(decryptBodyDataBuffer.asByteBuffer()).toString();
log.debug("请求Body解密,明文:{}", decryptRequestBody);
// 5、进行签名逻辑处理
try {
// 5.1、提取请求头信息
String appId = originalRequest.getHeaders().getFirst(XHeaders.X_APP_ID);
String appChannel = originalRequest.getHeaders().getFirst(XHeaders.X_APP_CHANNEL);
String appVersion = originalRequest.getHeaders().getFirst(XHeaders.X_APP_VERSION);
// 5.2、构建要签名的原文
String signContent = SignUtil.buildSignContent(appId, appVersion, appChannel, token, decryptQueryParams, decryptRequestBody);
// 5.2、对参数进行签名验证
String hash = this.doSign(signContent, key, iv, plainIsEncode);
if(!hash.equals(sign)) {
String message = getMessageSource().getMessage("request.signature.mismatch", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
}
} catch (Exception e){
log.error(e.getMessage());
String message = getMessageSource().getMessage("request.signature.incorrect", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
}
// 装饰新的请求体
ServerHttpRequestDecorator requestDecorator = serverHttpRequestDecorator(originalRequest, originalResponse, decryptQueryParams, decryptRequestBody);
// 装饰新的响应体
ServerHttpResponseDecorator responseDecorator = serverHttpResponseDecorator(originalResponse, algorithmType, encMode, padMode, key, iv, plainIsEncode);
// 使用新的请求和响应转发
ServerWebExchange serverWebExchange = exchange.mutate().request(requestDecorator).response(responseDecorator).build();
// 放行拦截
return chain.filter(serverWebExchange);
} catch (Exception e) {
log.error("密文过滤器加解密错误", e);
String message = getMessageSource().getMessage("request.crypto.error", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
} finally {
DataBufferUtils.release(dataBuffer);
}
});
}
private MultiValueMap<String, String> decryptQueryParams(MultiValueMap<String, String> queryParams,
String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode) {
// 1、从参数中获取加密的参数【必须】
String queryString = queryParams.getFirst(ENCRYPTED_PARAM);
if(StringUtils.isEmpty(queryString)){
log.debug("请求参数解密,未发现加密参数");
return queryParams;
}
log.debug("请求参数解密,原文:{}", queryString);
// 2、获取解密器
queryString = this.decryptQueryString(queryString, algorithmType, encMode, padMode, key, iv, plainIsEncode);
log.debug("请求参数解密,明文:{}", queryString);
// 3、解析查询参数
UrlQuery urlQuery = new UrlQuery();
urlQuery.parse(queryString, StandardCharsets.UTF_8);
for (Map.Entry<CharSequence, CharSequence> entry : urlQuery.getQueryMap().entrySet()) {
String key1 = String.valueOf(entry.getKey());
String value = String.valueOf(entry.getValue());
queryParams.put(key1, Collections.singletonList(value));
}
// 5、移除加密参数
queryParams.remove(ENCRYPTED_PARAM);
log.debug("请求参数解密,解密后参数数组:{}", queryParams);
return queryParams;
}
/**
* 获取原始的请求参数
*
* @param dataBuffer 数据缓冲
* @return 原始的请求参数
*/
private String getOriginalRequestBody(DataBuffer dataBuffer) {
// 1、创建一个容量为dataBuffer容量大小的字节数组
byte[] bytes = new byte[dataBuffer.readableByteCount()];
// 2、dataBuffer类容读取到bytes中
dataBuffer.read(bytes);
// 3、将字节数组转换为字符串
// String bodyString = new String(bytes, StandardCharsets.UTF_8);
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(bytes));
return charBuffer.toString();
}
private boolean isNeedFilterMethod(HttpMethod method) {
return NEED_FILTER_METHOD_SET.contains(method);
}
private boolean isNeedFilterContentType(MediaType mediaType) {
return NEED_FILTER_MEDIA_TYPE_SET.contains(mediaType) || "json".equals(mediaType.getSubtype());
}
private ServerHttpRequestDecorator serverHttpRequestDecorator(ServerHttpRequest originalRequest, ServerHttpResponse originalResponse,
MultiValueMap<String, String> decryptQueryParams, String decryptRequestBody) {
return new ServerHttpRequestDecorator(originalRequest) {
@Override
public HttpHeaders getHeaders() {
if(Objects.nonNull(decryptRequestBody)){
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
return httpHeaders;
}
return super.getHeaders();
}
@Override
public MultiValueMap<String, String> getQueryParams() {
return CollectionUtils.isEmpty(decryptQueryParams) ? super.getQueryParams() : decryptQueryParams;
}
@Override
public Flux<DataBuffer> getBody() {
if(Objects.nonNull(decryptRequestBody)){
byte[] bytes = decryptRequestBody.getBytes(StandardCharsets.UTF_8);
return Flux.just(originalResponse.bufferFactory().wrap(bytes));
}
return super.getBody();
}
};
}
private ServerHttpResponseDecorator serverHttpResponseDecorator(ServerHttpResponse originalResponse, String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode) {
return new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
// 1、判断返回体是否是Flux类型
if (body instanceof Flux) {
// 2、强转body为Flux类型
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
// 3、对返回体进行加密
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
// 3.2、将多个DataBuffer进行合并
DataBuffer dataBuffer = getDelegate().bufferFactory().join(dataBuffers);
// 3.3、创建一个容量为dataBuffer容量大小的字节数组
byte[] byteArray = new byte[dataBuffer.readableByteCount()];
// 3.4、dataBuffer类容读取到byteArray中
dataBuffer.read(byteArray);
// 3.5、通过DataBufferUtils.release(dataBuffer)方法释放原始的响应DataBuffer
DataBufferUtils.release(dataBuffer);
// 3.6、将byteArray转换为字符串
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(byteArray));
// String originalResponseBody = new String(byteArray, StandardCharsets.UTF_8);
String originalResponseBody = charBuffer.toString();
log.debug("响应结果加密,原文:{}", originalResponseBody);
// 3.7、对responseBody进行加密
String result = encryptResponseString(originalResponseBody, algorithmType, encMode, padMode, key, iv, plainIsEncode);
log.debug("响应结果加密,密文:{}", result);
// 3.8、将加密后的数据返回
byte[] encryptedByteArray = result.getBytes(StandardCharsets.UTF_8);
getDelegate().getHeaders().setContentLength(encryptedByteArray.length);
return getDelegate().bufferFactory().wrap(encryptedByteArray);
}));
}
return super.writeWith(body);
}
@Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return writeWith(Flux.from(body).flatMapSequential(p -> p));
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.putAll(originalResponse.getHeaders());
return headers;
}
};
}
protected abstract String decryptQueryString(String queryString, String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode);
protected abstract byte[] decryptBodyString(String bodyString, String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode);
protected abstract String encryptResponseString(String responseString, String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode);
protected abstract String doSign(String signContent, String key, String iv, boolean plainIsEncode);
}
DefaultRequestCryptoStrategy
/**
* 请求加解密内部服务实现
* 传输机密性:SM4(国密对称加密算法)
* 传输完整性:SM3(国密摘要签名算法)
*/
@Component
@Slf4j
public class DefaultRequestCryptoStrategy extends AbstractRequestCryptoStrategy {
@Autowired
private RedisOperationTemplate redisOperationTemplate;
@Autowired
public DefaultRequestCryptoStrategy(MessageSource messageSource) {
super(messageSource);
}
@Override
public CryptoType getType() {
return CryptoType.INTERNAL;
}
@Override
protected String decryptQueryString(String queryString, String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode) {
// 1、判断是否需要先解码,如果需要则先Base64解码
if(plainIsEncode){
queryString = Base64.decodeStr(queryString);
log.debug("Base64 Decode Request Query String : {}", queryString);
}
// 2、获取解密器
SymmetricCrypto crypto = SymmetricCryptoUtil.getSymmetricCrypto(algorithmType, encMode, padMode, key, iv);
// 3、解密请求参数
return crypto.decryptStr(queryString);
}
@Override
protected byte[] decryptBodyString(String bodyString, String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode) {
// 1、判断是否需要先解码,如果需要则先Base64解码
if(plainIsEncode){
bodyString = Base64.decodeStr(bodyString);
log.debug("Base64 Decode Request Body String : {}", bodyString);
}
// 2、获取解密器
SymmetricCrypto crypto = SymmetricCryptoUtil.getSymmetricCrypto(algorithmType, encMode, padMode, key, iv);
// 3、解密请求体
return crypto.decrypt(bodyString);
}
@Override
protected String encryptResponseString(String responseString, String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode) {
if (StringUtils.isBlank(responseString)) {
return responseString;
}
// 1、获取加密器
SymmetricCrypto crypto = SymmetricCryptoUtil.getSymmetricCrypto(algorithmType, encMode, padMode, key, iv);
// 2、加密响应结果
if(plainIsEncode){
String encryptedResponseString = crypto.encryptBase64(responseString);
log.debug("Base64 Encode Encrypted Response String : {}", encryptedResponseString);
return encryptedResponseString;
}
String encryptedResponseString = new String(crypto.encrypt(responseString, StandardCharsets.UTF_8));
log.debug("Encrypted Response String : {}", encryptedResponseString);
return encryptedResponseString;
}
@Override
protected String doSign(String signContent, String key, String iv, boolean plainIsEncode) {
if(plainIsEncode){
HMac hMac = SmUtil.hmacSm3(Base64.decode(key));
return hMac.digestBase64(signContent, StandardCharsets.UTF_8, Boolean.TRUE);
}
HMac hMac = SmUtil.hmacSm3(key.getBytes(StandardCharsets.UTF_8));
return hMac.digestBase64(signContent, StandardCharsets.UTF_8, Boolean.TRUE);
}
}
FlksecRequestCryptoStrategy
/**
* 请求加解密服务实现
*/
@Component
@Slf4j
public class FlksecRequestCryptoStrategy extends AbstractRequestCryptoStrategy {
@Autowired
private static OkHttp3Template okHttp3Template;
@Value("${spring.cloud.gateway.crypto.flksec.address:'127.0.0.1'}")
private String address;
@Value("${spring.cloud.gateway.crypto.flksec.port:'9443'}")
private String port;
@Autowired
public FlksecRequestCryptoStrategy(MessageSource messageSource) {
super(messageSource);
}
@Override
public CryptoType getType() {
return CryptoType.FLKSEC;
}
@Override
protected String decryptQueryString(String queryString, String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode) {
String url = String.format("https://%s:%s/api/crypto/sysDecrypt", address, port);
Map<String, String> bodyContent = new HashMap<>();
// 加密的算法类型,目前系统支持 sm1,sm4
bodyContent.put("algorithmType", algorithmType);
// 采用的加密模式,系统支持 ecb,cbc,cfb,ofb
bodyContent.put("encMode", encMode);
// 解密运算所采用的填充模式,系统支持 PKCS5Padding 和 NoPadding
bodyContent.put("padMode", padMode);
// Base64 格式的密钥字符串
bodyContent.put("key", key);
// Base64 格式的初始向量, 加密模式为 cbc,cfb,ofb 时该参数不能为空,解码后长度为 16 位,可自定义
bodyContent.put("iv", iv);
// 需要进行解密的数据
bodyContent.put("data", queryString);
// 明文是否编码
bodyContent.put("plainIsEncode", String.valueOf(plainIsEncode));
try {
FlkSecDecryptResponseVO decryptResponse = okHttp3Template.post(url, bodyContent, FlkSecDecryptResponseVO.class);
if (decryptResponse.getCode() == 200) {
return StringUtils.defaultString(decryptResponse.getData());
} else {
throw new BizRuntimeException(decryptResponse.getMsg());
}
} catch (IOException e) {
log.error("请求参数解密失败:{}", e.getMessage());
throw new BizRuntimeException("请求参数解密失败,请稍后重试");
}
}
@Override
protected byte[] decryptBodyString(String bodyString, String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode) {
String url = String.format("https://%s:%s/api/crypto/sysDecrypt", address, port);
Map<String, String> bodyContent = new HashMap<>();
// 加密的算法类型,目前系统支持 sm1,sm4
bodyContent.put("algorithmType", algorithmType);
// 采用的加密模式,系统支持 ecb,cbc,cfb,ofb
bodyContent.put("encMode", encMode);
// 解密运算所采用的填充模式,系统支持 PKCS5Padding 和 NoPadding
bodyContent.put("padMode", padMode);
// Base64 格式的密钥字符串
bodyContent.put("key", key);
// Base64 格式的初始向量, 加密模式为 cbc,cfb,ofb 时该参数不能为空,解码后长度为 16 位,可自定义
bodyContent.put("iv", iv);
// 需要进行解密的数据
bodyContent.put("data", bodyString);
// 明文是否编码
bodyContent.put("plainIsEncode", String.valueOf(plainIsEncode));
try {
FlkSecDecryptResponseVO decryptResponse = okHttp3Template.post(url, bodyContent, FlkSecDecryptResponseVO.class);
if (decryptResponse.getCode() == 200) {
return StringUtils.defaultString(decryptResponse.getData()).getBytes(StandardCharsets.UTF_8);
} else {
throw new BizRuntimeException(decryptResponse.getMsg());
}
} catch (IOException e) {
log.error("请求内容解密失败:{}", e.getMessage());
throw new BizRuntimeException("请求内容解密失败,请稍后重试");
}
}
@Override
protected String encryptResponseString(String responseString, String algorithmType, String encMode, String padMode, String key, String iv, boolean plainIsEncode) {
String url = String.format("https://%s:%s/api/crypto/sysEncrypt", address, port);
Map<String, String> bodyContent = new HashMap<>();
// 加密的算法类型,目前系统支持 sm1,sm4
bodyContent.put("algorithmType", algorithmType);
// 采用的加密模式,系统支持 ecb,cbc,cfb,ofb
bodyContent.put("encMode", encMode);
// 解密运算所采用的填充模式,系统支持 PKCS5Padding 和 NoPadding
bodyContent.put("padMode", padMode);
// Base64 格式的密钥字符串
bodyContent.put("key", key);
// Base64 格式的初始向量, 加密模式为 cbc,cfb,ofb 时该参数不能为空,解码后长度为 16 位,可自定义
bodyContent.put("iv", iv);
// 需要进行加密的数据
bodyContent.put("data", responseString);
// 明文是否编码
bodyContent.put("plainIsEncode", String.valueOf(plainIsEncode));
try {
FlkSecEncryptResponseVO encryptResponse = okHttp3Template.post(url, bodyContent, FlkSecEncryptResponseVO.class);
if (encryptResponse.getCode() == 200) {
return StringUtils.defaultString(encryptResponse.getData());
} else {
throw new BizRuntimeException(encryptResponse.getMsg());
}
} catch (IOException e) {
log.error("响应内容加密失败:{}", e.getMessage());
throw new BizRuntimeException("响应内容加密失败,请稍后重试");
}
}
@Override
protected String doSign(String signContent, String key, String iv, boolean plainIsEncode) {
String url = String.format("https://%s:%s/api/hmac/sm3hmac", address, port);
Map<String, String> bodyContent = new HashMap<>();
// 明文是否编码
bodyContent.put("plainIsEncode", String.valueOf(plainIsEncode));
// Base64 格式的密钥字符串
bodyContent.put("key", key);
// 进行杂凑的数据,数据大小建议不要超过 100M,比较大的数据可以每 100M分块计算,最后进行比较
bodyContent.put("data", signContent);
try {
FlkSecSignResponseVO signResponse = okHttp3Template.post(url, bodyContent, FlkSecSignResponseVO.class);
if (signResponse.getCode() == 200) {
return StringUtils.defaultString(signResponse.getData());
} else {
throw new BizRuntimeException(signResponse.getMsg());
}
} catch (IOException e) {
log.error("请求内容签名失败:{}", e.getMessage());
throw new BizRuntimeException("请求内容签名失败,请稍后重试");
}
}
}
NoOpRequestCryptoStrategy
/**
* 请求加解密内部服务实现
*/
@Component
@Slf4j
public class NoOpRequestCryptoStrategy implements RequestCryptoStrategy {
@Override
public CryptoType getType() {
return CryptoType.NOOP;
}
@Override
public Mono<Void> doExchange(ServerWebExchange exchange, GatewayFilterChain chain, String sign, String token) {
log.debug("NoOpRequestCryptoStrategy .. doExchange ");
return GatewayUtils.returnMono(exchange, chain);
}
}
全局请求签名验证过滤器 RequestSignOnlyFilter
- 仅进行传输完整性验证,如果需要同时进行传输机密性和传输完整性验证,请使用 RequestCryptoFilter
- 传输完整性:SM3(国密摘要签名算法)
/**
* 1、全局请求签名验证过滤器:
* - 仅进行传输完整性验证,如果需要同时进行传输机密性和传输完整性验证,请使用 RequestCryptoFilter
* - 传输完整性:SM3(国密摘要签名算法)
* 参考资料:
* https://www.haoyizebo.com/posts/876ed1e8/
* https://www.cnblogs.com/grimm/p/14031241.html
*/
@Component
@Slf4j
public class RequestSignOnlyFilter extends RequestSafetyFilter implements InitializingBean {
private static final AntPathMatcher PATH_MATHCER = new AntPathMatcher();
public static final String ENCRYPTED_PARAM = "encryptedParam";
private List<String> ignorePatterns;
@Autowired
private GatewayFilterProperties gatewayProperties;
@Autowired
private MessageSource messageSource;
@Override
public void afterPropertiesSet() throws Exception {
List<Entry<String, String>> noneEntries = gatewayProperties.getFilterMap().entrySet().stream()
.filter((entry) -> {
String[] array = StringUtils.tokenizeToStringArray(entry.getValue());
// 指定no-sign与指定anon但是没指定sign的都是要忽略掉的请求
if(ArrayUtils.contains(array, "no-sign") || (ArrayUtils.contains(array, "anon") && !ArrayUtils.contains(array, "sign"))) {
return true;
}
return false;
})
.collect(Collectors.toList());
List<String> ignorePatterns = new ArrayList<String>();
if (!CollectionUtils.isEmpty(noneEntries)) {
ignorePatterns = noneEntries.stream().map(Entry::getKey).collect(Collectors.toList());
}
this.setIgnorePatterns(ignorePatterns);
}
@Override
public int getOrder() {
// RequestBlacklistFilter > RequestAuthorizationFilter > RequestCryptoFilter > RequestSignOnlyFilter
return Ordered.HIGHEST_PRECEDENCE + 300;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
ServerHttpResponse originalResponse = exchange.getResponse();
// Sign 未开启
if(Objects.isNull(gatewayProperties.getSignature()) || !gatewayProperties.getSignature().isEnabled()) {
return chain.filter(exchange);
}
// 1、忽略部分请求
String path = exchange.getRequest().getPath().value();
log.info("Path:{}", path);
log.info("WhiteList:{}", gatewayProperties.getSignature().getWhiteList());
if(StringUtils.hasText(gatewayProperties.getSignature().getWhiteList())) {
for (String pattern : StringUtils.tokenizeToStringArray(gatewayProperties.getSignature().getWhiteList())) {
if(PATH_MATHCER.match(pattern, path)) {
return chain.filter(exchange);
}
}
}
log.info("IgnorePatterns:{}", ignorePatterns);
if(!CollectionUtils.isEmpty(ignorePatterns)) {
for (String pattern : ignorePatterns) {
if(PATH_MATHCER.match(pattern, path)) {
return chain.filter(exchange);
}
}
}
Locale locale = LocaleContextHolder.getLocale(exchange.getLocaleContext());
exchange.getAttributes().put("startTime", System.currentTimeMillis());
// 提取 Token
String token = obtainToken(serverHttpRequest);
// 2.1、提取 Sign
String sign = obtainSign(serverHttpRequest);
// 2.2、返回1403状态码和提示信息
if (!StringUtils.hasText(sign)) {
String message = getMessageSource().getMessage("request.signature.required", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
}
// 3、进行签名逻辑处理
try {
// 3.1、提取请求头信息
String appId = serverHttpRequest.getHeaders().getFirst(XHeaders.X_APP_ID);
String appChannel = serverHttpRequest.getHeaders().getFirst(XHeaders.X_APP_CHANNEL);
String appVersion = serverHttpRequest.getHeaders().getFirst(XHeaders.X_APP_VERSION);
// 3.2、请求参数,post从请求里获取请求体
String bodyString = this.matches(serverHttpRequest) ? WebFluxUtils.resolveBodyFromRequest(serverHttpRequest) : null;
// 3.3、对参数进行签名验证
String hash = SignUtil.sm3(appId, appVersion, appChannel, token, exchange.getRequest().getQueryParams(), bodyString);
if(!hash.equals(sign)) {
String message = getMessageSource().getMessage("request.signature.mismatch", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
}
} catch (Exception e){
log.error(e.getMessage());
String message = getMessageSource().getMessage("request.signature.incorrect", null, locale);
return GatewayUtils.returnMono(originalResponse, ApiRestResponse.fail(1403, message));
}
return GatewayUtils.returnMono(exchange, chain);
}
private boolean matches(ServerHttpRequest originalRequest) {
return HttpMethod.valueOf(originalRequest.getMethodValue()).compareTo(HttpMethod.POST) == 0
|| HttpMethod.valueOf(originalRequest.getMethodValue()).compareTo(HttpMethod.PUT) == 0
|| HttpMethod.valueOf(originalRequest.getMethodValue()).compareTo(HttpMethod.PATCH) == 0;
}
public List<String> getIgnorePatterns() {
return ignorePatterns;
}
public void setIgnorePatterns(List<String> ignorePatterns) {
this.ignorePatterns = ignorePatterns;
}
public GatewayFilterProperties getGatewayProperties() {
return gatewayProperties;
}
public void setGatewayProperties(GatewayFilterProperties gatewayProperties) {
this.gatewayProperties = gatewayProperties;
}
public MessageSource getMessageSource() {
return messageSource;
}
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
}
作者:Jeebiz 创建时间:2024-11-13 15:56
最后编辑:Jeebiz 更新时间:2024-11-14 21:58
最后编辑:Jeebiz 更新时间:2024-11-14 21:58