From 7672968d78a959559f6067aa9aa13b28dc28f8aa Mon Sep 17 00:00:00 2001 From: zhibing.pu <393733352@qq.com> Date: 星期三, 07 八月 2024 12:06:29 +0800 Subject: [PATCH] 添加网关参数签名校验 --- ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/HMACSHA1.java | 37 +++++++ ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/XssFilter.java | 115 ++++++++++++++++++++++ ruoyi-gateway/src/main/resources/bootstrap.yml | 4 ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java | 82 ++++++++-------- ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/TokenConstants.java | 10 ++ 5 files changed, 204 insertions(+), 44 deletions(-) diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/TokenConstants.java b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/TokenConstants.java index e1e5c2f..a6d2b55 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/TokenConstants.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/TokenConstants.java @@ -21,5 +21,15 @@ * 令牌秘钥 */ public final static String SECRET = "abcdefghijklmnopqrstuvwxyz"; + + /** + * 参数签名 + */ + public static final String SING = "sing"; + + /** + * 参数随机字符串 + */ + public static final String NONCE_STR = "nonce_str"; } diff --git a/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java b/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java index 101de63..3b4a17d 100644 --- a/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java +++ b/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java @@ -3,6 +3,7 @@ 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.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; @@ -27,8 +28,7 @@ * @author ruoyi */ @Component -public class AuthFilter implements GlobalFilter, Ordered -{ +public class AuthFilter implements GlobalFilter, Ordered { private static final Logger log = LoggerFactory.getLogger(AuthFilter.class); // 排除过滤的 uri 地址,nacos自行添加 @@ -37,56 +37,59 @@ @Autowired private RedisService redisService; + + @Value("${security.sign}") + private boolean parameter_signature; @Override - public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) - { + public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest.Builder mutate = request.mutate(); String url = request.getURI().getPath(); // 跳过不需要验证的路径 - if (StringUtils.matches(url, ignoreWhite.getWhites())) - { + if (StringUtils.matches(url, ignoreWhite.getWhites())) { return chain.filter(exchange); } - String token = getToken(request); - if (StringUtils.isEmpty(token)) - { - return unauthorizedResponse(exchange, "令牌不能为空"); +// String token = getToken(request); +// if (StringUtils.isEmpty(token)) { +// return unauthorizedResponse(exchange, "令牌不能为空"); +// } +// Claims claims = JwtUtils.parseToken(token); +// if (claims == null) { +// return unauthorizedResponse(exchange, "令牌已过期或验证不正确!"); +// } +// String userkey = JwtUtils.getUserKey(claims); +// boolean islogin = redisService.hasKey(getTokenKey(userkey)); +// if (!islogin) { +// return unauthorizedResponse(exchange, "登录状态已过期"); +// } +// String userid = JwtUtils.getUserId(claims); +// String username = JwtUtils.getUserName(claims); +// if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) { +// return unauthorizedResponse(exchange, "令牌验证失败"); +// } + if(parameter_signature){ + String sign = request.getHeaders().getFirst(TokenConstants.SING); + String nonce_str = request.getHeaders().getFirst(TokenConstants.NONCE_STR); + if(StringUtils.isEmpty(sign) || StringUtils.isEmpty(nonce_str)){ + log.error("[鉴权签名异常处理]请求路径:{}", exchange.getRequest().getPath()); + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "签名校验失败", HttpStatus.BAD_REQUEST); + } } - Claims claims = JwtUtils.parseToken(token); - if (claims == null) - { - return unauthorizedResponse(exchange, "令牌已过期或验证不正确!"); - } - String userkey = JwtUtils.getUserKey(claims); - boolean islogin = redisService.hasKey(getTokenKey(userkey)); - if (!islogin) - { - return unauthorizedResponse(exchange, "登录状态已过期"); - } - String userid = JwtUtils.getUserId(claims); - String username = JwtUtils.getUserName(claims); - if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) - { - return unauthorizedResponse(exchange, "令牌验证失败"); - } - + // 设置用户信息到请求 - addHeader(mutate, SecurityConstants.USER_KEY, userkey); - addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid); - addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username); +// addHeader(mutate, SecurityConstants.USER_KEY, userkey); +// addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid); +// addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username); // 内部请求来源参数清除 removeHeader(mutate, SecurityConstants.FROM_SOURCE); return chain.filter(exchange.mutate().request(mutate.build()).build()); } - private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) - { - if (value == null) - { + private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) { + if (value == null) { return; } String valueStr = value.toString(); @@ -94,13 +97,11 @@ mutate.header(name, valueEncode); } - private void removeHeader(ServerHttpRequest.Builder mutate, String name) - { + private void removeHeader(ServerHttpRequest.Builder mutate, String name) { mutate.headers(httpHeaders -> httpHeaders.remove(name)).build(); } - private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) - { + private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) { log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath()); return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED); } @@ -116,8 +117,7 @@ /** * 获取请求token */ - private String getToken(ServerHttpRequest request) - { + private String getToken(ServerHttpRequest request) { String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION); // 如果前端设置了令牌前缀,则裁剪掉前缀 if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) diff --git a/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/HMACSHA1.java b/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/HMACSHA1.java new file mode 100644 index 0000000..976f086 --- /dev/null +++ b/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/HMACSHA1.java @@ -0,0 +1,37 @@ +package com.ruoyi.gateway.filter; + + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +public class HMACSHA1 { + + private static final String MAC_NAME = "HmacSHA1"; + private static final String ENCODING = "UTF-8"; + + /** + * 使用 HMAC-SHA1 签名方法对对encryptText进行签名 + * + * @param encryptText + * 被签名的字符串 + * @param encryptKey + * 密钥 + * @return + * @throws Exception + */ + public static byte[] HmacSHA1Encrypt(String encryptText, String encryptKey) throws Exception { + byte[] data = encryptKey.getBytes(ENCODING); + // 根据给定的字节数组构造一个密钥,第二参数指定一个密钥算法的名称 + Mac mac = Mac.getInstance(MAC_NAME); + SecretKey secretKey = new SecretKeySpec(data, MAC_NAME); + // 生成一个指定 Mac 算法 的 Mac 对象 + // 用给定密钥初始化 Mac 对象 + mac.init(secretKey); + + byte[] text = encryptText.getBytes(ENCODING); + // 完成 Mac 操作 + return mac.doFinal(text); + } + +} diff --git a/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/XssFilter.java b/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/XssFilter.java index 6fe6285..fe449a9 100644 --- a/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/XssFilter.java +++ b/ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/XssFilter.java @@ -1,7 +1,19 @@ package com.ruoyi.gateway.filter; import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.ruoyi.common.core.constant.HttpStatus; +import com.ruoyi.common.core.constant.TokenConstants; +import com.ruoyi.common.core.utils.ServletUtils; +import org.apache.commons.codec.binary.Base64; +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.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; @@ -34,9 +46,13 @@ @ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true") public class XssFilter implements GlobalFilter, Ordered { + private static final Logger log = LoggerFactory.getLogger(XssFilter.class); // 跨站脚本的 xss 配置,nacos自行添加 @Autowired private XssProperties xss; + + @Value("${security.sign}") + private boolean parameter_signature; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) @@ -65,6 +81,10 @@ return chain.filter(exchange); } ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange); + if(parameter_signature && !authSign(httpRequestDecorator)){ + log.error("[鉴权签名异常处理]请求路径:{}", exchange.getRequest().getPath()); + return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "签名校验失败", HttpStatus.BAD_REQUEST); + } return chain.filter(exchange.mutate().request(httpRequestDecorator).build()); } @@ -120,7 +140,100 @@ String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); } - + + + /** + * 签名校验 + * @param httpRequestDecorator + * @return + */ + private boolean authSign(ServerHttpRequestDecorator httpRequestDecorator) { + HttpHeaders headers = httpRequestDecorator.getHeaders(); + AtomicReference<JSONObject> jsonObject = new AtomicReference<>(new JSONObject()); + httpRequestDecorator.getBody().buffer().map(dataBuffers -> { + DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); + DataBuffer join = dataBufferFactory.join(dataBuffers); + byte[] content = new byte[join.readableByteCount()]; + join.read(content); + DataBufferUtils.release(join); + String bodyStr = new String(content, StandardCharsets.UTF_8); + jsonObject.set(JSON.parseObject(bodyStr)); + + // 防xss攻击过滤 + bodyStr = EscapeUtil.clean(bodyStr); + // 转成字节 + byte[] bytes = bodyStr.getBytes(); + NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); + DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length); + buffer.write(bytes); + return buffer; + }); + JSONObject params = jsonObject.get(); + String sign = headers.getFirst(TokenConstants.SING); + if(StringUtils.isEmpty(sign)){ + return false; + } + String nonce_str = headers.getFirst(TokenConstants.NONCE_STR); + if(StringUtils.isEmpty(nonce_str)){ + return false; + } + + String signUrlEncode = localSignUrl(params, nonce_str); + signUrlEncode = signUrlEncode.replaceAll("& #40;", "\\(") + .replaceAll("& #41;", "\\)") + .replaceAll("\\+", " "); + if(sign.equals(signUrlEncode)){ + return true; + } + return false; + } + + + /** + * 组装签名路径 + * @param params + * @return + */ + public static String localSignUrl(JSONObject params, String key) { + List<String> keySet = new ArrayList<>(params.keySet()); + // 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序) + Collections.sort(keySet, new Comparator<String>() { + @Override + public int compare(String o1, String o2) { + return o1.compareTo(o2); + } + }); + // 构造签名键值对的格式 + StringBuilder sb = new StringBuilder(); + for (String k : keySet) { + String v = params.getString(k); + if(StringUtils.isNotEmpty(v)){ + sb.append(k + "=" + v + "&"); + } + } + String signUrl = sb.substring(0, sb.length() - 1); + return signUrlEncode(signUrl, key); + } + + + /** + * 签名字符串加密 + * @param signUrl + * @param encryptKey + * @return + */ + public static String signUrlEncode(String signUrl, String encryptKey) { + byte[] signByte = new byte[0]; + try { + signByte = HMACSHA1.HmacSHA1Encrypt(signUrl, encryptKey); + } catch (Exception e) { + throw new RuntimeException(e); + } + String localSign = Base64.encodeBase64String(signByte); + return localSign; + } + + @Override public int getOrder() { diff --git a/ruoyi-gateway/src/main/resources/bootstrap.yml b/ruoyi-gateway/src/main/resources/bootstrap.yml index 71689d1..8fb6e4a 100644 --- a/ruoyi-gateway/src/main/resources/bootstrap.yml +++ b/ruoyi-gateway/src/main/resources/bootstrap.yml @@ -41,12 +41,12 @@ eager: true transport: # 控制台地址 - dashboard: 192.168.110.34:8718 + dashboard: 192.168.110.169:8718 # nacos配置持久化 datasource: ds1: nacos: - server-addr: 127.0.0.1:8848 + server-addr: 192.168.110.169:8848 dataId: sentinel-ruoyi-gateway groupId: DEFAULT_GROUP data-type: json -- Gitblit v1.7.1