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