Spring Boot如何优雅实现数据加密存储、模糊匹配和脱敏

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
数据安全中心,免费版
简介: 我们也都知道在日常开发系统过程中,数据安全是非常重要的。特别是在当今互联网时代,个人隐私安全极其重要,一旦个人用户数据遭到攻击泄露,将会造成灾难级的事故问题。所有之前我们基于接口层进行数据安全处理是远远不够的,今天我们就来谈谈如何Model层(数据访问层)怎样做到优雅数据加密存储、模糊匹配及其脱敏展示,本文的主题:**数据加密存储、模糊匹配和脱敏展示**。

1.概述

近来我们都在围绕着使用Spring Boot开发业务系统时如何保证数据安全性这个主题展开总结,当下大部分的B/S架构的系统也都是基于Spring Boot + SpringMVC三层架构开发的,之前我们总结了在接口层面如何加固数据安全性:Spring Boot如何优雅提高接口数据安全性,可以认为是在SpringMVC的三层架构中的controller层(逻辑控制层)对接口数据进行安全处理操作,更直接点说就是在接口请求参数传入进行逻辑处理或者响应参数输出到页面展示之前进行数据处理的,所以只是在SpringMVC三层架构中的一层中进行安全加固,还不是很稳固,接下来今天我们就再来讲讲在SpringMVC三层架构另一层中如何进行数据安全加固,在进入今天主题之前先来看看什么是SpringMVC架构?

什么是SpringMVC三层架构?

SpringMVC的工程结构一般来说分为三层,自下而上是Modle层(模型,数据访问层)、Cotroller层(控制,逻辑控制层)、View层(视图,页面显示层),其中Modle层分为两层:dao层、service层,MVC架构分层的主要作用是解耦。采用分层架构的好处,普遍接受的是系统分层有利于系统的维护,系统的扩展。就是增强系统的可维护性和可扩展性。对于Spring这样的框架,(View\Web)表示层调用控制层(Controller),控制层调用业务层(Service),业务层调用数据访问层(Dao)
可以这么说,现在90%以上的业务系统都是基于该三层架构模式开发的,这种架构模式也有人说是设计模式中一种,可见其重要性不言而喻,所以我们需重视。

我们也都知道在日常开发系统过程中,数据安全是非常重要的。特别是在当今互联网时代,个人隐私安全极其重要,一旦个人用户数据遭到攻击泄露,将会造成灾难级的事故问题。所有之前我们基于接口层进行数据安全处理是远远不够的,今天我们就来谈谈如何Model层(数据访问层)怎样做到优雅数据加密存储、模糊匹配及其脱敏展示,本文的主题:数据加密存储、模糊匹配和脱敏展示

银行系统对数据安全性的要求在业务系统中是首屈一指的,所以今天我们就以常见的个人银行账户数据:密码、手机号、详细地址、银行卡号等信息字段为例,进行主题的宣讲与浅析。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

2.数据加密存储

我们之前总结的是在接口层进行数据加解密传输,也强调过这种方式保证不了数据的绝对安全,只是有效提高接口数据安全性,抬高数据被抓取的门槛而已。所以接下来我们就来讲述一下如何在数据的源头存储层保障其安全。我们都知道一些核心私密字段,比如说密码,手机号等在数据库层存储就不能明文存储,必须加密存储保证即使数据库泄露了也不会轻易曝光数据。

2.1 优雅实现数据库字段加解密原理

Mybatis-plus提供企业高级特性就有支持[数据加密解密],不过是收费的。。。但是我们可以细细探究其原理进行功能的自我实现。

其实在我们上面推荐的快速开发框架中就已经优雅整合了数据加解密功能了,EncryptTypeHandler:实现数据库的字段加密与解密。

默认提供了基于base64加密算法Base64EncryptService和AES加密算法AESEncryptService,当然业务侧也可以自定义加密算法,这需要实现接口EncryptService,并把实现类注入到容器中即可。加密功能核心逻辑

@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;
}

接下来就可以基于加密算法,扩展mybatis的typeHandler对实体字段数据进行加密解密了: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 加密与解密示例

首先创建一张user表:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL,
  `name` varchar(255) DEFAULT NULL COMMENT '姓名',
  `phone` varchar(255) DEFAULT NULL COMMENT '手机号',
  `id_card` varchar(255) DEFAULT NULL COMMENT '身份证号',
  `bank_card` varchar(255) DEFAULT NULL COMMENT '银行卡号',
  `address` varchar(255) DEFAULT NULL COMMENT '住址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这时候我们正常插入一条数据:

    @Test
    public void test() {
        User user = new User();
        user.setName("shepherd");
        user.setMobile("17812345678");
        user.setIdCard("213238199601182111");
        user.setBankCard("3222022046741500");
        user.setAddress("杭州市余杭区未来科技城");
        userDAO.insert(user);
    }

数据库存储查询结果如下:

id name mobile id_card bank_card address
1567402046481436673 shepherd 17812345678 213238199601182111 3222022046741500 杭州市余杭区未来科技城

这就是我们平时不加密存储查询的结果,这里id是通过分布式id算法自动生成的哈。

