1.概述
近來(lái)我們都在圍繞著使用Spring Boot
開(kāi)發(fā)業(yè)務(wù)系統(tǒng)時(shí)如何保證數(shù)據(jù)安全性這個(gè)主題展開(kāi)總結(jié),當(dāng)下大部分的B/S架構(gòu)
的系統(tǒng)也都是基于Spring Boot + SpringMVC三層架構(gòu)
開(kāi)發(fā)的,可以認(rèn)為是在SpringMVC
的三層架構(gòu)中的controller層(邏輯控制層)
對(duì)接口數(shù)據(jù)進(jìn)行安全處理操作,更直接點(diǎn)說(shuō)就是在接口請(qǐng)求參數(shù)傳入進(jìn)行邏輯處理或者響應(yīng)參數(shù)輸出到頁(yè)面展示之前就是數(shù)據(jù)處理的,所以只是在SpringMVC
三層架構(gòu)中的一層中進(jìn)行安全加固,還不是很穩(wěn)固,接下來(lái)今天我們就再來(lái)講講在SpringMVC
三層架構(gòu)另一層中如何進(jìn)行數(shù)據(jù)安全加固,在今天主題之前先來(lái)看看什么是SpringMVC
架構(gòu)?
什么是SpringMVC三層架構(gòu)?
SpringMVC的工程結(jié)構(gòu)一般來(lái)說(shuō)分為三層,自下而上是Modle層(模型,數(shù)據(jù)訪問(wèn)層)、Cotroller層(控制,邏輯控制層)、View層(視圖,頁(yè)面顯示層),其中Modle層分為兩層:dao層、service層,MVC架構(gòu)分層的主要作用是解耦。采用分層架構(gòu)的好處,普遍接受的是系統(tǒng)分層有利于系統(tǒng)的維護(hù),系統(tǒng)的擴(kuò)展。就是增強(qiáng)系統(tǒng)的可維護(hù)性和可擴(kuò)展性。對(duì)于Spring這樣的框架,(View\\Web)表示層調(diào)用控制層(Controller),控制層調(diào)用業(yè)務(wù)層(Service),業(yè)務(wù)層調(diào)用數(shù)據(jù)訪問(wèn)層(Dao) 可以這么說(shuō),現(xiàn)在90%以上的業(yè)務(wù)系統(tǒng)都是基于該三層架構(gòu)模式開(kāi)發(fā)的,這種架構(gòu)模式也有人說(shuō)是設(shè)計(jì)模式中一種,可見(jiàn)其重要性不言而喻,所以我們需重視。
我們也都知道在日常開(kāi)發(fā)系統(tǒng)過(guò)程中,數(shù)據(jù)安全是非常重要的。特別是在當(dāng)今互聯(lián)網(wǎng)時(shí)代,個(gè)人隱私安全極其重要,一旦個(gè)人用戶數(shù)據(jù)遭到攻擊泄露,將會(huì)造成災(zāi)難級(jí)的事故問(wèn)題。所有之前我們基于接口層進(jìn)行數(shù)據(jù)安全處理是遠(yuǎn)遠(yuǎn)不夠的,今天我們就來(lái)談?wù)勅绾蜯odel層(數(shù)據(jù)訪問(wèn)層)怎樣做到優(yōu)雅數(shù)據(jù)加密存儲(chǔ)、模糊匹配及其脫敏展示,本文的主題: 數(shù)據(jù)加密存儲(chǔ)、模糊匹配和脫敏展示 。
銀行系統(tǒng)對(duì)數(shù)據(jù)安全性的要求在業(yè)務(wù)系統(tǒng)中是首屈一指的,所以今天我們就以常見(jiàn)的個(gè)人銀行賬戶數(shù)據(jù):密碼、手機(jī)號(hào)、詳細(xì)地址、銀行卡號(hào)等信息字段為例,進(jìn)行主題的宣講與淺析。
2.數(shù)據(jù)加密存儲(chǔ)
我們之前總結(jié)的是在接口層進(jìn)行數(shù)據(jù)加解密傳輸,也強(qiáng)調(diào)過(guò)這種方式保證不了數(shù)據(jù)的絕對(duì)安全,只是有效提高接口數(shù)據(jù)安全性,抬高數(shù)據(jù)被抓取的門檻而已。所以接下來(lái)我們就來(lái)講述一下如何在數(shù)據(jù)的源頭存儲(chǔ)層保障其安全。我們都知道一些核心私密字段,比如說(shuō)密碼,手機(jī)號(hào)等在數(shù)據(jù)庫(kù)層存儲(chǔ)就不能明文存儲(chǔ),必須加密存儲(chǔ)保證即使數(shù)據(jù)庫(kù)泄露了也不會(huì)輕易曝光數(shù)據(jù)。
2.1 優(yōu)雅實(shí)現(xiàn)數(shù)據(jù)庫(kù)字段加解密原理
Mybatis-plus提供企業(yè)高級(jí)特性就有支持?jǐn)?shù)據(jù)加密解密,不過(guò)是收費(fèi)的。。。但是我們可以細(xì)細(xì)探究其原理進(jìn)行功能的自我實(shí)現(xiàn)。
其實(shí)在我們上面推薦的快速開(kāi)發(fā)框架中就已經(jīng)優(yōu)雅整合了數(shù)據(jù)加解密功能了,EncryptTypeHandler:實(shí)現(xiàn)數(shù)據(jù)庫(kù)的字段加密與解密。
默認(rèn)提供了基于base64加密算法Base64EncryptService和AES加密算法AESEncryptService,當(dāng)然業(yè)務(wù)側(cè)也可以自定義加密算法,這需要實(shí)現(xiàn)接口EncryptService,并把實(shí)現(xiàn)類注入到容器中即可。加密功能核心邏輯
@Bean
@ConditionalOnMissingBean(EncryptService.class)
public EncryptService encryptService() {
Algorithm algorithm = encryptProperties.getAlgorithm();
EncryptService encryptService;
switch (algorithm) {
case BASE64:
encryptService = new Base64EncryptService();
break;
case AES:
encryptService = new AESEncryptService();
break;
default:
encryptService = null;
}
return encryptService;
}
接下來(lái)就可以基于加密算法,擴(kuò)展mybatis的typeHandler
對(duì)實(shí)體字段數(shù)據(jù)進(jìn)行加密解密了:EncryptTypeHandler
public class EncryptTypeHandler< T > extends BaseTypeHandler< T > {
@Resource
private EncryptService encryptService;
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, encryptService.encrypt((String)parameter));
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
String columnValue = rs.getString(columnName);
return StrUtil.isBlank(columnValue) ? (T)columnValue : (T)encryptService.decrypt(columnValue);
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String columnValue = rs.getString(columnIndex);
return StrUtil.isBlank(columnValue) ? (T)columnValue : (T)encryptService.decrypt(columnValue);
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String columnValue = cs.getString(columnIndex);
return StrUtil.isBlank(columnValue) ? (T)columnValue : (T)encryptService.decrypt(columnValue);
}
}
2.2 加密與解密示例
首先創(chuàng)建一張user
表:
CREATE TABLE `user` (
`id` bigint(20) NOT NULL,
`name` varchar(255) DEFAULT NULL COMMENT '姓名',
`phone` varchar(255) DEFAULT NULL COMMENT '手機(jī)號(hào)',
`id_card` varchar(255) DEFAULT NULL COMMENT '身份證號(hào)',
`bank_card` varchar(255) DEFAULT NULL COMMENT '銀行卡號(hào)',
`address` varchar(255) DEFAULT NULL COMMENT '住址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
這時(shí)候我們正常插入一條數(shù)據(jù):
@Test
public void test() {
User user = new User();
user.setName("shepherd");
user.setMobile("17812345678");
user.setIdCard("213238199601182111");
user.setBankCard("3222022046741500");
user.setAddress("杭州市余杭區(qū)未來(lái)科技城");
userDAO.insert(user);
}
數(shù)據(jù)庫(kù)存儲(chǔ)查詢結(jié)果如下:
這就是我們平時(shí)不加密存儲(chǔ)查詢的結(jié)果,這里id是通過(guò)分布式id算法自動(dòng)生成的哈。
接下來(lái)我們來(lái)看看實(shí)現(xiàn)對(duì)數(shù)據(jù)的加密,只需要在配置文件配置使用哪一種加密算法和在實(shí)體類的字段屬性加上注解@TableField(typeHandler = EncryptTypeHandler.class)
即可。
這里我們使用aes加密算法:
ptc:
encrypt:
algorithm: aes
實(shí)體類:
@Data
@TableName(autoResultMap = true)
public class User {
private Long id;
private String name;
@TableField(typeHandler = EncryptTypeHandler.class)
private String mobile;
@TableField(typeHandler = EncryptTypeHandler.class)
private String idCard;
@TableField(typeHandler = EncryptTypeHandler.class)
private String bankCard;
@TableField(typeHandler = EncryptTypeHandler.class)
private String address;
}
再次插入數(shù)據(jù),數(shù)據(jù)庫(kù)存儲(chǔ)查詢結(jié)果如下:
然后我們可以測(cè)試對(duì)這條數(shù)據(jù)進(jìn)行查詢:
@Test
public void get() {
User user = userDAO.selectById(1567405175268642818l);
System.out.println(user);
}
結(jié)果如下:
User(id=1567405175268642818, name=shepherd, mobile=17812345678, idCard=213238199601182111, bankCard=3222022046741500, address=杭州市余杭區(qū)未來(lái)科技城)
基于以上完美展示了數(shù)據(jù)加密存儲(chǔ)和解密查詢。
2.3 數(shù)據(jù)加密后怎么進(jìn)行模糊匹配
密碼、手機(jī)號(hào)、詳細(xì)地址、銀行卡號(hào)這些信息對(duì)加解密的要求也不一樣,比如說(shuō)密碼我們需要加密存儲(chǔ),一般使用的都是不可逆的慢hash算法,慢hash算法可以避免暴力破解(典型的用時(shí)間換安全性)。
在檢索時(shí)我們既不需要解密也不需要模糊查找,直接使用密文完全匹配,但是手機(jī)號(hào)就不能這樣做,因?yàn)槭謾C(jī)號(hào)我們要查看原信息,并且對(duì)手機(jī)號(hào)還需要支持模糊查找,因此我們今天就針對(duì)可逆加解密的數(shù)據(jù)支持模糊查詢來(lái)看看有哪些實(shí)現(xiàn)方式。
我們接下來(lái)看看常規(guī)的做法,也是最廣泛使用的方法,此類方法及滿足的數(shù)據(jù)安全性,又對(duì)查詢友好。
在數(shù)據(jù)庫(kù)實(shí)現(xiàn)加密算法函數(shù),在模糊查詢的時(shí)候使用decode(key) like '%partial%
在數(shù)據(jù)庫(kù)中實(shí)現(xiàn)與程序一致的加解密算法,修改模糊查詢條件,使用數(shù)據(jù)庫(kù)加解密函數(shù)先解密再模糊查找,這樣做的優(yōu)點(diǎn)是實(shí)現(xiàn)成本低,開(kāi)發(fā)使用成本低,只需要將以往的模糊查找稍微修改一下就可以實(shí)現(xiàn),但是缺點(diǎn)也很明顯,這樣做無(wú)法利用數(shù)據(jù)庫(kù)的索引來(lái)優(yōu)化查詢,甚至有一些數(shù)據(jù)庫(kù)可能無(wú)法保證與程序?qū)崿F(xiàn)一致的加解密算法,但是對(duì)于常規(guī)的加解密算法都可以保證與應(yīng)用程序一致。如果對(duì)查詢性能要求不是特別高、對(duì)數(shù)據(jù)安全性要求一般,可以使用常見(jiàn)的加解密算法比如說(shuō)AES、DES之類的也是一個(gè)不錯(cuò)的選擇。
對(duì)密文數(shù)據(jù)進(jìn)行分詞組合,將分詞組合的結(jié)果集分別進(jìn)行加密,然后存儲(chǔ)到擴(kuò)展列,查詢時(shí)通過(guò)key like '%partial%'
[先對(duì)字符進(jìn)行固定長(zhǎng)度的分組,將一個(gè)字段拆分為多個(gè),比如說(shuō)根據(jù)4位英文字符(半角),2個(gè)中文字符(全角)為一個(gè)檢索條件,舉個(gè)例子
shepherd
使用4個(gè)字符為一組的加密方式,第一組shep ,第二組heph ,第三組ephe ,第四組pher … 依次類推。
如果需要檢索所有包含檢索條件4個(gè)字符的數(shù)據(jù)比如:pher ,加密字符后通過(guò) key like “%partial%”
查庫(kù)。
分詞加密實(shí)現(xiàn)
public static String splitValueEncrypt(String value, int splitLength) {
//檢查參數(shù)是否合法
if (StringUtils.isBlank(value) && splitLength <= 0) {
return null;
}
String encryptValue = "";
//獲取整個(gè)字符串可以被切割成字符子串的個(gè)數(shù)
int n = (value.length() - splitLength + 1);
//分詞(規(guī)則:分詞長(zhǎng)度根據(jù)【splitLength】且每次分割的開(kāi)始跟結(jié)束下標(biāo)加一)
for (int i = 0; i < n; i++) {
String splitValue = value.substring(i, splitLength++);
encryptValue += encrypt(splitValue);
}
return encryptValue;
}
/**
* 獲取加密值
*
* @param value 加密值
* @return
*/
private static String encrypt(String value) {
// 這里進(jìn)行加密
return null;
}
基于上面分詞加密保存到擴(kuò)展列,同時(shí)要求對(duì)原字段的正刪改查對(duì)需要對(duì)其相應(yīng)的擴(kuò)展列適配,還要注意由于分詞之后導(dǎo)致擴(kuò)展列的長(zhǎng)度可能是原字段幾倍甚至幾十倍,所以務(wù)必在開(kāi)發(fā)之前選擇和合適分詞長(zhǎng)度和加密算法,一旦加密開(kāi)始之后,再更改成本就較高了。像如果手機(jī)號(hào)我們只支持后8位搜索、身份證號(hào)只支持后4位搜索,這樣我們就可以通過(guò)原字段截取后面位數(shù)直接加密存儲(chǔ)到擴(kuò)展列,不需要再分詞。
3.數(shù)據(jù)脫敏
實(shí)際的業(yè)務(wù)開(kāi)發(fā)過(guò)程中,我們經(jīng)常需要對(duì)用戶的隱私數(shù)據(jù)進(jìn)行脫敏處理。所謂脫敏處理其實(shí)就是將數(shù)據(jù)進(jìn)行混淆隱藏,例如用戶手機(jī)信息展示178****5939
,以免泄露個(gè)人隱私信息。
3.1實(shí)現(xiàn)思路
思路比較簡(jiǎn)單:在接口返回?cái)?shù)據(jù)之前按要求對(duì)數(shù)據(jù)進(jìn)行脫敏加工之后再返回前端。
一開(kāi)始打算用@ControllerAdvice去實(shí)現(xiàn),但發(fā)現(xiàn)需要自己去反射類獲取注解,當(dāng)返回對(duì)象比較復(fù)雜,需要遞歸去反射,性能一下子就會(huì)降低,于是換種思路,我想到平時(shí)使用的@JsonFormat,跟我現(xiàn)在的場(chǎng)景很類似,通過(guò)自定義注解跟字段解析器,對(duì)字段進(jìn)行自定義解析。
脫敏字段類型枚舉
public enum MaskEnum {
/**
* 中文名
*/
CHINESE_NAME,
/**
* 身份證號(hào)
*/
ID_CARD,
/**
* 座機(jī)號(hào)
*/
FIXED_PHONE,
/**
* 手機(jī)號(hào)
*/
MOBILE_PHONE,
/**
* 地址
*/
ADDRESS,
/**
* 電子郵件
*/
EMAIL,
/**
* 銀行卡
*/
BANK_CARD
}
脫敏注解類 :用在脫敏字段之上
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotationsInside
@JsonSerialize(using = MaskSerialize.class)
public @interface FieldMask {
/**
* 脫敏類型
* @return
*/
MaskEnum value();
}
脫敏序列化類
public class MaskSerialize extends JsonSerializer< String > implements ContextualSerializer {
/**
* 脫敏類型
*/
private MaskEnum type;
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
switch (this.type) {
case CHINESE_NAME:
{
jsonGenerator.writeString(MaskUtils.chineseName(s));
break;
}
case ID_CARD:
{
jsonGenerator.writeString(MaskUtils.idCardNum(s));
break;
}
case FIXED_PHONE:
{
jsonGenerator.writeString(MaskUtils.fixedPhone(s));
break;
}
case MOBILE_PHONE:
{
jsonGenerator.writeString(MaskUtils.mobilePhone(s));
break;
}
case ADDRESS:
{
jsonGenerator.writeString(MaskUtils.address(s, 4));
break;
}
case EMAIL:
{
jsonGenerator.writeString(MaskUtils.email(s));
break;
}
case BANK_CARD:
{
jsonGenerator.writeString(MaskUtils.bankCard(s));
break;
}
}
}
@Override
public JsonSerializer < ? > createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
// 為空直接跳過(guò)
if (beanProperty == null) {
return serializerProvider.findNullValueSerializer(beanProperty);
}
// 非String類直接跳過(guò)
if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
FieldMask fieldMask = beanProperty.getAnnotation(FieldMask.class);
if (fieldMask == null) {
fieldMask = beanProperty.getContextAnnotation(FieldMask.class);
}
if (fieldMask != null) {
// 如果能得到注解,就將注解的 value 傳入 MaskSerialize
return new MaskSerialize(fieldMask.value());
}
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
public MaskSerialize() {}
public MaskSerialize(final MaskEnum type) {
this.type = type;
}
}