mirror of
				https://github.com/continew-org/continew-starter.git
				synced 2025-11-04 09:01:40 +08:00 
			
		
		
		
	feat(security/mask): 新增安全模块-脱敏,支持 JSON 数据脱敏
This commit is contained in:
		@@ -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>
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
}
 | 
			
		||||
@@ -15,6 +15,7 @@
 | 
			
		||||
 | 
			
		||||
    <modules>
 | 
			
		||||
        <module>continew-starter-security-password</module>
 | 
			
		||||
        <module>continew-starter-security-mask</module>
 | 
			
		||||
    </modules>
 | 
			
		||||
 | 
			
		||||
    <dependencies>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user