金融系统脱敏解决方案总结如下文:
一、数据库脱敏
数据库表在设计时对敏感字段用两列来存储:一个用来存对称加密的密文,一个用来存脱敏显示用的信息。
比如用户信息表,mobile列存储的值形如"13500001111", 而mobile_encrypt列存储的值形如“dop9zr17xxW0D2kmr1iLDA==”
import org.apache.commons.lang3.StringUtils;
/**
* @Title:数据脱敏处理工具
* @Author:wangchenggong
* @Date 2020/11/23 22:22
* @Description
* @Version
*/
public class DesensitizedUtil {
/**
* 手机号脱敏处理:显示前三位,后四位,其他隐藏。共计11位,例如:139****1234
* @param mobile
* @return
*/
public static String mobilePhone(String mobile){
if (StringUtils.isEmpty(mobile)) {
return "";
}
return StringUtils.overlay(mobile, "****", 3, 7);
}
/**
* 身份证号脱敏处理: 显示前六位,隐藏中间八位,剩余显示。共计18位或者15位,比如:110110********1234
* @param idCardNo
* @return
*/
public static String idCardNum(String idCardNo){
if (StringUtils.isEmpty(idCardNo)) {
return "";
}
return StringUtils.overlay(idCardNo, "********", 6, 14);
}
/**
* 银行卡号脱敏处理:显示前六位,隐藏中间六位,剩余后几位显示。共计12-19位,例如:622688******125
* @param bankCardNo
* @return
*/
public static String bankCardNo(String bankCardNo){
if (StringUtils.isEmpty(bankCardNo)) {
return "";
}
return StringUtils.overlay(bankCardNo, "******", 6, 12);
}
}
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.security.Key;
import java.util.Base64;
/**
* @Title:Des对称加密工具类
* @Author:wangchenggong
* @Date 2020/11/23 22:17
* @Description
* @Version
*/
public class DESUtil {
/**
* 偏移变量,固定占8位字节
*/
private final static String IV_PARAMETER = "12345678";
/**
* 密钥算法
*/
private static final String ALGORITHM = "DES";
/**
* 加密/解密算法-工作模式-填充模式
*/
private static final String CIPHER_ALGORITHM = "DES/CBC/PKCS5Padding";
/**
* 默认编码
*/
private static final String CHARSET = "utf-8";
/**
* 生成key
*
* @param password
* @return
* @throws Exception
*/
private static Key generateKey(String password) throws Exception {
DESKeySpec dks = new DESKeySpec(password.getBytes(CHARSET));
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
return keyFactory.generateSecret(dks);
}
/**
* DES加密字符串
*
* @param password 加密密码,长度不能够小于8位
* @param data 待加密字符串
* @return 加密后内容
*/
public static String encrypt(String password, String data) {
if (password== null || password.length() < 8) {
throw new RuntimeException("加密失败,key不能小于8位");
}
if (data == null)
return null;
try {
Key secretKey = generateKey(password);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
IvParameterSpec iv = new IvParameterSpec(IV_PARAMETER.getBytes(CHARSET));
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
byte[] bytes = cipher.doFinal(data.getBytes(CHARSET));
//JDK1.8及以上可直接使用Base64,JDK1.7及以下可以使用BASE64Encoder
return new String(Base64.getEncoder().encode(bytes));
} catch (Exception e) {
e.printStackTrace();
return data;
}
}
/**
* DES解密字符串
*
* @param password 解密密码,长度不能够小于8位
* @param data 待解密字符串
* @return 解密后内容
*/
public static String decrypt(String password, String data) {
if (password== null || password.length() < 8) {
throw new RuntimeException("加密失败,key不能小于8位");
}
if (data == null)
return null;
try {
Key secretKey = generateKey(password);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
IvParameterSpec iv = new IvParameterSpec(IV_PARAMETER.getBytes(CHARSET));
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
return new String(cipher.doFinal(Base64.getDecoder().decode(data.getBytes(CHARSET))), CHARSET);
} catch (Exception e) {
e.printStackTrace();
return data;
}
}
public static void main(String[] args) {
String mobileEncrypt = DESUtil.encrypt(DESKeyConstant.DES_KEY, "13500001111");
System.out.println(mobileEncrypt);//dop9zr17xxW0D2kmr1iLDA
String mobile = DESUtil.decrypt(DESKeyConstant.DES_KEY, "dop9zr17xxW0D2kmr1iLDA");
System.out.println(mobile);
}
}
密钥可以存储在配置文件中
二、后台管理页面数据列表脱敏
由于后台管理页面的数据列表是否脱敏显示是取决于当前用户权限的,所以可以利用过滤器或拦截器获取用户的权限,将用户是否具有查看敏感信息权限的标识(布尔值)存储在ThreadLocal
中,这样就可以在返回数据列表时根据这个标识来决定是否要对数据列表做脱敏处理了。
用来存储敏感信息权限的标识的ThreadLocal代码示例:
public class SensitiveAuthTransfer extends ThreadLocal<Boolean> {
private static final String SENSITIVE_POWER = "/sensitiveAuthorization";
private static SensitiveAuthTransfer sensitiveAuthTransfer = new SensitiveAuthTransfer();
@Override
protected Boolean initialValue() {
return Boolean.FALSE;
}
/**
* 构造方法私有
*/
private SensitiveAuthTransfer() {}
/**
* 当前用户是否授权脱敏
* @return true or false
*/
public static boolean isAuthorized(){
boolean a= sensitiveAuthTransfer.get();
return a;
}
/**
* 设置脱敏权限
*
* @param powers
*/
public static void setAuthority(Set<String> powers) {
//判断是否授权敏感信息查看权限,pwers权限在UserServiceImpl.findByLogin方法中被toLowerCase
if (powers != null && powers.contains(SENSITIVE_POWER.toLowerCase())) {
sensitiveAuthTransfer.set(true);
}else{
sensitiveAuthTransfer.set(false);
}
}
}
以下是对包含了敏感信息的实体类进行脱敏处理:
/**
* @Title:敏感信息VO
* @Author:wangchenggong
* @Date 2020/11/24 8:25
* @Description 所有包含了敏感字段的实体需继承该类
* @Version
*/
@Data
public class SensitiveInfo {
private String bankcardNo;
private String mobile;
private String idCardNo;
private String idcardNo;
private String regMobile;
public static void handle(Object obj) throws InvocationTargetException, IllegalAccessException {
modifySensitiveParam(obj, a -> a instanceof SensitiveInfo, b -> {
SensitiveInfo baseVo = (SensitiveInfo) b;
baseVo.setMobile(DesensitizedUtil.mobilePhone(baseVo.getMobile()));
baseVo.setIdCardNo(DesensitizedUtil.idCardNum(baseVo.getIdCardNo()));
baseVo.setIdcardNo(DesensitizedUtil.idCardNum(baseVo.getIdcardNo()));
baseVo.setRegMobile(DesensitizedUtil.mobilePhone(baseVo.getRegMobile()));
baseVo.setBankcardNo(DesensitizedUtil.bankCardNo(baseVo.getBankcardNo()));
});
}
/**
* 修改敏感参数
*
* @param obj 待修改参数所在对象
* @throws InvocationTargetException
* @throws IllegalAccessException
*/
private static void modifySensitiveParam(Object obj, Predicate<Object> pre, Consumer<Object> con) throws InvocationTargetException, IllegalAccessException {
if (obj == null) return;
if (pre.test(obj)) {
con.accept(obj);
return;
}
if (isBaseType(obj)){
return;
}
Iterator iterator = null;
if (obj.getClass().isArray()) {
iterator = Arrays.asList((Object[]) obj).iterator();
} else if (obj instanceof Iterable) {
iterator = ((Iterable) obj).iterator();
} else if (obj instanceof Map) {
iterator = ((Map) obj).values().iterator();
} else {
//对各个属性值进行脱敏处理
PropertyDescriptor[] pds = PropertyUtils.getPropertyDescriptors(obj.getClass());
for (PropertyDescriptor p : pds) {
if (p.getReadMethod() != null) {
Object value = p.getReadMethod().invoke(obj);
if (value == obj) {
continue;
}
modifySensitiveParam(value, pre, con);
}
}
}
if (iterator != null) {
while (iterator.hasNext()) {
modifySensitiveParam(iterator.next(), pre, con);
}
}
}
/**
* 判断object是否为基本类型
*
* @param object 对象
* @return boolean
*/
private static boolean isBaseType(Object object) {
Class className = object.getClass();
return className.equals(Integer.class) ||
className.equals(Byte.class) ||
className.equals(Long.class) ||
className.equals(Double.class) ||
className.equals(Float.class) ||
className.equals(Character.class) ||
className.equals(Short.class) ||
className.equals(Boolean.class) ||
className.equals(Class.class) ||
className.equals(String.class);
}
}
最后,将敏感权限标识与脱敏处理关联起来,示例代码如下:
//OrderBindDeductVO 需要继承SensitiveInfo
List<OrderBindDeductVO> orderBindDeductVOS = this.orderBindDeductService.selectListVOByParams(orderBindDeductParams);
if(!SensitiveAuthTransfer.isAuthorized()){
SensitiveInfo.handle(orderBindDeductVOS);
}
三、日志脱敏
- json序列化时进行脱敏
//对敏感字段脱敏
public final String encryptRequest(String str) {
JSONObject reqDataObj = JSONObject.parseObject(str);
String businessSeqNo = reqDataObj.getString("businessSeqNo");
String returnMsg = "";
try{
//脱敏过滤器
ValueFilter filter = new ValueFilter() {
@Override
public Object process(Object o, String key, Object value) {
if(!(value instanceof String)){
return value;
}
//将key转为小写
String valueStr = (String)value;
if(StringUtils.isBlank(valueStr)){
return value;
}
String lowerKey = key.toLowerCase();
if(!lowerKey.contains("mobile") && !lowerKey.contains("no")){
return value;
}
//银行卡号脱敏
if(Arrays.asList("bankcardno","payeeacctno","payeracctno").contains(lowerKey) || lowerKey.contains("bankno")){
value = StringUtils.overlay(valueStr, "******", 6, 12);
}
//证件号码脱敏
if (lowerKey.contains("certno") || lowerKey.contains("idcardno")) {
value = StringUtils.overlay(valueStr, "********", 6, 14);
}
//手机号脱敏
if (lowerKey.contains("mobile") || lowerKey.contains("phoneno")) {
value = StringUtils.overlay(valueStr, "****", 3, 7);
}
return value;
}
};
returnMsg = JSONObject.toJSONString(reqDataObj,filter);
}catch (Exception e){
e.printStackTrace();
logger.error("发送银行请求参数日志信息脱敏,数据格式转换失败,流水号为+"+businessSeqNo);
}
return returnMsg;
}
-
logback写入数据时脱敏
基本的思路是:通过自定义logback格式转换器来实现(继承MessageConverter类,重写convert方法)
public class LogMessageConvert extends MessageConverter {
private static final String RECORD_LOGGER = "recordLogger";
/**
* 手机号正则
*/
private static final String PHONE_REGEX = "1[3|4|5|7|8|9][0-9]\\d{8}";
/**
* 身份证号正则匹配
*/
private static final String IDCARD_REGEX = "([1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx])|(^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3})";
/**
* 银行卡正则
*/
private static final String BANK_CARD = "[^a-zA-Z]([1-9]{1})(\\d{12,19})";
@Override
public String convert(ILoggingEvent event){
String msg=event.getFormattedMessage();
//这里可以利用排除对指定的appender脱敏
if(!RECORD_LOGGER.equals(event.getLoggerName())){
msg = filterSensitive(msg);
}
return msg;
}
private String filterSensitive(String content) {
try {
if(StringUtils.isBlank(content)) {
return content;
}
content = filterIdcard(content);
content = filterBankCard(content);
return filterMobile(content);
}catch(Exception e) {
return content;
}
}
/**
* [身份证号] <例子:1101**********5762>
*/
private String filterIdcard(String num){
Pattern pattern = Pattern.compile(IDCARD_REGEX);
Matcher matcher = pattern.matcher(num);
StringBuffer sb = new StringBuffer() ;
while(matcher.find()){
matcher.appendReplacement(sb, baseSensitive(matcher.group(), 4, 4));
}
matcher.appendTail(sb) ;
return sb.toString();
}
/**
* [手机号码] 前三位,后四位,其他隐藏<例子:138******1234>
*/
private String filterMobile(String num){
Pattern pattern = Pattern.compile(PHONE_REGEX);
Matcher matcher = pattern.matcher(num);
StringBuffer sb = new StringBuffer() ;
while(matcher.find()){
matcher.appendReplacement(sb, baseSensitive(matcher.group(), 3, 4)) ;
}
matcher.appendTail(sb) ;
return sb.toString();
}
/**
* [银行卡号] 前六位,后四位,其他隐藏
*/
private String filterBankCard(String num){
Pattern pattern = Pattern.compile(BANK_CARD);
Matcher matcher = pattern.matcher(num);
StringBuffer sb = new StringBuffer() ;
while(matcher.find()){
matcher.appendReplacement(sb, baseSensitive(matcher.group(), 6, 4)) ;
}
matcher.appendTail(sb) ;
return sb.toString();
}
private static String baseSensitive(String str, int startLength, int endLength) {
if (StringUtils.isBlank(str)) {
return "";
}
return StringUtils.left(str, startLength).concat(StringUtils.leftPad(StringUtils.right(str, endLength), str.length() - startLength, "*"));
}
}
logback对应的配置如下:
<property name="log.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS}|%level|%thread|%X{clientIp}|%X{traceId}|%X{rpcId}|%c.%M[%L]|%msg%n" />
<!--日志脱敏-->
<conversionRule conversionWord="msg" converterClass="com.success.util.LogMessageConvert" />
自定义格式转换符 msg会被%msg的形式引用