接下来我们来看看实现对数据的加密,只需要在配置文件配置使用哪一种加密算法和在实体类的字段属性加上注解@TableField(typeHandler = EncryptTypeHandler.class)即可。

这里我们使用aes加密算法:

ptc:
  encrypt:
    algorithm: aes

实体类:

@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;
}

再次插入数据,数据库存储查询结果如下:

id name mobile id_card bank_card address
1567405175268642818 shepherd 9MgWngwLcd/vbYYYpG9pGQ== 97vlZQahK+y548ofQbXlW9JUwuzuj3xCkNF/is1KLa4= 2oQv5+y4+rVyN23IzudtOz+Zd7Aj1Bv2toBzmnwTXxo= 0Wj7qqLl6jWkBu+TcxuwGYcdIjv+zIJHDM7d1dU/c8D2jc2wLp+zVvpSwBKWjX44

然后我们可以测试对这条数据进行查询:

    @Test
    public void get() {
        User user = userDAO.selectById(1567405175268642818l);
        System.out.println(user);
    }

结果如下:

User(id=1567405175268642818, name=shepherd, mobile=17812345678, idCard=213238199601182111, bankCard=3222022046741500, address=杭州市余杭区未来科技城)

基于以上完美展示了数据加密存储和解密查询。

2.3 数据加密后怎么进行模糊匹配

密码、手机号、详细地址、银行卡号这些信息对加解密的要求也不一样,比如说密码我们需要加密存储,一般使用的都是不可逆的慢hash算法,慢hash算法可以避免暴力破解(典型的用时间换安全性)。

在检索时我们既不需要解密也不需要模糊查找,直接使用密文完全匹配,但是手机号就不能这样做,因为手机号我们要查看原信息,并且对手机号还需要支持模糊查找,因此我们今天就针对可逆加解密的数据支持模糊查询来看看有哪些实现方式。

