若依微服务版本JSON.parse: unexpected character错误排查

一、问题场景

前端某功能模块入参携带包含HTML标签的字符串,提交到后端报错:

JSON parse error: Unexpected character ('>' (code 62)): was expecting either '*' or '/' for a comment; nested exception is com.fasterxml.jackson.core.JsonParseException: Unexpected character ('>' (code 62)): was expecting either '*' or '/' for a comment\n at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 3, column: 473]

前端入参

{
  "content": "<p>南朝时期(公元420年至589年):九江地区成为南朝的楚州之一,属于南宋、齐、梁的管辖范围。</p><p><br></p><p>唐朝时期(公元618年至907年):九江地区属于唐朝的吉州,成为交通要地和商贸心。</p><p><br></p><p>宋、元、明、清时期(公元960年至191年):九江地区历经宋代南唐、宋朝,元朝、明朝、清朝的统治。九江成为江西省重要的政治、经济、文化中心之一。<img src=\"http://47.96.173.203:9000/lu-yi-fa/2024/01/08/100j0u000000j1zf01511_R_1600_10000_20240108142420A002.jpg\"></p>" ,
  "coverImage": "",
  "id": null,
  "status": "1",
  "title": "test",
  "type": "1",
}

问题原因:

若依框架在网关采用配置拦截器的方式来处理XSS攻击,一旦请求被过滤器拦截,就会转入到自定义的拦截器XssFilter当中,首先解决的就是判断是否启用XSS拦截器和是否需要拦截(默认是开启状态),若依这里是采用在配置文件当中填写具体信息的方式,来配置是否启用xss,是否是白名单,是否是匹配链接。按照后台填写的数据处理请求,如果是不启用或者是该请求为白名单,就直接将请求放过如果不通过就交给XssFilter来处理。

XssFilter里面就是一些核心代码,就是获取请求参数,就是进行一些html标签的过滤和json字符串的处理。如下:

public class XssFilter implements GlobalFilter, Ordered
{
    // 跨站脚本的 xss 配置,nacos自行添加
    @Autowired
    private XssProperties xss;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
    {
        ServerHttpRequest request = exchange.getRequest();
        // xss开关未开启 或 通过nacos关闭,不过滤
        if (!xss.getEnabled())
        {
            return chain.filter(exchange);
        }
        // GET DELETE 不过滤
        HttpMethod method = request.getMethod();
        if (method == null || method == HttpMethod.GET || method == HttpMethod.DELETE)
        {
            return chain.filter(exchange);
        }
        // 非json类型,不过滤
        if (!isJsonRequest(exchange))
        {
            return chain.filter(exchange);
        }
        // excludeUrls 不过滤
        String url = request.getURI().getPath();
        if (StringUtils.matches(url, xss.getExcludeUrls()))
        {
            return chain.filter(exchange);
        }
        ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange);
        return chain.filter(exchange.mutate().request(httpRequestDecorator).build());

    }

    private ServerHttpRequestDecorator requestDecorator(ServerWebExchange exchange)
    {
        ServerHttpRequestDecorator serverHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest())
        {
            @Override
            public Flux<DataBuffer> getBody()
            {
                Flux<DataBuffer> body = super.getBody();
                return body.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);
                    // 防xss攻击过滤
                    bodyStr = EscapeUtil.clean(bodyStr);
                    // 转成字节
                    byte[] bytes = bodyStr.getBytes(StandardCharsets.UTF_8);
                    NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
                    DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
                    buffer.write(bytes);
                    return buffer;
                });
            }

            @Override
            public HttpHeaders getHeaders()
            {
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                // 由于修改了请求体的body,导致content-length长度不确定,因此需要删除原先的content-length
                httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
                httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                return httpHeaders;
            }

        };
        return serverHttpRequestDecorator;
    }

    /**
     * 是否是Json请求
     * 
     * @param exchange HTTP请求
     */
    public boolean isJsonRequest(ServerWebExchange exchange)
    {
        String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
        return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
    }

    @Override
    public int getOrder()
    {
        return -100;
    }
}

解决方案:
在网关配置Xss过滤白名单接口

