SpringBoot项目实现数据脱敏
通常情况下,将敏感信息如手机号、身份证等脱敏后存储在数据库是更安全的做法,同时在返回给前端时进行必要的脱敏处理,脱敏过程主要分为两步:
存储时脱敏: 将手机号等敏感信息在存储到数据库之前进行脱敏处理,以确保即使数据库遭到未授权访问,也不会直接暴露真实的敏感信息。脱敏后的数据仍然保留了结构,但敏感部分已经被替换或加密。这样做可以最大程度地保护用户隐私。
返回给前端时脱敏: 当从数据库中获取数据用于前端展示时,可以将数据再次进行脱敏,以保护终端用户的隐私。这意味着前端界面上显示的数据不会包含真实的敏感信息。例如,只显示部分号码、隐藏中间几位数字,或者用星号(*) 替代一部分数字。
需要注意的是,脱敏并不意味着所有情况下都是安全的。在某些情况下,即使脱敏后的数据也可能被攻击者通过各种方法还原或破解。因此,在进行数据脱敏时,需要综合考虑具体的安全需求、法规合规性以及技术实施的可行性。
最终的选择可能会受到具体情况的影响,比如所在行业的法规要求、隐私政策、数据使用场景等。在实施数据脱敏时,最好的做法是与安全专家和法律顾问合作,确保采取的措施符合最佳安全实践和法律法规。
配置Mybatis
数据加密解密很简单,如果自己手动在插入数据前对数据加密,读取到数据后进行手动解密,那么将会很麻烦。
因此,对于数据库数据的加密解密将采用Mybatis的TypeHandler处理,在对数据库提供数据后,可根据业务的需求,对部分数据进行加密。在读取到真实数据之前,会对读取到数据进行解密。
自定义TypeHandler
| 12
 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
 
 | import cn.hutool.crypto.SecureUtil;import cn.hutool.crypto.symmetric.AES;
 import org.apache.ibatis.type.BaseTypeHandler;
 import org.apache.ibatis.type.JdbcType;
 import org.springframework.util.StringUtils;
 
 import java.nio.charset.StandardCharsets;
 import java.sql.CallableStatement;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 
 
 
 
 
 
 
 public class SensitiveColumnHandler extends BaseTypeHandler<String> {
 
 private static final String KEY = "byurself2023wlsq";
 
 @Override
 public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
 
 if (!StringUtils.hasText(parameter)) {
 ps.setString(i, parameter);
 return;
 }
 
 AES aes = SecureUtil.aes(KEY.getBytes(StandardCharsets.UTF_8));
 String secretStr = aes.encryptHex(parameter);
 ps.setString(i, secretStr);
 }
 
 @Override
 public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
 String value = rs.getString(columnName);
 if (!StringUtils.hasText(value)) {
 return null;
 }
 AES aes = SecureUtil.aes(KEY.getBytes(StandardCharsets.UTF_8));
 return aes.decryptStr(value);
 }
 
 @Override
 public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
 String value = rs.getString(columnIndex);
 if (!StringUtils.hasText(value)) {
 return null;
 }
 AES aes = SecureUtil.aes(KEY.getBytes(StandardCharsets.UTF_8));
 return aes.decryptStr(value);
 }
 
 @Override
 public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
 String value = cs.getString(columnIndex);
 if (!StringUtils.hasText(value)) {
 return null;
 }
 AES aes = SecureUtil.aes(KEY.getBytes(StandardCharsets.UTF_8));
 return aes.decryptStr(value);
 }
 }
 
 | 
使用Handler
- 全局使用(不推荐)
全局使用需要在配置文件中指定处理器包位置,指定之后,在默认情况下,遇到该处理器能够处理的类型,都将使用该处理器。不建议使用全局的方式使用自定义处理器,比如本文自定义处理器是用于处理String字符串的,全局注册处理器之后,所有的String值将会使用该处理器。但实际情况是,只有在字符串是敏感数据时,我们才需要用到自定义的处理器。
| 12
 
 | # 指定TypeHandler处理器的包位置type-handlers-package: com.lpc.handler
 
 | 
