首頁 > 軟體

SpringBoot使用自定義註解實現資料脫敏過程詳細解析

2023-02-16 06:02:20

前言

對於某些介面返回的資訊,涉及到敏感資料的必須進行脫敏操作,例如銀行卡號、身份證號、手機號等,脫敏方式有多種方式。可以修改SQL語句,也可以寫硬程式碼,也可以修改JSON序列化,這裡介紹通過修改Jackson序列化方式實現資料脫敏。

一、引入hutool工具類

maven:

<dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.5</version>
</dependency>

gradle:

// https://mvnrepository.com/artifact/cn.hutool/hutool-all
implementation group: 'cn.hutool', name: 'hutool-all', version: '5.8.5'

二、定義常用需要脫敏的資料型別的列舉

其中 OTHER型別為自定義型別,需在後面自定義脫敏的長度等。

package com.iscas.authentication.model.enums;
import lombok.Getter;
/**
 *
 * @version 1.0
 * @since jdk1.8
 */
@Getter
public enum PrivacyTypeEnum {
    /**
     * 中文名
     * */
    CHINESE_NAME,
    /**
     * 固話
     * */
    FIXED_PHONE,
    /**
     * 手機號
     * */
     MOBILE_PHONE,
    /**
     * 住址
     * */
    ADDRESS,
    /**
     * 密碼
     * */
    PASSWORD,
    /**
     * 銀行卡號
     * */
    BANK_CARD,
    /**
     * 郵箱
     * */
    EMAIL,
    /**
     * 身份證
     * */
    ID_CARD,
    /**
     * 其他型別
     * */
    OTHER;
}

三、定義脫敏方式列舉

其中,DEFAULT型別時,需要資料型別為上一步列舉中除OTHER外的已確定的型別,NONE表示不做脫敏,其他型別為註釋的意思。

package com.iscas.authentication.model.enums;
/**
 *
 * @version 1.0
 * @since jdk1.8
 */
public enum DesensitizationTypeEnum {
    /**
     * 預設方式
     * */
    DEFAULT,
    /**
     * 頭部脫敏
     * */
    HEAD,
    /**
     * 尾部脫敏
     * */
    TAIL,
    /**
     * 中間脫敏
     * */
    MIDDLE,
    /**
     * 頭尾脫敏
     * */
    HEAD_TAIL,
    /**
     * 全部脫敏
     * */
    ALL,
    /**
     * 不脫敏,相當於沒打這個註解
     * */
    NONE;
}

四、自定義脫敏的註解

其中,mode預設為DEFAULT,此時只需要設定dataType的型別為除OTHER外的確定型別即可,當mode不是DEFAULT或NONE時,根據不同的型別,headNoMaskLen等長度屬性需要設定,見上面的註釋的字面意思。

package com.iscas.authentication.annotation;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.iscas.authentication.model.enums.DesensitizationTypeEnum;
import com.iscas.authentication.model.enums.PrivacyTypeEnum;
import com.iscas.authentication.service.DesensitizationSerializer;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 脫敏註解
 *
 * @version 1.0
 * @since jdk1.8
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = DesensitizationSerializer.class)
public @interface Desensitization {
    /**
     * 脫敏的隱私資料型別
     */
    PrivacyTypeEnum dataType();
    /**
     * 脫敏方式,預設方式不需要定義下面脫敏長度等資訊,根據脫敏的隱私資料型別自動脫敏
     */
    DesensitizationTypeEnum mode() default DesensitizationTypeEnum.DEFAULT;
    /**
     * 尾部不脫敏的長度,當mode為HEAD或MIDDLE時使用
     */
    int tailNoMaskLen() default 1;
    /**
     * 頭部不脫敏的長度,當mode為TAIL或MIDDLE時使用
     */
    int headNoMaskLen() default 1;
    /**
     * 中間不脫敏的長度,當mode為HEAD_TAIL時使用
     */
    int middleNoMaskLen() default 1;
    /**
     * 打碼
     */
    char maskCode() default '*';
}

五、自定義Jackson的序列化方式

package com.iscas.authentication.service;
import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.StrUtil;
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 com.iscas.authentication.annotation.Desensitization;
import com.iscas.authentication.model.enums.DesensitizationTypeEnum;
import com.iscas.authentication.model.enums.PrivacyTypeEnum;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import java.io.IOException;
import java.util.Objects;
/**
 * 脫敏序列化類
 *
 * @author zhuquanwen
 * @version 1.0
 * @date 2023/1/5 9:24
 * @since jdk1.8
 */
