背景
数据脱敏技术在合规性方面也非常重要,它有助于企业遵守《网络安全法》、《数据安全法》、《个人信息保护法》等相关法律法规的要求,确保数据处理活动的合法性和安全性。例如,《数据安全法》第二十七条提到,开展数据处理活动应当建立健全全流程数据安全管理制度,采取相应的技术措施保障数据安全,其中就包括数据脱敏技术的应用。
总的来说,数据脱敏是确保数据在各种使用场景下安全、合规的关键技术,它通过多种算法和策略来实现对敏感信息的有效保护。随着数据安全和隐私保护意识的增强,数据脱敏技术的应用将越来越广泛。
数据脱敏的实施流程通常包括敏感数据发现、敏感数据梳理、脱敏方案制定和脱敏任务执行等步骤。在敏感数据发现阶段,可以通过人工或自动的方式识别敏感数据。敏感数据梳理阶段则涉及到对数据列和数据关系的调整,以保持数据的关联性。脱敏方案制定阶段需要根据业务需求配置脱敏规则和策略,而脱敏任务执行阶段则是实际进行数据脱敏操作的过程。
考虑方案
如需要对数据进行脱敏处理,可能考虑的解决方案有:
- 手动编程实现:通过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框架中的一个接口,用于对控制器方法的返回值进行处理。
主要方法:
- boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
这个方法用于判断当前的 ResponseBodyAdvice 实现是否支持处理特定的方法返回值。
你可以根据返回类型和消息转换器类型来决定是否应用该处理逻辑。
- 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 注解
实现响应即脱敏的数据安全防范功能。