SpringBoot项目实现数据脱敏
通常情况下,将敏感信息如手机号、身份证等脱敏后存储在数据库是更安全的做法,同时在返回给前端时进行必要的脱敏处理,脱敏过程主要分为两步:
存储时脱敏: 将手机号等敏感信息在存储到数据库之前进行脱敏处理,以确保即使数据库遭到未授权访问,也不会直接暴露真实的敏感信息。脱敏后的数据仍然保留了结构,但敏感部分已经被替换或加密。这样做可以最大程度地保护用户隐私。
返回给前端时脱敏: 当从数据库中获取数据用于前端展示时,可以将数据再次进行脱敏,以保护终端用户的隐私。这意味着前端界面上显示的数据不会包含真实的敏感信息。例如,只显示部分号码、隐藏中间几位数字,或者用星号(*) 替代一部分数字。
需要注意的是,脱敏并不意味着所有情况下都是安全的。在某些情况下,即使脱敏后的数据也可能被攻击者通过各种方法还原或破解。因此,在进行数据脱敏时,需要综合考虑具体的安全需求、法规合规性以及技术实施的可行性。
最终的选择可能会受到具体情况的影响,比如所在行业的法规要求、隐私政策、数据使用场景等。在实施数据脱敏时,最好的做法是与安全专家和法律顾问合作,确保采取的措施符合最佳安全实践和法律法规。
配置Mybatis
数据加密解密很简单,如果自己手动在插入数据前对数据加密,读取到数据后进行手动解密,那么将会很麻烦。
因此,对于数据库数据的加密解密将采用Mybatis的TypeHandler处理,在对数据库提供数据后,可根据业务的需求,对部分数据进行加密。在读取到真实数据之前,会对读取到数据进行解密。
自定义TypeHandler
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
| 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值将会使用该处理器。但实际情况是,只有在字符串是敏感数据时,我们才需要用到自定义的处理器。
1 2
| # 指定TypeHandler处理器的包位置 type-handlers-package: com.lpc.handler
|
- 局部使用(推荐)
在需要的字段上使用typeHandler表明指定的处理器,没有标注typeHandler的字段,将采用默认的处理器。
mapper.xml
(对phone
字段进行加解密处理)
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
| <?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注解,实现在序列化过程中对属性值进行处理。
定义脱敏注解
1 2 3 4 5 6 7
| @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD}) @JacksonAnnotationsInside @JsonSerialize(using = SecretJsonSerializer.class) public @interface SecretColumn { SecretStrategy strategy(); }
|
定义序列化策略
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
|
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序列化器
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
|
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注解
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
| 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(); } }
|
测试
查询后返回结果
1 2 3 4 5 6 7 8 9 10
| { "msg": "操作成功", "code": 200, "data": { "assistanceId": 232, "type": "测试", "username": "林*杰", "phone": "135****3760" } }
|
总结
以上通过Mybatis的配置实现存储时脱敏,通过Jackson+注解的方式实现返回给前端时脱敏,后续可根据实际业务需求进行修改。