Java及MySQL的数据脱敏实现方案


一、背景

  • 关于《个人信息保护法》的要求
  • 要实现和解决的方案与技术问题
  • 目标群体:带有敏感信息的B端或C端用户
  • 技术应用场景:数据库与前端及后台的展示
  • 整体思路:对敏感信息做安全加密及脱敏处理

二、简要说明《个人信息保护法》

个人信息的处理包括个人信息的收集、存储、使用、加工、传输、提供、公开、删除等。 查看完整《个人信息保护法》,请点击关于《个人信息保护法》的相关规定

大致内容:

  1. 收集个人信息,应当限于实现处理目的的最小范围,不得过度收集个人信息。
  2. 公开个人信息处理规则,明示处理的目的、方式和范围。对要收集的信息做相关说明
  3. 处理个人信息应当取得个人单独同意或者书面同意的
  4. 个人信息的处理目的、处理方式和处理的个人信息种类发生变更的,应当重新取得个人同意。
  5. 基于个人同意处理个人信息的,个人有权撤回其同意。个人信息处理者应当提供便捷的撤回同意的方式。
  6. 个人信息处理者通过制定个人信息处理规则的方式告知第一款规定事项的,处理规则应当公开,并且便于查阅和保存。
  7. 个人信息处理者委托处理个人信息的,应当与受托人约定委托处理的目的、期限、处理方式、个人信息的种类、保护措施以及双方的权利和义务等。
  8. 委托合同不生效、无效、被撤销或者终止的,受托人应当将个人信息返还个人信息处理者或者予以删除,不得保留。
  9. 个人信息处理者处理不满十四周岁未成年人个人信息的,应当取得未成年人的父母或者其他监护人的同意。
  10. 个人信息处理者应当公开个人信息保护负责人的联系方式,并将个人信息保护负责人的姓名、联系方式等报送履行个人信息保护职责的部门。
  11. 发生或者可能发生个人信息泄露、篡改、丢失的,个人信息处理者应当立即采取补救措施,并通知履行个人信息保护职责的部门和个人。
  12. 由履行个人信息保护职责的部门责令改正,给予警告,没收违法所得,对违法处理个人信息的应用程序,责令暂停或者终止提供服务;拒不改正的,并处一百万元以下罚款;对直接负责的主管人员和其他直接责任人员处一万元以上十万元以下罚款。 等

三、数据脱敏

系统中的用户信息、订单信息、地址信息等等,都应在公司数据安全红线范围之内,并且,如果公司需要申请一些安全资质,数据保密也是必须拿下的一关,如果发生数据泄露,对公司,对用户都是极大的损失。数据安全架构体系包括很多东西,如DB、日志、文件等数据转换的场景等。就需要我们进行:数据库中的数据脱敏。

2.1 Base64加密(不推荐)

简单介绍: Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。 标准的Base64并不适合直接放在URL里传输,因为URL编码器会把标准Base64中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为ANSI SQL中已将“%”号用作通配符。 为解决此问题,可采用一种用于URL的改进Base64编码,它在末尾填充'='号,并将标准Base64中的“+”和“/”分别改成了“-”和“_”,这样就免去了在URL编解码和数据库存储时所要作的转换,避免了编码信息长度在此过程中的增加,并统一了数据库、表单等处对象标识符的格式。 Base64要求把每三个8Bit的字节转换为四个6Bit的字节(38 = 46 = 24),然后把6Bit再添两位高位0,组成四个8Bit的字节,也就是说,转换后的字符串理论上将要比原来的长1/3。

结论:

  • 不推荐用于数据脱敏 从数据存储的安全角度来说,Base64加密作为简单加解密操作,可直接解码到加密前原数据。因此盗库者获取数据后,依然可直接获取到原文,等同于无效加密。
  • 推荐用于URL参数传输 URL改进的Base64编码还是可以用于URL链接的参数传输,因为URL中某些字符可能会使格式错误,因此传参前可用Base64加密一遍,传输后解码即可得到原文。比如所传的参数为链接等

2.2 对称加密(推荐)

简单介绍:

