【数据安全】 脱敏

背景

        数据脱敏技术在合规性方面也非常重要,它有助于企业遵守《网络安全法》、《数据安全法》、《个人信息保护法》等相关法律法规的要求,确保数据处理活动的合法性和安全性。例如,《数据安全法》第二十七条提到,开展数据处理活动应当建立健全全流程数据安全管理制度,采取相应的技术措施保障数据安全,其中就包括数据脱敏技术的应用。

        总的来说,数据脱敏是确保数据在各种使用场景下安全、合规的关键技术,它通过多种算法和策略来实现对敏感信息的有效保护。随着数据安全和隐私保护意识的增强,数据脱敏技术的应用将越来越广泛。

        数据脱敏的实施流程通常包括敏感数据发现、敏感数据梳理、脱敏方案制定和脱敏任务执行等步骤。在敏感数据发现阶段,可以通过人工或自动的方式识别敏感数据。敏感数据梳理阶段则涉及到对数据列和数据关系的调整,以保持数据的关联性。脱敏方案制定阶段需要根据业务需求配置脱敏规则和策略,而脱敏任务执行阶段则是实际进行数据脱敏操作的过程。

考虑方案

如需要对数据进行脱敏处理,可能考虑的解决方案有:

  • 手动编程实现:通过Java的字符串处理和正则表达式功能,手动编写代码来替换敏感信息。
  • 使用第三方脱敏库:使用Hutool可以一行代码实现脱敏。
  • 使用数据处理框架:对于大数据量的脱敏处理,可以使用Apache Spark等数据处理框架进行批量处理。

以上方法都能解决当前问题实现数据脱敏处理,但可能考虑存在以下问题:

  • 手动编程,第三方脱敏库这两种方式虽然灵活,但需要较多的代码编写和维护工作。
  • 数据处理框架需要引入外部依赖,对于小规模数据有点小题大做,浪费过多的资源不说,对研发的要求也相对提高。

        选择哪种方案取决于具体的需求、数据规模和安全要求。对于小规模数据,手动编程或使用第三方库可能更为简单快捷;而对于大规模数据处理,则可能需要借助数据处理框架来实现高效的脱敏操作。在实际应用中,还需要考虑数据的合法性和合规性,确保脱敏后的数据符合相关法规和监管要求。

利用Spring boot自定义脱敏注解实现

可以结合Spring Boot 自定义注解的功能结合第三方库(Hutool)脱敏库 的方式,实现数据脱敏处理,通过这种方式可以使脱敏逻辑与业务逻辑进行分离,对代码的编写和维护工作有较大的提升,对不同业务处理提供良好的后续扩展。

定义脱敏标识注解

自定义一个脱敏注解,该注解在程序运行时进行,应用于方法上:

package liuyuxiang.service.utils.convert.intlpermission.util;

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

/**
 * @author Lyx
 * 用于标记处理脱敏的方法	
 * */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SensitiveResponse {
}

编写脱敏逻辑,对有脱敏注解的方法做脱敏操作

1.实现数据操作实现数据脱敏的可控性: isOpen()
2.通过约定参数 "eyeCode" 灵活实现数据的脱敏与可见性

”eyeCode=open“ 数据可见

”eyeCode=close / 为空“ 数据脱敏

package liuyuxiang.service.utils.convert.intlpermission;

import com.github.pagehelper.PageInfo;
import liuyuxiang.service.utils.convert.intlpermission.util.ConvertEncryptIntl;
import liuyuxiang.service.utils.convert.intlpermission.util.SensitiveResponse;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.List;
import java.util.Objects;

/**
 * 敏感信息响应处理类
 * */
@Component
@ControllerAdvice
public class SensitiveResponseAdvice implements ResponseBodyAdvice<Object> {

