如何基于java代理对大数据缓存组件返回的数据进行脱敏和阻断

  • 背景
  • 架构拓扑图
  • 实现方式对比
    • UDF方案
      • 优点:
      • 缺点:
    • 改写返回结果方案
      • 优点:
      • 缺点:
  • 说明
  • 实现
    • 默认处理方式
    • redis报文解析器
      • 代码解析
  • 测试方案
    • 前提条件
    • 测试脚本及命令
    • 最终效果
  • 思考
  • 温馨提示:

背景

上周刚把基于关系型数据库的拦截及脱敏的代码做了一些完善与修复,开源关系型数据库已经都做了,其他的数据库也不方便再公开了,但是问题来了,其原理是拦截客户端的请求修改请求发送给服务端的,如果说服务端是非关系型的大数据组件数据库不支持这样的复杂请求又该怎么办呢,那就只能拦截返回的结果进行修改了,这篇文章将尝试解析基于非关系型数据库的返回结果报文拦截脱敏,本文暂时以redis为例子,其他类型后续有时间再更新,这里可以先分析下两种实现方式的架构、原理及优缺点。

架构拓扑图


图片来源于网络借鉴
因为我们的要求只是要把展示给用户的数据进行修改,并没有修改到数据本身,肯定是不能到数据库中去修改数据的,所以只能在数据库和客户端之间下功夫,一种方案是直接在在请求数据的时候用复杂的sql去截取字让数据库返回脱敏后的内容,另外一种是数据库返回完整的数据客户端自己对结果进行替换遮盖,如果没有代理服务在中间的话由客户端操作那么每一个客户端都需要做同样的事,而且服务端还是把明文数据给客户端了,存在一定的风险,目标就是不希望客户端看到明文。最好的结果就是对于客户端和服务端都是无感知的进行,客户端只需要正常发送请求,而服务端也只需要正常执行请求返回结果就可以了,因此解决方案就在代理服务器上模拟一个数据库服务端,对拦截到的数据进行处理,客户端去访问代理服务器,代理服务器去访问真实服务器,然后由代理服务器对数据进行处理,这个代理服务器也就是常用的tcp端口代理服务,前面已经讲过这个服务怎么实现,可用bio,nio,netty等方式实现都可以。

实现方式对比

UDF方案

优点:

1、传输安全,从数据库中出来的数据就已经是脱敏后的内容了,不存在泄露风险。
2、脱敏效率高、压力分散化,代理服务器只修改请求的sql修改的内容较少,代理服务器只管数据转发压力相对较轻,把压力都分散到了各个数据库上去,对代理服务器要求相对较低。

缺点:

1、兼容性稍微差点,必须要相应的数据库支持UDF功能才行。
2、对数据库服务器有一定的影响,如果替换的sql特别复杂效率低,在数据库中执行耗时影响其他业务系统使用。

改写返回结果方案

优点:

1、兼容性强,不需要管服务端的数据来源是什么,只要能返回数据就可以脱敏,支持任意数据库。
2、数据库不需要做任何计算,对数据库不会有什么负担。

缺点:

1、存在一定的安全隐患,数据从真实服务器到代理服务器之间传输的是真实数据,有被拦截的风向(但是两个服务器本来就是互相信任你的安全级别也不在这里)。
2、业务逻辑复杂,因为请求包和返回结果是不同服务器来的包,需要互通作为判断条件,所以需要把请求包存储在代理服务器上,且返回的数据包格式太多,兼容适配起来比较麻烦,返回的数据量也比较大,需要分包处理。
3、运行效率不高,因为所有的请求的数据都需要在代理服务器上进行解析,脱敏,封装并且计算,这样一来所有压力都在代理服务器上,而且需要处理的数据量也比较多,如果一条sql查询了十万条数据那么这十万条数据都需要在代理服务器上进行解析脱敏和计算,网络传输中的每一个数据包都是有大小限制的,数据量多了数据会在多个数据包中,这样就会让代理服务器解析比较麻烦,极端情况有可能需要把所有数据读取到本地才能解析,会有很大的性能影响,非必要场合不建议使用。

