springboot实现返回数据脱敏

介绍

  • SpringBoot实现返回数据脱敏 对controller返回前端的对象序列化时进行修改。

  • 导出等不在前端展示的不满足,需要用aop的方式

  • 有时,敏感数据返回时,需要进行隐藏处理,但是如果一个字段一个字段的进行硬编码处理的话,不仅增加了工作量,而且后期需求变动的时候,更加是地狱般的工作量变更。

  • 下面,通过身份证,姓名,密码,手机号等等示例去演示脱敏的流程,当然你也可以在此基础上添加自己的实现方式

原理

  • 项目使用的是SpringBoot,所以需要在序列化的时候,进行脱敏处理,springboot内置的序列化工具为jackson
  • 通过实现com.fasterxml.jackson.databind.JsonSerializer进行自定义序列化
  • 通过重写com.fasterxml.jackson.databind.ser.ContextualSerializer.createContextual获取自定义注解的信息

实现

自定义注解类

?

1

2

3

4

5

6

7

@Target(ElementType.FIELD) //作用于字段上
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside  // 表示自定义自己的注解Sensitive
@JsonSerialize(using = SensitiveInfoSerialize.class) // 该注解使用序列化的方式
public @interface Sensitive {
    SensitizedType value();
}

复制

创建脱敏字段类型枚举

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

public enum SensitizedType {

    /**

     * 用户id

     */

    USER_ID,

    /**

     * 中文名

     */

    CHINESE_NAME,

    /**

     * 身份证号

     */

    ID_CARD,

    /**

     * 座机号

     */

    FIXED_PHONE,

    /**

     * 手机号

     */

    MOBILE_PHONE,

    /**

     * 地址

     */

    ADDRESS,

    /**

     * 电子邮件

     */

    EMAIL,

    /**

     * 密码

     */

    PASSWORD,

    /**

     * 中国大陆车牌,包含普通车辆、新能源车辆

     */

    CAR_LICENSE,

    /**

     * 银行卡

     */

    BANK_CARD,

    /**

     * IPv4地址

     */

    IPV4,

    /**

     * IPv6地址

     */

    IPV6,

    /**

     * 定义了一个first_mask的规则,只显示第一个字符。

     */

    FIRST_MASK

}

复制

脱敏工具类

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

import cn.hutool.core.util.CharUtil;

import cn.hutool.core.util.StrUtil;

/**

 * @Auther: wu

 * @Date: 2023/7/11

 * @Description: com.wu.demo.common.my_sensitive

 */

public class SensitizedUtil {

    public static String desensitized(CharSequence str, SensitizedType desensitizedType) {

        if (StrUtil.isBlank(str)) {

            return StrUtil.EMPTY;

        }

        String newStr = String.valueOf(str);

        switch (desensitizedType) {

            case USER_ID:

                newStr = String.valueOf(userId());

                break;

            case CHINESE_NAME:

                newStr = chineseName(String.valueOf(str));

                break;

            case ID_CARD:

                newStr = idCardNum(String.valueOf(str), 34);

                break;

            case FIXED_PHONE:

                newStr = fixedPhone(String.valueOf(str));

                break;

            case MOBILE_PHONE:

                newStr = mobilePhone(String.valueOf(str));

                break;

            case ADDRESS:

                newStr = address(String.valueOf(str), 8);

                break;

            case EMAIL:

                newStr = email(String.valueOf(str));

                break;

            case PASSWORD:

                newStr = password(String.valueOf(str));

                break;

            case CAR_LICENSE:

                newStr = carLicense(String.valueOf(str));

                break;

            case BANK_CARD:

                newStr = bankCard(String.valueOf(str));

                break;

            case IPV4:

                newStr = ipv4(String.valueOf(str));

                break;

            case IPV6:

                newStr = ipv6(String.valueOf(str));

                break;

            case FIRST_MASK:

                newStr = firstMask(String.valueOf(str));

                break;

            default:

        }

        return newStr;

    }

    /**

     * 【用户id】不对外提供userId

     *

     * @return 脱敏后的主键

     */

    public static Long userId() {

        return 0L;

    }

    /**

     * 定义了一个first_mask的规则,只显示第一个字符。<br>

     * 脱敏前:123456789;脱敏后:1********。

     *

     * @param str 字符串

     * @return 脱敏后的字符串

     */

