基于jackson自定义注解,实现对数据的序列化控制

15 篇文章 0 订阅

需求

考虑到数据安全问题,数据库中存储的数据是加密了的数据。

所以contoller的接口在返回数据时候,想要判断当前用户是否具备查看解密数据的权限,有则返回解密数据,没有则返回加密数据。

此需求由‘获取权限’与‘返回数据的序列化控制’两因素组成。

本文要做的是‘返回数据序列化控制’的部分。权限部分用户自己意淫一个接口实现即可。

环境

1. 数据库的数据是加密的

2. 项目采用的spring框架

设计

返回加密数据的接口有很多,都需要做序列化控制,简单的实现就是每个controller或者service层做解密操作,但是这样一来需要在这些函数中依次写入解密代码。一两个接口倒还好,若是几十个那咋办,不得累死。

所以,采用注解的方式,spring采用的jackson序列化框架,提供了可控的接口。我们只需要实现注解以及序列化逻辑的工具类,然后在实体类上标注上我们的注解,就可以自动按照我们的逻辑进行序列化了。

四步骤:

1. 自定义一个序列化注解(以下简称A),用于标注需要被保密控制的字段上。考虑到不同的业务字段的加解密算法会有区别,所以给注解中定义一个属性,告诉注解其修饰的字段是哪种类型的字段,标识区别。

2. 编写保密序列化工具类(以下简称B),内部具备依据是否需要返回加、解密数据的序列化逻辑。该工具类可以依据注解A的属性,来做不同的解密算法操作。考虑到通用性,本序列化工具类将把具体的序列化逻辑以及一些序列化前、序列化后的生命周期定义出‘钩子’,让子类去实现。这样使用者就可以自行实现具体的序列化方法。

3. 告诉spring和jackson,在序列化时候,针对被我们标记了A注解字段,执行我们植入的序列化逻辑。 至于怎么告诉spring和jackson。1. 给目标类上使用jackson的注解:@JsonSerialize,并在其‘using’属性中指定我们的工具类B。这样一来,就告诉jackson,序列化该类的时候,会调用我们的序列化方法。2. 让工具类实现jackson的接口,这样jackson才能调度我们的工具类

4. 基于2,开发在使用2的工具类B时,需要写一个类,继承B并实现抽象解密方法。

5. 创建一个开关,用于标识是否需要解密。在2的工具类B中用此来判断是否需要解密。

实现

ok,接下来开始实现。

自定义注解

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface Confusion {
    ConfusedFieldTypeEnum type() default ConfusedFieldTypeEnum.NOT_CONFUSED;

    boolean turnOn() default true;
}

其中的type属性,用一个enum来表达,在enum中指定字段类型

字段类型enum

public enum ConfusedFieldTypeEnum {
    NOT_CONFUSED(-1),
    EMAIL(0),
    LOGIN_NAME(1),
    PHONE(2);

    private int code;

    private ConfusedFieldTypeEnum(int code) {
        this.code = code;
    }

    public int getCode() {
        return this.code;
    }

    public void setCode(int code) {
        this.code = code;
    }
}

序列化控制工具类(抽象类)


public abstract class MySerializer extends JsonSerializer<Object> {

