简介

  • nacos作为一款优秀的注册发现中心和配置管理工具,能够实现微服务配置的热更新,同时从代码中解耦出去,更加自由的控制服务的上线和下线,使所有的操作全部可视化,独立化。
  • nacos官网
  • 但是在nacos原生版本中,nacos持久化连接mysql数据库是通过配置文件读取数据库的ip、port、user及password的。
    这其实是一种不安全的操作,如果数据库权限约束做的不好,那么数据库密码泄露将是一件很严重的事情

修改源码

  • 完成个性化定制,自定义数据库密码的加密方式,其实并不难,只需要修改nacos源码中数据库连接部分即可
  • nacos(1.4.1版本)数据库连接初始化所在的类是com.alibaba.nacos.config.service.DataSource包下的ExternalDataSourceProperties类,完成数据库信息装配的方法是 build 方法
  • 我们需要做的只是在nacos读取到配置文件中的密码之后,并且将属性值赋值到实体类之前将密码解析出来即可,以下是源码以及修改的部分
List<HikariDataSource> build(Environment environment, Callback<HikariDataSource> callback) {
        List<HikariDataSource> dataSources = new ArrayList<>();
        Binder.get(environment).bind("db", Bindable.ofInstance(this));
        Preconditions.checkArgument(Objects.nonNull(num), "db.num is null");
        Preconditions.checkArgument(CollectionUtils.isNotEmpty(user), "db.user or db.user.[index] is null");
        Preconditions.checkArgument(CollectionUtils.isNotEmpty(password), "db.password or db.password.[index] is null");
        for (int index = 0; index < num; index++) {
            int currentSize = index + 1;
            Preconditions.checkArgument(url.size() >= currentSize, "db.url.%s is null", index);
            DataSourcePoolProperties poolProperties = DataSourcePoolProperties.build(environment);
            poolProperties.setDriverClassName(JDBC_DRIVER_NAME);
            poolProperties.setJdbcUrl(url.get(index).trim());
            poolProperties.setUsername(getOrDefault(user, index, user.get(0)).trim());
            /* nacos在这里读取数据库密码 */
//---------------------------------------------------------------------------------------
            /*poolProperties.setPassword(getOrDefault(password, index, password.get(0)).trim());*/
            // 在这里可以自定义密码的解密规则,此处采用sm4加密为例,可以替换成自己的解密类
            String key = "86C63180C2806ED1F47B859DE501215B";
            poolProperties.setPassword(
                Sm4Util.decryptEcb(key, getOrDefault(password, index, password.get(0)).trim()));
//-------------------------------------------------------------------------------------------
            HikariDataSource ds = poolProperties.getDataSource();
            ds.setConnectionTestQuery(TEST_QUERY);
            dataSources.add(ds);
            callback.accept(ds);
        }
        Preconditions.checkArgument(CollectionUtils.isNotEmpty(dataSources), "no datasource available");
        return dataSources;
    }

同时这个类中其他所有的属性都可以根据自己的需要去进行修改,也可以自己实现这个实现类的父接口,不过只是修改密码规则的话还是建议直接在这里修改

sm4工具类

依赖:

<!--sm4加密算法-->
	<dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcprov-jdk15on</artifactId>
        <version>1.66</version>
    </dependency>

代码:

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Arrays;

@SuppressWarnings("all")	// nacos一键打包时跳过检查,不加可能会报错
public class Sm4Util {
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private static final String ENCODING = "UTF-8";
    public static final String ALGORITHM_NAME = "SM4";

    //加密算法/分组加密模式/分组填充方式
    //PKCS5Padding-以8个字节为一组分组加密
    //定义分组加密模式使用:PKCS5Padding

    public static final String ALGORITHM_NAME_ECB_PADDING = "SM4/ECB/PKCS5Padding";

    //128-32位16进制;256-64位16进制

    public static final int DEFAULT_KEY_SIZE = 128;

    /**
     * 生成ECB暗号
     *
     * @param algorithmName 算法名称
     * @param mode          模式
     * @param key
     * @return
     * @throws Exception
     * @explain ECB模式(电子密码本模式:Electronic codebook)
     */
    private static Cipher generateEcbCipher(String algorithmName, int mode, byte[] key) throws Exception {
        Cipher cipher = Cipher.getInstance(algorithmName, BouncyCastleProvider.PROVIDER_NAME);
        Key sm4Key = new SecretKeySpec(key, ALGORITHM_NAME);
        cipher.init(mode, sm4Key);
        return cipher;
    }

