龙空技术网

Java注解实现数据脱敏-与主业务解耦且权限管控关联

小杬的学习日记 209

前言:

而今你们对“java拦截器返回值json加密”大约比较着重,你们都需要了解一些“java拦截器返回值json加密”的相关知识。那么小编也在网上收集了一些关于“java拦截器返回值json加密””的相关内容,希望朋友们能喜欢,看官们快快来了解一下吧!

目录标题相关类用途解释 1. @JacksonAnnotationsInside 2.@JsonSerialize(using=SensitiveInfoSerialize.class) 3. JsonSerializer 4. ContextualSerializer实现步骤 1. 声明自定义注解 SensitiveInfo 2. 声明序列化处理类 3. 类型枚举实现 4. 密文处理工具实现 5. 对应VO实体类加上注解验证方式 main函数 运行结果:

最近业务需求,为了防止用户手机号等敏感信息泄漏,需要把手机号进行密文处理,考虑到以下几个问题

业务多逻辑复杂,挨个找代码改,太费劲,而且无法保证能全部找到并改完不同用户需求不同,部分用户需要查看手机号,部分用户不需要查看手机号脱敏跟主业务相关度低,不想代码侵入太严重

想到这个逻辑跟转json日期格式化逻辑比较相似,于是上网查找实现方案,上网查询自定义注解和json处理自定义注解的相关信息,因为Spring MVC中默认使用的是Jackson工具包,这里查找Jackson相关资料,找到如下几个Jackson提供的注解和接口相关类用途解释1. @JacksonAnnotationsInside

com.fasterxml.jackson.annotation.JacksonAnnotationsInside

官方: 引用文本 元注释(在其他注释上使用的注释)用于指示Jackson应该使用它拥有的元注释,而不是使用目标注释(使用此注释注释的注释)。这在创建“组合注释”时非常有用,因为它有一个容器注释,这个容器注释需要用这个注释以及它“包含”的所有注释进行注释。

通俗点说,就是自定义的注解加上这个注解,自定义的注解就会被Jackson的注解拦截器(JacksonAnnotationIntrospector)findSerializer发现拦截并处理

2.@JsonSerialize(using=SensitiveInfoSerialize.class)

com.fasterxml.jackson.databind.annotation.JsonSerialize

官方:JsonSerialize类 — 通过附加到“getter”方法或字段或值类,用于配置序列化方面的注释。注释值类时,配置用于值类的实例,但可以由更具体的注释(附加到方法或字段的注释)覆盖。

注释示例如下:

@JsonSerialize(using=MySerializer.class,

as=MySubClass.class,

typing=JsonSerialize.typing.STATIC

)

(这是as=MySubClass.class多余的,因为某些属性会阻止其他属性:特别是,“using”优先于“as”,后者优先于“typing”设置)

using属性 — 用于序列化关联值的序列化程序类。根据注释的内容,值 要么是注释类的实例(可在需要类序列化程序的任何地方全局使用);或者仅用于通过getter方法序列化属性访问。

这里我的理解是JsonSerialize注解是给我自定义的注解配置序列化工具的信息,using就是设置这个注解要用哪个序列化工具。

3. JsonSerializer

com.fasterxml.jackson.databind.JsonSerializer

抽象类,该类定义ObjectMapper(以及其他链式JSONSerializer)使用提供的JsonGenerator将任意类型的对象序列化为JSON所使用的API。com.fasterxml.jackson.databind.ser.std.StdSerializer而不是此类,因为它将实现此类的许多可选方法。

注意:各种serialize方法永远不会用空值调用——调用方必须处理空值,通常是通过调用SerializerProvider.findNullValueSerializer来获取要使用的序列化程序。这也意味着在序列化空值时,不能直接使用自定义序列化程序来更改要生成的输出。

如果序列化程序是聚合序列化程序(这意味着它通过使用其他序列化程序来委托对其某些内容的处理),那么它通常还需要实现com.fasterxml.jackson.databind.ser.ResolvableSerializer,它可以找到所需的辅助序列化程序。这对于允许序列化程序的动态重写很重要;需要单独的调用接口来分离辅助序列化程序的解析(可能直接或间接地将循环链接回序列化程序本身)。