在对称加密算法中,数据发信方将明文(原始数据)和加密密钥(mi yao)一起经过特殊加密算法处理后,使其变成复杂的加密密文发送出去。收信方收到密文后,若想解读原文,则需要使用加密用过的密钥及相同算法的逆算法对密文进行解密,才能使其恢复成可读明文。在对称加密算法中,使用的密钥只有一个,发收信双方都使用这个密钥对数据进行加密和解密,这就要求解密方事先必须知道加密密钥。
PS:通过密钥对原文进行加密,加密后的密文也需要通过密钥才能解密回原文。

常见的对称加解密算法有DES、3DES、AES、Blowfish;
对于较小的内容(不应超过200个字符),推荐使用AES加密,其速度,大小和简单性具有较好的表现。

概述:

DES 数据加密标准是IBM提出的,第一个基于Lucifer算法的加密技术。作为第一个加密标准,自然会带有些许瑕疵漏洞使其不是特别的安全。(不推荐) 3DES 三重DES是DES的加强版,提供了DES的三重安全性。他和DES使用同样的算法,只是做了三次加密来提升安全级别。(不推荐) Blowfish Blowfish是一种对称分组密码,可以使用它来代替 DES 或 IDEA 算法。 Blowfish 采用32 位指令处理器设计,比现有的 DES(数据加密标准)快得多。如今,DES 对于大多数现代应用程序来说并不安全。 Blowfish 一次操作 8 个字节。对于具有 8 字节块大小的密码,在大约 32 GB 数据之后重复一个块的可能性很高。这意味着,如果您对大于 32 GB 的单个消息进行加密,这几乎是统计上的保证,您将有一个重复的块。那很不好。出于这个原因,GnuPG 建议您不要使用带有 8 字节数据块的密码来对整个硬盘进行批量加密。如果您将要加密的消息或数据保持在大约 4 GB 以下,那么您不太可能会遇到任何问题。 但是这个算法仍然是相关的,并且有些人使用它,因为它在计算能力有限的情况下很容易实现。Blowfish 是免许可、无专利的,这意味着任何人都可以在他们的应用程序中自由使用它。 Blowfish 是为软件设计的。它使用大量内存并且密钥设置时间相对较长,但之后速度很快。它接受高达 448 位的密钥(这是巨大的过度杀伤力),并一次加密 64 位数据。 AES AES 代表高级加密标准,它是世界上最流行的加密标准之一。它由比利时密码学家 Joan Daemen 和 Vincent Rijmen 于 2001 年创立,因此也被称为Rijndael。

AES 最初在美国使用,但随后在世界范围内广受欢迎,因为它被批准为美国联邦标准,旨在取代DES加密技术。

AES 是一种对称分组密码,因为它使用一个密钥进行解密和加密。

AES 具有三种分组密码,具体取决于用于加密、解密的密钥长度: - AES-128(128 位), - AES-192(192 位), - AES-256(256 位)。 - 这些密码加密和解密 128 位块中的数据。但是,它们使用不同大小的键。

AES 一次处理 16 个字节。对于具有 16 字节块大小的密码,您需要加密包含比整个 Internet 中更多数据的单个消息。换句话说,块的重复不是问题。

AES 旨在加密敏感的政府级数据。它易于在软件和硬件中实施,通常用于保护数据免受网络攻击。

如今,AES 可免费用于公共和私人组织。但是,非政府组织在使用 AES 方面存在一些限制。

主要用于:
- 无线网络安全, 
- 文件加密, 
- SSL/TLS。 

AES 也可以包含在某些商业产品中,例如 Wi-Fi、消息传递应用程序、VPN,以及用于本机处理器支持。AES 包含在许多标准中,例如国际标准化组织 (ISO) 标准和国际电工委员会 (IEC) 标准。

AES 是一种有效且安全的加密方式。流行的应用程序,如 WhatsApp 和 Signal,正在使用这种类型的加密。

AES 为世界各地的许多企业和政府提供安全服务。高级加密标准在不同的加密包中可用。它也是唯一获得美国国家安全局批准的可供公众使用的密码。

AES 在硬件和软件方面都很快。它接受 128、192 或 256 位密钥。它具有相当快的密钥设置时间和相对较小的内存需求,并且一次加密 128 位数据。

