feat(security/mask): 新增安全模块-脱敏,支持 JSON 数据脱敏

This commit is contained in:
2024-02-07 17:38:31 +08:00
parent 00798bdb4c
commit 7b795194d3
7 changed files with 404 additions and 0 deletions

View File

@@ -104,6 +104,11 @@ public class StringConstants {
*/
public static final char C_AT = CharPool.AT;
/**
* 字符常量:星号 {@code '*'}
*/
public static final char C_ASTERISK = '*';
/**
* 字符串常量:制表符 {@code "\t"}
*/

View File

@@ -382,6 +382,13 @@
<version>${revision}</version>
</dependency>
<!-- 安全模块 - 脱敏 -->
<dependency>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-security-mask</artifactId>
<version>${revision}</version>
</dependency>
<!-- 安全模块 - 密码编码器 -->
<dependency>
<groupId>top.charles7c.continew</groupId>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>top.charles7c.continew</groupId>
<artifactId>continew-starter-security</artifactId>
<version>${revision}</version>
</parent>
<artifactId>continew-starter-security-mask</artifactId>
<description>ContiNew Starter 安全模块 - 脱敏</description>
</project>

View File

@@ -0,0 +1,67 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.security.mask.annotation;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.security.mask.core.JsonMaskSerializer;
import top.charles7c.continew.starter.security.mask.enums.MaskType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* JSON 脱敏注解
*
* @author Charles7c
* @since 1.4.0
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = JsonMaskSerializer.class)
public @interface JsonMask {
/**
* 脱敏类型
*/
MaskType value() default MaskType.CUSTOM;
/**
* 左侧保留位数
* <p>
* 仅在脱敏类型为 {@code DesensitizedType.CUSTOM } 时使用
* </p>
*/
int left() default 0;
/**
* 右侧保留位数
* <p>
* 仅在脱敏类型为 {@code DesensitizedType.CUSTOM } 时使用
* </p>
*/
int right() default 0;
/**
* 脱敏符号(默认:*
*/
char character() default StringConstants.C_ASTERISK;
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.security.mask.core;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.security.mask.annotation.JsonMask;
import top.charles7c.continew.starter.security.mask.enums.MaskType;
import java.io.IOException;
import java.util.Objects;
/**
* JSON 脱敏序列化器
*
* @author Charles7c
* @since 1.4.0
*/
public class JsonMaskSerializer extends JsonSerializer<String> implements ContextualSerializer {
private JsonMask jsonMask;
public JsonMaskSerializer(JsonMask jsonMask) {
this.jsonMask = jsonMask;
}
public JsonMaskSerializer() {
}
@Override
public void serialize(String str,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
if (CharSequenceUtil.isBlank(str)) {
jsonGenerator.writeString(StringConstants.EMPTY);
return;
}
MaskType maskType = jsonMask.value();
jsonGenerator.writeString(maskType.mask(str, jsonMask.character(), jsonMask.left(), jsonMask.right()));
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider,
BeanProperty beanProperty) throws JsonMappingException {
if (null == beanProperty) {
return serializerProvider.findNullValueSerializer(null);
}
if (!Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
JsonMask jsonMaskAnnotation = ObjectUtil.defaultIfNull(beanProperty.getAnnotation(JsonMask.class), beanProperty
.getContextAnnotation(JsonMask.class));
if (null == jsonMaskAnnotation) {
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
return new JsonMaskSerializer(jsonMaskAnnotation);
}
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.continew.starter.security.mask.enums;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.CharUtil;
import top.charles7c.continew.starter.core.constant.StringConstants;
/**
* 脱敏类型
*
* @author Charles7c
* @since 1.4.0
*/
public enum MaskType {
/**
* 自定义脱敏
*/
CUSTOM {
@Override
public String mask(String str, char character, int left, int right) {
return CharSequenceUtil.replace(str, left, str.length() - right, character);
}
},
/**
* 手机号码
* <p>保留前 3 位和后 4 位例如135****2210</p>
*/
MOBILE_PHONE {
@Override
public String mask(String str, char character, int left, int right) {
return CharSequenceUtil.replace(str, 3, str.length() - 4, character);
}
},
/**
* 固定电话
* <p>
* 保留前 4 位和后 2 位
* </p>
*/
FIXED_PHONE {
@Override
public String mask(String str, char character, int left, int right) {
return CharSequenceUtil.replace(str, 4, str.length() - 2, character);
}
},
/**
* 电子邮箱
*
* <p>
* 邮箱前缀仅保留第 1 个字母,@ 符号及后面的地址不脱敏例如d**@126.com
* </p>
*/
EMAIL {
@Override
public String mask(String str, char character, int left, int right) {
int index = str.indexOf(StringConstants.AT);
if (index <= 1) {
return str;
}
return CharSequenceUtil.replace(str, 1, index, character);
}
},
/**
* 身份证号
* <p>
* 保留前 1 位和后 2 位
* </p>
*/
ID_CARD {
@Override
public String mask(String str, char character, int left, int right) {
return CharSequenceUtil.replace(str, 1, str.length() - 2, character);
}
},
/**
* 银行卡
* <p>
* 由于银行卡号长度不定,所以只保留前 4 位,后面保留的位数根据卡号决定展示 1-4 位
* <ul>
* <li>1234 2222 3333 4444 6789 9 => 1234 **** **** **** **** 9</li>
* <li>1234 2222 3333 4444 6789 91 => 1234 **** **** **** **** 91</li>
* <li>1234 2222 3333 4444 678 => 1234 **** **** **** 678</li>
* <li>1234 2222 3333 4444 6789 => 1234 **** **** **** 6789</li>
* </ul>
* </p>
*/
BANK_CARD {
@Override
public String mask(String str, char character, int left, int right) {
String cleanStr = CharSequenceUtil.cleanBlank(str);
if (cleanStr.length() < 9) {
return cleanStr;
}
final int length = cleanStr.length();
final int endLength = length % 4 == 0 ? 4 : length % 4;
final int midLength = length - 4 - endLength;
final StringBuilder buffer = new StringBuilder();
buffer.append(cleanStr, 0, 4);
for (int i = 0; i < midLength; ++i) {
if (i % 4 == 0) {
buffer.append(CharUtil.SPACE);
}
buffer.append(character);
}
buffer.append(CharUtil.SPACE).append(cleanStr, length - endLength, length);
return buffer.toString();
}
},
/**
* 中国大陆车牌(包含普通车辆、新能源车辆)
* <p>
* 例如苏D40000 => 苏D4***0
* </p>
*/
CAR_LICENSE {
@Override
public String mask(String str, char character, int left, int right) {
// 普通车牌
int length = str.length();
if (length == 7) {
return CharSequenceUtil.replace(str, 3, 6, character);
}
// 新能源车牌
if (length == 8) {
return CharSequenceUtil.replace(str, 3, 7, character);
}
return str;
}
},
/**
* 中文名
* <p>
* 只保留第 1 个汉字,例如:李**
* </p>
*/
CHINESE_NAME {
@Override
public String mask(String str, char character, int left, int right) {
return CharSequenceUtil.replace(str, 1, str.length(), character);
}
},
/**
* 密码
* <p>
* 密码的全部字符都使用脱敏符号代替,例如:******
* </p>
*/
PASSWORD {
@Override
public String mask(String str, char character, int left, int right) {
return CharSequenceUtil.repeat(character, str.length());
}
},
/**
* 地址
* <p>
* 只显示到地区,不显示详细地址,例如:北京市海淀区****
* </p>
*/
ADDRESS {
@Override
public String mask(String str, char character, int left, int right) {
int length = str.length();
return CharSequenceUtil.replace(str, length - 8, length, character);
}
},
/**
* IPv4 地址
* <p>
* 例如192.0.2.1 => 192.*.*.*
* </p>
*/
IPV4 {
@Override
public String mask(String str, char character, int left, int right) {
return CharSequenceUtil.subBefore(str, StringConstants.DOT, false) + String
.format(".%s.%s.%s", character, character, character);
}
},
/**
* IPv6 地址
* <p>
* 例如2001:0db8:86a3:08d3:1319:8a2e:0370:7344 => 2001:*:*:*:*:*:*:*
* </p>
*/
IPV6 {
@Override
public String mask(String str, char character, int left, int right) {
return CharSequenceUtil.subBefore(str, StringConstants.COLON, false) + String
.format(":%s:%s:%s:%s:%s:%s:%s", character, character, character, character, character, character, character);
}
},;
/**
* 数据脱敏
*
* @param str 原始字符串
* @param character 脱敏符号
* @param left 左侧保留位数
* @param right 右侧保留位数
* @return 脱敏后的数据
*/
public abstract String mask(String str, char character, int left, int right);
}

View File

@@ -15,6 +15,7 @@
<modules>
<module>continew-starter-security-password</module>
<module>continew-starter-security-mask</module>
</modules>
<dependencies>