基于springboot+aop的自定义注解脱敏,利用java反射机制,更改controller切面返回值,增加返回安全,一个注解即可。

自定义注解脱敏

前言

帮助项目快速接入脱敏能力,企业安全要求增加,要求后端数据需要脱敏返回,本项目旨在帮助现有项目快速接入脱敏能力,减少代码改动,减少代码入侵,快速实现,如果有兴趣,可以看看评论区的maven,引入后即可快速接入脱敏能力,增加开发效率。

快速接入

<dependency>
	<groupId>io.github.HHs11</groupId>
	<artifactId>desensitization</artifactId>
	<version>1.0.4</version>
</dependency>

源码解析

创建一个自定义注解,用于定于aop的切点

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Desensitized {
    String value() default "Desensitized_null";
	// 用于接收脱敏方法名字
    String method() default "desensitizedPhoneNumber";
}

定义了一个可以用在方法和变量的注解Desensitized ;声名method变量用于接收脱敏方法名。默认为手机脱敏方法,value参数暂时没有用。

创建一个工具类

package com.example.demo.utils;

import org.apache.commons.lang.StringUtils;

/**
 * @className:关键信息脱敏工具类
 * @author:hhs
 * @date:2021-07-07 16:51
 */
public class DesensitizedUtil {

    /**
     * 地址脱敏
     *
     * @param address
     * @return
     */
    public static String desensitizedAddress(String address) {
        if (!StringUtils.isNotEmpty(address)) {
            return StringUtils.left(address, 3).concat(StringUtils.removeStart(StringUtils.leftPad(StringUtils.right(address, address.length() - 11), StringUtils.length(address), "*"), "***"));
        }
        return address;
    }

    /**
     * 地址脱敏v2
     *
     * @param address
     * @return
     */
    public static String desensitizedAddressV2(String address) {
        return address.replaceAll(String.format("[0-9]"), "*");
    }

    /**
     * 电话脱敏
     */
    public static String desensitizedPhoneNumber(String phoneNumber) {
        if (phoneNumber.length() < 11) {
            return phoneNumber.replaceAll("(\\w{2})\\w*(\\w{3})", "$1****$2");
        }
        if (StringUtils.isNotEmpty(phoneNumber)) {
            phoneNumber = phoneNumber.replaceAll("(\\w{3})\\w*(\\w{4})", "$1****$2");
        }
        return phoneNumber;
    }

    /**
     * 姓名脱敏
     */
    public static String desensitizedName(String name) {
        String retName = name.charAt(0) + "";
        if (StringUtils.isNotEmpty(name)) {
            for (int i = 1; i < name.length(); i++) {
                retName += "*";
            }
        }
        return retName;
    }

}


工具类中包括了一些常见的脱敏方法。包括地址脱敏,电话脱敏,等。可以在里面按照格式继续添加脱敏方法,常见的地址脱敏有隐藏中间信息,还有隐藏具体门牌数字等,手机脱敏则显示前三后四。

创建Aop,切入注解切面处理数据

package com.example.demo.aop.impl;

import com.example.demo.aop.Desensitized;
import com.example.demo.utils.DesensitizedUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;

/**
 * @className:DesensitizedAop
 * @author:hhs
 * @date:2021-07-30 10:04
 */
@Component
@Aspect
public class DesensitizedAop {


    @Around(value = "@annotation(desensitized)")
    public Object paramCheck(ProceedingJoinPoint joinPoint, Desensitized desensitized) throws Throwable {
        // 获取返回值并脱敏修改后返回
        return  execute(joinPoint.proceed());
    }

    /**
     * 判断是否为基本类型:包括String
     *
     * @param clazz clazz
     * @return true:是; false:不是
     */
    private boolean isPrimite(Class<?> clazz) {
        return clazz.isPrimitive() || clazz == String.class;
    }

	/**
	 * 递归实现脱敏,如果入参不是一个基本数据类型,则需要递归,直到基本类型结束
	*/
    private Object execute(Object obj) throws IllegalAccessException {
        Class<?> clazz = obj instanceof Field ? ((Field) obj).getType() : obj.getClass();
        // 基本类型直接返回
        if (isPrimite(clazz)) {
            return obj;
        }

		// 反射拿到属性
        Field[] declaredFields = clazz.getDeclaredFields();
        // 遍历属性并依次判断,是否基本类型,是否有自定义注解
        for (Field declaredField : declaredFields) {
        	// 设置accessible属性为真,可以修改非public类型属性
            declaredField.setAccessible(true);
            if (isPrimite(declaredField.getType())) {
                Desensitized annotation = declaredField.getAnnotation(Desensitized.class);
                if (annotation != null) {
                    declaredField.set(obj, desensitization(declaredField.get(obj), annotation));
                }
            } else { // 非基本类型继续递归
                if (declaredField.get(obj) != null) {
                    declaredField.set(obj, execute(declaredField.get(obj)));
                }
            }
        }

        return obj;
    }

	/**
	* 脱敏方法
	*/
    private Object desensitization(Object obj, Desensitized annotation) {
        //利用反射,调用自定义注解中的参数方法
        MethodType methodType = MethodType.methodType(String.class, String.class);
        // 获取静态方法的句柄
        MethodHandle method = null;
        try {
        	// 反射拿到该属性上自定义注解中的method值,并调用DesensitizedUtil中方法名为该值的方法
            method = MethodHandles.lookup()
                    .findStatic(DesensitizedUtil.class, annotation.method(), methodType);
        } catch (NoSuchMethodException e) {
            return obj;
        } catch (IllegalAccessException e) {
            return obj;
        }

        Object r;
        try {
            r = method.invoke((String) obj); // invoke|invoke
        } catch (Throwable e) {
            return obj;
        }

        return r;
    }

}

利用aop的joinPoint.proceed()方法拦截返回值,然后分析返回值,判断是否是基本数据类型,如果不是,则利用递归逐级处理,如果是基本类型,则在有脱敏注解的字段处理成脱敏数据后重新写入。详情看注释。

使用

@Data
public class Student {

    @Desensitized(method = "desensitizedName")
    String name;
    String number;
    @Desensitized(method = "desensitizedPhoneNumber")
    String phone;
    // 需要脱敏的字段加上自定义注解,并指定method脱敏方法名
    @Desensitized(method = "desensitizedAddressV2")
    String address;

    Book book;
}

在需要脱敏的实体类字段上添加 @Desensitized ,method的值是需要执行工具类中脱敏方法的方法名,如手机脱敏,则是调用desensitizedPhoneNumber。(需要脱敏的controller上也需要加上这个注解@Desensitized(),参数为null就行)

测试

可在git中运行查看
https://gitee.com/pinus_taiwanensis/desensitization

总结

根据我的方法可以解决大部分返回数据敏感问题,建议直接使用maven引入,更加快速接入。如有疑问和建议反馈或者指出错误,可以在评论区留言讨论,如有转载,请署名并表明来源。

  • 6
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值