    public static String firstMask(String str) {

        if (StrUtil.isBlank(str)) {

            return StrUtil.EMPTY;

        }

        return StrUtil.hide(str, 1, str.length());

    }

    /**

     * 【中文姓名】只显示第一个汉字,其他隐藏为2个星号,比如:李**

     *

     * @param fullName 姓名

     * @return 脱敏后的姓名

     */

    public static String chineseName(String fullName) {

        return firstMask(fullName);

    }

    /**

     * 【身份证号】前1位 和后2位

     *

     * @param idCardNum 身份证

     * @param front     保留:前面的front位数;从1开始

     * @param end       保留:后面的end位数;从1开始

     * @return 脱敏后的身份证

     */

    public static String idCardNum(String idCardNum, int front, int end) {

        //身份证不能为空

        if (StrUtil.isBlank(idCardNum)) {

            return StrUtil.EMPTY;

        }

        //需要截取的长度不能大于身份证号长度

        if ((front + end) > idCardNum.length()) {

            return StrUtil.EMPTY;

        }

        //需要截取的不能小于0

        if (front < 0 || end < 0) {

            return StrUtil.EMPTY;

        }

        return StrUtil.hide(idCardNum, front, idCardNum.length() - end);

    }

    /**

     * 【固定电话 前四位,后两位

     *

     * @param num 固定电话

     * @return 脱敏后的固定电话;

     */

    public static String fixedPhone(String num) {

        if (StrUtil.isBlank(num)) {

            return StrUtil.EMPTY;

        }

        return StrUtil.hide(num, 4, num.length() - 2);

    }

    /**

     * 【手机号码】前三位,后4位,其他隐藏,比如135****2210

     *

     * @param num 移动电话;

     * @return 脱敏后的移动电话;

     */

    public static String mobilePhone(String num) {

        if (StrUtil.isBlank(num)) {

            return StrUtil.EMPTY;

        }

        return StrUtil.hide(num, 3, num.length() - 4);

    }

    /**

     * 【地址】只显示到地区,不显示详细地址,比如:北京市海淀区****

     *

     * @param address       家庭住址

     * @param sensitiveSize 敏感信息长度

     * @return 脱敏后的家庭地址

     */

    public static String address(String address, int sensitiveSize) {

        if (StrUtil.isBlank(address)) {

            return StrUtil.EMPTY;

        }

        int length = address.length();

        return StrUtil.hide(address, length - sensitiveSize, length);

    }

    /**

     * 【电子邮箱】邮箱前缀仅显示第一个字母,前缀其他隐藏,用星号代替,@及后面的地址显示,比如:d**@126.com

     *

     * @param email 邮箱

     * @return 脱敏后的邮箱

     */

    public static String email(String email) {

        if (StrUtil.isBlank(email)) {

            return StrUtil.EMPTY;

        }

        int index = StrUtil.indexOf(email, '@');

        if (index <= 1) {

            return email;

        }

        return StrUtil.hide(email, 1, index);

    }

    /**

     * 【密码】密码的全部字符都用*代替,比如:******

     *

     * @param password 密码

     * @return 脱敏后的密码

     */

    public static String password(String password) {

        if (StrUtil.isBlank(password)) {

            return StrUtil.EMPTY;

        }

        return StrUtil.repeat('*', password.length());

    }

    /**

     * 【中国车牌】车牌中间用*代替

     * eg1:null       -》 ""

     * eg1:""         -》 ""

     * eg3:苏D40000   -》 苏D4***0

     * eg4:陕A12345D  -》 陕A1****D

     * eg5:京A123     -》 京A123     如果是错误的车牌,不处理

     *

     * @param carLicense 完整的车牌号

     * @return 脱敏后的车牌

     */

