From 7b795194d3db979c239ab30d78fdb61d95f06896 Mon Sep 17 00:00:00 2001 From: Charles7c Date: Wed, 7 Feb 2024 17:38:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(security/mask):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E6=A8=A1=E5=9D=97-=E8=84=B1=E6=95=8F?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=20JSON=20=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=84=B1=E6=95=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/constant/StringConstants.java | 5 + continew-starter-dependencies/pom.xml | 7 + .../continew-starter-security-mask/pom.xml | 14 ++ .../security/mask/annotation/JsonMask.java | 67 +++++ .../mask/core/JsonMaskSerializer.java | 79 ++++++ .../starter/security/mask/enums/MaskType.java | 231 ++++++++++++++++++ continew-starter-security/pom.xml | 1 + 7 files changed, 404 insertions(+) create mode 100644 continew-starter-security/continew-starter-security-mask/pom.xml create mode 100644 continew-starter-security/continew-starter-security-mask/src/main/java/top/charles7c/continew/starter/security/mask/annotation/JsonMask.java create mode 100644 continew-starter-security/continew-starter-security-mask/src/main/java/top/charles7c/continew/starter/security/mask/core/JsonMaskSerializer.java create mode 100644 continew-starter-security/continew-starter-security-mask/src/main/java/top/charles7c/continew/starter/security/mask/enums/MaskType.java diff --git a/continew-starter-core/src/main/java/top/charles7c/continew/starter/core/constant/StringConstants.java b/continew-starter-core/src/main/java/top/charles7c/continew/starter/core/constant/StringConstants.java index b8f51290..4e3828b0 100644 --- a/continew-starter-core/src/main/java/top/charles7c/continew/starter/core/constant/StringConstants.java +++ b/continew-starter-core/src/main/java/top/charles7c/continew/starter/core/constant/StringConstants.java @@ -104,6 +104,11 @@ public class StringConstants { */ public static final char C_AT = CharPool.AT; + /** + * 字符常量:星号 {@code '*'} + */ + public static final char C_ASTERISK = '*'; + /** * 字符串常量:制表符 {@code "\t"} */ diff --git a/continew-starter-dependencies/pom.xml b/continew-starter-dependencies/pom.xml index 9665f5ab..9389a0ae 100644 --- a/continew-starter-dependencies/pom.xml +++ b/continew-starter-dependencies/pom.xml @@ -382,6 +382,13 @@ ${revision} + + + top.charles7c.continew + continew-starter-security-mask + ${revision} + + top.charles7c.continew diff --git a/continew-starter-security/continew-starter-security-mask/pom.xml b/continew-starter-security/continew-starter-security-mask/pom.xml new file mode 100644 index 00000000..7e53b964 --- /dev/null +++ b/continew-starter-security/continew-starter-security-mask/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + top.charles7c.continew + continew-starter-security + ${revision} + + + continew-starter-security-mask + ContiNew Starter 安全模块 - 脱敏 + \ No newline at end of file diff --git a/continew-starter-security/continew-starter-security-mask/src/main/java/top/charles7c/continew/starter/security/mask/annotation/JsonMask.java b/continew-starter-security/continew-starter-security-mask/src/main/java/top/charles7c/continew/starter/security/mask/annotation/JsonMask.java new file mode 100644 index 00000000..24769506 --- /dev/null +++ b/continew-starter-security/continew-starter-security-mask/src/main/java/top/charles7c/continew/starter/security/mask/annotation/JsonMask.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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; + + /** + * 左侧保留位数 + *

+ * 仅在脱敏类型为 {@code DesensitizedType.CUSTOM } 时使用 + *

+ */ + int left() default 0; + + /** + * 右侧保留位数 + *

+ * 仅在脱敏类型为 {@code DesensitizedType.CUSTOM } 时使用 + *

+ */ + int right() default 0; + + /** + * 脱敏符号(默认:*) + */ + char character() default StringConstants.C_ASTERISK; +} \ No newline at end of file diff --git a/continew-starter-security/continew-starter-security-mask/src/main/java/top/charles7c/continew/starter/security/mask/core/JsonMaskSerializer.java b/continew-starter-security/continew-starter-security-mask/src/main/java/top/charles7c/continew/starter/security/mask/core/JsonMaskSerializer.java new file mode 100644 index 00000000..94befdc5 --- /dev/null +++ b/continew-starter-security/continew-starter-security-mask/src/main/java/top/charles7c/continew/starter/security/mask/core/JsonMaskSerializer.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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 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); + } +} diff --git a/continew-starter-security/continew-starter-security-mask/src/main/java/top/charles7c/continew/starter/security/mask/enums/MaskType.java b/continew-starter-security/continew-starter-security-mask/src/main/java/top/charles7c/continew/starter/security/mask/enums/MaskType.java new file mode 100644 index 00000000..220fa30b --- /dev/null +++ b/continew-starter-security/continew-starter-security-mask/src/main/java/top/charles7c/continew/starter/security/mask/enums/MaskType.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved. + *