    public static byte[] generateKey() throws Exception {
        return generateKey(DEFAULT_KEY_SIZE);
    }

    public static byte[] generateKey(int keySize) throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance(ALGORITHM_NAME, BouncyCastleProvider.PROVIDER_NAME);
        kg.init(keySize, new SecureRandom());
        return kg.generateKey().getEncoded();
    }

    /**
     * sm4加密
     *
     * @param hexKey   16进制秘钥(忽略大小写)
     * @param paramStr 待加密字符串
     * @return 返回16进制的加密字符串
     * @throws Exception
     */
    public static String encryptEcb(String hexKey, String paramStr) throws Exception {
        String cipherText = "";
        // 16进制字符串-->byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        // String-->byte[]
        byte[] srcData = paramStr.getBytes(ENCODING);
        // 加密后的数组
        byte[] cipherArray = encrypt_Ecb_Padding(keyData, srcData);
        // byte[]-->hexString
        cipherText = ByteUtils.toHexString(cipherArray);
        return cipherText;
    }

    /**
     * 加密模式之Ecb
     *
     * @param key
     * @param data
     * @return
     * @throws Exception
     * @explain
     */
    public static byte[] encrypt_Ecb_Padding(byte[] key, byte[] data) throws Exception {
        Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.ENCRYPT_MODE, key);
        return cipher.doFinal(data);
    }

    /**
     * sm4解密
     *
     * @param hexKey     16进制密钥
     * @param cipherText 16进制的加密字符串(忽略大小写)
     * @return 解密后的字符串
     * @throws Exception
     * @explain 解密模式:采用ECB
     */
    public static String decryptEcb(String hexKey, String cipherText) throws Exception {
        // 用于接收解密后的字符串
        String decryptStr = "";
        // hexString-->byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        // hexString-->byte[]
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        // 解密
        byte[] srcData = decrypt_Ecb_Padding(keyData, cipherData);
        // byte[]-->String
        decryptStr = new String(srcData, ENCODING);
        return decryptStr;
    }

    /**
     * @param key
     * @param cipherText
     * @return
     * @throws Exception
     */
    public static byte[] decrypt_Ecb_Padding(byte[] key, byte[] cipherText) throws Exception {

        Cipher cipher = generateEcbCipher(ALGORITHM_NAME_ECB_PADDING, Cipher.DECRYPT_MODE, key);
        return cipher.doFinal(cipherText);
    }

    /**
     * 校验加密前后的字符串是否为同一数据
     *
     * @param hexKey     16进制密钥(忽略大小写)
     * @param cipherText 16进制加密后的字符串
     * @param paramStr   加密前的字符串
     * @return 是否为同一数据
     * @throws Exception
     * @explain
     */
    public static boolean verifyEcb(String hexKey, String cipherText, String paramStr) throws Exception{
        // 用于接收校验结果
        boolean flag = false;
        // hexString-->byte[]
        byte[] keyData = ByteUtils.fromHexString(hexKey);
        // 将16进制字符串转换成数组
        byte[] cipherData = ByteUtils.fromHexString(cipherText);
        // 解密
        byte[] decryptData = decrypt_Ecb_Padding(keyData, cipherData);
        // 将原字符串转换成byte[]
        byte[] srcData = paramStr.getBytes(ENCODING);
        // 判断2个数组是否一致
        flag = Arrays.equals(decryptData, srcData);
        return flag;
    }

    public static void main(String[] args) throws Exception {
        String key = "86C63180C2806ED1F47B859DE501215B";
        String s = "QAZXSWEDCVFR";
        String jiami = Sm4Util.encryptEcb(key, s);
        System.out.println("加密====" + jiami);
        String jiemi = Sm4Util.decryptEcb(key, jiami);
        System.out.println("解密====" + jiemi);
        byte[] bs = ByteUtils.fromHexString(key);
        System.out.println(Arrays.toString(bs));
        System.out.println(ByteUtils.toHexString(generateKey()));

        String str = "86C63180C2806ED1F47B859DE501215B";
        int byte_len = str.getBytes("utf-8").length;
        int len = str.length();
        System.out.println("字节长度为:" + byte_len);
        System.out.println("字符长度为:" + len);
        System.out.println("系统默认编码方式:" + System.getProperty("file.encoding"));
    }
}

