Scenario: In response to government requirements, commercial software should ensure that users' basic information is not "leaked" and cannot "directly display" sensitive user information such as phone numbers, ID cards, addresses, etc.
According to the above scenario description, we can analyze two points:
- "Not leaked" means that user information should be encrypted and stored.
-
"Cannot directly display" means that user information should be desensitized when displayed.
Solution
-
Foolish programming: Encrypt the fields related to user information entities in the project, such as name, mobile phone number, ID number, address, etc., before adding them to the database; Decrypt and desensitize the data in the database when displaying the user information list, and then return it to the frontend.
-
Aspect-oriented programming: Mark the fields related to user information entities in the project (here we use UserBO to indicate, add @EncryptField to the name, phone fields in UserBO) with annotations, return the user information entity class (here we use UserDO to indicate, add @DecryptField to the name, phone fields in UserDO); then use @EncryptField and @DecryptField as entry points to implement encryption and decryption desensitization in an aspect-oriented way.
Foolish programming does not mean foolish, it is like aspect-oriented programming. Foolish programming requires encrypting and decrypting desensitization logic processing for all interfaces related to user information
, where the changes are relatively large, high risk, repetitive operations on the same logic, high workload, and difficult to maintain later; Aspect-oriented programming only needs to add annotations to user information fields, and uniformly perform encryption and decryption desensitization logic processing on fields with annotations, which is easy to operate, highly cohesive, and easy to maintain.
Implementation
Foolish programming is not difficult, here I will show you aspect-oriented programming implementation. Before implementation, let me preheat the knowledge of annotations, reflection and AOP for you.
Annotation Practice
Create Annotation
Create an annotation that can only be marked on methods:
@Target(ElementType.METHOD) // METHOD indicates this annotation can only be used on methods
@Retention(RetentionPolicy.RUNTIME) // RUNTIME indicates this annotation takes effect at runtime
public @interface Encryption {
}
Create an annotation that can only be marked on fields:
@Target(ElementType.FIELD) // FIELD indicates this annotation can only be used on fields
@Retention(RetentionPolicy.RUNTIME) // RUNTIME indicates this annotation takes effect at runtime
public @interface EncryptField {
}
Create an annotation that is marked on fields and has values:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptField {
DesensitizationEnum value(); // Annotations can have values, here can be arrays, Strings, enums, etc.
}
Use Annotation
Create Enum
public enum DesensitizationEnum {
name,
address,
phone;
}
Create UserDO Class
// User information return entity
public class UserDO {
@DecryptField(DesensitizationEnum.name)
private String name;
@DecryptField(DesensitizationEnum.address)
private String address;
// getters and setters
public static void main(String[] args) throws IllegalAccessException {
// Generate and initialize object
UserDO userDO = new UserDO("Dream", "Wuhan, Hubei");
// Get all fields of current object via reflection
Field[] fields = userDO.getClass().getDeclaredFields();
// Traverse fields
for (Field field : fields) {
// Check if @DecryptField annotation exists on field
boolean hasSecureField = field.isAnnotationPresent(DecryptField.class);
// Exists
if (hasSecureField) {
// Violently crack otherwise can't operate private fields
field.setAccessible(true);
// If field in userDo is not null, i.e. name, address fields have value
if (field.get(userDO) != null) {
// Get annotation value on field
DesensitizationEnum desensitizationEnum = field.getAnnotation(DecryptField.class).value();
// Print to console
System.out.println(desensitizationEnum);
// According to different values, we can perform different desensitization logic on fields,
// such as name desensitization - Wei*, mobile phone desensitization - 187****2275
}
}
}
}
}
Reflection Practice
Create UserBO Class
// User information addition entity
public class UserBO {
@EncryptField
private String name;
@EncryptField
private String address;
// getters and setters
public static void main(String[] args) throws IllegalAccessException {
UserBO userBO = new UserBO("Zhou Chuanxiong", "Wuhan, Hubei");
Field[] fields = userBO.getClass().getDeclaredFields();
for (Field field : fields) {
boolean annotationPresent = field.isAnnotationPresent(EncryptField.class);
if(annotationPresent){
// Current field content is not empty
if(field.get(userBO) != null){
// Encrypt field content here
Object obj = encrypt(field.get(userBO));
// After encrypting field content, reassign it to the field via reflection
field.set(userBO, obj);
}
}
}
System.out.println(userBO);
}
public static Object encrypt(Object obj){
return "Encrypt: " + obj;
}
}
AOP Practice
Cut-in point:
@RestController
@RequestMapping("/encrypt")
@Slf4j
public class EncryptController {
@PostMapping("/v1")
@Encryption // Cut-in point
public UserBO insert(@RequestBody UserBO user) {
log.info("Encrypted object: {}", user);
return user;
}
}
Aspect:
@Aspect
@Component
public class EncryptAspect {
@Pointcut("@annotation(Encryption)")
public void point() { }
@Around("point()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// Encryption logic
encrypt(joinPoint);
return joinPoint.proceed();
}
}
Why use AOP here: No matter annotations or reflections, they need a startup method. I demonstrated above via the main function. Using AOP, after the project starts, as long as the method corresponding to the cut-in point is called, an aspect will be formed according to the cut-in point for unified logical enhancement. If you are familiar with SpringMVC, SpringMVC provides two interfaces ResponseBodyAdvice and RequestBodyAdvice that can preprocess requests and responses, so AOP may not be needed.
Encryption Decryption Desensitization Practice
Project Directory:
pom.xml:
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Spring Boot Test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.20</version>
</dependency>
<!-- Aspect AOP -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
</dependencies>
Entity Class
User information addition entity: UserBO
// Entity class
public class UserBO {
@EncryptField
private String name;
@EncryptField
private String address;
// getters and setters
}
User information return entity: UserDO
// Entity class
public class UserDO {
@DecryptField(DesensitizationEnum.name)
private String name;
@DecryptField(DesensitizationEnum.address)
private String address;
// getters and setters
}
Desensitization Enum
public enum DesensitizationEnum {
name,
address,
phone;
}
Annotation
Decrypt field annotation (field):
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptField {
DesensitizationEnum value();
}
Decrypt method annotation (method as cut-in point):
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Decryption {
}
Encrypt field annotation (field):
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptField {
}
Encrypt method annotation (method as cut-in point):
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encryption {
}
Controller
Decrypt Controller:
@RestController
@RequestMapping("/decrypt")
public class DecryptController {
@GetMapping("/v1")
@Decryption
public UserDO decrypt() {
return new UserDO("7c29e296e92893476db5f9477480ba7f", "b5c7ff86ac36c01dda45d9ffb0bf73194b083937349c3901f571d42acdaa7bae");
}
}
Encrypt Controller:
@RestController
@RequestMapping("/encrypt")
@Slf4j
public class EncryptController {
@PostMapping("/v1")
@Encryption
public UserBO insert(@RequestBody UserBO user) {
log.info("Encrypted object: {}", user);
return user;
}
}
Aspect
Decrypt desensitization aspect:
@Aspect
@Component
public class DecryptAspect {
@Pointcut("@annotation(Decryption)")
public void point() {}
@Around("point()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// Decrypt
return decrypt(joinPoint);
}
// Decrypt logic
private Object decryptData(Object obj) throws IllegalAccessException {
if (obj instanceof ArrayList) {
decryptList(obj);
} else {
decryptObj(obj);
}
return obj;
}
// Decrypt list
// Decrypt object
private void decryptValue() {
log.info("Decrypt and desensitize based on object, do nothing for single field!");
}
}
Encrypt aspect:
@Aspect
@Component
public class EncryptAspect {
@Pointcut("@annotation(Encryption)")
public void point() {}
@Around("point()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// Encryption logic
encrypt(joinPoint);
return joinPoint.proceed();
}
public void encrypt(ProceedingJoinPoint joinPoint) {
Object[] objects = joinPoint.getArgs();
if (objects.length != 0) {
for (Object object : objects) {
if (object instanceof UserBO) {
Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(EncryptField.class)) {
field.setAccessible(true);
if (field.get(object) != null) {
// Encrypt
Object encrypt = AesUtil.encrypt(field.get(object));
field.set(object, encrypt);
}
}
}
}
}
}
}
}
Utility Class
Encryption utility class: AesUtil
public class AesUtil {
public static String AES_KEY = "Wk#qerdfdshbd910";
public static AES aes = SecureUtil.aes(AES_KEY.getBytes());
public static Object encrypt(Object obj) {
return aes.encryptHex((String) obj);
}
public static Object decrypt(Object obj, DesensitizationEnum desensitizationEnum) {
// Decrypt
Object decrypt = decrypt(obj);
// Desensitize
return DesensitizationUtil.desensitization(decrypt, desensitizationEnum);
}
public static Object decrypt(Object obj) {
return aes.decryptStr((String) obj, CharsetUtil.CHARSET_UTF_8);
}
}
Desensitization utility class: DesensitizationUtil
public class DesensitizationUtil {
public static Object desensitization(Object obj, DesensitizationEnum desensitizationEnum) {
Object result;
switch (desensitizationEnum) {
case name:
result = strUtilHide(obj, 1);
break;
case address:
result = strUtilHide(obj, 3);
break;
default:
result = "";
}
return result;
}
public static Object strUtilHide(Object obj, int start) {
return StrUtil.hide(obj, start, ((String) obj).length());
}
}
Conclusion
The above code is not difficult. Copy it to local and run through, you can basically understand. I hope every programmer takes less detours!
Comments