SpringBoot接口返回?cái)?shù)據(jù)脫敏(Mybatis、Jackson)
一、前言
有時(shí)候,我們接口返回的數(shù)據(jù)需要做一些處理,有一些敏感數(shù)據(jù),我們不能全部返回給用戶(hù),需要用*號(hào)隱藏掉一部分關(guān)鍵數(shù)據(jù),使得該敏感數(shù)據(jù)變得不完全,其他人無(wú)法知道脫敏前的數(shù)據(jù)是什么樣的。同時(shí),存儲(chǔ)在底層數(shù)據(jù)庫(kù)的數(shù)據(jù),一些關(guān)鍵信息如用戶(hù)密碼、身份證、手機(jī)號(hào)等敏感信息,也不能夠通過(guò)明文的方式存放在數(shù)據(jù)庫(kù)中。
數(shù)據(jù)脫敏有以下幾種做法:
1、通過(guò)Mybatis處理
2、通過(guò)自定義Jackson注解,實(shí)現(xiàn)在屬性序列化過(guò)程中處理數(shù)據(jù)
3、其他方式
二、Mybatis數(shù)據(jù)脫敏
在數(shù)據(jù)庫(kù)中,根據(jù)業(yè)務(wù)需求存放了不少的有關(guān)用戶(hù)的敏感信息,比如身份證、手機(jī)、住址、密碼等信息,如果這些數(shù)據(jù)都是以明文的形式存放的,那么,當(dāng)數(shù)據(jù)庫(kù)被破解后,用戶(hù)這些重要的信息都會(huì)被泄露。
數(shù)據(jù)加密解密很簡(jiǎn)單,如果自己手動(dòng)在插入數(shù)據(jù)前對(duì)數(shù)據(jù)加密,讀取到數(shù)據(jù)后進(jìn)行手動(dòng)解密,那么將會(huì)很麻煩。
因此,對(duì)于數(shù)據(jù)庫(kù)數(shù)據(jù)的加密解密將采用Mybatis的TypeHandler處理,在我們?yōu)閿?shù)據(jù)庫(kù)提供數(shù)據(jù)后,會(huì)根據(jù)我們的需求,對(duì)部分?jǐn)?shù)據(jù)進(jìn)行加密。在讀取后數(shù)據(jù)最終到我們手上前,會(huì)對(duì)讀取到數(shù)據(jù)進(jìn)行解密。請(qǐng)給位跟隨以下的做法,實(shí)現(xiàn)Mybatis數(shù)據(jù)脫敏把。
1、自定義一個(gè)TypeHandler類(lèi)型的處理器,用于處理數(shù)據(jù)的加密和解密
TypeHandler用于處理數(shù)據(jù)在數(shù)據(jù)庫(kù)類(lèi)型和java類(lèi)型之間的轉(zhuǎn)換,默認(rèn)情況下,我們常用的String、Long、Date等java類(lèi)型都有對(duì)應(yīng)的TypeHandler進(jìn)行處理,我們自定義TypeHanler的情況在于目前沒(méi)有對(duì)應(yīng)的數(shù)據(jù)轉(zhuǎn)換處理器。
以下為自定義的字符串加密解密處理器:
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類(lèi)型敏感字段處理類(lèi)
* 可以通過(guò)實(shí)現(xiàn)TypeHandler,但是BaseTypeHandler已經(jīng)實(shí)現(xiàn)了,我們可以繼承它
*/
public class SensitiveColumnHandler extends BaseTypeHandler<String> {
private static final String key = "wjfgncvkdkd25fc2";
/**
* 設(shè)置參數(shù)值,在此處對(duì)字段值進(jìn)行加密處理
*/
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
// 如果字段或字段值為空,不進(jìn)行處理
if(StringUtils.isEmpty(parameter)){
ps.setString(i, parameter);
return;
}
// 對(duì)字段值進(jìn)行加密,此處使用hutool工具,有其他使用其他即可
AES aes = SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8));
String secretStr = aes.encryptHex(parameter);
ps.setString(i, secretStr);
}
/**
* 獲取值內(nèi)容
*/
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
if(Objects.isNull(value)){
return null;
}
// 對(duì)字段值進(jìn)行加密,此處使用hutool工具,有其他使用其他即可
AES aes = SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8));
return aes.decryptStr(value);
}
/**
* 獲取值內(nèi)容
*/
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
if(Objects.isNull(value)){
return null;
}
// 對(duì)字段值就行加密,此處使用hutool工具,有其他使用其他即可
AES aes = SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8));
return aes.decryptStr(value);
}
/**
* 獲取值內(nèi)容
*/
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
if(Objects.isNull(value)){
return null;
}
// 對(duì)字段值進(jìn)行加密,此處使用hutool工具,有其他使用其他即可
AES aes = SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8));
return aes.decryptStr(value);
}
}
以上就是我們自定義一個(gè)TypeHandler類(lèi)型的處理器,以下是關(guān)于處理器的使用,有分以下兩種情況:
1)、全局使用
全局使用需要在配置文件中指定處理器包位置,指定之后,在默認(rèn)情況下,遇到該處理器能夠處理的類(lèi)型,都將使用該處理器。不建議使用全局的方式使用自定義處理器,比如本文我們的自定義處理器是用于處理String字符串的,全局注冊(cè)處理器之后,所有的String值將會(huì)使用該處理器。但實(shí)際情況是,只有在字符串是敏感數(shù)據(jù)時(shí),我們才需要用到自定義的處理器。
#指定TypeHandler處理器的包位置 type-handlers-package: com.sensitive.learn.handler
2)、局部使用
在我們需要的字段上使用typeHandler表明指定的處理器,沒(méi)有標(biāo)注typeHandler的字段,將采用默認(rèn)的處理器。(此處使用見(jiàn)Mapper.xml)。
2、創(chuàng)建Test實(shí)體類(lèi)
@Data
@Accessors(chain = true)
public class Test {
private Long id;
private String idCard;
private String phone;
}
3、創(chuàng)建TestMapper接口類(lèi)
@Mapper
public interface TestMapper {
List<Test> selectAll();
Boolean insert(Test test);
}
4、Myatis Mapper文件
在需要自定義TypeHandler的字段是使用typeHandler屬性進(jìn)行指定
<?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、測(cè)試插入與查詢(xún)
@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);
}
}
插如之后查看數(shù)據(jù)庫(kù)表情況:

