• SpringBoot+ShardingSphere彻底解决生产环境数据库字段加解密问题



    前言

      互联网行业公司,对于数据库的敏感字段是一定要进行加密的,方案有很多,最直接的比如写个加解密的工具类,然后在每个业务逻辑中手动处理,在稍微有点规模的项目中这种方式显然是不现实的,不仅工作量大而且后期很难维护。

      目前mybatis-plus已经提供了非常好的加解密方案,居士也试过效果很好,但很多互联网公司不一定会引入mybatis-plus作为数据层工具,反而就喜欢使用mybatis,甚至有不少使用SpringDataJPA,那么就没有必要为了加解密专门引入mybatis-plus。

      那有什么更合适的方案呢?答案是肯定的,shardingsphere就提供了方案,为什么选择它呢,因为互联网公司大概率会考虑分库分表,目前最佳的分库分表方案实际上也就是shardingsphere了,既然如此,直接用它的数据库加解密方案就不需要再额外引入第三方工具了。


    使用

    1、案例准备

    技术体系如下,其中引入MybatisPlus是为了方便案例更快成型,ShardingSphere不建议使用5.x版本,因为版本差异较大一直都是大家对其诟病的原因,甚至小版本都有不少差异,4.x版本相较来说资料更多。

    技术 版本
    SpringBoot 2.6.3
    MybatisPlus 3.5.1
    ShardingSphere 4.1.1

    2、引入依赖
    <dependencies>
        <!-- spring web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- sharding-jdbc -->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>4.1.1</version>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!-- 代码生成器 mybatisPlus自带的生成器 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!-- freemarker模板生成器 引入代码生成器需要 -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>
        <!-- swagger 因为mybatisPlus代码生成器会自带swagger的注解 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>
        <!-- 启动后加载配置文件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- lombok 简化实体类管理工具-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- fastjson 解析json用到,也可以换成自己喜欢用的 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.79</version>
        </dependency>
        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    

    3、yml配置
    server:
      port: 8888
    
    # 数据源
    spring:
      # shardingsphere配置
      shardingsphere:
        props:
          sql:
            show: false
          query:
            with:
              cipher:
                column: true # 查询是否使用密文列
        datasource:
          name: master
          master:
            type: com.zaxxer.hikari.HikariDataSource
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: jdbc:mysql://localhost:3306/encrypt_demo?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&useSSL=false
            username: root
            password: 123456
            initial-size: 20
            max-active: 200
            min-idle: 10
            max-wait: 60000
            pool-prepared-statements: true
            max-pool-prepared-statement-per-connection-size: 20
            time-between-eviction-runs-millis: 60000
            min-evictable-idle-time-millis: 300000
            validation-query: SELECT 1
            test-while-idle: true
            test-on-borrow: false
            test-on-return: false
            filter:
              stat:
                log-slow-sql: true
                slow-sql-millis: 1000
                merge-sql: true
              wall:
                config:
                  multi-statement-allow: true
        # 加密配置
        encrypt:
          encryptors:
            my_encryptor:
              type: mySharding # 声明加密处理器的类型,自定义。
              props:
                aes.key.value: fly13579@# # 加密处理器创建密钥会用到,10个数字英文字母组合,自定义。
          # 需要加密哪张表中的哪些字段,每个字段使用哪个加密处理器,这里的my_encryptor就是上面配置的处理器名称。
          tables:
            tb_order:
              columns:
                id_card:
                  cipherColumn: id_card
                  encryptor: my_encryptor
                name:
                  cipherColumn: name
                  encryptor: my_encryptor
    

    4、自定义加解密处理器

    ShardingSphere有自己的加解密处理器,可以直接使用,生产环境中还是偏向于自定义处理器,因为更安全,不容易被暴力破解。

    1)、增加SPI指向

    在resources目录下新建META-INF/services目录,编写文件指向自定义的处理器。

    文件名称:org.apache.shardingsphere.encrypt.strategy.spi.Encryptor

    PS:4.1.1版本和4.0.0版本的名称不同,因为许多类名和包名都更改了。
    自定义加解密处理器.jpg

    2)、编写自定义加解密处理器
    package com.example.encrypt.config;
    
    import com.google.common.base.Preconditions;
    import org.apache.commons.codec.binary.Base64;
    import org.apache.commons.codec.binary.StringUtils;
    import org.apache.commons.codec.digest.DigestUtils;
    import org.apache.shardingsphere.encrypt.strategy.impl.AESEncryptor;
    import org.apache.shardingsphere.encrypt.strategy.spi.Encryptor;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.context.annotation.Configuration;
    
    import javax.crypto.Cipher;
    import javax.crypto.NoSuchPaddingException;
    import javax.crypto.spec.SecretKeySpec;
    import java.security.InvalidKeyException;
    import java.security.NoSuchAlgorithmException;
    import java.util.Properties;
    
    /**
     * <p>
     *  Shardingsphere加密处理器
     * </p>
     *
     * @author 福隆苑居士,公众号:【Java分享客栈】
     * @since 2022-02-20
     */
    @Configuration
    public class MyShardingEncryptor implements Encryptor {
    
        private final Logger log = LoggerFactory.getLogger(MyShardingEncryptor.class);
    
        // AES KEY
        private static final String AES_KEY = "aes.key.value";
    
        private Properties properties = new Properties();
    
        public MyShardingEncryptor(){
    
        }
    
        @Override
        public void init() {
    
        }
    
        @Override
        public String encrypt(Object plaintext) {
            try {
                byte[] result = this.getCipher(1).doFinal(StringUtils.getBytesUtf8(String.valueOf(plaintext)));
                log.debug("[MyShardingEncryptor]>>>> 加密: {}", Base64.encodeBase64String(result));
                return Base64.encodeBase64String(result);
            } catch (Exception ex) {
                log.error("[MyShardingEncryptor]>>>> 加密异常:", ex);
            }
            return null;
    
        }
    
        @Override
        public Object decrypt(String ciphertext) {
            try {
                if (null == ciphertext) {
                    return null;
                } else {
                    byte[] result = this.getCipher(2).doFinal(Base64.decodeBase64(ciphertext));
                    log.debug("[MyShardingEncryptor]>>>> 解密: {}", new String(result));
                    return new String(result);
                }
            } catch (Exception ex) {
                log.error("[MyShardingEncryptor]>>>> 解密异常:", ex);
            }
            return null;
        }
    
        @Override
        public String getType() {
            return "mySharding"; // 和yml配置一致
        }
    
        @Override
        public Properties getProperties() {
            return this.properties;
        }
    
        @Override
        public void setProperties(Properties properties) {
            this.properties = properties;
        }
    
       /**
        * 加解密算法
        * @param decryptMode 1-加密,2-解密,还有其他类型可以点进去看源码。
        */
        private Cipher getCipher(int decryptMode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {
            Preconditions.checkArgument(this.properties.containsKey("aes.key.value"), "No available secret key for `%s`.",
                AESEncryptor.class.getName());
            Cipher result = Cipher.getInstance("AES");
            result.init(decryptMode, new SecretKeySpec(this.createSecretKey(), "AES"));
            return result;
        }
    
       /**
        * 创建密钥,规则根据自己需要定义。
        * -- PS: 生产环境规范要求不能打印出密钥相关日志,以免发生意外泄露情况。
        */
       private byte[] createSecretKey() {
          // yml中配置的原始密钥
            String oldKey = this.properties.get("aes.key.value").toString();
            Preconditions.checkArgument(null != oldKey, String.format("%s can not be null.", "aes.key.value"));
            /*
             * 将原始密钥和自定义的盐一起再次加密生成新的密钥返回.
             * 注意,因为我们用的AES加解密方式最终密钥必须16位,否则AES会报错,
             * 而application.yml中配置的aes.key.value是10位字符组合,所以这里才substring(0,5),否则最终没有返回16位会抛AES异常,可以自己试验下。
             */
          String secretKey = DigestUtils.sha1Hex(oldKey + AES_KEY).toUpperCase().substring(0, 5) + "!" + oldKey;
          // 密钥打印在上线前一定要删掉,避免泄露引起安全事故。
          log.debug("[MyShardingEncryptor]>>>> 密钥: {}", secretKey);
            return secretKey.getBytes();
        }
    
    }
    

    5、编写测试接口
    package com.example.encrypt.controller;
    
    
    import com.alibaba.fastjson.JSONObject;
    import com.example.encrypt.entity.Order;
    import com.example.encrypt.enums.ResponseCodeEnum;
    import com.example.encrypt.service.IOrderService;
    import com.example.encrypt.util.ResultEntity;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.ResponseEntity;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.*;
    
    import java.time.LocalDateTime;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    
    /**
     * <p>
     *  前端控制器
     * </p>
     *
     * @author 福隆苑居士,公众号:【Java分享客栈】
     * @since 2022-02-21
     */
    @RestController
    @RequestMapping("/api/order")
    @Slf4j
    public class OrderController {
    
       private final IOrderService orderService;
    
       public OrderController(IOrderService orderService) {
          this.orderService = orderService;
       }
    
       /**
        * 查询订单
        */
       @GetMapping("/list")
       public ResponseEntity<List<Order>> list() {
          return ResponseEntity.ok().body(orderService.list());
       }
    
       /**
        * 插入订单
        */
       @PostMapping("/save")
       public ResponseEntity<List<Order>> save(@RequestBody Order order) {
          // 这里只是简单的演示,正式代码记得不要直接传实体对象,要传递VO对象进行转换,并且要做参数校验。
          log.debug("[插入订单]>>>> order={}", JSONObject.toJSONString(order));
          order.setCreatedAt(new Date());
          order.setUpdatedAt(new Date());
          boolean ret = orderService.save(order);
          return ret ? ResponseEntity.ok().body(orderService.list())
                : ResponseEntity.badRequest().body(new ArrayList<>());
       }
    }
    

    6、效果

    插入订单

    插入订单.jpg


    我们yml中配置的对姓名和身份证号加密,看下数据库这笔订单记录,发现已经自动加密了。

    数据库表字段加密.jpg


    再试下查询订单,看是否会对数据库加密字段进行解密后返回结果,发现解密也没问题。

    查询订单.jpg


    7、密钥在数据库的用法

    这里专门给大家说明一下密钥如何在数据库中通过SQL直接对字段值加解密,因为有很多公司会使用堡垒机或云桌面来访问生产环境数据库,这个时候排查线上问题时,往往要知道加密字段是什么。

    1)、拿到密钥

    PS:切记,这个密钥在上线前保留一次即可,打印密钥的日志一定要删掉,可以避免生产环境泄露密钥引起的事故,一般正规点的公司都会有要求。
    密钥打印的地方在自定义的加解密处理器中,可以debug打印出来。

    console日志.jpg

    2)、数据库中使用

    语句如下,保存下来,以后使用时复制出来即可。

    # 加密 
    SELECT to_base64(AES_ENCRYPT('要加密的值','密钥')) 
    # 解密 
    SELECT AES_DECRYPT(FROM_BASE64('要解密的值'),'密钥') 
    # 中文解密,防乱码。 
    select CAST(AES_DECRYPT(FROM_BASE64('要解密的中文值'),'密钥') as char)
    

    总结

      ShardingSphere的加解密本身步骤简单,但这个工具其实不熟悉或者没用过的人会踩很多坑,包括版本差异、未解决的缺陷等等,可是它的优势远大于弊端,所以才会被非常多的公司所接受,也是学习分库分表的必修课。

      居士给大家提供了可以直接运行起来的源码以及个人踩坑的小手记,有需要的小伙伴可以下载下来跑起来做做试验。

      源码链接会在评论中分享出来哦~

    源码和手记.jpg


    本人原创文章纯手打,如果觉得有一滴滴帮助的话,就请伸出纤纤玉手点个推荐吧~~~


  • 相关阅读:
    TWinControl、TCustomControl和TGraphicControl对WM_PAINT消息的三种不同处理(虚函数的特点就是升升降降)
    VCL里的构造函数
    从良难
    TTimer源码研究
    Delphi的RTTI(许多参考链接)
    对ShortCut和TWMKey的研究
    TTimer很特殊
    TEdit的创建与显示过程
    VMware vSphere 服务器虚拟化之二十六 桌面虚拟化之View Persona Management
    Delphi Math里的基本函数,以及浮点数比较函数
  • 原文地址:https://www.cnblogs.com/fulongyuanjushi/p/15919231.html
Copyright © 2020-2023  润新知