    @Override
    public void serialize(Object obj, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
        boolean switchState = CurrentThreadConfusedSwitch.getSwitchState();
        jsonGenerator.writeStartObject();
        try {
            Class<?> aClass = obj.getClass();
            List<Field> declaredFields = getDeclaredFields(aClass);
            for (Field declaredField : declaredFields) {
                declaredField.setAccessible(true);
                Object oldValue = declaredField.get(obj);
                if (oldValue != null && declaredField.isAnnotationPresent(Confusion.class)) {//this thread turn on the switch and with the @interface of Confusion
                    Confusion confusion = declaredField.getDeclaredAnnotation(Confusion.class);
                    Object result = null;
                    ConfusedFieldTypeEnum type = confusion.type();
                    if (type != null && type != ConfusedFieldTypeEnum.NOT_CONFUSED) {//type is not NOT_CONFUSED,then set the wrapped value to obj
                        if (switchState) {//turning on, then set the opening-wrapped value to it.
                            result = wrapperValueOnSwitchOpen(type, oldValue);
                        } else {//turning off, then set the offing-wrapped value to it.
                            result = wrapperValueOnSwitchOff(type, oldValue);
                        }
//                        if (result != null) {
                        jsonGenerator.writeObjectField(declaredField.getName(), result);
//                        } else {
//                            jsonGenerator.writeObjectField(declaredField.getName(), oldValue);
//                        }
                    } else {
                        jsonGenerator.writeObjectField(declaredField.getName(), result);
                    }
                } else {//without being marked by @interface Confusion, then set the source value to it.
                    jsonGenerator.writeObjectField(declaredField.getName(), oldValue);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        jsonGenerator.writeEndObject();

    }

    private List<Field> getDeclaredFields(Class<?> aClass) {
        List<Field> fieldList = new ArrayList<>();
        Field[] declaredFields1 = aClass.getDeclaredFields();
        fieldList.addAll(Arrays.asList(declaredFields1));
        List<Class<?>> classes = getAllSuperclass(aClass);
        if (classes != null) {
            for (Class<?> clazz: classes) {
                Field[] declaredFields = clazz.getDeclaredFields();
                fieldList.addAll(Arrays.asList(declaredFields));
            }
        }

        return fieldList;
    }

    private List<Class<?>> getAllSuperclass(Class<?> aClass) {
        List<Class<?>> allClasses = new ArrayList<>();
        Class<?> superclass;
        while ((superclass = aClass.getSuperclass()) != null) {
            allClasses.add(superclass);
            aClass = superclass;
        }
        return allClasses;
    }

    private Field[] getDeclaredFieldsJustThisClass(Class<?> aClass) {
        return aClass.getDeclaredFields();
    }

    public abstract Object wrapperValueOnSwitchOff(ConfusedFieldTypeEnum type, Object value);

    public abstract Object wrapperValueOnSwitchOpen(ConfusedFieldTypeEnum type, Object value);

}

是一个抽象类,其中定义了四个抽象方法。是应序列化的四个生命周期的钩子。

使用自定义注解到实体类


public class User2 implements Serializable {

	private static final long serialVersionUID = 5243885779545855210L;

	private Long id;


	@UserUuid
	private String userUuid;    //uuid
	
	@Confusion(type = ConfusedFieldTypeEnum.EMAIL)
	private String loginEmail; // 登录邮箱
	
	@Confusion(type = ConfusedFieldTypeEnum.LOGIN_NAME)
	private String loginName; // 登录名称

	private String realName; // 真实姓名

	@Confusion(type = ConfusedFieldTypeEnum.PHONE)
	private String mobilePhone; // 联系方式


	public static long getSerialVersionUID() {
		return serialVersionUID;
	}

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	public String getUserUuid() {
		return userUuid;
	}

	public void setUserUuid(String userUuid) {
		this.userUuid = userUuid;
	}

	public String getLoginEmail() {
		return loginEmail;
	}

	public void setLoginEmail(String loginEmail) {
		this.loginEmail = loginEmail;
	}

	public String getLoginName() {
		return loginName;
	}

	public void setLoginName(String loginName) {
		this.loginName = loginName;
	}

	public String getRealName() {
		return realName;
	}

	public void setRealName(String realName) {
		this.realName = realName;
	}

	public String getMobilePhone() {
		return mobilePhone;
	}

	public void setMobilePhone(String mobilePhone) {
		this.mobilePhone = mobilePhone;
	}
}

接下来,就是写一个实现类,继承它,实现序列化和判断是否需要解密的内容即可,给个demo:

序列化控制工具类(实现类)


public class PoJoConfusedSerializer extends MySerializerEx {
    //    private Logger logger = LoggerFactory.getLogger(PoJoConfusedSerializer.class);
    private ThreadLocal<CachedPojo> cachedPojoThreadLocal = new ThreadLocal<>();

    @Override
    protected void afterWrapper(Object o) {
        cachedPojoThreadLocal.remove();
    }

    @Override
    public void beforeWrapper(Object obj) {
        boolean switchState = CurrentThreadConfusedSwitch.getSwitchState();
        if (switchState) {//开启了,就需要解混淆
            String userUuid = getUserUuid(obj);
            CachedPojo cachedUser = UserCacheManager.loadUser(userUuid);//init to get
            cachedPojoThreadLocal.set(cachedUser);
        }
    }

    @Override
    public Object wrapperValueOnSwitchOff(ConfusedFieldTypeEnum confusedFieldTypeEnum, Object value, Object o1) {
        //off,关闭开关,返回加密数据
        return value;
    }

    @Override
    public Object wrapperValueOnSwitchOpen(ConfusedFieldTypeEnum confusedFieldTypeEnum, Object value, Object o1) {
        //on,打开开关,返回解密数据
        CachedPojo cachedPojo = cachedPojoThreadLocal.get();
        if (cachedPojo != null) {
            switch (confusedFieldTypeEnum) {
                case EMAIL:
                    return cachedPojo.getEmail();
                case PHONE:
                    return cachedPojo.getPhone();
                case LOGIN_NAME:
                    return cachedPojo.getLoginName();
                default:
                    return null;
            }
        }
        //没有查询到就返回原值吧
        return value;
    }

    public String getUserUuid(Object object) {
        Class<?> aClass = object.getClass();
        Field[] declaredFields = aClass.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            boolean annotationPresent = declaredField.isAnnotationPresent(UserUuid.class);
            if (annotationPresent) {
                try {
                    declaredField.setAccessible(true);
                    return (String) declaredField.get(object);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
                break;
            }
        }
        return null;
    }

}

其中的beforeWrapper用来判断是否需要解密,以及获取该是同一类的解密版本,并放到threadlocal中传递。然后afterWrapper函数中,再移除;wrapperValueOnSwitchOff 、wrapperValueOnSwitchOpen这两个函数负责处理开关开、关的时候的处理。

开关

本开关利用的session,controller或者service层在判断当前用户可以先看解密数据时,调用此工具进行设置,然后序列化工具使用这个工具类进行不同处理。

public class CurrentThreadConfusedSwitch {
	private static Logger logger = LoggerFactory.getLogger(CurrentThreadConfusedSwitch.class);
	private static final String SESSION_KEY = "_e_g_k";
	private static ThreadLocal<Boolean> cached = new ThreadLocal();

	public CurrentThreadConfusedSwitch() {
	}


	private static void updateSwitchStateToSession(boolean v, HttpServletRequest request) {
		HttpSession session = request.getSession(true);
		session.setAttribute("_e_g_k", v);
	}

	public static void turnOnConfusedSwitch(HttpServletRequest request) {
		if (request == null) {
			logger.error("request is null! could not turn on the switch!");
		} else {
			updateSwitchStateToSession(true, request);
		}

	}

	public static void turnOffConfusedSwitch(HttpServletRequest request) {
		if (request == null) {
			logger.error("request is null! could not trun off the switch!");
		} else {
			updateSwitchStateToSession(false, request);
		}

	}

	public static void holdSwitchState(HttpServletRequest request) {
		boolean b = internal_getSwitchStateFromSession(request);
		internal_setSwitchState(b);
	}

	public static boolean getSwitchState() {
		return cached.get() != null && (Boolean)cached.get();
	}

	private static boolean internal_getSwitchStateFromSession(HttpServletRequest request) {
		HttpSession session = request.getSession(true);
		Boolean session_confused = (Boolean)session.getAttribute("_e_g_k");
		return session_confused != null && session_confused;
	}

	private static void internal_setSwitchState(boolean value) {
		cached.set(value);
	}
}

问题

@JsonSerialize的类继承问题

答案:可以继承。类B是类A的子类,类A使用了该注解,那么类B在序列化时,也是会受控制的。

自定义注解的类继承问题

我们要返回的数据类B.class是一个类A的子类,而A中有用我们修饰的字段,它能被监测到并序列化控吗码?

答案:能。

为什么?

答案:因为这段代码:

函数getDeclaredFields的代码在上面的‘序列化控制工具类(抽象类)’有写,我再写一遍大家看看:

    private List<Field> getDeclaredFields(Class<?> aClass) {
        List<Field> fieldList = new ArrayList<>();
        Field[] declaredFields1 = aClass.getDeclaredFields();
        fieldList.addAll(Arrays.asList(declaredFields1));
        List<Class<?>> classes = getAllSuperclass(aClass);
        if (classes != null) {
            for (Class<?> clazz: classes) {
                Field[] declaredFields = clazz.getDeclaredFields();
                fieldList.addAll(Arrays.asList(declaredFields));
            }
        }

        return fieldList;
    }
 
    private List<Class<?>> getAllSuperclass(Class<?> aClass) {
        List<Class<?>> allClasses = new ArrayList<>();
        Class<?> superclass;
        while ((superclass = aClass.getSuperclass()) != null) {
            allClasses.add(superclass);
            aClass = superclass;
        }
        return allClasses;
    }

 调用了getAllSuperclass函数,把所有的超类都的属性都拿到手了。

测试

去试试吧

待优化

上述的工具类考虑到通用性,对序列化方法进行了抽象。但是,enum枚举类没有做出相应的抽象。这样一来,二次开发者虽然可以自行实现序列化方法,但是却不能增加新的enum类型。比如,我是二次开发者,我想要对‘爱好’这一属性进行解密,我定义一个类A继承了工具类,但是在序列化方法中,我无法得知当前的字段为‘爱好’这一类型,因为enum已经被写死了。

所以,这里还有待提升。至于提升的办法有很多,读者自行思考。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值