    public static String carLicense(String carLicense) {

        if (StrUtil.isBlank(carLicense)) {

            return StrUtil.EMPTY;

        }

        // 普通车牌

        if (carLicense.length() == 7) {

            carLicense = StrUtil.hide(carLicense, 36);

        else if (carLicense.length() == 8) {

            // 新能源车牌

            carLicense = StrUtil.hide(carLicense, 37);

        }

        return carLicense;

    }

    /**

     * 银行卡号脱敏

     * eg: 1101 **** **** **** 3256

     *

     * @param bankCardNo 银行卡号

     * @return 脱敏之后的银行卡号

     * @since 5.6.3

     */

    public static String bankCard(String bankCardNo) {

        if (StrUtil.isBlank(bankCardNo)) {

            return bankCardNo;

        }

        bankCardNo = StrUtil.trim(bankCardNo);

        if (bankCardNo.length() < 9) {

            return bankCardNo;

        }

        final int length = bankCardNo.length();

        final int midLength = length - 8;

        final StringBuilder buf = new StringBuilder();

        buf.append(bankCardNo, 04);

        for (int i = 0; i < midLength; ++i) {

            if (i % 4 == 0) {

                buf.append(CharUtil.SPACE);

            }

            buf.append('*');

        }

        buf.append(CharUtil.SPACE).append(bankCardNo, length - 4, length);

        return buf.toString();

    }

    /**

     * IPv4脱敏,如:脱敏前:192.0.2.1;脱敏后:192.*.*.*。

     *

     * @param ipv4 IPv4地址

     * @return 脱敏后的地址

     */

    public static String ipv4(String ipv4) {

        return StrUtil.subBefore(ipv4, '.'false) + ".*.*.*";

    }

    /**

     * IPv4脱敏,如:脱敏前:2001:0db8:86a3:08d3:1319:8a2e:0370:7344;脱敏后:2001:*:*:*:*:*:*:*

     *

     * @param ipv6 IPv4地址

     * @return 脱敏后的地址

     */

    public static String ipv6(String ipv6) {

        return StrUtil.subBefore(ipv6, ':'false) + ":*:*:*:*:*:*:*";

    }

}

复制

上述枚举类和脱敏工具类,我使用了hutool中的代码,如果hutool满足你的需求,可以直接把上述自定义注解类和自定义序列化类使用到的SensitizedType类直接替换为hutool中的cn.hutool.core.util.DesensitizedUtil.DesensitizedType的枚举类,

添加自定义序列化实现类

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

@NoArgsConstructor
@AllArgsConstructor
@Getter
public class SensitiveInfoSerialize extends JsonSerializer<String> implements ContextualSerializer {
    private SensitizedType sensitizedType;
    /**
     * 步骤一
     * 方法来源于ContextualSerializer,获取属性上的注解属性,同时返回一个合适的序列化器
     */
    @Override
    public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException, JsonMappingException {
        // 获取自定义注解
        Sensitive annotation = beanProperty.getAnnotation(Sensitive.class);
        // 注解不为空,且标注的字段为String
        if(Objects.nonNull(annotation) && Objects.equals(String.class, beanProperty.getType().getRawClass())){
            this.sensitizedType = annotation.value();
            //自定义情况,返回本序列化器,将顺利进入到该类中的serialize方法中
            return this;
        }
        // 注解为空,字段不为String,寻找合适的序列化器进行处理
        return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
    }
    /**
     * 步骤二
     * 方法来源于JsonSerializer<String>:指定返回类型为String类型,serialize()将修改后的数据返回
     */
    @Override
    public void serialize(String str, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, IOException {
        if(Objects.isNull(sensitizedType)){
            // 定义策略为空,返回原字符串
            jsonGenerator.writeString(str);
        }else {
            // 定义策略不为空,返回策略处理过的字符串
            jsonGenerator.writeString(SensitizedUtil.desensitized(str,sensitizedType));
        }
    }
}

复制

测试验证

在需要的脱敏的实体类字段上加上相应的注解

?

1

2

3

4

5

6

7

8

@Data

public class SensitiveBody {

    private String name;

    @Sensitive(SensitizedType.MOBILE_PHONE)

    private String mobile;

    @Sensitive(SensitizedType.ID_CARD)

    private String idCard;

}

复制

?

1

2

3

4

5

6

7

8

9

@ApiOperation(value = "脱敏测试处理")

@GetMapping("sensitiveTest")

public AjaxResult sensitiveTest(){

    SensitiveBody body = new SensitiveBody();

    body.setMobile("13041064026");

    body.setIdCard("411126189912355689");

    body.setName("Tom");

    return AjaxResult.success(body);

}

复制

  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值