此外,为了支持每个属性的注释(根据每个属性配置序列化的各个方面),序列化程序可能需要实现com.fasterxml.jackson.databind.ser.ContextualSerializer,它允许序列化程序的专门化:调用com.fasterxml.jackson.databind.ser.ContextualSerializer.createContext传递有关属性的信息,并可以创建新配置的序列化程序来处理该特定属性。

如果同时实现了com.fasterxml.jackson.databind.ser.ResolvableSerializer和com.fasterxml.jackson.databind.ser.ContextualSerializer,则序列化程序的解析将在上下文化之前进行。

序列化处理类,用于实现具体脱敏的逻辑,把原数据变成带*的数据

4. ContextualSerializer

官方:JsonSerializer可以实现的附加接口获得一个回调,该回调可用于创建序列化程序的上下文实例,以用于处理支持的类型的属性。这对于可以通过批注配置的序列化程序很有用,或者应具有不同的行为,具体取决于要序列化的属性的类型。

就是通过回调函数实现自定义注解中的字段信息传递给自定义JsonSerializer对象的操作,比如我们这的自定义注解中存在type和permission两个字段,自定义JsonSerializer序列化工具中也存在type和permission两个字段,把注解中的两个字段值赋值给自定义JsonSerializer。

实现步骤

前提:项目使用的是SpringMVC框架,且Spring自动序列化MessageConverter使用的是默认的Jackson序列化,如果项目已改成fastjson,类比实现fastjson的自定义注解。

1.声明自定义注解 SensitiveInfo

这里我们想要区分手机号、身份证、银行卡号等多种敏感信息类型,所以使用了type;我们想要区分权限来确定是否脱敏操作,所以使用了permission字段,代码如下:

import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;import com.fasterxml.jackson.databind.annotation.JsonSerialize;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;@Retention(RetentionPolicy.RUNTIME)@JacksonAnnotationsInside  //让此注解可以被Jackson扫描到@JsonSerialize(using = SensitiveInfoSerialize.class)  //配置处理此注解的序列化处理类public @interface SensitiveInfo {	/**	 * 拥有此权限不加密	 * @return	 */	public String permission() default "MobileEncryptPermission";	/**	 * 脱敏  加密的类型	 * @return	 */	public SensitiveType type() default SensitiveType.MOBILE_PHONE;}
2.声明序列化处理类

这里实现了hasPermission()方法,来判断是否拥有指定权限。

createContextual()是实现ContextualSerializer的回调函数,根据注解内容创建SensitiveInfoSerialize并赋值。

serialize()中实现具体的脱敏密文等处理