    @Autowired
    private IOpenManager openManager;
    private static String ENCRYPT_FIELD_CODE="ENCRYPT_FIELD_CODE";

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return Objects.requireNonNull(returnType.getMethod()).isAnnotationPresent(SensitiveResponse.class);
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  @NotNull MethodParameter returnType,
                                  @NotNull MediaType selectedContentType,
                                  @NotNull Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  @NotNull ServerHttpRequest request,
                                  @NotNull ServerHttpResponse response) {

        if (isOpen()) return body;

        try {
            boolean shouldEncrypt = shouldEncrypt(request);
            if (shouldEncrypt) {
                return body;
            }
        } catch (IOException e) {
            logger.error("BCP脱敏接口:'"+request.getURI()+"'异常",e);
        }

        if (body instanceof PageInfo<?>) {
            ConvertEncryptIntl.convertToEncryptPage((PageInfo<?>) body,request);
            return body;
        }
        if (body instanceof List<?>) {
            ConvertEncryptIntl.convertToEncryptList((List<?>) body,request);
            return body;
        }

        if (body != null) {
            ConvertEncryptIntl.encrypt(body,request);
            return body;
        }
        return body;
    }

    private boolean isOpen() {
        String code=iSysConfManager.findByCode(ENCRYPT_FIELD_CODE);
        if(StringUtils.isBlank(code) || StringUtils.equals("close",code.toLowerCase())){
            return false;
        }
        return true;
    }

    public boolean shouldEncrypt(ServerHttpRequest request) throws IOException {
        String requestBody = StreamUtils.copyToString(request.getBody(), StandardCharsets.UTF_8);
        if (StringUtils.isNotBlank(requestBody)) {
            ObjectMapper objectMapper = new ObjectMapper();
            Map<String, Object> requestParams = objectMapper.readValue(requestBody, Map.class);
            return shouldEncryptBasedOnRequestParams(requestParams);
        }
        // 如果请求体为空,检查查询字符串中的eyeCode,针对GET请求
        return "open".equals(findEyeCode(request.getURI().getQuery()));
    }


    private boolean shouldEncryptBasedOnRequestParams(Map<String, Object> requestParams) {
        if (requestParams.containsKey("eyeCode") && StringUtils.equals( "open", (CharSequence) requestParams.get("eyeCode"))) {
            return true;
        }
        return false;
    }

    private String findEyeCode(String queryString){
        if(StringUtils.isBlank(queryString)){
            return null;
        }
        Pattern pattern = Pattern.compile("eyeCode=(\\w+)");
        Matcher matcher = pattern.matcher(queryString);

        if (matcher.find()) {
            return matcher.group(1); // 匹配到的参数值
        }
        return null;
    }
}
ResponseBodyAdvice:Spring框架中的一个接口,用于对控制器方法的返回值进行处理。
主要方法:
  1. boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

        这个方法用于判断当前的 ResponseBodyAdvice 实现是否支持处理特定的方法返回值。

你可以根据返回类型和消息转换器类型来决定是否应用该处理逻辑。

  1. T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);

        这个方法在响应体写入之前被调用。你可以在这里对响应体进行修改。例如,添加额外的包装层、修改数据格式等。

编写脱敏工具类

package liuyuxiang.service.utils.convert.intlpermission.util;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.github.pagehelper.PageInfo;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Objects;

/**
 * @author Lyx
 * desc 方法脱敏工具类
 **/
@Component
public class ConvertEncryptIntl {

    @Autowired
    public void init(List<PermissionValidatedManager> permissionValidatedManagers) {
        ConvertEncryptIntl.permissionValidatedManagers = permissionValidatedManagers;
    }

    private static List<PermissionValidatedManager> permissionValidatedManagers;

    /**
     * 需要脱敏的属性名称
     */
    private static final String FIELD = "mobilePhone,regionalManagerPhone,contactPhone,contractPhone,contactTel," +
            "operationsManagerPhone,accountManagerPhone,regionalManagerPhone,serviceManagerPhone";

    private static final String EMAIL = "loginEmail,customerEmail,email," +
            "regionalManagerEmail,accountManagerEmail,contactEmail,operationsManagerEmail";

    /**
     * name 单独脱敏
     */
    private static final String FIELD_NAME = "customerName," +
            "userName," +
            "loginName," +
            "aliasName," +
            "acctName," +
            "legalPersonName," +
            "agentName," +
            "operationsManagerName," +
            "custName," +
            "certName," +
            "contractName" +
            ",contactName," +
            "regionalManagerName," +
            "partbName,accountManagerName,"+
            "lastName," +
            "firstName," +
            "companyName,";

    private static final String CERT_NUMBER = "certNumber,certNum,legalPersonCertNumber,agentCertNo";

    private static final String CERT_ADDRESS = "certAddress,address,contractAddress,street";


    public static <T> PageInfo<T> convertToEncryptPage(PageInfo<T> pageInfo,ServerHttpRequest request) {
        if (Objects.isNull(pageInfo)) {
            return new PageInfo<>();
        }
        convertToEncryptList(pageInfo.getList(),request);
        return pageInfo;
    }

    public static <T> void convertToEncryptList(List<T> list, ServerHttpRequest request) {
        if (CollUtil.isEmpty(list)) {
            return;
        }
        for (T t : list) {
            encrypt(t,request);
        }
    }

    public static <T> T encrypt(T t, ServerHttpRequest request) {
        if(ObjectUtil.isEmpty(request)){
            return t;
        }
        Field[] fields = ReflectUtil.getFields(t.getClass());
        for (Field field : fields) {
            desensitization(t, request, field);

            Object value = ReflectUtil.getFieldValue(t, field);
            if (ObjectUtil.isNotEmpty(value)) {
                if (value instanceof List) {
                    convertToEncryptList((List<? extends Object>) value,request);
                }
                encrypt(value,request);
            }
        }
        return t;
    }

