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è)面展示之前進(jìn)行數(shù)據(jù)處理的,所以只是在SpringMVC三層架構(gòu)中的一層中進(jìn)行安全加固,還不是很穩(wěn)固,接下來(lái)今天我們就再來(lái)講講在SpringMVC三層架構(gòu)另一層中如何進(jìn)行數(shù)據(jù)安全加固,在進(jìn)入今天主題之前先來(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這樣的框架,(ViewWeb)表示層調(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)行主題的宣講與淺析。
項(xiàng)目推薦:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企業(yè)級(jí)系統(tǒng)架構(gòu)底層框架封裝,解決業(yè)務(wù)開(kāi)發(fā)時(shí)常見(jiàn)的非功能性需求,防止重復(fù)造輪子,方便業(yè)務(wù)快速開(kāi)發(fā)和企業(yè)技術(shù)棧框架統(tǒng)一管理。引入組件化的思想實(shí)現(xiàn)高內(nèi)聚低耦合并且高度可配置化,做到可插拔。嚴(yán)格控制包依賴和統(tǒng)一版本管理,做到最少化依賴。注重代碼規(guī)范和注釋,非常適合個(gè)人學(xué)習(xí)和企業(yè)使用
Github地址:github.com/plasticene/…
Gitee地址:gitee.com/plasticene3…
2.數(shù)據(jù)加密存儲(chǔ)
我們之前總結(jié)的是在接口層進(jìn)行數(shù)據(jù)加解密傳輸,也強(qiáng)調(diào)過(guò)這種方式保證不了數(shù)據(jù)的絕對(duì)安全,只是有效提高接口數(shù)據(jù)安全性,抬高數(shù)據(jù)被抓取的門(mén)檻而已。所以接下來(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é)果如下:
id |
name |
mobile |
id_card |
bank_card |
address |
1567402046481436673 |
shepherd |
17812345678 |
213238199601182111 |
3222022046741500 |
杭州市余杭區(qū)未來(lái)科技城 |
這就是我們平時(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é)果如下:
id |
name |
mobile |
id_card |
bank_card |
address |
1567405175268642818 |
shepherd |
9MgWngwLcd/vbYYYpG9pGQ== |
97vlZQahK+y548ofQbXlW9JUwuzuj3xCkNF/is1KLa4= |
2oQv5+y4+rVyN23IzudtOz+Zd7Aj1Bv2toBzmnwTXxo= |
0Wj7qqLl6jWkBu+TcxuwGYcdIjv+zIJHDM7d1dU/c8D2jc2wLp+zVvpSwBKWjX44 |
然后我們可以測(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;
}
}
3.2使用示例
在發(fā)送短信記錄的接口上對(duì)手機(jī)號(hào)進(jìn)行脫敏:
@FieldMask(MaskEnum.MOBILE_PHONE)
private String mobile;
調(diào)用接口返回?cái)?shù)據(jù)如下:
{
"code": 200,
"msg": "OK",
"data": {
"list": [
{
"id": 1565599123774607362,
"signId": 8389008488923136,
"templateId": 8445337328943104,
"templateType": 1,
"content": "可愛(ài)的${name},博客文章已于${submitTime}上傳更新,請(qǐng)抽空瀏覽。",
"channelType": 0,
"mobile": "178****5939",
"sendStatus": 0,
"receiveStatus": 0
}
],
"total": 19,
"pages": 19
}
}
4.總結(jié)
基于上面內(nèi)容我們總結(jié)如何在數(shù)據(jù)存儲(chǔ)層進(jìn)行數(shù)據(jù)安全加固來(lái)達(dá)到系統(tǒng)的更安全性,可以這么說(shuō)沒(méi)有最安全的系統(tǒng)只有更安全的系統(tǒng)。所以我們?cè)陂_(kāi)發(fā)歷程中都會(huì)窮極一生去加固系統(tǒng)安全性能。當(dāng)然了,加強(qiáng)系統(tǒng)安全性的方式還有很多種,我們最近只是圍繞基于Spring Boot和SpringMVC框架中有效優(yōu)雅地實(shí)現(xiàn)數(shù)據(jù)安全性,感興趣的小伙伴可以自行了解其他加固方式。