情景分析
有时候在前端展示时,需要将电话号码,身份证等敏感信息过滤掉,显示成 *** 的字样;如果只是前端进行修改,那么其实数据还是会返回,只能进行后端的修改,
疑难点:
1:并不是所有页面都要进行模糊,比如管理员等操作不能进行模糊掉,
2:有部分导入的功能,导出的数据也可能需要模糊掉;新增时不能进行加密,修改有加密处理的,保存时需要再进行恢复;
3:一些返回的字段名字并不统一,有的叫 phoneNum,有的叫 phone,有点叫 managerPhone
4:部分前端组件比如客户下拉框等也包含身份证信息,同样需要进行脱敏处理;
5:返回结果进行处理时,可能是封装起来的对象,需要遍历加递归进行处理;
实现思路:
1:权限控制
设置页面给操作人员添加权限控制,哪些字段可以显示,哪些字段需要进行脱敏;
2:自定义注解,将需要进行模糊类型统一封装成一个实体,让需要脱敏的返回类型继承该实体,这样可以避免每一个实体中都去添加注解,然后进行AOP编程,将数据进行模糊处理;
切面的处理几乎每个都要搞,将其需要进行处理的权限字段放入到Redis中,修改权限控制时删除并更新Redis;
@IgnoreEncrypt 注解,使用该注解标注的controller不会校验权限;
接口传入 ignoreEncrypt=1 或者使用 IgnoreEncrypt标注controller,都可以使该次请求不校验权限
@FieldRight注解:
@FieldRight(fieldRightType = FieldRightType.CARD_NO)
private String newcardNo;//证件号码,打*
实现代码:
1:声明脱敏的字段注解
/**
* 标记字段 使用何种策略来脱敏
*/
@Documented
@Inherited
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldRight {
FieldRightType fieldRightType() default FieldRightType.OTHER;
}
其中脱敏类型可根据情况进行自定义
package com.xqc.commoncommon.enums;
public enum FieldRightType {
/**
* 证件号码
*/
CARD_NO("cardNo","证件号码"),
/**
* 邮箱
*/
EMAIL("email","邮箱"),
/**
* 联系电话
*/
PHONE("phone","联系电话"),
/**
* 客户生日
*/
// BIRTHDAY("birthday","客户生日"),
/**
* 联系地址
*/
ADDRESS("address","联系地址"),
/**
* 证件地址
*/
CARD_ADDRESS("cardAddress","证件地址"),
/**
* 注册地址
*/
REGISTER_ADDRESS("registerAddress","注册地址"),
/**
* 工作地址
*/
WORK_ADDRESS("workAddress","工作地址"),
/**
* 门户登录账号
*/
LOGIN_NAME("loginName","门户登录账号"),
/**
* 其他(不受权限控制)
*/
OTHER("other","其他"),
private final String field;
private final String fieldName;
FieldRightType(String field, String fieldName) {
this.field = field;
this.fieldName = fieldName;
}
public String getField() {
return field;
}
public String getFieldName() {
return fieldName;
}
}
2:自定义脱敏的统一实体
3:将需要脱敏的实体进行改造
4:对请求的统一处理
//先从redis中查询权限,如果查询不到则从数据库中查询,并放在redis中
//userId
LoginUserInfo loginUserInfo = ServletUtil.getLoginUserInfo(request);
String ignoreEncrypt = request.getParameter("ignoreEncrypt");
if (loginUserInfo != null && !"1".equals(ignoreEncrypt)) {
// 获取容器
ServletContext sc = request.getSession().getServletContext();
XmlWebApplicationContext cxt = (XmlWebApplicationContext) WebApplicationContextUtils.getWebApplicationContext(sc);
// 从容器中获取DispersedCacheSerciceImpl
if (cxt != null && cxt.getBean("DispersedCacheSerciceImpl") != null && iDispersedCacheSercice == null) {
iDispersedCacheSercice = (DispersedCacheSerciceImpl) cxt.getBean("DispersedCacheSerciceImpl");
}
String key = "FieldRight:" + loginUserInfo.getUserId();
Object o = iDispersedCacheSercice.get(key);
List<String> userCustomerFieldRightList = new ArrayList<>();
if (o != null) {
userCustomerFieldRightList = JSONArray.parseArray(o.toString(), String.class);
} else {
UserCustomerFieldRightQuery query = new UserCustomerFieldRightQuery();
query.setCompanyId(loginUserInfo.getCompanyId());
query.setUserId(loginUserInfo.getUserId());
//查询需要隐藏的字段
query.setFieldRight(1);
List<CommonUserCustomerFieldRightDTO> userCustomerFieldRight = iUserService.getUserCustomerFieldRight(query);
//整理
userCustomerFieldRightList = userCustomerFieldRight.stream().map(CommonUserCustomerFieldRightDTO::getField)
.collect(Collectors.toList());
iDispersedCacheSercice.add(key, userCustomerFieldRightList);
}
//查询该用户的客户权限,如果查询不到则表示全部放开
request.setAttribute("userCustomerFieldRightList",userCustomerFieldRightList);
}
5:AOP对返回结果统一处理
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.xqc.annotation.FieldRight;
import com.xqc.annotation.IgnoreEncrypt;
import com.xqc.enums.FieldRightType;
import com.xqc.utils.CommonUtil;
import java.lang.reflect.Field;
import java.util.List;
/**
* controller的切面控制,目前用于字段权限控制<br/>
* 关于字段权限控制,详情请查看{@link FieldRight}
*/
@Aspect
@Component
@Slf4j
public class CommonControllerAspect {
/**
* 切入所有添加{@link IgnoreEncrypt}注解的controller
*/
@Pointcut("@annotation(com.xqc.annotation.IgnoreEncrypt)")
public void pointcut(){}
/**
* 添加{@link IgnoreEncrypt}注解的controller在进入之前去除字段权限校验标志
*/
@Before(value = "pointcut()")
public void before(JoinPoint joinPoint) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
requestAttributes.getRequest().setAttribute("userCustomerFieldRightList",null);
}
/**
* 1、切入所有的controller
* 2、目前(2021年8月4日)用于字段权限校验,需考虑该字段权限校验是否只过滤部分package下的controller
*/
@Pointcut("execution(* com.xqc.*.controller.*.*(..))")
public void allControllerPointCut(){}
/**
* 1、切入controller的返回后,
* @param joinPoint
* @param returnValue
*/
@AfterReturning(value = "allControllerPointCut()", returning="returnValue")
@SuppressWarnings("unchecked")
public void afterController(JoinPoint joinPoint,Object returnValue) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
List<String> fieldRightList = (List<String>)requestAttributes.getRequest().getAttribute("userCustomerFieldRightList");
try {
dealFieldRight(returnValue,fieldRightList);
} catch (Exception exception) {
exception.printStackTrace();
}
}
/**
* 字段权限处理主要逻辑
* 1、 如果传入的是list,则遍历后递归
* 2、 如果传入的是dto,则直接分析
* 3、 传入的是其他类型,直接忽略
*/
@SuppressWarnings("unchecked")
public static boolean dealFieldRight(Object model, List<String> fieldRightList) throws Exception{
//如果需要校验的字段,则直接返回false
if (fieldRightList == null || fieldRightList.isEmpty()){
return false;
}
//如果需要处理的object为空,则直接返回
if (model == null){
return false;
}
//根据typeName来判断是dto还是list
String typeName = model.getClass().getTypeName();
//判断是否是list,list的typeName: java.util.***List,所以根据java.utll和小写的list为关键字进行判断,若同时出现则认定为是list
if (typeName.contains("java.util") && typeName.toLowerCase().contains("list")){
/*
是list
循环对每一个元素递归处理
*/
List modelList = (List)model;
if (modelList.isEmpty()){
return false;
}
for (Object item : modelList) {
if (item == null){
continue;
}
//递归进行处理,但是当第一次遇到的元素无需进行处理的时候,表示后续的item也无需处理了
boolean canDoNext = dealFieldRight(item,fieldRightList);
if (!canDoNext){
break;
}
}
return true;
}else if (typeName.contains("com.xqc")){
//不是list,那么就根据com.xqc判断是不是项目的Object
//需要循环读取父类,直到遇到Object为止:Object的superClass是空
Class checkClass = model.getClass();
while (checkClass.getSuperclass()!=null){
Field[] fields = checkClass.getDeclaredFields();
for (Field field : fields) {
//如果是一个list,那么需要递归进行处理
String type = field.getType().toString();
if (type.contains("java.util") && type.toLowerCase().contains("list")){
field.setAccessible(true);
List o = (List)field.get(model);
if (o == null){
continue;
}
if (o.isEmpty()){
continue;
}
for (Object item : o) {
if (item == null){
continue;
}
boolean canDoNext = dealFieldRight(item,fieldRightList);
if (!canDoNext){
break;
}
}
}
//如果field是dto,也要递归处理
if (type.contains("com.xqc")){
field.setAccessible(true);
Object o = field.get(model);
if (o != null){
dealFieldRight(model,fieldRightList);
}
}
String fieldName;
FieldRight annotation = field.getAnnotation(FieldRight.class);
if (annotation != null){
if (annotation.fieldRightType() == FieldRightType.OTHER){
continue;
}else{
fieldName = annotation.fieldRightType().getField();
}
}else {
//没有注解的不进行加密处理
continue;
}
if (fieldRightList.contains(fieldName)) {
field.setAccessible(true);
Object o1 = field.get(model);
if (o1 == null || "".equals(o1.toString())){
continue;
}
//如果是String就设置为星号,否则设置为空
if ("class java.lang.String".equals(field.getGenericType().toString())){
//判断是否是cardNo
if (fieldName.equals(FieldRightType.CARD_NO.getField())){
//获取cardNo
field.set(model, CommonUtil.cardNoSet(o1.toString()));
}else{
field.set(model, "******");
}
}else{
field.set(model,null);
}
}
}
checkClass = checkClass.getSuperclass();
}
return true;
}else {
//如果一个其他类型传进来进行数据处理,直接忽略即可,只需要处理list和com.xqc下的dto
return false;
}
}
}