说明

由于第一种实现方式上一篇文章已经说过了,这里主要说讲解第二种方案,以redis为例进行解析,虽然java工程的redis可以直接用spring自带的序列化工具进行序列化,但是序列化之后就全是密文外部也看不懂了,且序列化内容比较占用空间,对于特殊的业务场景的还是有作用的。

实现

接下来将对数据库的报文进行解析:

默认处理方式


/*** @description: 默认的处理方式,不做任何处理* @author: yx* @date: 2021/12/8 10:20** <p>*/
@Slf4j
public class DefaultParser {//默认处理方式,对任何数据都不做处理,直接转发public void dealChannel(ChannelHandlerContext ctx, ProxyConfig config, Channel channel, Object msg) {channel.writeAndFlush(msg);}/*** 可以对删除语句自行做控制,这里只做日志记录** @param ctx* @param config* @param channel* @param sql*/void delete(ChannelHandlerContext ctx, ProxyConfig config, Channel channel, String cmd) {InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();log.info("{}主机在{}上执行了删除操作:{}", inetSocketAddress.getAddress(), config.getRemoteAddr(), cmd);}/*** 可以对修改语句自行做控制,检验或拦截,这里只做日志记录** @param ctx* @param config* @param channel* @param sql*/void update(ChannelHandlerContext ctx, ProxyConfig config, Channel channel, String cmd) {InetSocketAddress inetSocketAddress = (InetSocketAddress) ctx.channel().remoteAddress();log.info("{}主机在{}上执行了修改操作:{}", inetSocketAddress.getAddress(), config.getRemoteAddr(), cmd);}

redis报文解析器

代码解析


/*** @description: 处理redsi返回的数据报文对某些key进行脱敏处理,这里只处理key包含phone的* @author: yx* @date: 2022/2/18 9:17*/
@Slf4j
public class RedisParser extends DefaultParser {//处理当返回数据太大分包的情况,暂时不考虑Map<String, ByteBuf> bufferMap = new HashMap();///因为存储需要挎会话常规变量无法共享暂时做成静态的变量便于测试,后续再优化其他方案static Map<String, String> cmdMap = new HashMap();static Set cmdSet = new HashSet();//需要脱敏的key,后续需要做成可配置的,这里暂时写死便于测试static Set keySet = new HashSet();static {cmdSet.add("GET");cmdSet.add("LRANGE");cmdSet.add("SMEMBERS");cmdSet.add("HGET");cmdSet.add("ZRANGE");cmdSet.add("SSCAN");cmdSet.add("HGETALL");cmdSet.add("HSCAN");keySet.add("phone");}String split = new String(new byte[]{13, 10});public void dealChannel(ChannelHandlerContext ctx, ProxyConfig config, Channel channel, Object msg) {Channel ctxChannel = ctx.channel();InetSocketAddress inetSocketAddress = (InetSocketAddress) ctxChannel.remoteAddress();String hostString = inetSocketAddress.getHostString();int port = inetSocketAddress.getPort();ByteBuf readBuffer = (ByteBuf) msg;if (Objects.equals(config.getRemoteAddr(), hostString) && Objects.equals(port, config.getRemotePort())) {dealResultBuffer(ctx, config, channel, readBuffer);} else {dealCmdBuffer(ctx, config, channel, readBuffer);}}void dealCmdBuffer(ChannelHandlerContext ctx, ProxyConfig config, Channel channel, ByteBuf readBuffer) {String localPid = ctx.channel().remoteAddress().toString();String cmdContent = readBuffer.toString(Charset.defaultCharset());String[] split = cmdContent.split(this.split);//这里有可能有其他命令不足三位的,暂时不处理if (split.length > 2) {//获取命令的数量Integer integer = Integer.valueOf(split[0].replace("*", ""));//没啥用,只是暂时这样处理验证报文正确性后续好处理逻辑if (integer * 2 + 1 != split.length) {//            throw new RuntimeException("命令格式解析错误");}//获取命令类型String cmd = split[2];//如果扫描到命令是需要拦截的就存入map后续处理if (cmdSet.contains(cmd.toUpperCase())) {//获取key,一般是第五个为key,有问题后续再处理,hashkey单独处理,目前只处理前面的大keyString key = split[4];//因为只配置了一个key,所以改成包含phone的就脱敏,便于测试if (key.contains("phone")) {cmdMap.put(localPid, key);}}}readBuffer.retain();channel.writeAndFlush(readBuffer);}void dealResultBuffer(ChannelHandlerContext ctx, ProxyConfig config, Channel channel, ByteBuf readBuffer) {String localPid = channel.remoteAddress().toString();String content = readBuffer.toString(Charset.defaultCharset());if (cmdMap.containsKey(localPid)) {String resultContent = maskValue(content);readBuffer.writerIndex(0);readBuffer.readerIndex(0);readBuffer.writeBytes(resultContent.getBytes());readBuffer.writeByte(13);readBuffer.writeByte(10);cmdMap.remove(localPid);channel.writeAndFlush(readBuffer);} else {readBuffer.retain();channel.writeAndFlush(readBuffer);}}//处理整个结果返回脱敏后的结果,需要忽略一些系统报文格式的内容String maskValue(String content) {List<String> strings = Arrays.asList(content.split(this.split));for (int i = 1; i < strings.size(); i++) {String contetnValue = strings.get(i);if (contetnValue.startsWith("$") || contetnValue.startsWith("*")) {continue;}strings.set(i, replaceStr(contetnValue));//间隔是2,所以这里这里索引手动加1i++;}return StringUtils.join(strings, split);}/*** 这里固定脱敏百分之30到70%,后续再改写** @param content* @return*/String replaceStr(String content) {int start = (int) (content.length() * 0.3);int end = (int) (content.length() * 0.7);//不确定需要脱敏的长度是多少,先这样处理后续再优化List<String> replaceContent = new ArrayList<>();for (int a = start; a < end; a++) {replaceContent.add("*");}String join = StringUtils.join(replaceContent, "");return content.substring(0, start) + join + content.substring(end);}}

测试方案

前提条件

配置的只脱敏key包含phone的数据,其他数据不受影响,后续再做成key可配置的。

测试脚本及命令

set phoneA 13542156548
get phoneAlpush phoneB 13542156897
lpush phoneB 13542114578
lrange phoneB 0 -1zadd phoneC 1 13542159875
zadd phoneC 3 13684212456
zrange phoneC 0 -1 WITHSCORESsadd phoneD 14569871235
sadd phoneD 15465421589
SMEMBERS phoneDHSET phoneE zhangsan 13565421548
HSET phoneE lisi 15698745215
HGET phoneE zhangsan
HGETALL phoneE

最终效果

思考

如果是代理redis集群各个节点之前的无缝切换能实现吗,这个问题不做回答,留着给大家实验一下就知道了。

温馨提示:

这玩意比较耗费资源,代理服务器的压力会很大,但是由于特殊业务场景需要比较好奇就进行了研究,不要随意用到生产环境。
具体代码见github地址

本文纯属个人学习产物,因为网上一直没有相关资料所以分享出来给感兴趣的朋友一起研究,如有侵权请私信联系作者。

如何基于java代理对大数据缓存组件返回的数据进行脱敏和阻断相关推荐