可以看出數(shù)據(jù)已經(jīng)經(jīng)過(guò)加密了
查詢(xún)控制臺(tái)打印的結(jié)果:
Test(id=6, idCard=4493888665464654660, phone=1234567890)
可以看出能夠正常解密
好了,以上有關(guān)Mybatis的數(shù)據(jù)脫敏就到此為止了。
三、自定義Jackson數(shù)據(jù)脫敏
Jackson是Spring默認(rèn)的序列化框架,以下將通過(guò)自定義Jackson注解,實(shí)現(xiàn)在序列化過(guò)程中對(duì)屬性值進(jìn)行處理。
1、定義一個(gè)注解,標(biāo)注在需要脫敏的字段上
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) // 標(biāo)注在字段上
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside // 一般用于將其他的注解一起打包成"組合"注解
@JsonSerialize(using = SecretJsonSerializer.class) // 對(duì)標(biāo)注注解的字段采用哪種序列化器進(jìn)行序列化
public @interface SecretColumn {
// 脫敏策略
SecretStrategy strategy();
}
2、定義字段序列化策略,因?yàn)椴煌?lèi)型數(shù)據(jù)有不同脫敏后的展現(xiàn)形式。以下通過(guò)枚舉類(lèi)方式實(shí)現(xiàn)幾種策略:
/**
* 脫敏策略,不同數(shù)據(jù)可選擇不同的策略
*/
@Getter
public enum SecretStrategy {
/**
* 用戶(hù)名脫敏
*/
USERNAME(str -> str.replaceAll("(\\S)\\S(\\S*)", "$1*$2")),
/**
* 身份證脫敏
*/
ID_CARD(str -> str.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1****$2")),
/**
* 手機(jī)號(hào)脫敏
*/
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、定義一個(gè)Jackson序列化器,可以對(duì)標(biāo)注了@SecretColumn 的注解進(jìn)行處理
/**
* 序列化器實(shí)現(xiàn)
*/
public class SecretJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
private SecretStrategy secretStrategy;
/**
* 步驟一
* 方法來(lái)源于ContextualSerializer,獲取屬性上的注解屬性,同時(shí)返回一個(gè)合適的序列化器
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
// 獲取自定義注解
SecretColumn annotation = beanProperty.getAnnotation(SecretColumn.class);
// 注解不為空,且標(biāo)注的字段為String
if(Objects.nonNull(annotation) && Objects.equals(String.class, beanProperty.getType().getRawClass())){
this.secretStrategy = annotation.strategy();
// 符合我們自定義情況,返回本序列化器,將順利進(jìn)入到該類(lèi)中的serialize()方法中
return this;
}
// 注解為空,字段不為String,尋找合適的序列化器進(jìn)行處理
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
/**
* 步驟二
* 方法來(lái)源于JsonSerializer<String>:指定返回類(lèi)型為String類(lèi)型,serialize()將修改后的數(shù)據(jù)返回
*/
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if(Objects.isNull(secretStrategy)){
// 定義策略為空,返回原字符串
jsonGenerator.writeString(s);
}else {
// 定義策略不為空,返回策略處理過(guò)的字符串
jsonGenerator.writeString(secretStrategy.getDesensitizer().apply(s));
}
}
}
4、實(shí)體類(lèi)中使用@SecretColumn注解
@ToString
@Data
@Accessors(chain = true)
public class User {
/**
* 真實(shí)姓名
*/
@SecretColumn(strategy = SecretStrategy.USERNAME)
private String realName;
/**
* 地址
*/
@SecretColumn(strategy = SecretStrategy.ADDRESS)
private String address;
/**
* 電話(huà)號(hào)碼
*/
@SecretColumn(strategy = SecretStrategy.PHONE)
private String phoneNumber;
/**
* 身份證號(hào)碼
*/
@SecretColumn(strategy = SecretStrategy.ID_CARD)
private String idCard;
}
5、測(cè)試
@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;
}
}
輸出結(jié)果:
1)控制臺(tái)打印結(jié)果
User(realName=陳平安, address=浩然天下寶瓶洲驪珠洞天泥瓶巷, phoneNumber=12345678910, idCard=4493888665464654659)
2)瀏覽器結(jié)果
{"realName":"陳*安","address":"浩然天****瓶洲驪珠洞天泥****","phoneNumber":"123****8910","idCard":"4493****54659"}
結(jié)論:通過(guò)自定義Jackson實(shí)現(xiàn)數(shù)據(jù)脫敏,猜測(cè)生效的過(guò)程是在接口返回對(duì)象數(shù)據(jù)時(shí),序列化器將對(duì)象數(shù)據(jù)轉(zhuǎn)換成json數(shù)據(jù)時(shí)實(shí)現(xiàn)的。
可以在代碼中通過(guò)ObjectMapper序列化對(duì)象,同樣能夠得到脫敏后的數(shù)據(jù):
ObjectMapper objectMapper = new ObjectMapper();
try {
System.out.println(objectMapper.writeValueAsString(user));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
控制臺(tái)打印結(jié)果:
{"realName":"陳*安","address":"浩然天****瓶洲驪珠洞天泥****","phoneNumber":"123****8910","idCard":"4493****54659"}
注意:如果使用的不是Jackson,而是fastjson或者其他的序列化工具,則需要使用定義對(duì)應(yīng)組件能夠識(shí)別的序列化器,否則,序列化將不生效。
四、總結(jié)
以上是關(guān)于接口數(shù)據(jù)脫敏兩個(gè)方面的實(shí)現(xiàn),一方面是通過(guò)Mybatis實(shí)現(xiàn),一方面是通過(guò)Jackson實(shí)現(xiàn),大家可以通過(guò)自己的業(yè)務(wù)需求靈活組合使用,事半功倍!
到此這篇關(guān)于SpringBoot接口返回?cái)?shù)據(jù)脫敏(Mybatis、Jackson)的文章就介紹到這了,更多相關(guān)SpringBoot接口返回?cái)?shù)據(jù)脫敏內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)JSON與XML相互轉(zhuǎn)換的簡(jiǎn)明教程
Java實(shí)現(xiàn)復(fù)雜數(shù)據(jù)結(jié)構(gòu)(如嵌套對(duì)象、數(shù)組)在 JSON 與 XML 之間的相互轉(zhuǎn)換,可以使用 Jackson 和 Jackson XML 擴(kuò)展庫(kù)來(lái)完成,Jackson 是一個(gè)流行的 JSON 處理庫(kù),通過(guò) Jackson 的 XML 擴(kuò)展庫(kù),可以實(shí)現(xiàn) JSON 和 XML 之間的轉(zhuǎn)換,需要的朋友可以參考下2024-08-08
java編程SpringSecurity入門(mén)原理及應(yīng)用簡(jiǎn)介
Spring 是非常流行和成功的 Java 應(yīng)用開(kāi)發(fā)框架,Spring Security 正是 Spring 家族中的成員。Spring Security 基于 Spring 框架,提供了一套 Web 應(yīng)用安全性的完整解決方案2021-09-09
maven項(xiàng)目中<scope>provided</scope>的作用及說(shuō)明
這篇文章主要介紹了maven項(xiàng)目中<scope>provided</scope>的作用及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
ArrayList和LinkedList區(qū)別及使用場(chǎng)景代碼解析
這篇文章主要介紹了ArrayList和LinkedList區(qū)別及使用場(chǎng)景代碼解析,小編覺(jué)得還是挺不錯(cuò)的,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01
SpringBoot中JPA實(shí)現(xiàn)Sort排序的三種方式小結(jié)
這篇文章主要介紹了SpringBoot中JPA實(shí)現(xiàn)Sort排序的三種方式小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
Java通過(guò)JsApi方式實(shí)現(xiàn)微信支付
本文講解了Java如何實(shí)現(xiàn)JsApi方式的微信支付,代碼內(nèi)容詳細(xì),文章思路清晰,需要的朋友可以參考下2015-07-07
利用ClasserLoader實(shí)現(xiàn)jar包加載并調(diào)用里面的方法
classloader即是類(lèi)加載,虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從class字節(jié)碼文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行檢驗(yàn)、轉(zhuǎn)換解析和初始化,了解java的類(lèi)加載機(jī)制,可以快速解決運(yùn)行時(shí)的各種加載問(wèn)題并快速定位其背后的本質(zhì)原因,本文介紹了如何利用ClasserLoader來(lái)實(shí)現(xiàn)jar包加載并調(diào)用里面的方法2024-09-09