BLOWFISH和AES的区别 Blowfish 和 AES 都是对称加密算法,这意味着加密和解密密钥是相同的。这也意味着共享相同的密钥以实现安全通信。

这种类型的加密通常用于批量数据加密。它也可以很容易地通过硬件实现。对称加密的主要问题是拥有解密密钥的人可以解密所有数据。

Blowfish 因其批量加密和解密而运行迅速。Blowfish 使用 64 位的块大小。它甚至比在软件中实现的 AES 还要快,但仍然不如 AES 有效。Blowfish 不受专利保护,这使得这种加密软件的使用如此广泛。

  • 快大小 Blowfish 的块大小为 64 位,而 AES 为 128 位。小块大小可能是一个严重的安全问题;由于块大小较小,Blowfish 更容易受到攻击。 大块大小是 AES 的一个优势;但是,这取决于您如何使用密码,因为它有时会使消息比通常情况下更长。但是,如果大量数据被加密,这也可以增加对某些理论攻击的保护。

  • 硬件加速和 RAM AES 的巨大优势在于它具有硬件加速功能,使密码速度更快,同时仍能免受缓存定时攻击。它基于一个不同的概念:替代置换网络,并且速度非常快。Blowfish 没有硬件加速。 此外,AES 可以在没有 RAM 的硬件中实现,而 Blowfish 的密钥设置需要 RAM,这可能需要一些时间。Blowfish 尝试使密钥耗尽攻击变得困难,因为它使初始密钥设置过程变得缓慢。 低 RAM 要求和高速使 AES 成为许多组织的有利加密选项。它还在不同类型的硬件上表现良好,从小型芯片卡到高性能计算机。

结论

  • Blowfish 的块大小为 64 位,最多支持 448 位密钥,密钥设置需要 RAM;通常只用于加密文本文件;无专利
  • AES 的块大小为 128 位,最多支持 256 位密钥,可以在没有 RAM 的硬件中实现;主要用于 文件加密、无线网络安全、SSL/TLS;有专利

虽然Blowfish 可以支持更大的密钥大小,但是块大小影响密码的强度,不妨使用 AES。

因此,AES 是对称加密标准竞争的赢家,事实上是当今最流行的对称密码。

四、技术实现

Java实现AES加密解密

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;

/**
 * @Auther: leimingwen
 * @desc AES 加密工具类
 * @version V1.0
 * @Date: 2022/1/26 15:25
 */
public class AESUtil {

    private static final String KEY_ALGORITHM = "AES";
    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";//默认的加密算法

    /**
     * AES 加密操作
     *
     * @param content 待加密内容
     * @param key 加密密码
     * @return 返回Base64转码后的加密数据
     */
    public static String encrypt(String content, String key) {
        try {
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);// 创建密码器

            byte[] byteContent = content.getBytes("utf-8");

            cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key));// 初始化为加密模式的密码器

            byte[] result = cipher.doFinal(byteContent);// 加密

            return Base64.encodeBase64String(result);//通过Base64转码返回
        } catch (Exception ex) {
            Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
        }

        return null;
    }

    /**
     * AES 解密操作
     *
     * @param content
     * @param key
     * @return
     */
    public static String decrypt(String content, String key) {

        try {
            //实例化
            Cipher cipher = Cipher.getInstance(DEFAULT_CIPHER_ALGORITHM);

            //使用密钥初始化,设置为解密模式
            cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key));

            //执行操作
            byte[] result = cipher.doFinal(Base64.decodeBase64(content));

            return new String(result, "utf-8");
        } catch (Exception ex) {
            Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
        }

        return null;
    }

    /**
     * 生成加密秘钥
     *
     * @return
     */
    private static SecretKeySpec getSecretKey(final String password) {
        //返回生成指定算法密钥生成器的 KeyGenerator 对象
        KeyGenerator kg = null;

        try {
            kg = KeyGenerator.getInstance(KEY_ALGORITHM);

            //AES 要求密钥长度为 128
            kg.init(128, new SecureRandom(password.getBytes()));

            //生成一个密钥
            SecretKey secretKey = kg.generateKey();

            return new SecretKeySpec(secretKey.getEncoded(), KEY_ALGORITHM);// 转换为AES专用密钥
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(AESUtil.class.getName()).log(Level.SEVERE, null, ex);
        }

        return null;
    }

    
    public static void main(String[] args) {
        //加密
        String encode = encrypt("17376752153","nuobeimima");
        System.out.println(encode);
        //解密
        String decode = decrypt(encode,"nuobeimima");
        System.out.println(decode);
    }

}

