Java annotation realizes data desensitization - decoupled from the main business and associated with permission control


In recent business requirements, in order to prevent the leakage of sensitive information such as user's mobile phone number, it is necessary to deal with the ciphertext of the mobile phone number, taking into account the following two problems

  1. There are many businesses and complex logic. It is too hard to find and change the code one by one, and there is no guarantee that all of them can be found and changed
  2. Different users have different needs. Some users need to check their mobile phone number, and some users don't need to check their mobile phone number
  3. Desensitization has a low correlation with the main business. I don't want code intrusion to be too serious
    Thinking that this logic is similar to the format logic of transferring json date, I went online to find the implementation scheme, query the user-defined annotation and the information related to json processing user-defined annotation, because the Jackson toolkit is used by default in Spring MVC. Here I look up the relevant materials of Jackson and find the following annotations and interfaces provided by Jackson

Explanation of related uses

1. @JacksonAnnotationsInside

com.fasterxml.jackson.annotation.JacksonAnnotationsInside

Official: reference text meta annotations (annotations used on other annotations) are used to indicate that Jackson should use the meta annotations it owns instead of the target annotations (annotations annotated with this annotation). This is useful when creating a "composite Annotation" because it has a container annotation that needs to be annotated with this annotation and all the annotations it "contains".

Generally speaking, the user-defined annotation is added with this annotation, and the user-defined annotation will be found, intercepted and processed by Jackson annotation introspector findSerializer

2. @JsonSerialize(using = SensitiveInfoSerialize.class)

com.fasterxml.jackson.databind.annotation.JsonSerialize

Official: JsonSerialize class - used to configure serialization annotations by attaching to "getter" methods or fields or value classes. When annotating a value class, configure an instance for the value class, but can be overridden by more specific annotations (annotations attached to methods or fields).
Examples of notes are as follows:
@JsonSerialize(using=MySerializer.class,
as=MySubClass.class,
typing=JsonSerialize.typing.STATIC
)
(this is redundant as=MySubClass.class, because some attributes will prevent other attributes: in particular, "using" takes precedence over "as", which takes precedence over the "typing" setting)
using property - serializer class used to serialize the associated value. Depending on the content of the annotation, the value is either an instance of the annotation class (which can be used globally wherever a class serializer is needed); Or it can only be used to serialize property access through getter methods.

My understanding here is that the JsonSerialize annotation is the information to configure the serialization tool for my customized annotation. using is to set which serialization tool this annotation uses.

3. JsonSerializer

com.fasterxml.jackson.databind.JsonSerializer

Abstract class that defines the API used by ObjectMapper (and other chained jsonserializers) to serialize objects of any type into JSON using the provided JsonGenerator. com.fasterxml.jackson.databind.ser.std.StdSerializer is not this class because it will implement many optional methods of this class.
Note: various serialize methods will never be called with null values -- the caller must handle null values, usually by calling serializerprovider Findnullvalueserializer to get the serializer to use. This also means that when serializing null values, you cannot directly use a custom serializer to change the output to be generated.
If the serializer is an aggregate serializer (which means that it delegates processing of some of its contents by using other serializers), it usually also needs to implement com fasterxml. jackson. databind. ser. Resolvableserializer, which can find the required auxiliary serializer. This is important to allow dynamic rewriting of serializers; A separate calling interface is required to separate the parsing of the auxiliary serializer (possibly directly or indirectly linking the loop back to the serializer itself).
In addition, in order to support the annotation of each attribute (configuring various aspects of serialization according to each attribute), the serializer may need to implement com fasterxml. jackson. databind. ser. Contextualserializer, which allows specialization of serializers: call com fasterxml. jackson. databind. ser. ContextualSerializer. Createcontext passes information about a property and can create a newly configured serializer to handle that particular property.
If you also implement com fasterxml. jackson. databind. ser. Resolvableserializer and com fasterxml. jackson. databind. ser. Contextualserializer, the serializer will be parsed before the upper and lower culture.

Serialization processing class is used to realize the logic of specific desensitization and turn the original data into data with *

