参考上文: java日志脱敏实现
在交易管理系统中,由于数据库存储客户人脸图片和客户名称、客户证件号、手机号、银行卡号等相关敏感字段,为了防止数据泄露现根据用户权限
实现数据响应脱敏。
由于日志脱敏实现与客户数据信息录入,即数据请求阶段,参考上述实现方案,在数据响应阶段做公共处理
,具体设计如下:
public class Result<T> implements Serializable {
private static final long serialVersionUID = 3958386020691046240L;
private int code = 0;
private String msg;
private T data;
private int total = 1;
private int pageSize = 1;
private int page = 1;
public Result() {
}
public Result(int code, String msg) {
this.code = code;
this.msg = msg;
}
public Result(T t) {
this.data = t;
}
public Result(int total, int pageSize, int page, T t) {
this.total = total;
this.pageSize = pageSize;
this.page = page;
this.data = t;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getTotalPages() {
int totalPages = this.total / this.pageSize;
if (this.total % this.pageSize != 0){
totalPages++;
}
return totalPages;
}
public boolean isSuccess() {
return this.code == 0;
}
public Map<String, ?> toRetMap() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("ret", this.isSuccess() ? 1 : 0);
map.put("data", this.getData() == null ? new HashMap<>() : this.getData());
if (this.isSuccess()) {
map.put("error", null);
} else {
Map<String, Object> errorMap = new HashMap<String, Object>(2);
errorMap.put("code", this.getCode());
errorMap.put("msg", this.getMsg());
map.put("data", null);
map.put("error", errorMap);
}
return map;
}
public Map<String, ?> toRetPage() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("ret", this.isSuccess() ? 1 : 0);
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("rows", this.getData());
dataMap.put("page", this.getPage());
dataMap.put("pageSize", this.getPageSize());
dataMap.put("total", this.getTotal());
dataMap.put("totalPages", this.getTotalPages());
map.put("data", dataMap);
if (this.isSuccess()) {
map.put("error", null);
} else {
Map<String, Object> errorMap = new HashMap<String, Object>(2);
errorMap.put("code", this.getCode());
errorMap.put("msg", this.getMsg());
map.put("error", errorMap);
}
return map;
}
}
public static void main(String[] args) {
Map<Object, Object> ret = new HashMap<>();
Map<Object, Object> phones = new HashMap<>();
phones.put("phone", "12345678900");
phones.put("personPhone", "12345678901");
ret.put("name", "尚先生");
ret.put("phones", phones);
ret.put("idNo", "110223179919992792");
Result<Map<Object, Object>> originMap = new Result<>(ret);
System.err.println("数据脱敏前:" + JSON.toJSONString(originMap));
}
数据脱敏前:{"data":{"name":"尚先生","phones":{"personPhone":"12345678901","phone":"12345678900"},"idNo":"110223179919992792"},"code":0,"page":1,"pageSize":1,"success":true,"total":1,"totalPages":1}
toRetMap()
和toRetPage()
两个方法,如何处理脱敏数据的切入点在此处,这样处理的好处就是系统的侵入性比较小。权限的控制
,对于客诉或者风控干预时敏感信息也是必要信息,所以需要按照登录人的权限分别处理。Map
和Object
的脱敏处理,本次实现支持的范围:单层和两层Map
和Object
以及List
的数据类型支持,如果后续有需求,方便扩展,比如:支持多层Map
数据脱敏。public class ResultWrapper<T> implements Serializable {
private static final long serialVersionUID = 3958386020691046240L;
private int code = 0;
private String msg;
private T data;
private int total = 1;
private int pageSize = 1;
private int page = 1;
public ResultWrapper() {
}
public Result build(int code, String msg) {
return new Result <>(code, msg);
}
public Result build(T t) {
// 判断返回值类型
if (t instanceof Result){
desensity(((Result) t).getData());
return (Result) t;
}else {
desensity(t);
return new Result(t);
}
}
public Result build(int total, int pageSize, int page, T t) {
// 返回值类型 data
desensity(t);
return new Result<>(total, pageSize, page, t);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getTotalPages() {
int totalPages = this.total / this.pageSize;
if (this.total % this.pageSize != 0){
totalPages++;
}
return totalPages;
}
public boolean isSuccess() {
return this.code == 0;
}
public Map<String, ?> toRetMap() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("ret", this.isSuccess() ? 1 : 0);
map.put("data", this.getData() == null ? new HashMap<>() : this.getData());
if (this.isSuccess()) {
map.put("error", null);
} else {
Map<String, Object> errorMap = new HashMap<String, Object>(2);
errorMap.put("code", this.getCode());
errorMap.put("msg", this.getMsg());
map.put("data", null);
map.put("error", errorMap);
}
return map;
}
public Map<String, ?> toRetPage() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("ret", this.isSuccess() ? 1 : 0);
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("rows", this.getData());
dataMap.put("page", this.getPage());
dataMap.put("pageSize", this.getPageSize());
dataMap.put("total", this.getTotal());
dataMap.put("totalPages", this.getTotalPages());
map.put("data", dataMap);
if (this.isSuccess()) {
map.put("error", null);
} else {
Map<String, Object> errorMap = new HashMap<String, Object>(2);
errorMap.put("code", this.getCode());
errorMap.put("msg", this.getMsg());
map.put("error", errorMap);
}
return map;
}
/**
* 数据脱敏
* @param data
* @param
* @return
*/
public <T> T desensity(T data){
// 判空
if (Objects.isNull(data)){
return null;
}
// 当前登录人鉴权
if (SSOUtils.getCurrentUserInfo().hasCustSecret()){
return data;
}
// Map支持两层,可扩展
if (data instanceof Map){
DesensitizedUtils.getConverent(data);
for (Object retValue : ((Map) data).values()) {
DesensitizedUtils.getConverent(retValue);
}
}
// list 数据响应
else if (data instanceof List) {
List<Object> retDataList = (List<Object>) data;
for (Object retObj : retDataList) {
DesensitizedUtils.getConverent(retObj);
}
}
// Object 格式数据目前只支持单层
else {
DesensitizedUtils.getConverent(data);
}
return data;
}
}
public static void main(String[] args) {
Map<Object, Object> ret = new HashMap<>();
Map<Object, Object> phones = new HashMap<>();
phones.put("phone", "12345678900");
phones.put("personPhone", "12345678901");
ret.put("name", "尚先生");
ret.put("phones", phones);
ret.put("idNo", "110223179919992792");
Result <Map<Object, Object>> originMap = new Result <>(ret);
Result desMap = new ResultWrapper<>().build(originMap);
System.err.println("数据脱敏后:" + JSON.toJSONString(desMap));
}
数据脱敏后:{"data":{"name":"尚先生","phones":{"personPhone":"123*****901","phone":"123*****900"},"idNo":"110******2792"},"code":0,"page":1,"pageSize":1,"success":true,"total":1,"totalPages":1}
public enum TypeEnum {
/**客户名称**/
CUST_NAME,
/**客户证件号**/
ID_NO,
/**客户手机号**/
PHONE_NO,
/**客户银行卡名称**/
BANK_NAME,
/**客户银行卡号**/
BANK_NO,
/**密码**/
PASSWORD,
/**图片地址**/
IMAGE_URL,
}
/**
* @ClassName: DesensitizedAnnotation
* @Description: 注解类
* @Author: 尚先生
* @CreateDate: 2019/1/24 17:42
* @Version: 1.0
*/
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DesensitizedAnnotation {
/*脱敏数据类型(规则)*/
TypeEnum type();
/*判断注解是否生效,暂时没有用到*/
String isEffictiveMethod() default "";
}
/**
* @ClassName: BaseInfo
* @Description: 日志过滤字段基类
* @Author: 尚先生
* @CreateDate: 2019/1/24 17:38
* @Version: 1.0
*/
public class BaseInfo implements Serializable {
private static final long serialVersionUID = 7462164132214076087L;
// 证件号
@DesensitizedAnnotation(type = TypeEnum.ID_NO)
private String idNo;
// 手机号
@DesensitizedAnnotation(type = TypeEnum.PHONE_NO)
private String phone;
// 自定义手机号
@DesensitizedAnnotation(type = TypeEnum.PHONE_NO)
private String personPhone;
// 眨眼
@DesensitizedAnnotation(type = TypeEnum.IMAGE_URL)
private String faceImage;
// 张嘴
@DesensitizedAnnotation(type = TypeEnum.IMAGE_URL)
private String faceOpenMouseImage;
// 点头
@DesensitizedAnnotation(type = TypeEnum.IMAGE_URL)
private String faceNodImage;
// 摇头
@DesensitizedAnnotation(type = TypeEnum.IMAGE_URL)
private String faceShakingImage;
// 支付照片
@DesensitizedAnnotation(type = TypeEnum.IMAGE_URL)
private String faceImgUrl;
// OCR原图
@DesensitizedAnnotation(type = TypeEnum.IMAGE_URL)
private String oriImgUrl;
}
/**
* @ClassName: DesensitizedUtils
* @Description: 日志脱敏工具类
* @Author: 尚先生
* @CreateDate: 2019/1/24 17:52
* @Version: 1.0
*/
public class DesensitizedUtils {
private static final Logger logger = LoggerFactory.getLogger(DesensitizedUtils.class);
private static final Map<String, TypeEnum> annotationMaps = new HashMap<>();
/**
* 类加载时装配待脱敏字段
*/
static {
try {
Class<?> clazz = Class.forName(BaseInfo.class.getName());
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
fields[i].setAccessible(true);
DesensitizedAnnotation annotation = fields[i].getAnnotation(DesensitizedAnnotation.class);
if (annotation != null) {
TypeEnum type = annotation.type();
String name = fields[i].getName();
//name为注解字段名称,value为注解类型。方便后续根据注解类型扩展
annotationMaps.put(name, type);
}
}
} catch (ClassNotFoundException e) {
logger.error("脱敏字段异常,异常信息:{}", e);
}
}
/**
* 脱敏处理方法
*
* @param object
* @return
*/
public static Object getConverent(Object object) {
if (Objects.isNull(object)){
return null;
}
String objClassName = object.getClass().getName();
try {
// 1.处理Map数据类型
if (object instanceof Map) {
Map<String, Object> reqMap = (Map) object;
Iterator<String> iterator = annotationMaps.keySet().iterator();
iterator.forEachRemaining(annotationName -> {
if (reqMap.keySet().contains(annotationName)) {
doconverentForMap(reqMap, annotationName);
}
});
return JSON.toJSONString(reqMap);
}
// 2.处理Object数据类型
Object val = new Object();
Class<?> objClazz = Class.forName(objClassName);
Field[] declaredFields = objClazz.getDeclaredFields();
for (int j = 0; j < declaredFields.length; j++) {
Iterator<String> iterator = annotationMaps.keySet().iterator();
while (iterator.hasNext()) {
String annotationName = iterator.next();
if (declaredFields[j].getName().equals(annotationName)) {
declaredFields[j].setAccessible(true);
val = declaredFields[j].get(object);
//获取属性后现在默认处理的是String类型,其他类型数据可扩展
String value = doconverentForObject(val, annotationName);
declaredFields[j].set(object, value);
}
}
}
return object;
} catch (Exception e) {
logger.error("数据脱敏失败,异常信息:{}", e);
return object;
}
}
/**
* 脱敏数据源为Object时处理方式
*
* @param val
* @param annotationName
* @return
*/
private static String doconverentForObject(Object val, String annotationName) {
String value = String.valueOf(val);
if (StringUtils.isNotEmpty(value)) {
value = doConverentByType(value, annotationName);
}
return value;
}
/**
* 脱敏数据源为Map时处理方式
*
* @param reqMap
* @param annotationName
* @return
*/
private static void doconverentForMap(Map<String, Object> reqMap, String annotationName) {
String value = String.valueOf(reqMap.get(annotationName));
if (StringUtils.isNotEmpty(value)) {
value = doConverentByType(value, annotationName);
}
reqMap.put(annotationName, value);
}
/**
* 根据不同注解类型处理不同字段
*
* @param value
* @param annotationName
* @return
*/
private static String doConverentByType(String value, String annotationName) {
TypeEnum typeEnum = annotationMaps.get(annotationName);
switch (typeEnum) {
case CUST_NAME:
value = getStringByLength(value);
break;
case ID_NO:
value = getStringByLength(value);
break;
case PHONE_NO:
value = getStringByLength(value);
break;
case IMAGE_URL:
value = "";
break;
default:
value = getStringByLength(value);
}
return value;
}
/**
* 根据value长度取值(切分)
*
* @param value
* @return
*/
private static String getStringByLength(String value) {
int length = value.length();
if (length == 2){
value = value.substring(0, 1) + "*";
}else if (length == 3){
value = value.substring(0,1) + "*" + value.substring(length -1);
}else if (length > 3 && length <= 5){
value = value.substring(0,1) + "**" + value.substring(length -2);
}else if (length > 5 && length <= 7){
value = value.substring(0,2) + "***" + value.substring(length -2);
}else if (length > 7 && length <= 12){
value = value.substring(0,3) + "*****" + value.substring(length -3);
}else if (length > 12){
value = value.substring(0,3) + "******" + value.substring(length -4);
}
return value;
}
}
根据当前登录用户角色判断
区分不同url实现
public class UserInfo implements Serializable {
public static final String CUSTOMIZE_KEY = "customize_key";
private static final long serialVersionUID = -73465767568568712457L;
private String token;
private User user;
private List<String> urls;
public UserInfo(String token, User user, List<String> urls) {
this.token = token;
this.user = user;
this.urls = urls;
}
public String getToken() {
return token;
}
public User getUser() {
return user;
}
public boolean hasCustSecret() {
// 权限配置中心维护该url对应的角色权限
return urls.contains("/api/secret/user/info");
}
}
Request
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
...
request.setAttribute(UserInfo.CUSTOMIZE_KEY, new UserInfo(token, user, urls));
return true;
}
public static UserInfo getCurrentUserInfo() {
return (UserInfo) RequestContextHolder.currentRequestAttributes().getAttribute(UserInfo.CUSTOMIZE_KEY, RequestAttributes.SCOPE_REQUEST);
}
由此,根据当前登录用户权限可实现数据脱敏。