import com.fasterxml.jackson.core.JsonGenerator;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.BeanProperty;import com.fasterxml.jackson.databind.JsonMappingException;import com.fasterxml.jackson.databind.JsonSerializer;import com.fasterxml.jackson.databind.SerializerProvider;import com.fasterxml.jackson.databind.ser.ContextualSerializer;import org.springframework.security.core.Authentication;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.util.PatternMatchUtils;import org.springframework.util.StringUtils;import java.io.IOException;import java.util.Collection;import java.util.Objects;public class SensitiveInfoSerialize extends JsonSerializer<String>		implements ContextualSerializer{	/**接收注解脱敏类型*/	private SensitiveType type;	/**接收可查看的权限*/	private String permission;	public SensitiveInfoSerialize() {	}	public SensitiveInfoSerialize(final SensitiveType type ,final String permission ) {		this.type = type;		this.permission = permission;	}	/**序列化的逻辑处理	*/	@Override	public void serialize(final String s, final JsonGenerator jsonGenerator,						  final SerializerProvider serializerProvider)						   throws IOException, JsonProcessingException {		if (hasPermission(permission)){			//有权限,显示原文			jsonGenerator.writeString(s);			return;		}		switch (this.type) {			case CHINESE_NAME: {				jsonGenerator.writeString(SensitiveInfoUtils.chineseName(s));				break;			}			case ID_CARD: {				jsonGenerator.writeString(SensitiveInfoUtils.idCardNum(s));				break;			}			case FIXED_PHONE: {				jsonGenerator.writeString(SensitiveInfoUtils.fixedPhone(s));				break;			}			case MOBILE_PHONE: {				jsonGenerator.writeString(SensitiveInfoUtils.mobilePhone(s));				break;			}			case ADDRESS: {				jsonGenerator.writeString(SensitiveInfoUtils.address(s, 4));				break;			}			case EMAIL: {				jsonGenerator.writeString(SensitiveInfoUtils.email(s));				break;			}			case BANK_CARD: {				jsonGenerator.writeString(SensitiveInfoUtils.bankCard(s));				break;			}			case CNAPS_CODE: {				jsonGenerator.writeString(SensitiveInfoUtils.cnapsCode(s));				break;			}		}	}	/**自定义注解被拦截后的回调函数*/	@Override	public JsonSerializer<?> createContextual(final SerializerProvider 	serializerProvider,final BeanProperty beanProperty) throws JsonMappingException {		if (beanProperty != null) { // 为空直接跳过			if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) { 			// 非 String 类直接跳过				SensitiveInfo sensitiveInfo = 						beanProperty.getAnnotation(SensitiveInfo.class);				if (sensitiveInfo == null) {					sensitiveInfo = 					beanProperty.getContextAnnotation(SensitiveInfo.class);				}				if (sensitiveInfo != null) { 				// 如果能得到注解,就将注解的 value 传入 SensitiveInfoSerialize					return new SensitiveInfoSerialize(sensitiveInfo.type(),sensitiveInfo.permission());				}			}			return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);		}		return serializerProvider.findNullValueSerializer(beanProperty);	}	/**	 * 判断接口是否有任意xxx,xxx权限	 * 这里写的是Spring Security的判断方式,如果使用的是shiro可以参考注掉的那两行代码,如果使用的其他的认证框架,自行参考实现逻辑	 * @param permission 权限符	 * @return {boolean}	 */	public boolean hasPermission(String permission) {		/**Spring Securty验证方式*/		if (StringUtils.isEmpty(permission)) {			return true;		}		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();		if (authentication == null) {			return false;		}		Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();		return authorities.stream()				.map(GrantedAuthority::getAuthority)				.filter(StringUtils::hasText)				.anyMatch(x -> PatternMatchUtils.simpleMatch(permission, x));		/**Shiro 验证方式*///		Subject subject = SecurityUtils.getSubject();//		return subject.isPermitted(permission);	}}
3. 类型枚举实现
public enum SensitiveType {	/**	 * 中文名	 */	CHINESE_NAME,	/**	 * 身份证号	 */	ID_CARD,	/**	 * 座机号	 */	FIXED_PHONE,	/**	 * 手机号	 */	MOBILE_PHONE,	/**	 * 地址	 */	ADDRESS,	/**	 * 电子邮件	 */	EMAIL,	/**	 * 银行卡	 */	BANK_CARD,	/**	 * 公司开户银行联号	 */	CNAPS_CODE}
4. 密文处理工具实现
import org.apache.commons.lang3.StringUtils;public class SensitiveInfoUtils {	/**	 * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**>	 */	public static String chineseName(final String fullName) {		if (StringUtils.isBlank(fullName)) {			return "";		}		final String name = StringUtils.left(fullName, 1);		return StringUtils.rightPad(name, StringUtils.length(fullName), "*");	}	/**	 * [中文姓名] 只显示第一个汉字,其他隐藏为2个星号<例子:李**>	 */	public static String chineseName(final String familyName, final String givenName) {		if (StringUtils.isBlank(familyName) || StringUtils.isBlank(givenName)) {			return "";		}		return chineseName(familyName + givenName);	}	/**	 * [身份证号] 显示最后四位,其他隐藏。共计18位或者15位。<例子:*************5762>	 */	public static String idCardNum(final String id) {		if (StringUtils.isBlank(id)) {			return "";		}		return StringUtils.left(id, 3).concat(StringUtils				.removeStart(StringUtils.leftPad(StringUtils.right(id, 3), StringUtils.length(id), "*"),						"***"));	}	/**	 * [固定电话] 后四位,其他隐藏<例子:****1234>	 */	public static String fixedPhone(final String num) {		if (StringUtils.isBlank(num)) {			return "";		}		return StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*");	}	/**	 * [手机号码] 前三位,后四位,其他隐藏<例子:138******1234>	 */	public static String mobilePhone(final String num) {		if (StringUtils.isBlank(num)) {			return "";		}		return StringUtils.left(num, 2).concat(StringUtils				.removeStart(StringUtils.leftPad(StringUtils.right(num, 2), StringUtils.length(num), "*"),						"***"));	}	/**	 * [地址] 只显示到地区,不显示详细地址;我们要对个人信息增强保护<例子:北京市海淀区****>	 *	 * @param sensitiveSize 敏感信息长度	 */	public static String address(final String address, final int sensitiveSize) {		if (StringUtils.isBlank(address)) {			return "";		}		final int length = StringUtils.length(address);		return StringUtils.rightPad(StringUtils.left(address, length - sensitiveSize), length, "*");	}	/**	 * [电子邮箱] 邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示<例子:g**@163.com>	 */	public static String email(final String email) {		if (StringUtils.isBlank(email)) {			return "";		}		final int index = StringUtils.indexOf(email, "@");		if (index <= 1) {			return email;		} else {			return StringUtils.rightPad(StringUtils.left(email, 1), index, "*")					.concat(StringUtils.mid(email, index, StringUtils.length(email)));		}	}	/**	 * [银行卡号] 前六位,后四位,其他用星号隐藏每位1个星号<例子:6222600**********1234>	 */	public static String bankCard(final String cardNum) {		if (StringUtils.isBlank(cardNum)) {			return "";		}		return StringUtils.left(cardNum, 6).concat(StringUtils.removeStart(				StringUtils.leftPad(StringUtils.right(cardNum, 4), StringUtils.length(cardNum), "*"),				"******"));	}	/**	 * [公司开户银行联号] 公司开户银行联行号,显示前两位,其他用星号隐藏,每位1个星号<例子:12********>	 */	public static String cnapsCode(final String code) {		if (StringUtils.isBlank(code)) {			return "";		}		return StringUtils.rightPad(StringUtils.left(code, 2), StringUtils.length(code), "*");	}}
对应VO实体类加上注解

这一开始是想从数据库出库时就加密,但考虑到逻辑处理代码中可能会用到手机号做关联或者查询,最后只在视图对象VO上加了注解,只有返回给界面时才会加密,这就需要吧VO(视图对象)、PO(持久化对象)、DTO(数据传输对象)拆分开

@Datapublic class StoreInfoResponse implements Serializable {    private static final long serialVersionUID=1L;	//默认type是手机号加密,所以这省略了,没写	@SensitiveInfo(permission="sensitive_addressPhone_view")	private String addressPhone;}
验证方式main函数

因为这里使用的是Jackson 序列化,所以只有使用Jackson序列化时才能生效,使用fastjson序列化是不会生效的,这里我们使用手动序列化进行测试,实际项目中可以用页面请求的方式测试权限相关逻辑。

public static void main(String[] args) throws JsonProcessingException {		StoreInfoResponse  response = new StoreInfoResponse ();		response.setAddressPhone("18231148754");		String jsonstr = new ObjectMapper().writeValueAsString(response);		System.out.println("jackson序列化>>>" + jsonstr);		System.out.println("fastjson序列化>>>" + JSON.toJSONString(response));	}

运行调试

运行结果:

jackson序列化脱敏后>>>{"addressPhone":"18******54"}fastjson序列化未进行脱敏加密>>>{"addressPhone":"18231148754"}进程已结束,退出代码为 0

标签: #java拦截器返回值json加密