缺点: - 不易维护,增加排查问题难度。 - 别的业务需要脱敏时,依然需要对应的业务中进行同样处理。侵入性强,扩展性极低。

MySQL 实现加解密转换

- 加密函数:AES_ENCRYPT('待加密串', '自定义秘钥')
- 解密函数:AES_DECRYPT('待解密串', '自定义秘钥')

SQL示例: ①字段类型需要设置为blob

insert into user(mobile) values (AES_ENCRYPT('18812345678', 'nuobeimima')); 
select CAST(AES_DECRYPT(mobile, 'nuobeimima') as char) as mobile from user; 

②输出HEX,字段类型需要设置为blob

insert into user(mobile) values (HEX(AES_ENCRYPT('18812345678', 'nuobeimima')));
select CAST(AES_DECRYPT(UNHEX(mobile), 'nuobeimima') as char) as mobile from user;

③输出Base64,字段类型可以为blob 也可以为varchar

insert into user(mobile) values (to_base64(AES_ENCRYPT('18812345678', 'nuobeimima')));
select CAST(AES_DECRYPT(from_base64(mobile), 'nuobeimima') as char) as mobile from user;

缺点: - 直接在字段上使用函数急剧拉低MySQL性能

mybatis自定义类型处理器TypeHandler(推荐)

由于项目数据库中间件使用的是Mybatis,所以使用Mybatis中的BaseTypeHandler的一个类型处理器,对数据进行AES加密存入数据

  1. 第一步:pom.xml 配置
<dependency>
 <groupId>org.mybatis.spring.boot</groupId>
 <artifactId>mybatis-spring-boot</artifactId>
 <version>1.1.1</version>
</dependency>
  1. 第二步:自定义 加密方案,设置密钥
package com.klhtxxzh.common.config.mybatis;


import org.apache.commons.lang.StringUtils;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * @Auther: leimingwen
 * @Date: 2022/1/26 14:50
 */
public class DataDesensitizationUtils {
    private static final String OTHER_LOGIN_KEY = "8ce87b8ec346ff4c80635f667d1592ae";