security:
  # 防止XSS攻击
  xss:
    enabled: true
    excludeUrls:
      - /system/notice
      - /flowable/definition/save
      - /test/article/add
      - /test/article/edit

当然也有其它解决方案
利用算法进行加密和解密,及前端将对应的表单数据加密,后端解密,该方法最安全。
一、前端安装crypto-js 插件,编写工具类

import CryptoJS from 'crypto-js'
 
// 需要和后端一致
const KEY = CryptoJS.enc.Utf8.parse('abcdefj123456');
const IV = CryptoJS.enc.Utf8.parse('abcdefj123456');
 
export default {
 
  /**
   * 加密
   * @param {*} word
   * @param {*} keyStr
   * @param {*} ivStr
   */
  encrypt (word, keyStr, ivStr) {
    let key = KEY;
    let iv = IV;
    if (keyStr) {
      key = CryptoJS.enc.Utf8.parse(keyStr);
      iv = CryptoJS.enc.Utf8.parse(ivStr);
    }
    let srcs = CryptoJS.enc.Utf8.parse(word);
    var encrypted = CryptoJS.AES.encrypt(srcs, key, {
      iv: iv,
      mode: CryptoJS.mode.CBC,
      padding: CryptoJS.pad.ZeroPadding
    });
    return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
  },
 
  /**
   * 解密
   * @param {*} word
   * @param {*} keyStr
   * @param {*} ivStr
   */
  decrypt (word, keyStr, ivStr) {
    let key = KEY;
    let iv = IV;
 
    if (keyStr) {
      key = CryptoJS.enc.Utf8.parse(keyStr);
      iv = CryptoJS.enc.Utf8.parse(ivStr);
    }
 
    let base64 = CryptoJS.enc.Base64.parse(word);
    let src = CryptoJS.enc.Base64.stringify(base64);
 
    let decrypt = CryptoJS.AES.decrypt(src, key, {
      iv: iv,
      mode: CryptoJS.mode.CBC,
      padding: CryptoJS.pad.ZeroPadding
    });
 
    let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
    return decryptedStr.toString();
  }
}

二、表单提交前将数据加密

this.model.content = asc.encrypt(this.model.content);

三、后端接收数据并解密

  1. 引入依赖
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.6.0</version>
</dependency>
  1. 编写工具类

 
import org.apache.commons.codec.binary.Base64;
 
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
 
/**
 * @author root
 */
public class SecretUtil {
 
    /***
     * key和iv值需要和前端一致
     */
    public static final String KEY = "abcdefj123456";
 
    public static final String IV = "abcdefj123456";
 
    /**
     * 加密方法
     *
     * @param data 要加密的数据
     * @param key  加密key
     * @param iv   加密iv
     * @return 加密的结果
     */
    public static String encrypt(String data, String key, String iv) {
        try {
            //"算法/模式/补码方式"NoPadding PkcsPadding
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            int blockSize = cipher.getBlockSize();
 
            byte[] dataBytes = data.getBytes();
            int plaintextLength = dataBytes.length;
            if (plaintextLength % blockSize != 0) {
                plaintextLength = plaintextLength + (blockSize - (plaintextLength % blockSize));
            }
 
            byte[] plaintext = new byte[plaintextLength];
            System.arraycopy(dataBytes, 0, plaintext, 0, dataBytes.length);
 
            SecretKeySpec keyspec = new SecretKeySpec(key.getBytes(), "AES");
            IvParameterSpec ivspec = new IvParameterSpec(iv.getBytes());
 
            cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
            byte[] encrypted = cipher.doFinal(plaintext);
 
            return new Base64().encodeToString(encrypted);
 
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
 
    /**
     * 解密方法
     *
     * @param data 要解密的数据
     * @param key  解密key
     * @param iv   解密iv
     * @return 解密的结果
     */
    public static String desEncrypt(String data, String key, String iv) {
        try {
            byte[] encrypted1 = new Base64().decode(data);
 
            Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
            SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
            IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
            byte[] original = cipher.doFinal(encrypted1);
            return new String(original).trim();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}
  1. 数据解密
//将content内容解密
model.setContent(SecreUtil.desEncrypt(model.getContent(),SecreUtil.Key,SecreUtil.IV));
  • 11
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值