SpringBoot接口返回数据脱敏(Mybatis、Jackson)
作者:明天再去学习
一、前言
有时候,我们接口返回的数据需要做一些处理,有一些敏感数据,我们不能全部返回给用户,需要用*号隐藏掉一部分关键数据,使得该敏感数据变得不完全,其他人无法知道脱敏前的数据是什么样的。同时,存储在底层数据库的数据,一些关键信息如用户密码、身份证、手机号等敏感信息,也不能够通过明文的方式存放在数据库中。
数据脱敏有以下几种做法:
1、通过Mybatis处理
2、通过自定义Jackson注解,实现在属性序列化过程中处理数据
3、其他方式
二、Mybatis数据脱敏
在数据库中,根据业务需求存放了不少的有关用户的敏感信息,比如身份证、手机、住址、密码等信息,如果这些数据都是以明文的形式存放的,那么,当数据库被破解后,用户这些重要的信息都会被泄露。
数据加密解密很简单,如果自己手动在插入数据前对数据加密,读取到数据后进行手动解密,那么将会很麻烦。
因此,对于数据库数据的加密解密将采用Mybatis的TypeHandler处理,在我们为数据库提供数据后,会根据我们的需求,对部分数据进行加密。在读取后数据最终到我们手上前,会对读取到数据进行解密。请给位跟随以下的做法,实现Mybatis数据脱敏把。
1、自定义一个TypeHandler类型的处理器,用于处理数据的加密和解密
TypeHandler用于处理数据在数据库类型和java类型之间的转换,默认情况下,我们常用的String、Long、Date等java类型都有对应的TypeHandler进行处理,我们自定义TypeHanler的情况在于目前没有对应的数据转换处理器。
以下为自定义的字符串加密解密处理器:
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; import java.util.Objects; /** * mybatis String类型敏感字段处理类 * 可以通过实现TypeHandler,但是BaseTypeHandler已经实现了,我们可以继承它 */ public class SensitiveColumnHandler extends BaseTypeHandler<String> { private static final String key = "wjfgncvkdkd25fc2"; /** * 设置参数值,在此处对字段值进行加密处理 */ @Override public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { // 如果字段或字段值为空,不进行处理 if(StringUtils.isEmpty(parameter)){ ps.setString(i, parameter); return; } // 对字段值进行加密,此处使用hutool工具,有其他使用其他即可 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(Objects.isNull(value)){ return null; } // 对字段值进行加密,此处使用hutool工具,有其他使用其他即可 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(Objects.isNull(value)){ return null; } // 对字段值就行加密,此处使用hutool工具,有其他使用其他即可 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(Objects.isNull(value)){ return null; } // 对字段值进行加密,此处使用hutool工具,有其他使用其他即可 AES aes = SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8)); return aes.decryptStr(value); } }
以上就是我们自定义一个TypeHandler类型的处理器,以下是关于处理器的使用,有分以下两种情况:
1)、全局使用
全局使用需要在配置文件中指定处理器包位置,指定之后,在默认情况下,遇到该处理器能够处理的类型,都将使用该处理器。不建议使用全局的方式使用自定义处理器,比如本文我们的自定义处理器是用于处理String字符串的,全局注册处理器之后,所有的String值将会使用该处理器。但实际情况是,只有在字符串是敏感数据时,我们才需要用到自定义的处理器。
#指定TypeHandler处理器的包位置 type-handlers-package: com.sensitive.learn.handler
2)、局部使用
在我们需要的字段上使用typeHandler表明指定的处理器,没有标注typeHandler的字段,将采用默认的处理器。(此处使用见Mapper.xml)。
2、创建Test实体类
@Data @Accessors(chain = true) public class Test { private Long id; private String idCard; private String phone; }
3、创建TestMapper接口类
@Mapper public interface TestMapper { List<Test> selectAll(); Boolean insert(Test test); }
4、Myatis Mapper文件
在需要自定义TypeHandler的字段是使用typeHandler属性进行指定
<?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.sensitive.learn.mapper.TestMapper"> <resultMap id="testMap" type="com.sensitive.learn.model.Test"> <id property="id" column="id"/> <result property="idCard" column="id_card" typeHandler="com.sensitive.learn.handler.SensitiveColumnHandler"/> <result property="phone" column="phone" typeHandler="com.sensitive.learn.handler.SensitiveColumnHandler"/> </resultMap> <select id="selectAll" resultMap="testMap"> select id, id_card, phone from test </select> <insert id="insert" parameterType="com.sensitive.learn.model.Test"> insert into test(id, id_card, phone) values(#{id}, #{idCard, typeHandler=com.sensitive.learn.handler.SensitiveColumnHandler}, #{phone, typeHandler=com.sensitive.learn.handler.SensitiveColumnHandler}) </insert> </mapper>
5、测试插入与查询
@SpringBootTest @RunWith(SpringRunner.class) public class TestClass { @Resource private TestMapper testMapper; @Test public void test1(){ List<com.sensitive.learn.model.Test> tests = testMapper.selectAll(); tests.forEach(System.out::println); } @Test public void test2(){ com.sensitive.learn.model.Test test = new com.sensitive.learn.model.Test(); test.setId(6L) .setIdCard("4493888665464654660") .setPhone("1234567890"); testMapper.insert(test); } }
插如之后查看数据库表情况:
可以看出数据已经经过加密了
查询控制台打印的结果:
Test(id=6, idCard=4493888665464654660, phone=1234567890)
可以看出能够正常解密
好了,以上有关Mybatis的数据脱敏就到此为止了。
三、自定义Jackson数据脱敏
Jackson是Spring默认的序列化框架,以下将通过自定义Jackson注解,实现在序列化过程中对属性值进行处理。
1、定义一个注解,标注在需要脱敏的字段上
import com.boot.learn.enums.SecretStrategy; import com.boot.learn.serializer.SecretJsonSerializer; import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) // 标注在字段上 @Retention(RetentionPolicy.RUNTIME) @JacksonAnnotationsInside // 一般用于将其他的注解一起打包成"组合"注解 @JsonSerialize(using = SecretJsonSerializer.class) // 对标注注解的字段采用哪种序列化器进行序列化 public @interface SecretColumn { // 脱敏策略 SecretStrategy strategy(); }
2、定义字段序列化策略,因为不同类型数据有不同脱敏后的展现形式。以下通过枚举类方式实现几种策略:
/** * 脱敏策略,不同数据可选择不同的策略 */ @Getter 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; } }
3、定义一个Jackson序列化器,可以对标注了@SecretColumn 的注解进行处理
/** * 序列化器实现 */ public class SecretJsonSerializer extends JsonSerializer<String> implements ContextualSerializer { private SecretStrategy secretStrategy; /** * 步骤一 * 方法来源于ContextualSerializer,获取属性上的注解属性,同时返回一个合适的序列化器 */ @Override public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException { // 获取自定义注解 SecretColumn annotation = beanProperty.getAnnotation(SecretColumn.class); // 注解不为空,且标注的字段为String if(Objects.nonNull(annotation) && Objects.equals(String.class, beanProperty.getType().getRawClass())){ this.secretStrategy = annotation.strategy(); // 符合我们自定义情况,返回本序列化器,将顺利进入到该类中的serialize()方法中 return this; } // 注解为空,字段不为String,寻找合适的序列化器进行处理 return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty); } /** * 步骤二 * 方法来源于JsonSerializer<String>:指定返回类型为String类型,serialize()将修改后的数据返回 */ @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)); } } }
4、实体类中使用@SecretColumn注解
@ToString @Data @Accessors(chain = true) public class User { /** * 真实姓名 */ @SecretColumn(strategy = SecretStrategy.USERNAME) private String realName; /** * 地址 */ @SecretColumn(strategy = SecretStrategy.ADDRESS) private String address; /** * 电话号码 */ @SecretColumn(strategy = SecretStrategy.PHONE) private String phoneNumber; /** * 身份证号码 */ @SecretColumn(strategy = SecretStrategy.ID_CARD) private String idCard; }
5、测试
@RestController @RequestMapping("/secret") public class SecretController { @GetMapping("/test") public User test(){ User user = new User(); user.setRealName("陈平安") .setPhoneNumber("12345678910") .setAddress("浩然天下宝瓶洲骊珠洞天泥瓶巷") .setIdCard("4493888665464654659"); System.out.println(user); return user; } }
输出结果:
1)控制台打印结果
User(realName=陈平安, address=浩然天下宝瓶洲骊珠洞天泥瓶巷, phoneNumber=12345678910, idCard=4493888665464654659)
2)浏览器结果
{"realName":"陈*安","address":"浩然天****瓶洲骊珠洞天泥****","phoneNumber":"123****8910","idCard":"4493****54659"}
结论:通过自定义Jackson实现数据脱敏,猜测生效的过程是在接口返回对象数据时,序列化器将对象数据转换成json数据时实现的。
可以在代码中通过ObjectMapper序列化对象,同样能够得到脱敏后的数据:
ObjectMapper objectMapper = new ObjectMapper(); try { System.out.println(objectMapper.writeValueAsString(user)); } catch (JsonProcessingException e) { e.printStackTrace(); }
控制台打印结果:
{"realName":"陈*安","address":"浩然天****瓶洲骊珠洞天泥****","phoneNumber":"123****8910","idCard":"4493****54659"}
注意:如果使用的不是Jackson,而是fastjson或者其他的序列化工具,则需要使用定义对应组件能够识别的序列化器,否则,序列化将不生效。
四、总结
以上是关于接口数据脱敏两个方面的实现,一方面是通过Mybatis实现,一方面是通过Jackson实现,大家可以通过自己的业务需求灵活组合使用,事半功倍!
到此这篇关于SpringBoot接口返回数据脱敏(Mybatis、Jackson)的文章就介绍到这了,更多相关SpringBoot接口返回数据脱敏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!