+ * 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 + *

+ * http://www.gnu.org/licenses/lgpl.html + *

+ * 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); + } + }, + + /** + * 手机号码 + *

保留前 3 位和后 4 位,例如:135****2210

+ */ + MOBILE_PHONE { + @Override + public String mask(String str, char character, int left, int right) { + return CharSequenceUtil.replace(str, 3, str.length() - 4, character); + } + }, + + /** + * 固定电话 + *

+ * 保留前 4 位和后 2 位 + *

+ */ + FIXED_PHONE { + @Override + public String mask(String str, char character, int left, int right) { + return CharSequenceUtil.replace(str, 4, str.length() - 2, character); + } + }, + + /** + * 电子邮箱 + * + *

+ * 邮箱前缀仅保留第 1 个字母,@ 符号及后面的地址不脱敏,例如:d**@126.com + *

+ */ + 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); + } + }, + + /** + * 身份证号 + *

+ * 保留前 1 位和后 2 位 + *

+ */ + ID_CARD { + @Override + public String mask(String str, char character, int left, int right) { + return CharSequenceUtil.replace(str, 1, str.length() - 2, character); + } + }, + + /** + * 银行卡 + *

+ * 由于银行卡号长度不定,所以只保留前 4 位,后面保留的位数根据卡号决定展示 1-4 位 + *

+ *

+ */ + 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(); + } + }, + + /** + * 中国大陆车牌(包含普通车辆、新能源车辆) + *

+ * 例如:苏D40000 => 苏D4***0 + *

+ */ + 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; + } + }, + + /** + * 中文名 + *

+ * 只保留第 1 个汉字,例如:李** + *

+ */ + CHINESE_NAME { + @Override + public String mask(String str, char character, int left, int right) { + return CharSequenceUtil.replace(str, 1, str.length(), character); + } + }, + + /** + * 密码 + *

+ * 密码的全部字符都使用脱敏符号代替,例如:****** + *

+ */ + PASSWORD { + @Override + public String mask(String str, char character, int left, int right) { + return CharSequenceUtil.repeat(character, str.length()); + } + }, + + /** + * 地址 + *

+ * 只显示到地区,不显示详细地址,例如:北京市海淀区**** + *

+ */ + 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 地址 + *

+ * 例如:192.0.2.1 => 192.*.*.* + *

+ */ + 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 地址 + *

+ * 例如:2001:0db8:86a3:08d3:1319:8a2e:0370:7344 => 2001:*:*:*:*:*:*:* + *

+ */ + 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); +} diff --git a/continew-starter-security/pom.xml b/continew-starter-security/pom.xml index 929cd151..892a1bf9 100644 --- a/continew-starter-security/pom.xml +++ b/continew-starter-security/pom.xml @@ -15,6 +15,7 @@ continew-starter-security-password + continew-starter-security-mask