@AllArgsConstructor
@NoArgsConstructor
public class DesensitizationSerializer extends JsonSerializer<String> implements ContextualSerializer {
    private Desensitization desensitization;
    @Override
    public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        jsonGenerator.writeString(desensitize(s));
    }
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
        if (beanProperty != null) {
            if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
                Desensitization desensitization = beanProperty.getAnnotation(Desensitization.class);
                if (desensitization == null) {
                    desensitization = beanProperty.getContextAnnotation(Desensitization.class);
                }
                if (desensitization != null) {
                    return new DesensitizationSerializer(desensitization);
                }
            }
            return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
        }
        return serializerProvider.findNullValueSerializer(null);
    }
    /**
     * 脫敏處理
     * */
    private String desensitize(String s) {
        if (StrUtil.isNotBlank(s)) {
            PrivacyTypeEnum dataType = desensitization.dataType();
            DesensitizationTypeEnum mode = desensitization.mode();
            switch (mode) {
                case DEFAULT:
                    // 預設方式,根據dataType自動選擇脫敏方式
                    s = autoDesensitize(s, dataType);
                    break;
                case HEAD:
                    // 頭部脫敏
                    s = headDesensitize(s);
                    break;
                case TAIL:
                    // 尾部脫敏
                    s = tailDesensitize(s);
                    break;
                case MIDDLE:
                    s = middleDesensitize(s);
                    break;
                case HEAD_TAIL:
                    s = headTailDesensitize(s);
                    break;
                case ALL:
                    s = allDesensitize(s);
                    break;
                case NONE:
                    // 不做脫敏
                    break;
                default:
            }
        }
        return s;
    }
    /**
     * 全部脫敏
     * */
    private String allDesensitize(String s) {
        return String.valueOf(desensitization.maskCode()).repeat(s.length());
    }
    /**
    * 頭尾脫敏
    * */
    private String headTailDesensitize(String s) {
        int middleNoMaskLen = desensitization.middleNoMaskLen();
        if (middleNoMaskLen >= s.length()) {
            // 如果中間不脫敏的長度大於等於字串的長度,不進行脫敏
            return s;
        }
        int len = s.length() - middleNoMaskLen;
        // 頭部脫敏
        int headStart = 0;
        int headEnd = len / 2;
        s = StrUtil.replace(s, headStart, headEnd, desensitization.maskCode());
        // 尾部脫敏
        int tailStart = s.length() - (len - len / 2);
        int tailEnd = s.length();
        return StrUtil.replace(s, tailStart, tailEnd, desensitization.maskCode());
    }
    /**
     * 中間脫敏
     * */
    private String middleDesensitize(String s) {
        int headNoMaskLen = desensitization.headNoMaskLen();
        int tailNoMaskLen = desensitization.tailNoMaskLen();
        if (headNoMaskLen + tailNoMaskLen >= s.length()) {
            // 如果頭部不脫敏的長度+尾部不脫敏長度 大於等於字串的長度,不進行脫敏
            return s;
        }
        int start = headNoMaskLen;
        int end = s.length() - tailNoMaskLen;
        return StrUtil.replace(s, start, end, desensitization.maskCode());
    }
    /**
     * 尾部脫敏
     * */
    private String tailDesensitize(String s) {
        int headNoMaskLen = desensitization.headNoMaskLen();
        if (headNoMaskLen >= s.length()) {
            // 如果頭部不脫敏的長度大於等於字串的長度,不進行脫敏
            return s;
        }
        int start = headNoMaskLen;
        int end = s.length();
        return StrUtil.replace(s, start, end, desensitization.maskCode());
    }
    /**
     * 頭部脫敏
     * */
    private String headDesensitize(String s) {
        int tailNoMaskLen = desensitization.tailNoMaskLen();
        if (tailNoMaskLen >= s.length()) {
            // 如果尾部不脫敏的長度大於等於字串的長度,不進行脫敏
            return s;
        }
        int start = 0;
        int end = s.length() - tailNoMaskLen;
        return StrUtil.replace(s, start, end, desensitization.maskCode());
    }
    public static void main(String[] args) {
        System.out.println(StrUtil.replace("231085198901091813", 2, -10, '#'));
    }
    /**
     * 根據資料型別自動脫敏
     * */
    private String autoDesensitize(String s, PrivacyTypeEnum dataType) {
        switch (dataType) {
            case CHINESE_NAME:
                s = DesensitizedUtil.chineseName(s);
                break;
            case FIXED_PHONE:
                s = DesensitizedUtil.fixedPhone(s);
                break;
            case MOBILE_PHONE:
                s = DesensitizedUtil.mobilePhone(s);
                break;
            case ADDRESS:
                s = DesensitizedUtil.address(s, 8);
                break;
            case PASSWORD:
                s = DesensitizedUtil.password(s);
                break;
            case BANK_CARD:
                s = DesensitizedUtil.bankCard(s);
                break;
            case EMAIL:
                s = DesensitizedUtil.email(s);
                break;
            case ID_CARD:
                s = DesensitizedUtil.idCardNum(s, 1, 2);
                break;
            case OTHER:
                // 其他型別的不支援以預設方式脫敏,直接返回
                break;
            default:
        }
        return s;
    }
}

