java web 数据脱敏

java web 数据脱敏

参考上文: java日志脱敏实现

1. 题记

在交易管理系统中,由于数据库存储客户人脸图片和客户名称、客户证件号、手机号、银行卡号等相关敏感字段,为了防止数据泄露现根据用户权限实现数据响应脱敏。

2. 设计

由于日志脱敏实现与客户数据信息录入,即数据请求阶段,参考上述实现方案,在数据响应阶段做公共处理,具体设计如下:

2.1 分析原响应体:

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;
	}

}

2.2 原数据测试结果

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));

    }
2.3 打印结果
数据脱敏前:{"data":{"name":"尚先生","phones":{"personPhone":"12345678901","phone":"12345678900"},"idNo":"110223179919992792"},"code":0,"page":1,"pageSize":1,"success":true,"total":1,"totalPages":1}

2.4 综合分析

  1. 响应体包含toRetMap()toRetPage()两个方法,如何处理脱敏数据的切入点在此处,这样处理的好处就是系统的侵入性比较小。
  2. 除了处理数据脱敏之外就是权限的控制,对于客诉或者风控干预时敏感信息也是必要信息,所以需要按照登录人的权限分别处理。
  3. 数据脱敏支持的数据类型:上文java日志脱敏实现中实现了对单层MapObject的脱敏处理,本次实现支持的范围:单层和两层MapObject以及List的数据类型支持,如果后续有需求,方便扩展,比如:支持多层Map数据脱敏。

3. 数据脱敏实现

3.1 构造包装类

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 <T>
	 * @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;

	}
}

3.2 脱敏数据测试结果

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));

    }

3.3 打印日志

数据脱敏后:{"data":{"name":"尚先生","phones":{"personPhone":"123*****901","phone":"123*****900"},"idNo":"110******2792"},"code":0,"page":1,"pageSize":1,"success":true,"total":1,"totalPages":1}

4. 数据脱敏关联类

4.1 枚举类

public enum TypeEnum {
    
    

    /**客户名称**/
    CUST_NAME,
    /**客户证件号**/
    ID_NO,
    /**客户手机号**/
    PHONE_NO,
    /**客户银行卡名称**/
    BANK_NAME,
    /**客户银行卡号**/
    BANK_NO,
    /**密码**/
    PASSWORD,
    /**图片地址**/
    IMAGE_URL,
}

4.2 注解类

/**
 * @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 "";

}

4.3 脱敏字段

/**
 * @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;
}

4.4 脱敏实现工具类


/**
 * @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) {
    
    
                HashMap<String, Object> reqMap = (HashMap) 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(HashMap<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;
    }

}

5. 角色权限

5.1 鉴权

实现一

根据当前登录用户角色判断

实现二

区分不同url实现

5.2 推荐实现根据用户角色实现

1.配置客户敏感信息查看url
2.对改url进行不同角色赋权
3.在RequestContext中获取当前请求用户权限列表(List)
4.数据响应时判断 RequestContext中(List)是否包含 1中配置的url,进行相应的脱敏处理

5.3 UserInfo 类

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");
	}
}

5.4 请求数据保存至Request

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    
    

...
      request.setAttribute(UserInfo.CUSTOMIZE_KEY, new UserInfo(token, user,  urls));
      return true;
}

5.5 RequestContextHolder

 public static UserInfo getCurrentUserInfo() {
    
    
        return (UserInfo) RequestContextHolder.currentRequestAttributes().getAttribute(UserInfo.CUSTOMIZE_KEY, RequestAttributes.SCOPE_REQUEST);
    }

由此,根据当前登录用户权限可实现数据脱敏。

猜你喜欢

转载自blog.csdn.net/shang_xs/article/details/112964234