打包发布

  • 修改完源码需要将源码包打包成可以使用的jar包
    在nacos顶级包路径下打开控制台,输入以下命令
mvn -Prelease-nacos -Dmaven.test.skip=true -Drat.skip=true clean install -U
  • (也可以在IDEA的terminal输入命令进行打包操作)

mysql使用nacos用户 nacos和mysql_ci

  • 然后等待打包成功
    打包好的包位置:
    nacos-xxx\distribution\target\nacos-server-xxx.zip(windows)
    nacos-xxx\distribution\target\nacos-server-xxx.tar.gz(linux)
  • 修改配置文件(nacos-server-1.4.1\nacos\conf\application.properties)中的密码,用自己定义的加密方式加密一遍
#*************** Config Module Related Configurations ***************#
### If use MySQL as datasource:
spring.datasource.platform=mysql

### Count of DB:
db.num=1

### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=b916909c4b06919ed4435b0d4873ecd1
  • 启动nacos,登录到控制台,日志没有报错那么修改nacos密码连接方式就完成了
    启动:在nacos-server-1.4.1\nacos\bin打开cmd命令窗口,输入命令
startup.cmd -m standalone

(standalone代表着单机模式运行,非集群模式)

看到"Nacos started successfully in stand alone mode.”后表示服务已启动

Nacos源码Idea启动流程

下载Nacos源码

这里推荐下载1.4.1版本,2.0.0版本刚刚出来稳定性还不够。

编译运行

下载完成之后,把项目导入到Idea中,找到Nacos的主启动类,如下图所示:

mysql使用nacos用户 nacos和mysql_16进制_02

点击启动,会出现缺少com.alibaba.nacos.consistency.entity的错误。

出现的原因是因为nacos中用的是proto文件,而代码中没有生成对应的实体类,因此我们需要手动生成。方式如下。

安装protoc

protoc安装包下载地址(https://github.com/protocolbuffers/protobuf/releases),目前最新版本是3.17.0,下载方式点击对应的操作系统的zip包即可。windows版本下载如下:

mysql使用nacos用户 nacos和mysql_java_03

下载完成之后,解压到自己喜欢的目录。然后将目录地址下的bin文件夹配置到系统环境变量(path)中,配置完成之后在cmd中输入【protoc --version】检查是否配置成功。

反编译成对应的java实体类

进入到源码的consistency/src/main/proto/路径下,使用cmd命令,依次运行如下指令

protoc --java_out=../java/ ./consistency.proto
protoc --java_out=../java/ ./Data.proto

执行完成之后,可以在com.alibaba.nacos.consistency包下面看到对应的entity包,如下所示:

mysql使用nacos用户 nacos和mysql_数据库_04

启动运行

配置数据库信息

配置方式和nacos软件启动的时候是一样的,首先需要一个本地MySQL数据库,建立数据库nacos_config,如果之前配置过可以不用配置。

  • 在本地数据库新建数据库:nacos
  • 初始化数据库:在nacos数据库中执行 distribution/conf/nacos-mysql.sql 数据库脚本

修改项目中application.properties文件中的数据库连接部分,需要修改distribution.conf文件和console项目中resources下的application.properties,如下图示:

#*************** Config Module Related Configurations ***************#
### If use MySQL as datasource:
spring.datasource.platform=mysql

### Count of DB:
db.num=1

### Connect URL of DB:
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=b916909c4b06919ed4435b0d4873ecd1

配置单机启动参数

在Nacos应用程序的配置参数中添加:

-Dnacos.standalone=true -Dnacos.home=E:\code\project\source\nacos\nacos-1.4.1\distribution

第一个参数含义为单机模式启动,第二个参数为nacos.home地址(源码中distribution文件夹的位置)不可以配置错误。

mysql使用nacos用户 nacos和mysql_数据库_05

配置完成之后启动即可,启动成功日志如下:

mysql使用nacos用户 nacos和mysql_ci_06