  1. Vue常用的组件库大全【前端工程师必备】【实时更新】【移动端、PC端(web端)、数据可视化组件库(数据大屏) 、动画组件库、3D组件库】

    Vue常用的组件库大全[前端工程师必备] (一)移动端 常用组件库 1)Vant ui 2)Cube UI 3)VUX 4) NuTUI 5)Mint ui 6)Varlet UI 7)OnsenUI ...

  2. 大数据各组件安装(数据中台搭建)

    文章目录 一.基础环境配置(三台机器都操作) 1.修改主机名: 2.关闭防火墙: 3.关闭Selinux: 4.文件描述符配置: 5.关闭 THP: 6.自定义 JDK 安装: 6.1 删除默认ope ...

  3. java书号属性,基于Java的ISBN书号查询示例代码-六派数据

    示例代码 本代码示例是基于Java的六派数据接口进行数据请求API服务请求的代码示例,使用前你需要: 以下是完整代码示例: /** * 主函数 * @param args */ public stat ...

  4. java spring 传大数据类型_Spring MVC前后端数据交互总结

    控制器 作为控制器,大体的作用是作为V端的数据接收并且交给M层去处理,然后负责管理V的跳转.SpringMVC的作用不外乎就是如此,主要分为:接收表单或者请求的值,定义过滤器,跳转页面:其实就是ser ...

  5. layui表格展示数据时:返回的数据不符合规范,正确的成功状态码应为:code: 0

    1.今天,用django和layui结合,处理表格展示数据时,有数据的表格能正常显示列表,无数据的表格却提示了:返回的数据不符合规范,正确的成功状态码应为:"code": 0,于是 ...

  6. eplise怎么连接数据库_基于手机信令的大数据分析教程(一)数据导入数据库

    前言 该套教程以一个初学大数据的菜鸟视角,编写数据分析处理的整套流程.写得较为详(luo)细(suo),希望适用于任何城乡规划大数据的初学者.持续更新中,若有错误,望指正! 1.任务总纲 (1)职住数 ...

  7. JAVA实现1到100的平方根之和_手算平方根和基于 Java BigInteger 的大整数平方根的实现...

    为了实现任意大数的运算,long用BigInteger替换带哦. 好了废话少数,先说数学原理,也就是手算平方根计算机代码实现!那么什么叫手算平方根了??? 手开方图解 据说前苏联的普通工人都会的(毛熊 ...

  8. java 大整数平方根_手算平方根和基于 Java BigInteger 的大整数平方根的实现

    为了实现任意大数的运算,long用BigInteger替换带哦. 好了废话少数,先说数学原理,也就是手算平方根计算机代码实现!那么什么叫手算平方根了??? 手开方图解 据说前苏联的普通工人都会的(毛熊 ...

  9. Java @responsebody,springMVC 使用注解@ResponseBody 不能返回JSON数据

    控制器中代码 @RequestMapping(value = "/listArea",method = RequestMethod.GET) @ResponseBody priva ...

最新文章

  1. 聚类评价兰德系数讲明白的
  2. 存储过程编写经验和优化措施
  3. InnoDB体系结构
  4. Cookie的生命周期问题
  5. mysql使用某一列的内容赋值给另外一列,concat()函数
  6. 自动化测试用例设计原则
  7. 一篇文章彻底搞懂“分布式事务”
  8. Spark精华问答 | Spark的三种运行模式有何区别?
  9. 系统架构设计师 - 软件架构设计 - 架构评估
  10. 【C语言】最大的两个数(指针专题)
  11. dz开启php5.5,Discuz5.5.0代码高亮显示+运行代码框合成插件 下载第2/4页
  12. linux stubs 32.h,解决 error: gnu/stubs-32.h: No such file or directory
  13. credential provider filter注意
  14. 茂密林冠下实时语义SLAM的大规模自主飞行
  15. 【Linux系列文章】Shell开发
  16. 超美的天环星轨动态引导页html官网源码下载
  17. 服务器断电重启,mysql未启动。
  18. 记录小米设备事件获取
  19. Allegro贴片元件封装制作
  20. 微信服务号与订阅号的主要区别是什么?

热门文章

  1. 第一个子元素在未超过父元素高度的情况下设置margin-top导致出现竖向滚动条的问题
  2. vs2017发布exe
  3. 表单全选取消全选案例
  4. L1-038 新世界 (5 分)
  5. 语雀导出html,博客搭建
  6. VM虚拟机中无法使用鼠标滚轮(罗技鼠标)
  7. 讲真的,我刚开始工作的时候,也很迷茫
  8. 树莓派4b——设置DS3231模块 并从DS3231模块读取系统时间
  9. CAS单点登入登出原理
  10. 水溶性丙烯酸树脂增稠剂,还是有点小疑惑?