zhibing.pu
2024-08-07 7672968d78a959559f6067aa9aa13b28dc28f8aa
添加网关参数签名校验
1个文件已添加
4个文件已修改
248 ■■■■ 已修改文件
ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/TokenConstants.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java 82 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/HMACSHA1.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/XssFilter.java 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-gateway/src/main/resources/bootstrap.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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";
}
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))
ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/HMACSHA1.java
New file
@@ -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);
    }
}
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()
    {
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