4. ContextualSerializer

Official: the additional interface that JsonSerializer can implement gets a callback that can be used to create a context instance of the serializer to handle the properties of the supported types. This is useful for serializers that can be configured through annotations, or should have different behavior, depending on the type of property to serialize.

The callback function is used to transfer the field information in the user-defined annotation to the user-defined JsonSerializer object. For example, there are two fields of type and permission in our user-defined annotation, and there are also two fields of type and permission in the user-defined JsonSerializer serialization tool. Assign the values of the two fields in the annotation to the user-defined JsonSerializer.

Implementation steps

Premise: the project uses the Spring MVC framework, and the Spring automatic serialization MessageConverter uses the default Jackson serialization. If the project has been changed to fastjson, analogy will be used to implement the custom annotation of fastjson.

1. Declare user-defined annotation SensitiveInfo

Here, we want to distinguish various sensitive information types such as mobile phone number, ID card and bank card number, so we use type; We want to distinguish permissions to determine whether desensitization is performed, so we use the permission field. The code is as follows:

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  //So that this annotation can be scanned by Jackson
@JsonSerialize(using = SensitiveInfoSerialize.class)  //Configure the serialization processing class that handles this annotation
public @interface SensitiveInfo {
	/**
	 * Do not encrypt with this permission
	 * @return
	 */
	public String permission() default "MobileEncryptPermission";
	/**
	 * Type of desensitization encryption
	 * @return
	 */
	public SensitiveType type() default SensitiveType.MOBILE_PHONE;
}

2. Declare serialization processing class

Here, the hasPermission() method is implemented to judge whether it has the specified permission.
Createcontext() is a callback function that implements ContextualSerializer. It creates and assigns a sentitiveinfoserialize according to the annotation content.
Realize specific desensitization ciphertext processing in 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
{
	/**Receive annotation desensitization type*/
	private SensitiveType type;
	/**Receive viewable permissions*/
	private String permission;
	public SensitiveInfoSerialize() {
	}
	public SensitiveInfoSerialize(final SensitiveType type ,final String permission ) {
		this.type = type;
		this.permission = permission;
	}
	/**Logical processing of serialization
	*/
	@Override
	public void serialize(final String s, final JsonGenerator jsonGenerator,
						  final SerializerProvider serializerProvider)
						   throws IOException, JsonProcessingException {
		if (hasPermission(permission)){
			//Permission to display the original text
			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;
			}
		}
	}
	/**Callback function after the user-defined annotation is intercepted*/
	@Override
	public JsonSerializer<?> createContextual(final SerializerProvider 
	serializerProvider,final BeanProperty beanProperty) throws JsonMappingException {
		if (beanProperty != null) { // Skip directly if it is empty
			if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) { 
			// Skip directly for non String classes
				SensitiveInfo sensitiveInfo = 
						beanProperty.getAnnotation(SensitiveInfo.class);
				if (sensitiveInfo == null) {
					sensitiveInfo = 
					beanProperty.getContextAnnotation(SensitiveInfo.class);
				}
				if (sensitiveInfo != null) { 
				// If the annotation can be obtained, the value of the annotation is passed into SensitiveInfoSerialize
					return new SensitiveInfoSerialize(sensitiveInfo.type(),sensitiveInfo.permission());
				}
			}
			return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
		}
		return serializerProvider.findNullValueSerializer(beanProperty);
	}
	/**
	 * Judge whether the interface has any xxx and xxx permissions
	 * Here is the judgment method of Spring Security. If shiro is used, you can refer to the two lines of code noted. If other authentication frameworks are used, you can refer to the implementation logic by yourself
	 * @param permission Permission character
	 * @return {boolean}
	 */
	public boolean hasPermission(String permission) {
		/**Spring Securty Verification method*/
		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 Verification method*/
//		Subject subject = SecurityUtils.getSubject();
//		return subject.isPermitted(permission);
	}
}

3. Type enumeration implementation

public enum SensitiveType {
	/**
	 * Chinese name
	 */
	CHINESE_NAME,