我们接下来看看常规的做法,也是最广泛使用的方法,此类方法及满足的数据安全性,又对查询友好。

  • 在数据库实现加密算法函数,在模糊查询的时候使用decode(key) like '%partial%

    在数据库中实现与程序一致的加解密算法,修改模糊查询条件,使用数据库加解密函数先解密再模糊查找,这样做的优点是实现成本低,开发使用成本低,只需要将以往的模糊查找稍微修改一下就可以实现,但是缺点也很明显,这样做无法利用数据库的索引来优化查询,甚至有一些数据库可能无法保证与程序实现一致的加解密算法,但是对于常规的加解密算法都可以保证与应用程序一致。如果对查询性能要求不是特别高、对数据安全性要求一般,可以使用常见的加解密算法比如说AES、DES之类的也是一个不错的选择。

  • 对密文数据进行分词组合,将分词组合的结果集分别进行加密,然后存储到扩展列,查询时通过key like '%partial%'[先对字符进行固定长度的分组,将一个字段拆分为多个,比如说根据4位英文字符(半角),2个中文字符(全角)为一个检索条件,举个例子

    shepherd使用4个字符为一组的加密方式,第一组shep ,第二组heph ,第三组ephe ,第四组pher … 依次类推。

    如果需要检索所有包含检索条件4个字符的数据比如:pher ,加密字符后通过 key like “%partial%” 查库。

    分词加密实现

        public static String splitValueEncrypt(String value, int splitLength) {
            //检查参数是否合法
            if (StringUtils.isBlank(value) && splitLength <= 0) {
                return null;
            }
            String encryptValue = "";
    
            //获取整个字符串可以被切割成字符子串的个数
            int n = (value.length() - splitLength + 1);
    
            //分词(规则:分词长度根据【splitLength】且每次分割的开始跟结束下标加一)
            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) {
            // 这里进行加密
            return  null;
        }

    基于上面分词加密保存到扩展列,同时要求对原字段的正删改查对需要对其相应的扩展列适配,还要注意由于分词之后导致扩展列的长度可能是原字段几倍甚至几十倍,所以务必在开发之前选择和合适分词长度和加密算法,一旦加密开始之后,再更改成本就较高了。像如果手机号我们只支持后8位搜索、身份证号只支持后4位搜索,这样我们就可以通过原字段截取后面位数直接加密存储到扩展列,不需要再分词。

    3.数据脱敏

    实际的业务开发过程中,我们经常需要对用户的隐私数据进行脱敏处理。所谓脱敏处理其实就是将数据进行混淆隐藏,例如用户手机信息展示178****5939,以免泄露个人隐私信息。

    3.1实现思路

    思路比较简单:在接口返回数据之前按要求对数据进行脱敏加工之后再返回前端。

    一开始打算用@ControllerAdvice去实现,但发现需要自己去反射类获取注解,当返回对象比较复杂,需要递归去反射,性能一下子就会降低,于是换种思路,我想到平时使用的@JsonFormat,跟我现在的场景很类似,通过自定义注解跟字段解析器,对字段进行自定义解析。

    脱敏字段类型枚举

    public enum MaskEnum {
        /**
         * 中文名
         */
        CHINESE_NAME,
        /**
         * 身份证号
         */
        ID_CARD,
        /**
         * 座机号
         */
        FIXED_PHONE,
        /**
         * 手机号
         */
        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 {
            // 为空直接跳过
            if (beanProperty == null) {
                return serializerProvider.findNullValueSerializer(beanProperty);
            }
            // 非String类直接跳过
            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使用示例

    在发送短信记录的接口上对手机号进行脱敏:

        @FieldMask(MaskEnum.MOBILE_PHONE)
        private String mobile;

    调用接口返回数据如下:

    {
      "code": 200,
      "msg": "OK",
      "data": {
        "list": [
          {
            "id": 1565599123774607362,
            "signId": 8389008488923136,
            "templateId": 8445337328943104,
            "templateType": 1,
            "content": "可爱的${name},博客文章已于${submitTime}上传更新,请抽空浏览。",
            "channelType": 0,
            "mobile": "178****5939",
            "sendStatus": 0,
            "receiveStatus": 0
          }
        ],
        "total": 19,
        "pages": 19
      }
    }

    4.总结

    基于上面内容我们总结如何在数据存储层进行数据安全加固来达到系统的更安全性,可以这么说没有最安全的系统只有更安全的系统。所以我们在开发历程中都会穷极一生去加固系统安全性能。当然了,加强系统安全性的方式还有很多种,我们最近只是围绕基于Spring BootSpringMVC框架中有效优雅地实现数据安全性,感兴趣的小伙伴可以自行了解其他加固方式。(近来复阳了,状态不是很好,效率不高所以有点拖拉更新啦)

目录
相关文章
|
3月前
|
存储 NoSQL 数据库
认证服务---整合短信验证码,用户注册和登录 ,密码采用MD5加密存储 【二】
这篇文章讲述了在分布式微服务系统中添加用户注册和登录功能的过程,重点介绍了用户注册时通过远程服务调用第三方服务获取短信验证码、使用Redis进行验证码校验、对密码进行MD5加密后存储到数据库,以及用户登录时的远程服务调用和密码匹配校验的实现细节。
认证服务---整合短信验证码,用户注册和登录 ,密码采用MD5加密存储 【二】
|
3月前
|
存储 缓存 NoSQL
【Azure Redis 缓存】关于Azure Cache for Redis 服务在传输和存储键值对(Key/Value)的加密问题
【Azure Redis 缓存】关于Azure Cache for Redis 服务在传输和存储键值对(Key/Value)的加密问题
|
8天前
|
存储 安全 Java
|
8天前
|
存储 算法 安全
SpringBoot 接口加密解密实现
【10月更文挑战第18天】
|
19天前
|
NoSQL Java Redis
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
这篇文章介绍了如何使用Spring Boot整合Apache Shiro框架进行后端开发,包括认证和授权流程,并使用Redis存储Token以及MD5加密用户密码。
21 0
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
|
2月前
|
存储 安全 数据库
Uno Platform 安全数据存储秘籍大公开!加密、存储、读取全攻略,让你的数据固若金汤!
在软件开发中,安全的数据存储至关重要。本文介绍如何在跨平台开发框架 Uno Platform 中实现安全数据存储,包括选择合适的数据存储方式(如本地文件或 SQLite 数据库)和使用 Bouncy Castle 加密库对数据进行 AES 加密。通过示例代码展示了数据的加密、存储及解密过程,帮助开发者保护用户敏感信息,防止数据泄露。
42 3
|
25天前
|
存储 安全 数据库
Uno Platform 安全数据存储秘籍大公开!加密、存储、读取全攻略,让你的数据固若金汤!
在 Uno Platform 中实现安全的数据存储需要选择合适的数据存储方式,并对存储的数据进行加密。通过加密数据,可以保护用户的敏感信息,防止数据泄露。同时,在读取和使用数据时,需要进行解密操作,确保数据的可用性。希望本文对你在 Uno Platform 中实现安全的数据存储有所帮助。
27 0
|
3月前
|
存储 算法 Java
在Java中使用MD5对用户输入密码进行加密存储、同时登录验证。
这篇文章详细介绍了在Java项目中如何使用MD5算法对用户密码进行加密存储和登录验证,包括加入依赖、编写MD5工具类、注册时的密码加密和登录时的密码验证等步骤,并通过示例代码和数据库存储信息展示了测试效果。
在Java中使用MD5对用户输入密码进行加密存储、同时登录验证。
|
3月前
|
安全 Java Shell
"SpringBoot防窥秘籍大公开!ProGuard混淆+xjar加密,让你的代码穿上隐形斗篷,黑客也无奈!"
【8月更文挑战第11天】开发SpringBoot应用时,保护代码免遭反编译至关重要。本文介绍如何运用ProGuard和xjar强化安全性。ProGuard能混淆代码,去除未使用的部分,压缩字节码,使反编译困难。需配置ProGuard规则文件并处理jar包。xjar则进一步加密jar包内容,即使被解压也无法直接读取。结合使用这两种工具可显著提高代码安全性,有效保护商业机密及知识产权。
292 3
|
3月前
|
存储 Java API