如何基于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單獨處理,目前只處理前面的大key
String 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,所以這里這里索引手動加1
i++;
}
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 phoneA
lpush phoneB 13542156897
lpush phoneB 13542114578
lrange phoneB 0 -1
zadd phoneC 1 13542159875
zadd phoneC 3 13684212456
zrange phoneC 0 -1 WITHSCORES
sadd phoneD 14569871235
sadd phoneD 15465421589
SMEMBERS phoneD
HSET phoneE zhangsan 13565421548
HSET phoneE lisi 15698745215
HGET phoneE zhangsan
HGETALL phoneE
最終效果

溫馨提示:
這玩意比較耗費資源,代理服務器的壓力會很大,但是由于特殊業務場景需要比較好奇就進行了研究,不要隨意用到生產環境,
具體代碼見github地址
本文純屬個人學習產物,因為網上一直沒有相關資料所以分享出來給感興趣的朋友一起研究,如有侵權請私信聯系作者,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/431074.html
標籤:其他
上一篇:技術盤點:容器技術的演進路線是什么?未來有哪些想象空間?
下一篇:某線下水果店銷售資料分析