六、使用

下面是一個測試的例子:

package com.iscas.base.biz.test.controller;
import com.iscas.base.biz.desensitization.Desensitization;
import com.iscas.base.biz.desensitization.DesensitizationTypeEnum;
import com.iscas.base.biz.desensitization.PrivacyTypeEnum;
import lombok.Data;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
/**
 *
 * @author zhuquanwen
 * @version 1.0
 * @date 2023/1/6 8:40
 * @since jdk1.8
 */
@RestController
@RequestMapping("/test/desensitization")
public class TestDesensitizationController {
    @GetMapping
    public List<TestModel> test() {
        TestModel t1 = new TestModel();
        t1.setPassword("123456");
        t1.setEmail("zzz@163.com");
        t1.setPhone("137654879451");
        t1.setFixPhone("0453-4785462");
        t1.setBankCard("622648754896457");
        t1.setIdCard("245874563214578965");
        t1.setName("張王釗");
        t1.setAddress("北京市昌平區xxx街道xxx小區1-1-101");
        t1.setHeadStr("測試頭部脫敏");
        t1.setTailStr("測試尾部脫敏");
        t1.setMiddleStr("測試中間脫敏");
        t1.setHeadTailStr("測試頭尾脫敏");
        t1.setAllStr("測試全部脫敏");
        t1.setNoneStr("測試不脫敏");
        TestModel t2 = new TestModel();
        t2.setPassword("iscas123");
        t2.setEmail("xwg@sina.com");
        t2.setPhone("18547896547");
        t2.setFixPhone("010-62268795");
        t2.setBankCard("622648754896487");
        t2.setIdCard("100412547865478947");
        t2.setName("李二麻子");
        t2.setAddress("新疆省克拉瑪依市xxx街道xxx小區1-1-101");
        t2.setHeadStr("測試頭部脫敏");
        t2.setTailStr("測試尾部脫敏");
        t2.setMiddleStr("測試中間脫敏");
        t2.setHeadTailStr("測試頭尾脫敏");
        t2.setAllStr("測試全部脫敏");
        t2.setNoneStr("測試不脫敏");
        return new ArrayList<>(){{
            add(t1);
            add(t2);
        }};
    }
    @Data
    private static class TestModel {
        /**
         * 模擬密碼
         * */
        @Desensitization(dataType = PrivacyTypeEnum.PASSWORD)
        private String password;
        /**
         * 模擬郵箱
         * */
        @Desensitization(dataType = PrivacyTypeEnum.EMAIL)
        private String email;
        /**
         * 模擬手機號
         * */
        @Desensitization(dataType = PrivacyTypeEnum.MOBILE_PHONE)
        private String phone;
        /**
         * 模擬座機
         * */
        @Desensitization(dataType = PrivacyTypeEnum.FIXED_PHONE)
        private String fixPhone;
        /**
         * 模擬銀行卡
         * */
        @Desensitization(dataType = PrivacyTypeEnum.BANK_CARD)
        private String bankCard;
        /**
         * 模擬身份證號
         * */
        @Desensitization(dataType = PrivacyTypeEnum.ID_CARD)
        private String idCard;
        /**
         * 模擬中文名
         * */
        @Desensitization(dataType = PrivacyTypeEnum.CHINESE_NAME)
        private String name;
        /**
         * 模擬住址
         * */
        @Desensitization(dataType = PrivacyTypeEnum.ADDRESS)
        private String address;
        /**
         * 模擬自定義脫敏-頭部脫敏
         * */
        @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.HEAD, tailNoMaskLen = 4)
        private String headStr;
        /**
         * 模擬自定義脫敏-尾部脫敏
         * */
        @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.TAIL, headNoMaskLen = 4)
        private String tailStr;
        /**
         * 模擬自定義脫敏-中間脫敏
         * */
        @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.MIDDLE, headNoMaskLen = 2, tailNoMaskLen = 2)
        private String middleStr;
        /**
         * 模擬自定義脫敏-兩頭脫敏
         * */
        @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.HEAD_TAIL, middleNoMaskLen = 4)
        private String headTailStr;
        /**
         * 模擬自定義脫敏-全部脫敏
         * */
        @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.ALL)
        private String allStr;
        /**
         * 模擬自定義脫敏-不脫敏
         * */
        @Desensitization(dataType = PrivacyTypeEnum.OTHER, mode = DesensitizationTypeEnum.NONE)
        private String noneStr;
    }
}

