[Spring Boot]登录密码三种加密方式
简述
介绍其三种密码加密方法
1.SM2加密与验签
2.随机密码盐加密
3.MD5加密
推荐使用方法1,其次使用方法2,最不推荐的是方法3。方法3极其容易被密码字典破解,如果项目进行安全测试,通常是不允许的加密方式。
SM2加密与验签
引入bcprov,以使用SM2加密。
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15to18</artifactId><version>1.69</version></dependency>
工具类与测试方法
加密的主要工具类如下,其中带有测试的main方法。
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.gm.GMObjectIdentifiers;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ECDomainParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey;
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;import java.security.*;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;/*** sm2算法与签名的使用** @author fir* @date 2024/7/23 14:22*/
@Slf4j
public class Sm2SignatureUtils {static {Security.addProvider(new BouncyCastleProvider());}public static final String PUBLIC_KEY = "publicKey";public static final String PRIVATE_KEY = "privateKey";/*** 生成国密公私钥对*/public static Map<String, String> generateSmKey() throws Exception {KeyPairGenerator keyPairGenerator;SecureRandom secureRandom = new SecureRandom();ECGenParameterSpec sm2Spec = new ECGenParameterSpec("sm2p256v1");keyPairGenerator = KeyPairGenerator.getInstance("EC", new BouncyCastleProvider());keyPairGenerator.initialize(sm2Spec);keyPairGenerator.initialize(sm2Spec, secureRandom);KeyPair keyPair = keyPairGenerator.generateKeyPair();PrivateKey privateKey = keyPair.getPrivate();PublicKey publicKey = keyPair.getPublic();String publicKeyStr = new String(Base64.getEncoder().encode(publicKey.getEncoded()));String privateKeyStr = new String(Base64.getEncoder().encode(privateKey.getEncoded()));return Map.of(PUBLIC_KEY, publicKeyStr, PRIVATE_KEY, privateKeyStr);}/*** 将Base64转码的公钥串,转化为公钥对象*/public static PublicKey createPublicKey(String publicKey) {PublicKey publickey = null;try {X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey));KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());publickey = keyFactory.generatePublic(publicKeySpec);} catch (Exception e) {log.error("将Base64转码的公钥串,转化为公钥对象异常:{}", e.getMessage(), e);}return publickey;}/*** 将Base64转码的私钥串,转化为私钥对象*/public static PrivateKey createPrivateKey(String privateKey) {PrivateKey publickey = null;try {PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey));KeyFactory keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider());publickey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);} catch (Exception e) {log.error("将Base64转码的私钥串,转化为私钥对象异常:{}", e.getMessage(), e);}return publickey;}/*** 根据publicKey对原始数据data,使用SM2加密*/public static String encrypt(byte[] data, String publicKeyBase64) {PublicKey publicKey = createPublicKey(publicKeyBase64);ECPublicKeyParameters localEcPublicKeyParameters = getEcPublicKeyParameters(publicKey);SM2Engine localSm2Engine = new SM2Engine();localSm2Engine.init(true, new ParametersWithRandom(localEcPublicKeyParameters, new SecureRandom()));byte[] arrayOfByte2;try {arrayOfByte2 = localSm2Engine.processBlock(data, 0, data.length);return Base64.getEncoder().encodeToString(arrayOfByte2);} catch (InvalidCipherTextException e) {log.error("SM2加密失败:{}", e.getMessage(), e);return null;}}private static ECPublicKeyParameters getEcPublicKeyParameters(PublicKey publicKey) {ECPublicKeyParameters localEcPublicKeyParameters = null;if (publicKey instanceof BCECPublicKey localEcPublicKey) {ECParameterSpec localEcParameterSpec = localEcPublicKey.getParameters();ECDomainParameters localEcDomainParameters = new ECDomainParameters(localEcParameterSpec.getCurve(),localEcParameterSpec.getG(), localEcParameterSpec.getN());localEcPublicKeyParameters = new ECPublicKeyParameters(localEcPublicKey.getQ(), localEcDomainParameters);}return localEcPublicKeyParameters;}/*** 根据privateKey对加密数据encode data,使用SM2解密*/public static String decrypt(String encodeBase64, String privateKeyBase64) {SM2Engine localSm2Engine = new SM2Engine();PrivateKey privateKey = createPrivateKey(privateKeyBase64);BCECPrivateKey sm2PriK = (BCECPrivateKey) privateKey;byte[] encodeData = Base64.getDecoder().decode(encodeBase64);ECParameterSpec localEcParameterSpec = sm2PriK.getParameters();ECDomainParameters localEcDomainParameters = new ECDomainParameters(localEcParameterSpec.getCurve(),localEcParameterSpec.getG(), localEcParameterSpec.getN());ECPrivateKeyParameters localEcPrivateKeyParameters = new ECPrivateKeyParameters(sm2PriK.getD(),localEcDomainParameters);localSm2Engine.init(false, localEcPrivateKeyParameters);try {byte[] result = localSm2Engine.processBlock(encodeData, 0, encodeData.length);return new String(result);} catch (InvalidCipherTextException e) {log.error("SM2解密失败:{}", e.getMessage(), e);return null;}}/*** 私钥,数据,生成签名*/public static String signByPrivateKey(String dataStr, String privateKeyBase64) throws Exception {PrivateKey privateKey = createPrivateKey(privateKeyBase64);byte[] data = Base64.getDecoder().decode(dataStr);Signature sig = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), BouncyCastleProvider.PROVIDER_NAME);sig.initSign(privateKey);sig.update(data);byte[] sign = sig.sign();return Base64.getEncoder().encodeToString(sign);}/*** 公钥与签名验证数据合法性*/public static boolean verifyByPublicKey(String dataStr, String publicKeyBase64, String signatureBase64) throws Exception {PublicKey publicKey = createPublicKey(publicKeyBase64);byte[] signature = Base64.getDecoder().decode(signatureBase64);byte[] data = Base64.getDecoder().decode(dataStr);Signature sig = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), BouncyCastleProvider.PROVIDER_NAME);sig.initVerify(publicKey);sig.update(data);return sig.verify(signature);}public static void test() throws Exception {// 生成公私钥对Map<String, String> keys = generateSmKey();String publicKey = keys.get(PUBLIC_KEY);String privateKey = keys.get(PRIVATE_KEY);String testStr = "123456";System.out.println("原始字符串:" + testStr);System.out.println("公钥:" + keys.get(PUBLIC_KEY));System.out.println("私钥:" + keys.get(PRIVATE_KEY));System.out.println();// 公钥加密String encrypt = encrypt(testStr.getBytes(), publicKey);System.out.println("加密数据:" + encrypt);// 私钥签名,后续根据数据与公钥验签String sign = signByPrivateKey(encrypt, privateKey);System.out.println("数据签名:" + sign);// 公钥验签,验证通过后再进行数据解密boolean b = verifyByPublicKey(encrypt, publicKey, sign);System.out.println("数据验签:" + b);//私钥解密String decrypt = decrypt(encrypt, privateKey);System.out.println("解密数据:" + decrypt);}public static void uesCase() throws Exception {//生成公私钥对String publicKey = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEsrdE0XrAO2S7Ize0tm0r3diH9cPH23t0J9yVDtiVux6g71msH5YGTWW6/ogQSCVJ4iaofgCS/ly5+wkXa+/IGg==";String privateKey = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgNxb+Jcu1vhGt9UEbeFUYeCC+RWL7+sfUL1vnhBp2KtKgCgYIKoEcz1UBgi2hRANCAASyt0TResA7ZLsjN7S2bSvd2If1w8fbe3Qn3JUO2JW7HqDvWawflgZNZbr+iBBIJUniJqh+AJL+XLn7CRdr78ga";String testStr = "123456";System.out.println("原始字符串:" + testStr);System.out.println("公钥:" + publicKey);System.out.println("私钥:" + privateKey);System.out.println();//公钥加密String encrypt = encrypt(testStr.getBytes(), publicKey);System.out.println("加密数据:" + encrypt);// 私钥签名,后续根据数据与公钥验签String sign = signByPrivateKey(encrypt, privateKey);System.out.println("数据签名:" + sign);//公钥验签,验证通过后再进行数据解密boolean b = verifyByPublicKey(encrypt, publicKey, sign);System.out.println("数据验签:" + b);//私钥解密String decrypt = decrypt(encrypt, privateKey);System.out.println("解密数据:" + decrypt);}public static void main(String[] args) {try {test();
// uesCase();} catch (Exception e) {throw new RuntimeException(e);}}
}
使用案例
验证密码
接受到用户输入的用户与密码之后,在数据库中查询出旧的密码,并进行旧密码进行验签、解密。解密后判断用户输入的密码与数据库存储的密码是否相同。
// 验证密码是否正确String password = "123456";String passwordOld = user.getPassword();String signature = user.getSignature();String decryptPasswordOld = null;try {// 公钥验签,查看数据与签名是否有效boolean b = Sm2SignatureUtils.verifyByPublicKey(passwordOld, public, signature);if(!b){throw new CommonException("数据损坏");}decryptPasswordOld = Sm2SignatureUtils.decrypt(passwordOld, private);}catch (Exception e){throw new CommonException("数据损坏");}if (decryptPasswordOld == null || !decryptPasswordOld.equals(password)) {throw new CommonException("用户密码错误");}
修改密码
接收到用户的密码后,根据公私要生成加密数据串与私钥签名。并存储到数据库,用于之后的密码验证。
// 公钥加密String password = "123456";String encrypt = Sm2SignatureUtils.encrypt(password.getBytes(), public);// 根据数据与私钥,生成私钥签名String sign = Sm2SignatureUtils.signByPrivateKey(encrypt, private);user.setPassword(encrypt);user.setSignature(sign);
随机密码盐加密
工具类与测试方法
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;/*** @author fir* @date 2024/7/25 12:22*/
public class SaltUtils {/*** 生成随机安全盐** @return 盐*/public static String generateSalt() {// 使用SecureRandom生成安全的随机盐SecureRandom random = new SecureRandom();byte[] salt = new byte[16];random.nextBytes(salt);return Base64.getEncoder().encodeToString(salt);}/*** 生成加盐密码** @return 盐*/public static String hashPassword(String password, String salt){// 将密码和盐结合String saltedPassword = password + salt;// 使用SHA-256进行哈希MessageDigest md;try {md = MessageDigest.getInstance("SHA-256");}catch (NoSuchAlgorithmException e){throw new RuntimeException("加密盐处理失败");}byte[] hashedBytes = md.digest(saltedPassword.getBytes());// 将哈希值转换为字符串return Base64.getEncoder().encodeToString(hashedBytes);}public static void main(String[] args){String salt = generateSalt();String password = "123456";String hashPassword = hashPassword(password, salt);System.out.println("盐:" + salt);System.out.println("密码:" + password);System.out.println("加密密码:" + hashPassword);}}
使用案例
验证密码
查询出用户的密码,将用户输入的密码盐加密,并判断与数据库的加密密码是否一致。
String password = "123456";String salt = user.getSalt();String passwordOld = user.getPassword();String mPassword = SaltUtils.hashPassword(password, salt);if (!mPassword.equals(passwordOld)) {throw new CommonException("密码错误");}
修改密码
将用户输入的密码md5加密之后,存在数据库中,用于之后的密码验证
String password = "123456";String salt = SaltUtils.generateSalt();String hashPassword = SaltUtils.hashPassword(password, salt);user.setPassword(hashPassword);
MD5加密
引入hutool包,以使用md5加密。
<dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.19</version></dependency>
测试方法
import cn.hutool.crypto.digest.MD5;/*** @author fir* @date 2024/7/22 10:07*/
public class Md5Utils {public static void main(String[] args){// MD5取值String mPassword = MD5.create().digestHex("123456");System.out.println(mPassword);}
}
使用案例
验证密码
查询出用户的密码,将用户输入的密码MD5加密,并判断与数据库的加密密码是否一致。
String password = "123456";String mPassword = MD5.create().digestHex(password);String passwordOld = user.getPassword();if (!mPassword.equals(passwordOld)) {throw new CommonException("密码错误");}
修改密码
将用户输入的密码md5加密之后,存在数据库中,用于之后的密码验证
String password = "123456";String passwordMd5 = MD5.create().digestHex(password);user.setPassword(passwordMd5);
相关文章:
[Spring Boot]登录密码三种加密方式
简述 介绍其三种密码加密方法 1.SM2加密与验签 2.随机密码盐加密 3.MD5加密 推荐使用方法1,其次使用方法2,最不推荐的是方法3。方法3极其容易被密码字典破解,如果项目进行安全测试,通常是不允许的加密方式。 SM2加密与验签 引入…...

前端面试项目细节重难点分享(十三)
面试题提问:分享你最近做的这个项目,并讲讲该项目的重难点? 答:最近这个项目是一个二次迭代开发项目,迭代周期一年,在做这些任务需求时,确实有很多值得分享的印象深刻的点,我讲讲下面…...

每天五分钟深度学习:向量化方式完成逻辑回归m个样本的前向传播
本文重点 我们已经知道了向量化可以明显的加速程序的运行速度,本节课程将使用向量化来完成逻辑回归的前向传播,不使用一个for循环。 逻辑回归的前向传播 我们先来回忆一下逻辑回归的前向传播,如果我们有m个训练样本,首先对第一个样本进行预测,我们需要计算z,然后计算预…...
以线程完成并发的UDP服务端
网络(九)并发的UDP服务端 以线程完成功能 客户端 // todo UDP发送端 #include <stdio.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/types.h> #include <stdlib.h> #include <string.h…...
linux c 特殊字符分割
/* * brief: 根据split_symbol分割字符串 * param: str为要分割的字符串,split_symbol是分隔符 * return:返回garray的指针数组,如果返回非空需要自己处理释放 */ GPtrArray_autoptr char_sz_spilt(pchar* str, pchar split_symbol) {if (NUL…...

搭建本地私有知识问答系统:MaxKB + Ollama + Llama3 (wsl网络代理配置、MaxKB-API访问配置)
目录 搭建本地私有知识问答系统:MaxKB、Ollama 和 Llama3 实现指南引言MaxKB+Ollama+Llama 3 Start buildingMaxKB 简介:1.1、docker部署 MaxKB(方法一)1.1.1、启用wls或是开启Hyper使用 WSL 2 的优势1.1.2、安装docker1.1.3、docker部署 MaxKB (Max Knowledge Base)MaxKB …...

谷粒商城实战笔记-65-商品服务-API-品牌管理-表单校验自定义校验器
文章目录 1,el-form品牌logo图片自定义显示2,重新导入和注册element-ui组件3,修改brand-add-or-update.vue控件的表单校验规则firstLetter 校验规则sort 校验规则 1,el-form品牌logo图片自定义显示 为了在品牌列表中自定义显示品…...
学好C++之——命名空间
c开始学习之时,你不可避免会遇到一个新朋友,那就是——namespace(命名空间)。 那么这篇文章就来为你解决这个小麻烦喽~ 目录 1.namespace存在的意义 2.namespace的定义 3.namespace的使用 1.namespace存在的意义 在C中&#…...
pytorch lightning报错all tensors to be on the same device
RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu! 修改指定为gpu trainer pl.Trainer(max_epochstrain_params.iterations, loggertb_logger,acceleratorgpu, devices1)...

Redis中的哨兵(Sentinel)
上篇文章我们讲述了Redis中的主从复制(Redis分布式系统中的主从复制-CSDN博客),本篇文章针对主从复制中的问题引出Redis中的哨兵,希望本篇文章会对你有所帮助。 文章目录 一、引入哨兵机制 二、基本概念 三、主从复制的问题 四、哨…...

产业创新研究杂志产业创新研究杂志社产业创新研究编辑部2024年第12期目录
高质量发展 如何在新一轮产业链变革中平稳应对挑战 王宏利; 1-3《产业创新研究》投稿:cnqikantg126.com 基于ERGM的城市间绿色低碳技术专利转让网络结构及演化研究 吕彦朋;姜军;张宁; 4-6 数字基础设施建设对城市FDI的影响——基于“宽带中国”试点政策…...

网闸(Network Gatekeeper或Security Gateway)
本心、输入输出、结果 文章目录 网闸(Network Gatekeeper或Security Gateway)前言网闸主要功能网闸工作原理网闸使用场景网闸网闸(Network Gatekeeper或Security Gateway) 编辑 | 简简单单 Online zuozuo 地址 | https://blog.csdn.net/qq_15071263 如果觉得本文对你有帮助…...
C#中的字符串
String 在实例方法中string虽然传入的是引用类型 但是修改string 并不是修改原来堆里面的值 而是又重新创建一个堆值 用来然后用方法内的变量指向新的堆值 C# 中的字符串(string 类型)提供了许多有用的方法来处理字符串数据。以下是一些常用的字符…...

docker安装部署elasticsearch7.15.2
docker安装部署elasticsearch7.15.2 1.拉取es镜像 docker pull docker.elastic.co/elasticsearch/elasticsearch:7.15.2如果不想下载或者镜像拉去太慢可以直接下载文章上面的镜像压缩包 使用镜像解压命令 docker load -i elasticsearch-7-15-2.tar如下图所示就表示镜像解压成…...
Symfony 入门指南:快速安装与基础配置
Symfony 入门指南:快速安装与基础配置 Symfony 是一个强大而灵活的 PHP 框架,广泛应用于构建现代 Web 应用程序。本指南将带您一步一步地了解如何快速安装 Symfony,并完成基本配置,以便您能够开始使用这个强大的框架。 目录 引…...

3.3V升压至5V的AH6922芯片:高效能的SOP8封装解决方案
# 3.3V升压至5V的AH6922芯片:高效能的SOP8封装解决方案 在当今快速发展的电子设备领域,对于电源管理的需求日益增长。特别是对于便携式产品和手持设备,一个高效、稳定且体积小巧的升压解决方案变得至关重要。本文将介绍一款专为这些需求设计…...

赋能未来教育,3DCAT助力深圳鹏程技师学院打造5G+XR实训室
随着国家对教育行业的重视,实训室建设已成为推动教育现代化的关键。《教育信息化2.0行动计划》、《职业教育示范性虚拟仿真实训基地建设指南》等政策文件,明确指出了加强虚拟仿真实训教学环境建设的重要性。 在这一大背景下,教育行业对于实训…...

力扣141环形链表问题|快慢指针算法详细推理,判断链表是否有环|龟兔赛跑算法
做题链接 目录 前言: 一、算法推导: 1.假设有环并且一定会相遇,那么一定是在环内相遇,且是快指针追上慢指针。 2.有环就一定会相遇吗?快指针是每次跳两步,有没有可能把慢指针跳过去? 3.那一定…...
React 常见的报错及解决方法
1、Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons(无效的钩子调用。钩子只能在函数组件的内部调用。这可能是由于以下原因之一) 原因&#x…...
更新服务器nginx 1.26.1版本
今天在官网下载了nginx1的1.26.1版本,使用gpt的脚本想直接覆盖安装,脚本如下 #!/bin/bash# 设置变量 NGINX_VERSION"1.26.1" TAR_FILE"nginx-$NGINX_VERSION.tar.gz" SRC_DIR"nginx-$NGINX_VERSION"# 检查是否存在tar包 …...

UE5 学习系列(二)用户操作界面及介绍
这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…...

利用最小二乘法找圆心和半径
#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …...
论文解读:交大港大上海AI Lab开源论文 | 宇树机器人多姿态起立控制强化学习框架(二)
HoST框架核心实现方法详解 - 论文深度解读(第二部分) 《Learning Humanoid Standing-up Control across Diverse Postures》 系列文章: 论文深度解读 + 算法与代码分析(二) 作者机构: 上海AI Lab, 上海交通大学, 香港大学, 浙江大学, 香港中文大学 论文主题: 人形机器人…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...
【android bluetooth 框架分析 04】【bt-framework 层详解 1】【BluetoothProperties介绍】
1. BluetoothProperties介绍 libsysprop/srcs/android/sysprop/BluetoothProperties.sysprop BluetoothProperties.sysprop 是 Android AOSP 中的一种 系统属性定义文件(System Property Definition File),用于声明和管理 Bluetooth 模块相…...
是否存在路径(FIFOBB算法)
题目描述 一个具有 n 个顶点e条边的无向图,该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序,确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数,分别表示n 和 e 的值(1…...

C++使用 new 来创建动态数组
问题: 不能使用变量定义数组大小 原因: 这是因为数组在内存中是连续存储的,编译器需要在编译阶段就确定数组的大小,以便正确地分配内存空间。如果允许使用变量来定义数组的大小,那么编译器就无法在编译时确定数组的大…...

[大语言模型]在个人电脑上部署ollama 并进行管理,最后配置AI程序开发助手.
ollama官网: 下载 https://ollama.com/ 安装 查看可以使用的模型 https://ollama.com/search 例如 https://ollama.com/library/deepseek-r1/tags # deepseek-r1:7bollama pull deepseek-r1:7b改token数量为409622 16384 ollama命令说明 ollama serve #:…...
redis和redission的区别
Redis 和 Redisson 是两个密切相关但又本质不同的技术,它们扮演着完全不同的角色: Redis: 内存数据库/数据结构存储 本质: 它是一个开源的、高性能的、基于内存的 键值存储数据库。它也可以将数据持久化到磁盘。 核心功能: 提供丰…...