	/**
	 * ID number
	 */
	ID_CARD,
	/**
	 * Landline number
	 */
	FIXED_PHONE,
	/**
	 * cell-phone number
	 */
	MOBILE_PHONE,
	/**
	 * address
	 */
	ADDRESS,
	/**
	 * E-mail
	 */
	EMAIL,
	/**
	 * bank card
	 */
	BANK_CARD,
	/**
	 * Joint bank no. of the company
	 */
	CNAPS_CODE
}

4. Implementation of ciphertext processing tool

import org.apache.commons.lang3.StringUtils;

public class SensitiveInfoUtils {

	/**
	 * [Chinese name] only the first Chinese character is displayed, and the others are hidden as two asterisks < example: Li * * >
	 */
	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), "*");
	}

	/**
	 * [Chinese name] only the first Chinese character is displayed, and the others are hidden as two asterisks < example: Li * * >
	 */
	public static String chineseName(final String familyName, final String givenName) {
		if (StringUtils.isBlank(familyName) || StringUtils.isBlank(givenName)) {
			return "";
		}
		return chineseName(familyName + givenName);
	}

	/**
	 * [ID number] shows the last four bits, other hidden. A total of 18 or 15< Example: ************ 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), "*"),
						"***"));
	}

	/**
	 * [Fixed telephone] last four digits, others hidden < example: * * * * 1234 >
	 */
	public static String fixedPhone(final String num) {
		if (StringUtils.isBlank(num)) {
			return "";
		}
		return StringUtils.leftPad(StringUtils.right(num, 4), StringUtils.length(num), "*");
	}

	/**
	 * [Mobile phone number] first three digits, last four digits, others hidden < example: 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), "*"),
						"***"));

	}
	/**
	 * [Address] only the region is displayed, and the detailed address is not displayed; We need to stren gt hen the protection of personal information < example: Haidian District, Beijing * * * * >
	 *
	 * @param sensitiveSize Sensitive information length
	 */
	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, "*");
	}
	/**
	 * [E-mail] the e-mail prefix only displays the first letter, the other prefixes are hidden, replaced by asterisks, @ and the following address are displayed < example: 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)));
		}
	}

	/**
	 * [[bank card No.] the first six digits and the last four digits, and others hide one asterisk for each with an asterisk < example: 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), "*"),
				"******"));
	}
	/**
	 * [Joint bank number of company deposit bank] joint bank number of company deposit bank, the first two digits are displayed, others are hidden with asterisks, one asterisk for each < example: 12 **********>
	 */
	public static String cnapsCode(final String code) {
		if (StringUtils.isBlank(code)) {
			return "";
		}
		return StringUtils.rightPad(StringUtils.left(code, 2), StringUtils.length(code), "*");
	}
}

Annotate the corresponding VO entity class

At the beginning, we want to encrypt the data when it comes out of the database. Considering that the mobile phone number may be used for association or query in the logic processing code, we only annotate the view object VO and encrypt it only when it is returned to the interface. This requires the separation of VO (view object), PO (persistent object) and DTO (data transmission object)

@Data
public class StoreInfoResponse implements Serializable {
    private static final long serialVersionUID=1L;
	//The default type is mobile number encryption, so this is omitted and not written
	@SensitiveInfo(permission="sensitive_addressPhone_view")
	private String addressPhone;
}

Verification method

main function

Because Jackson serialization is used here, it can only take effect when Jackson serialization is used. fastjson serialization will not take effect. Here, we use manual serialization for testing. In the actual project, we can test the logic related to permission in the way of page request.

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 serialize>>>" + jsonstr);
		System.out.println("fastjson serialize>>>" + JSON.toJSONString(response));

	}

Operation debugging

Operation results:

jackson serialize>>>{"addressPhone":"18******54"}
fastjson serialize>>>{"addressPhone":"18231148754"}

The process has ended with exit code 0

Keywords: Java Spring

Added by joelhop on Thu, 10 Feb 2022 01:35:14 +0200