    /**
     * 加密
     * @param text
     * @return
     * @throws Exception
     */
    public static String encrypt(String text) {
        try {
            byte[] plaintext = text.getBytes();
            IvParameterSpec ivspec = new IvParameterSpec(OTHER_LOGIN_KEY.substring(16).getBytes());
            SecretKeySpec keyspec = new SecretKeySpec(OTHER_LOGIN_KEY.substring(0, 16).getBytes(), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec);
            byte[] encrypted = cipher.doFinal(plaintext);
            return new BASE64Encoder().encode(encrypted).trim();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 解密
     * @param text
     * @return
     */
    public static String decrypt(String text) {
        try {
            byte[] encrypted1 = new BASE64Decoder().decodeBuffer(text);
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKeySpec keyspec = new SecretKeySpec(OTHER_LOGIN_KEY.substring(0, 16).getBytes(), "AES");
            IvParameterSpec ivspec = new IvParameterSpec(OTHER_LOGIN_KEY.substring(16).getBytes());
            cipher.init(Cipher.DECRYPT_MODE, keyspec, ivspec);
            byte[] original = cipher.doFinal(encrypted1);
            String originalString = new String(original);
            return originalString.trim();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    // 手机号码前三后四脱敏
    public static String mobileEncrypt(String mobile) {
        if (StringUtils.isEmpty(mobile) || (mobile.length() != 11)) {
            return mobile;
        }
        return mobile.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }

}
  1. 第三步:自定义 JavaType的 Handler,实现BaseTypeHandler
package com.klhtxxzh.common.config.mybatis;


import org.apache.commons.lang.StringUtils;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * @Auther: leimingwen
 * @Date: 2022/1/26 15:36
 */
public class AESTypeHandler extends BaseTypeHandler<Object> {

    /**
     * 非空字段加密
     */
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) {
        try {
            if (StringUtils.isBlank((String) parameter)) return;

            ps.setString(i, DataDesensitizationUtils.encrypt((String) parameter));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    /**
     * 非空字段解密
     */
    @Override
    public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
        String col = rs.getString(columnName);
        try {
            if (StringUtils.isBlank(col)) return col;

            return DataDesensitizationUtils.decrypt(col);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return col;
    }

    /**
     * 可空字段加密
     */
    @Override
    public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        String col = rs.getString(columnIndex);
        try {
            if (StringUtils.isBlank(col)) return col;
            return DataDesensitizationUtils.encrypt(col);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return col;
    }


    /**
     * 可空字段解密
     */
    @Override
    public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        String col = cs.getString(columnIndex);
        try {
            if (StringUtils.isBlank(col)) return col;
            return DataDesensitizationUtils.decrypt(col);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return col;
    }

}
  1. 第四步:application.yml 配置

在原有的type-aliases-package后面拼接上:com.klhtxxzh.common.config.mybatis

# MyBatis
mybatis:
    # 搜索指定包别名
    type-aliases-package: com.klhtxxzh.**.domain,com.klhtxxzh.common.config.mybatis
    type-handlers-package: com.klhtxxzh.common.config.mybatis
  1. 第五步:xml 中代码

返回处设置 typeHandler="AESTypeHandler"

<!--返回模型-->
<resultMap type="User" id="UserResult">
    <result property="realName" column="real_name"  typeHandler="AESTypeHandler"  />
</resultMap>

编辑处设置 typeHandler=AESTypeHandler

<!--插入-->
<insert id="insertUser" parameterType="User">
    insert into user
    <trim prefix="(" suffix=")" suffixOverrides=",">
        <if test="realName != null  and realName != ''"> real_name,</if>
     </trim>
    <trim prefix="values (" suffix=")" suffixOverrides=",">
        <if test="realName != null  and realName != ''">#{realName, typeHandler=AESTypeHandler},</if>
     </trim>
</insert>

<!--修改-->
<update id="updateCareProduct" parameterType="CareProduct">
    update user
    <trim prefix="SET" suffixOverrides=",">
        <if test="realName != null  and realName != ''"> real_name = #{realName, typeHandler=AESTypeHandler},</if>
    </trim>
    where id = #{id}
</update>
  1. 完成

数据显示正常

数据库为密文

五、总结

  • 在选择正确的加密选项时,重要的是不仅要完全关注密钥的大小,还要考虑其他属性。选择错误的加密方式会导致敏感信息的攻击或泄露。
  • 有权访问消费者信息的企业应密切注意加密和保护信息。数字安全已成为每个人的首要任务之一,因为我们在网上购物、分享我们的地址、银行账户信息等。这就是为什么数据加密是在线安全的核心。
  • 《个人信息保护法》的出台也说明了用户敏感信息隐私越来越重要,保护用户敏感信息安全是基础。

=============补充 Mybatis-Plus的实现=============

因为Mybatis-Plus的实现可能不会编写mapper文件

所以需要在实体类中加注解实现

1、Mybatis-Plus 不是所有版本都支持相关注解或注解内方法,在 pom.xml 需添加支持对应组件的版本

<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
	<version>${mybatisplus.version}</version>
</dependency>

2、在实体类上添加@TableName注解,并设置autoResultMap = true

3、在需要进行处理加解密的字段上添加 @TableField(typeHandler = XxxxxxHandler.class)

如:

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableField;

@TableName(value = "my_table", autoResultMap = true)
public class MyEntity {
    @TableField(typeHandler = WstTypeHandler.class)
    private String myField;

    // 其他字段和方法
}

PS:注意@TableName一定要加上autoResultMap = true,否则字段的@TableField注解typeHandler将不会生效

Java
MySQL
Nginx
  • 作者:remember(联系作者)
  • 发表时间:2023-10-09 14:17
  • 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  • 公众号转载:请在文末添加作者公众号二维码
  • 评论