aop+反射实现电话号加密

1,661 阅读2分钟

0. 背景

线上项目涉及大量查询接口中,存在电话号明文展示不合规的问题。如果对每个接口返回结果中电话号相关字段修改相关代码逻辑,则工作量较大花费时间多。因此设计电话号加密注解,减少工作量。

1. 引入依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.16.18</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

2. 业主信息类、业主信息服务类

@Data
@AllArgsConstructor
public class CustomerInfo {

    private String name;

    private String phoneNum;
}

@Service
public class CustomerInfoService {

    @PhoneEncryptionForList(fields = "phoneNum", clazz = CustomerInfo.class)
    public List<CustomerInfo> listCustomerInfo() {
        return Arrays.asList(
                new CustomerInfo("小王", "15112368569"),
                new CustomerInfo("小李", "13652298565"),
                new CustomerInfo("小武", "18965653698"),
                new CustomerInfo("小天", "13192558569")
        );
    }
}

3. 电话号加密注解,电话号加密切面类

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PhoneEncryptionForList {

    /**
     * 加密字段
     *
     * @return
     */
    String[] fields();

    /**
     * 加密对象类型
     *
     * @return
     */
    Class<?> clazz();
}

@Aspect
@Component
public class PhoneEncryptionForListAspect {

    private static final int PHONE_SIZE = 11;

    @AfterReturning(value = "@annotation(com.zzq.spring.phone.encryption.PhoneEncryptionForList)", returning = "result")
    public void doEncrypt(JoinPoint joinPoint, Object result) {
        if (result == null || !(result instanceof List)) {
            System.out.println("result class type is not list, annotation is invalid");
            return;
        }

        List list = (List) result;
        if (CollectionUtils.isEmpty(list)) {
            return;
        }

        // 获取注解的属性值
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        PhoneEncryptionForList annotation = method.getAnnotation(PhoneEncryptionForList.class);
        String[] fields = annotation.fields();
        Class<?> clazz = annotation.clazz();

        for (String field : fields) {
            try {
                // 反射获取实体类加密字段相应的 set 和 get 方法
                char[] chars = field.toCharArray();
                chars[0] = (char) (chars[0] - 32);
                String setMethodName = "set" + new String(chars);
                String getMethodName = "get" + new String(chars);
                Method getMethod = clazz.getDeclaredMethod(getMethodName);
                Method setMethod = clazz.getDeclaredMethod(setMethodName, String.class);

                for (Object obj : list) {
                    doEncryptPhone(getMethod, setMethod, obj);
                }
            } catch (Exception e) {
                System.out.println(field + " encrypt exception, " + e.getMessage());
                continue;
            }
        }
    }

    private static void doEncryptPhone(Method getMethod, Method setMethod, Object obj) throws Exception {
        // 反射调用 get 方法
        String originalPhone = (String)getMethod.invoke(obj);
        if (!StringUtils.hasText(originalPhone) || originalPhone.length() != PHONE_SIZE) {
            System.out.println("phone field value is blank or formal error");
            return;
        }

        String encryptedPhone = originalPhone.replaceAll("(\\d{3})\\d{4}(\\d{4})","$1****$2");
        // 反射调用 set 方法
        setMethod.invoke(obj, encryptedPhone);
    }
}

4. 测试类

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.zzq.spring.phone.encryption")
public class TestApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TestApplication.class);

        CustomerInfoService customerInfoService = ctx.getBean(CustomerInfoService.class);
        List<CustomerInfo> afterPhoneEncryptCustomerInfos = customerInfoService.listCustomerInfo();

        afterPhoneEncryptCustomerInfos.forEach(customerInfos -> {
            System.out.println(customerInfos);
        });
    }
}

5. 结果和总结

电话号加密注解测试结果.png

该加密注解只简单实现了电话号加密,注解中还可以新增一些属性扩展注解功能,比如增加加密字段格式加密规则的正则表达式的属性值,这样加密的字段类型(手机号、身份证等)和方式(中间多少位加密、首位加密)就可以根据具体需求处理。