下面是一個實際使用的例子如下,在tel、password、email上新增了@Desensitization註解,自定義的@TbField等註解請忽略

package com.iscas.authentication.model.sys;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.iscas.authentication.annotation.Desensitization;
import com.iscas.authentication.model.enums.PrivacyTypeEnum;
import com.iscas.templet.annotation.table.TbField;
import com.iscas.templet.annotation.table.TbFieldRule;
import com.iscas.templet.annotation.table.TbSetting;
import com.iscas.templet.view.table.TableFieldType;
import com.iscas.templet.view.table.TableSearchType;
import com.iscas.templet.view.table.TableViewType;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.experimental.Accessors;
import java.util.List;
/**
 * @author zhuquanwen
 * @version 1.0
 * @date 2022/3/11 21:23
 * @since jdk11
 */
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Schema(title = "使用者")
@TableName(value = "oauth_sys_user")
@Accessors(chain = true)
@TbSetting(title = "使用者", checkbox = true, viewType = TableViewType.multi)
public class User extends BaseEntity {
    @TableId(type = IdType.AUTO)
    @Schema(title = "id")
    @TbField(field = "id", header = "id",
            type = TableFieldType.text, hidden = true)
    private Integer id;
    @Schema(title = "使用者名稱")
    @TbField(field = "name", header = "名稱", search = true, searchType = TableSearchType.like,
            type = TableFieldType.text, rule=@TbFieldRule(required = true, minLength = 2, maxLength = 20, distinct = true, desc = "使用者名稱不能為空,且長度介於2-20個字元之間"))
    private String name;
    @Schema(title = "密碼")
    @TbField(field = "password", header = "密碼", hidden = true, editable = false,
            type = TableFieldType.text)
    @Desensitization(dataType = PrivacyTypeEnum.PASSWORD)
    private String password;
    @Schema(title = "type")
    @TbField(field = "type", header = "使用者型別", search = true, searchType = TableSearchType.exact,
            type = TableFieldType.select, option = "[{"label":"正常使用者","value":"1"},{"label":"戰位IP使用者","value":"2"}]")
    private String type;
    @Schema(title = "status")
    @TbField(field = "status", header = "狀態", search = true, searchType = TableSearchType.exact,
            type = TableFieldType.select, option = "[{"label":"正常","value":"1"},{"label":"禁用","value":"0"}]")
    private String status;
    @Schema(title = "真實姓名")
    @TbField(field = "realName", header = "真實姓名",
            type = TableFieldType.text, rule=@TbFieldRule(required = true, minLength = 2, maxLength = 20, desc = "真實姓名不能為空,且長度介於2-20個字元之間"))
    private String realName;
    @Schema(title = "電話號碼")
    @TbField(field = "tel", header = "電話號碼",
            type = TableFieldType.text, rule=@TbFieldRule(reg = "^(13[0-9]|14[01456879]|15[0-3,5-9]|16[2567]|17[0-8]|18[0-9]|19[0-3,5-9])\d{8}$", desc = "電話號碼需符規則"))
    @Desensitization(dataType = PrivacyTypeEnum.MOBILE_PHONE)
    private String tel;
    @Schema(title = "郵箱")
    @TbField(field = "email", header = "郵箱",
            type = TableFieldType.text, rule=@TbFieldRule(reg = "^\w+([-+.]\w+)*@\w+([-.]\w+)*.\w+([-.]\w+)*$", desc = "郵箱需符規則"))
    @Desensitization(dataType = PrivacyTypeEnum.EMAIL)
    private String email;
    @Schema(title = "部門")
    @TbField(field = "orgIds", header = "部門",
            type = TableFieldType.multiSelect, selectUrl = "/api/v1/orgs/combobox/tree?status=1")
    @TableField(exist = false)
    private List<Integer> orgIds;
    @Schema(title = "角色")
    @TbField(field = "roleIds", header = "角色",
            type = TableFieldType.multiSelect, selectUrl = "/api/v1/roles/combobox?status=1")
    @TableField(exist = false)
    private List<Integer> roleIds;
    @Schema(title = "崗位")
    @TbField(field = "postIds", header = "崗位",
            type = TableFieldType.multiSelect, selectUrl = "/api/v1/posts/combobox?status=1")
    @TableField(exist = false)
    private List<Integer> postIds;
}

七、脫敏效果

下面是測試的結果:

下面是一個查詢介面返回帶User實體的結果:

到此這篇關於SpringBoot使用自定義註解實現資料脫敏過程詳細解析的文章就介紹到這了,更多相關SpringBoot資料脫敏內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com