- 局部使用(推荐)
在需要的字段上使用typeHandler表明指定的处理器,没有标注typeHandler的字段,将采用默认的处理器。
mapper.xml(对phone字段进行加解密处理)
| 12
 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
 
 | <?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.lpc.mapper.AssistanceMapper">
 
 <resultMap type="Assistance" id="AssistanceResult">
 <result property="assistanceId" column="assistance_id"/>
 <result property="type" column="type"/>
 <result property="username" column="username"/>
 <result property="phone" column="phone" typeHandler="com.ruoyi.nishisei.handler.SensitiveColumnHandler"/>
 </resultMap>
 
 <sql id="selectAssistanceVo">
 select assistance_id,
 type,
 username,
 phone
 from nishisei_assistance
 </sql>
 
 <select id="selectAssistanceList" parameterType="Assistance" resultMap="AssistanceResult">
 <include refid="selectAssistanceVo"/>
 <where>
 <if test="type != null  and type != ''">
 and type = #{type}
 </if>
 <if test="username != null  and username != ''">
 and username = #{username}
 </if>
 <if test="phone != null  and phone != ''">
 and phone = #{phone}
 </if>
 </where>
 </select>
 
 <select id="selectAssistanceByAssistanceId" parameterType="Long"
 resultMap="AssistanceResult">
 <include refid="selectAssistanceVo"/>
 where assistance_id = #{assistanceId}
 </select>
 
 <insert id="insertAssistance" parameterType="Assistance" useGeneratedKeys="true"
 keyProperty="assistanceId">
 insert into nishisei_assistance
 <trim prefix="(" suffix=")" suffixOverrides=",">
 <if test="type != null and type != ''">type,
 </if>
 <if test="username != null and username != ''">username,
 </if>
 <if test="phone != null and phone != ''">phone,
 </if>
 </trim>
 <trim prefix="values (" suffix=")" suffixOverrides=",">
 <if test="type != null and type != ''">#{type},
 </if>
 <if test="username != null and username != ''">#{username},
 </if>
 <if test="phone != null and phone != ''">#{phone, typeHandler=com.ruoyi.nishisei.handler.SensitiveColumnHandler},
 </if>
 </trim>
 </insert>
 
 <update id="updateAssistance" parameterType="Assistance">
 update nishisei_assistance
 <trim prefix="SET" suffixOverrides=",">
 <if test="type != null and type != ''">type =
 #{type},
 </if>
 <if test="username != null and username != ''">username =
 #{username},
 </if>
 <if test="phone != null and phone != ''">phone =
 #{phone, typeHandler=com.ruoyi.nishisei.handler.SensitiveColumnHandler},
 </if>
 </trim>
 where assistance_id = #{assistanceId}
 </update>
 </mapper>
 
 | 
测试
插入之后查看数据表
| assistance_id | type | username | phone | 
| 234 | 测试 | byu_rself | f044441c7e0921d1302db05956caea10 | 
可以看到手机号已经加密了
进行查询即可返回正常的手机号
自定义Jackson数据脱敏
Jackson是Spring默认的序列化框架,以下将通过自定义Jackson注解,实现在序列化过程中对属性值进行处理。
定义脱敏注解
| 12
 3
 4
 5
 6
 7
 
 | @Retention(RetentionPolicy.RUNTIME)@Target({ElementType.FIELD})
 @JacksonAnnotationsInside
 @JsonSerialize(using = SecretJsonSerializer.class)
 public @interface SecretColumn {
 SecretStrategy strategy();
 }
 
 | 
定义序列化策略
| 12
 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
 
 | 
 
 
 
 
 public enum SecretStrategy {
 
 
 
 
 USERNAME(str -> str.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
 
 
 
 
 ID_CARD(str -> str.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1****$2")),
 
 
 
 
 PHONE(str -> str.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
 
 
 
 
 ADDRESS(str -> str.replaceAll("(\\S{3})\\S{2}(\\S*)\\S{2}", "$1****$2****"));
 
 private final Function<String, String> desensitizer;
 
 SecretStrategy(Function<String, String> desensitizer){
 this.desensitizer = desensitizer;
 }
 
 public Function<String, String> getDesensitizer() {
 return desensitizer;
 }
 }
 
 | 
定义Jackson序列化器
| 12
 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
 
 | 
 
 
 
 
 public class SecretJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
 
 private SecretStrategy secretStrategy;
 
 
 
 
 
 @Override
 public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
 
 SecretColumn annotation = beanProperty.getAnnotation(SecretColumn.class);
 
 if (annotation != null && Objects.equals(String.class, beanProperty.getType().getRawClass())) {
 this.secretStrategy = annotation.strategy();
 
 return this;
 }
 
 return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
 }
 
 
 
 
 
 @Override
 public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
 if (Objects.isNull(secretStrategy)) {
 
 jsonGenerator.writeString(s);
 } else {
 
 jsonGenerator.writeString(secretStrategy.getDesensitizer().apply(s));
 }
 }
 }
 
 | 
在实体类中使用@SecretColumn注解
| 12
 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
 
 | public class Assistance extends BaseEntity {private static final long serialVersionUID = 1L;
 
 
 
 
 private Long assistanceId;
 
 
 
 
 @Excel(name = "类型")
 private String type;
 
 
 
 
 @SecretColumn(strategy = SecretStrategy.USERNAME)
 @Excel(name = "联系人")
 private String username;
 
 
 
 
 @SecretColumn(strategy = SecretStrategy.PHONE)
 @Excel(name = "手机号")
 private String phone;
 
 public void setAssistanceId(Long assistanceId) {
 this.assistanceId = assistanceId;
 }
 
 public Long getAssistanceId() {
 return assistanceId;
 }
 
 public void setType(String type) {
 this.type = type;
 }
 
 public String getType() {
 return type;
 }
 
 public String getUsername() {
 return username;
 }
 
 public void setUsername(String username) {
 this.username = username;
 }
 
 public String getPhone() {
 return phone;
 }
 
 public void setPhone(String phone) {
 this.phone = phone;
 }
 
 @Override
 public String toString() {
 return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
 .append("assistanceId", getAssistanceId())
 .append("type", getType())
 .append("username", getUsername())
 .append("phone", getPhone())
 .toString();
 }
 }
 
 | 
测试
查询后返回结果
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | {"msg": "操作成功",
 "code": 200,
 "data": {
 "assistanceId": 232,
 "type": "测试",
 "username": "林*杰",
 "phone": "135****3760"
 }
 }
 
 | 
总结
以上通过Mybatis的配置实现存储时脱敏,通过Jackson+注解的方式实现返回给前端时脱敏,后续可根据实际业务需求进行修改。