    private static <T> void desensitization(T t, ServerHttpRequest request, Field field) {
        String fieldName = ReflectUtil.getFieldName(field);
        for (PermissionValidatedManager s : permissionValidatedManagers) {
            if (Boolean.TRUE.equals(s.isAction(request.getURI().getPath()))) {
                if (s.validated(fieldName)) {
                    Object value = ReflectUtil.getFieldValue(t, field);
                    Object name = switchEncryptName(value, fieldName);/*字段脱敏处理*/
                    ReflectUtil.setFieldValue(t, field, name);
                }
            }
        }
    }

    private static Object switchEncryptName(Object value,String fieldName) {
        if (existsCertAddress(fieldName)) {
            value=addressDetail(value);
        }
        if (existsName(fieldName)) {
            value= userNameHyposensitization(String.valueOf(value));
        }
        if (existsMobile(fieldName)) {
            value= DesensitizedUtil.mobilePhone(String.valueOf(value));
        }
        if (existsNumber(fieldName)) {
            value= DesensitizedUtil.idCardNum(String.valueOf(value), 6, 4);
        }
        if (existsEmail(fieldName)) {
            value= DesensitizedUtil.email(String.valueOf(value));
        }
        return value;
    }

    private static Object addressDetail(Object value) {
        if (ObjectUtil.isNotEmpty(value)) {
            String address = String.valueOf(value);
            int index = address.indexOf("市");
            String pre = address.substring(0, index + 1);
            if (StringUtils.isNotEmpty(pre)) {
                return pre + generate(address.length() - index);
            }
        }
        return null;
    }

    private static boolean existsName(String str) {
        for (String s : FIELD_NAME.split(StrUtil.COMMA)) {
            if (str.toLowerCase().contains(s.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    private static boolean existsCertAddress(String str) {
        for (String s : CERT_ADDRESS.split(StrUtil.COMMA)) {
            if (str.toLowerCase().contains(s.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    private static boolean existsEmail(String str) {
        for (String s : EMAIL.split(StrUtil.COMMA)) {
            if (str.toLowerCase().contains(s.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    private static boolean existsMobile(String str) {
        for (String s : FIELD.split(StrUtil.COMMA)) {
            if (str.toLowerCase().contains(s.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    private static boolean existsNumber(String str) {
        for (String s : CERT_NUMBER.split(StrUtil.COMMA)) {
            if (str.toLowerCase().contains(s.toLowerCase())) {
                return true;
            }
        }
        return false;
    }


    public static String userNameHyposensitization(String userName) {
        if (userName.length() == 2) {
            return DesensitizedUtil.chineseName(userName);
        } else if (userName.length() == 3) {
            String[] split = userName.split("");
            return StrUtil.format("{}*{}", split[0], split[2]);
        } else {
            String[] split = userName.split("");
            StringBuilder stringBuilder = new StringBuilder();
            int length = split.length;
            int header = length / 3;

            int encry = length / 3 + 1;

            for (int i = 0; i < split.length; i++) {
                if (i < header) {
                    stringBuilder.append(split[i]);
                }
                if (i == encry) {
                    stringBuilder.append(generate(encry));
                }
                if (i > (header * 2)) {
                    stringBuilder.append(split[i]);
                }
            }
            return stringBuilder.toString();
        }
    }

    private static String generate(Integer length) {
        StringBuilder stringBuilder = new StringBuilder();
        for (Integer integer = 0; integer < length; integer++) {
            stringBuilder.append("*");
        }
        return stringBuilder.toString();
    }
}

编写脱敏接口选择器

package liuyuxiang.service.utils.convert.intlpermission;


/**
 * @author Lyx
 * */
public interface PermissionValidatedManager {


    /**
     * 指定执行bean
     */
    Boolean isAction(String url);


    /**
     * 校验脱敏字段
     *
     * @param attrName
     * @return
     */
    boolean validated(String attrName);

}

具体接口及字段脱敏初始化

package liuyuxiang.service.utils.convert.intlpermission.impl;

import liuyuxiang.service.utils.convert.intlpermission.PermissionValidatedManager;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 脱敏接口:/liuyuxiang/getAccountExtraInfo
 * 脱敏参数:contactPhone
 * */
@Service
public class AccountExtraInfoManagerImpl implements PermissionValidatedManager, InitializingBean {

    private static final String path="/liuyuxiang/getAccountExtraInfo";
    private static final List<String> FIELD_LIST = new ArrayList<>();


    @Override
    public Boolean isAction(String url) {
        int endIndex = url.indexOf("/", path.length());
        if (endIndex != -1) {
            // 如果找到了指定字符 "/", 执行下面的代码
            String result = url.substring(0, endIndex);
            return StringUtils.equals(result,result);
        }
        return StringUtils.equals(path,url);
    }

    @Override
    public boolean validated(String attrName) {
        return FIELD_LIST.stream().anyMatch(value -> StringUtils.equals(value.toLowerCase(), attrName.toLowerCase()));
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        FIELD_LIST.add("contactPhone");
    }
}

实现参考类图:

实现逻辑:

根据springMvc 工作原理,在Controller层给需要脱敏的方法添加:@SensitiveResponse 注解

实现响应即脱敏的数据安全防范功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值