您当前的位置:首页 > 电脑百科 > 程序开发 > 框架

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

时间:2023-05-31 17:18:27  来源:今日头条  作者:Java架构学习指南

1.概述

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

什么是SpringMVC三层架构?

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

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

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

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

Github地址:github.com/plasticene/…

Gitee地址:gitee.com/plasticene3…

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 Boot和SpringMVC框架中有效优雅地实现数据安全性,感兴趣的小伙伴可以自行了解其他加固方式。



Tags:Spring Boot   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
Spring Boot2.0深度实践 核心原理拆解+源码分析
Spring Boot2.0深度实践:核心原理拆解与源码分析一、引言Spring Boot是一个基于Java的轻量级框架,它简化了Spring应用程序的创建过程,使得开发者能够快速搭建一个可运行的应用...【详细内容】
2024-01-15  Search: Spring Boot  点击:(95)  评论:(0)  加入收藏
Spring Boot 3.0是什么?
Spring Boot 3.0是一款基于Java的开源框架,用于简化Spring应用程序的构建和开发过程。与之前的版本相比,Spring Boot 3.0在多个方面进行了改进和增强,使其更加易用、高效和灵活...【详细内容】
2024-01-11  Search: Spring Boot  点击:(133)  评论:(0)  加入收藏
GraalVM与Spring Boot 3.0:加速应用性能的完美融合
在2023年,SpringBoot3.0的发布标志着Spring框架对GraalVM的全面支持,这一支持是对Spring技术栈的重要补充。GraalVM是一个高性能的多语言虚拟机,它提供了Ahead-of-Time(AOT)编...【详细内容】
2024-01-11  Search: Spring Boot  点击:(124)  评论:(0)  加入收藏
Spring Boot虚拟线程的性能还不如Webflux?
早上看到一篇关于Spring Boot虚拟线程和Webflux性能对比的文章,觉得还不错。内容较长,抓重点给大家介绍一下这篇文章的核心内容,方便大家快速阅读。测试场景作者采用了一个尽可...【详细内容】
2024-01-10  Search: Spring Boot  点击:(115)  评论:(0)  加入收藏
Spring Boot Starter的原理
Spring Boot Starter 是 Spring Boot 框架的特性之一,用于简化应用程序的依赖管理和配置。1. 概述: - Spring Boot Starter 是一种包含了一组特定功能和依赖关系的依赖项,旨在...【详细内容】
2024-01-05  Search: Spring Boot  点击:(94)  评论:(0)  加入收藏
Spring Boot 统一响应体处理器详解
在Spring Boot应用中,统一处理响应体是一项非常重要的任务,它可以让我们更方便地统一规范API的返回格式。今天,我们将深入探讨一个优雅的解决方案&mdash;&mdash;使用ResultHand...【详细内容】
2023-11-30  Search: Spring Boot  点击:(150)  评论:(0)  加入收藏
Spring Boot 调优内嵌 Tomcat 的三种方法
在 Spring Boot 中优化 Apache Tomcat 有三种方式,以便实现更好的性能和资源利用率。 线程池(连接器和执行器)设置 使用 NIO 或 APR 连接器 JVM优化线程池设置在 Spring Boot...【详细内容】
2023-11-23  Search: Spring Boot  点击:(227)  评论:(0)  加入收藏
一文搞懂Spring Boot控制器的关键要点
Spring Boot 应用程序中的控制器扮演着重要角色,负责处理传入的 HTTP 请求并确定应发送的适当响应。本文深入介绍 Spring Boot 中的控制器,包括如何创建控制器、处理各种类型...【详细内容】
2023-11-20  Search: Spring Boot  点击:(151)  评论:(0)  加入收藏
Spring Boot中实现订单30分钟自动取消的策略思路及源代码
方式一:使用定时任务 首先,创建一个定时任务,比如每30分钟执行一次检查订单是否需要取消的逻辑。 在订单生成的时候,保存一条记录到数据库,标记订单的状态为"待处理"。 在定时任...【详细内容】
2023-11-20  Search: Spring Boot  点击:(218)  评论:(0)  加入收藏
Spring Boot + Vue3 前后端分离 实战wiki知识库系统
下栽の地止:https://www.itwangzi.cn/2508.html Spring Boot + Vue3 前后端分离 实战wiki知识库系统在当今的Web应用开发中,前后端分离已经成为了一种主流的开发模式。Spring...【详细内容】
2023-11-18  Search: Spring Boot  点击:(144)  评论:(0)  加入收藏
▌简易百科推荐
Web Components实践:如何搭建一个框架无关的AI组件库
一、让人又爱又恨的Web ComponentsWeb Components是一种用于构建可重用的Web元素的技术。它允许开发者创建自定义的HTML元素,这些元素可以在不同的Web应用程序中重复使用,并且...【详细内容】
2024-04-03  京东云开发者    Tags:Web Components   点击:(8)  评论:(0)  加入收藏
Kubernetes 集群 CPU 使用率只有 13% :这下大家该知道如何省钱了
作者 | THE STACK译者 | 刘雅梦策划 | Tina根据 CAST AI 对 4000 个 Kubernetes 集群的分析,Kubernetes 集群通常只使用 13% 的 CPU 和平均 20% 的内存,这表明存在严重的过度...【详细内容】
2024-03-08  InfoQ    Tags:Kubernetes   点击:(12)  评论:(0)  加入收藏
Spring Security:保障应用安全的利器
SpringSecurity作为一个功能强大的安全框架,为Java应用程序提供了全面的安全保障,包括认证、授权、防护和集成等方面。本文将介绍SpringSecurity在这些方面的特性和优势,以及它...【详细内容】
2024-02-27  风舞凋零叶    Tags:Spring Security   点击:(54)  评论:(0)  加入收藏
五大跨平台桌面应用开发框架:Electron、Tauri、Flutter等
一、什么是跨平台桌面应用开发框架跨平台桌面应用开发框架是一种工具或框架,它允许开发者使用一种统一的代码库或语言来创建能够在多个操作系统上运行的桌面应用程序。传统上...【详细内容】
2024-02-26  贝格前端工场    Tags:框架   点击:(47)  评论:(0)  加入收藏
Spring Security权限控制框架使用指南
在常用的后台管理系统中,通常都会有访问权限控制的需求,用于限制不同人员对于接口的访问能力,如果用户不具备指定的权限,则不能访问某些接口。本文将用 waynboot-mall 项目举例...【详细内容】
2024-02-19  程序员wayn  微信公众号  Tags:Spring   点击:(39)  评论:(0)  加入收藏
开发者的Kubernetes懒人指南
你可以将本文作为开发者快速了解 Kubernetes 的指南。从基础知识到更高级的主题,如 Helm Chart,以及所有这些如何影响你作为开发者。译自Kubernetes for Lazy Developers。作...【详细内容】
2024-02-01  云云众生s  微信公众号  Tags:Kubernetes   点击:(50)  评论:(0)  加入收藏
链世界:一种简单而有效的人类行为Agent模型强化学习框架
强化学习是一种机器学习的方法,它通过让智能体(Agent)与环境交互,从而学习如何选择最优的行动来最大化累积的奖励。强化学习在许多领域都有广泛的应用,例如游戏、机器人、自动驾...【详细内容】
2024-01-30  大噬元兽  微信公众号  Tags:框架   点击:(68)  评论:(0)  加入收藏
Spring实现Kafka重试Topic,真的太香了
概述Kafka的强大功能之一是每个分区都有一个Consumer的偏移值。该偏移值是消费者将读取的下一条消息的值。可以自动或手动增加该值。如果我们由于错误而无法处理消息并想重...【详细内容】
2024-01-26  HELLO程序员  微信公众号  Tags:Spring   点击:(86)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19   Java中文社群  微信公众号  Tags:SpringBoot   点击:(86)  评论:(0)  加入收藏
花 15 分钟把 Express.js 搞明白,全栈没有那么难
Express 是老牌的 Node.js 框架,以简单和轻量著称,几行代码就可以启动一个 HTTP 服务器。市面上主流的 Node.js 框架,如 Egg.js、Nest.js 等都与 Express 息息相关。Express 框...【详细内容】
2024-01-16  程序员成功  微信公众号  Tags:Express.js   点击:(88)  评论:(0)  加入收藏
站内最新
站内热门
站内头条