2023年3月

在机器学习中,学习的目标是选择期望风险
\(R_{exp}\)
(expected loss)最小的模型,但在实际情况下,我们不知道数据的真实分布(包含已知样本和训练样本),仅知道训练集上的数据分布。因此,我们的目标转化为最小化训练集上的平均损失,这也被称为经验风险
\(R_{emp}\)
(empirical loss)。

严格地说,我们应该计算所有训练数据的损失函数的总和,以此来更新模型参数(Batch Gradient Descent)。但随着数据集的不断增大,以
ImagNet
数据集为例,该数据集的数据量有百万之多,计算所有数据的损失函数之和显然是不现实的。若采用计算单个样本的损失函数更新参数的方法(Stochastic Gradient Descent),会导致
\(R_{emp}\)
难以达到最小值,而且在数值处理上不能使用
向量化的方法提高运算速度

于是,我们采取一种折衷的想法,即取一部分数据,作为全部数据的代表,让神经网络从这每一批数据中学习,这里的“一部分数据”称为mini-batch,这种方法称为mini-batch学习。

以下图为例,蓝色的线表示Batch Gradient Descent,紫色的线表示Stochastic Gradient Descent,绿色的线表示Mini-Batch Gradient Descent。
image

从上图可以看出,Mini-Batch相当于结合了Batch Gradient Descent和Stochastic Gradient Descent各自的优点,既能利用向量化方法提高运算速度,又能基本接近全局最小值。

对于mini-batch学习的介绍到此为止。下面我们将MINIST数据集上的分类问题作为背景,以交叉熵cross-entropy损失函数为例,来实现一下mini-bacth版的cross-entropy error。

给出cross-entropy error的定义如下:

\[E = - \sum_{k}t_k \log(y_k)\tag{1}
\]

其中
\(y_k\)
表示神经网络输出,
\(t_k\)
表示正确解标签。

等式1表示的是针对单个数据的损失函数,现在我们给出在mini-batch下的损失函数,如下

\[E = -\frac{1}{N}\sum_{n}\sum_{k}t_{nk}\log(y_{nk})\tag{2}
\]

其中N表示这一部分数据的数量,
\(t_{nk}\)
表示第n个数据在第k个元素的值(
\(y_{nk}\)
表示神经网络输出,
\(t_{nk}\)
表示监督数据)

我们来看一下用Python如何实现mini-batch版的cross-entropy error。针对监督数据
\(t_{nk}\)
的标签形式是否为one-hot,我们分类讨论处理。

此外,需要明确的一点是,对于一个分类神经网络,最后一层经过softmax函数处理后,输出
\(y_{nk}\)
是一个
\(n\)
x
\(k\)
的矩阵,
\(y_{ij}\)
表示第i个数据被预测为
\(j(0 \leq j\leq10)\)
的概率,特别地,当
\(N=1\)
时,
\(y\)
是一个包含10个元素的向量,类似于[0.1,0.2...0.3],其中0.1表示输入数据预测为0的概率为0.1,0.2表示将输入数据预测为1的概率为0.2,其他情况以此类推。

首先,对于
\(t_{nk}\)
为one-hot表示的情况,代码块1如下

def cross_entropy_error(y,t):
    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + 1e-7)) / batch_size

在上面的代码中,我们在y上加了一个微小值,防止出现np.log(0)的情况,因为np.log(0)会变成负无穷大-inf,从而导致后续的计算无法继续进行。在等式2中
\(y_{nk}\)

\(t_{nk}\)
下标相同,所以我们直接使用
*
做element-wise运算,即对应元素相乘。

但当我们希望同时能够处理单个数据和批量数据时,代码块1还不能满足我们的要求。因为当
\(N=1\)
时,
\(y\)
是一个包含10个元素的一维向量,输入到函数中,batch_size将等于10而不是1,于是我们将代码块1进行进一步完善,如下:

def cross_entropy_error(y,t):
    if y.ndim == 1:
        y = y.reshape(1,y.size)
        t = t.reshape(1,t.size)
        
    batch_size = y.shape[0]
    return -np.sum(t * np.log(y + 1e-7)) / batch_size

最后,来讨论一下
\(t_{nk}\)
为非one-hot表示的情况。在one-hot情况的计算中,t为0的元素cross-entropy error也为0,所以对于这些元素的计算可以忽略。换言之,在非one-hot表示的情况下,我们只需要计算正确解标签的交叉熵误差即可。代码如下:

def cross_entropy_error(y,t):
    if y.ndim == 1:
        y = y.reshape(1,y.size)
        t = t.reshape(1,t.size)
        
    batch_size = y.shape[0]
    return -np.sum(1 * np.log(y[np.arange(batch_size),t]+1e-7))/batch_size

在上面的代码中,
y[np.arange(batch_size),t]
表示将从神经网络的输出中抽出与正确解标签相对应的元素。

参考文献

[1]
深度学习入门
[2]
DeepLearning.ai深度学习课程笔记
[3]
统计学习方法

img

1 Broker

Kafka集群包含一个或多个服务器,服务器节点称为broker。

如图,我们有2个broker,6个partition,则会均分;如果只有1个partition,那么另一个broker会闲置。

理想情况,我们希望broker数量等于partition数量,然后每个partition对应一块硬盘,那样能保证顺序读写的吞吐量最大化。

具体的数量安排请看:
https://www.cnblogs.com/HappyTeemo/p/17109381.html

2 Topic

每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)

  • 如果我们使用随机策略,则生产者投递到哪个partition是随机的。
  • 我也可以制定生产者1的消息固定就投递到partition1中。
  • kafka只保证partition内部的顺序性,如果我们要顺序执行,可以使用哈希算法,比如用userId这样的标志,将他的消息都投递到固定的partition上。
  • 总之,我们可以自由控制消息的投递算法。

3 Partition

topic中的数据分割为一个或多个partition。每个topic至少有一个partition。每个partition中的数据使用多个segment文件存储。

partition中的数据是有序的,不同partition间的数据丢失了数据的顺序。如果topic有多个partition,消费数据时就不能保证数据的顺序。在需要严格保证消息的消费顺序的场景下,需要将partition数目设为1。

关于偏移量offest

img

4 Producer

生产者即数据的发布者,该角色将消息发布到Kafka的topic中。broker接收到生产者发送的消息后,broker将该消息
追加
到当前用于追加数据的segment文件中。生产者发送的消息,存储到一个partition中,生产者也可以指定数据存储的partition。

轮训算法

随机算法

哈希算法

5 Consumer

消费者可以从broker中读取数据。消费者可以消费多个topic中的数据。

6 Consumer Group

每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group
name则属于默认的group)。

这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个consumer)的手段。一个topic可以有多个CG。topic的消息会复制给consumer。如果需要实现广播,只要每个consumer有一个独立的CG就可以了。要实现单播只要所有的consumer在同一个CG。用CG还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic。

7 Leader

每个partition有多个副本,其中有且仅有一个作为Leader,Leader是当前负责数据的读写的partition。

8 Follower

Follower跟随Leader,所有写请求都通过Leader路由,数据变更会广播给所有Follower,Follower与Leader保持数据同步。

ISR:
in-sync-replica,处于同步状态的副本集合,是指副本数据和主副本数据相差在一定返回(时间范围或数量范围)之内的副本,当然主副本肯定是一直在ISR中的。 当主副本挂了之后,新的主副本将从ISR中被选出来接替它的工作。

OSR:
和IRS相对应 out-sync-replica,其实就是指那些不在ISR中的副本。

9 Offset

kafka的存储文件都是按照offset.kafka来命名,用offset做名字的好处是方便查找。例如你想找位于2049的位置,只要找到2048.kafka的文件即可。当然the first offset就是00000000000.kafka

消息 Message

一条消息包含key和value,value是具体信息,key主要是用来指定写入分区的策略。

比如为键生成一个一致性性散列值,然后使用散列值对主题分区数进行取模,为消息选取分区。

批次

批次就是一组消息,用于减少网络开销。网络开销和CPU往往需要取平衡。

流水线插件
是基于 Rainbond
插件体系
扩展实现,通过插件化的方式,可以实现对 Rainbond 构建体系的扩展。该插件由社区合作伙伴
拓维信息
参与开发并贡献,底层是基于 GitLab CI/CD 实现。

流水线构建与 Rainbond 源码构建的区别是:

  • Rainbond 源码构建:使用简单,固定的构建模式,用户只需提供源代码,但不是很灵活。
  • 流水线构建:自定义构建步骤,使用更加灵活。

本文将介绍使用流水线插件部署 RuoYi SpringBoot 项目,并实现提交代码后自动构建、自动部署。

安装 GitLab 和 Runner

流水线插件是基于 GitLab 实现,所以需要依赖 GitLab 和 GitLab Runner,如果已有则可跳过此步。

通过 Rainbond 开源应用商店部署 GitLab 和 Runner,进入到
平台管理 -> 应用市场 -> 开源应用商店
中分别搜索
GitLab

GitLab-runner
,选择版本进行安装,分别安装到同一个应用内。

部署完成后,访问 GitLab 默认的域名进行用户注册。然后关闭 GitLab 默认的 AutoDevOps:
Admin -> Settings -> CI/CD -> Continuous Integration and Deployment
取消勾选
Default to Auto DevOps pipeline for all projects

注册 Runner

GitLab 和 Runner 都部署完成后,需要将 Runner 注册到 GitLab 中。

进组 Runner
组件内 -> Web 终端
,执行以下命令进行注册:

  • <URL>
    为 GitLab 访问地址
  • <TOKEN>
    在 GitLab 的
    Admin -> Runners
    获取
    Registration token
  • <TAG>
    自定义 Runner 的标签。
gitlab-runner register \
  --non-interactive \
  --executor "docker" \
  --docker-image alpine:latest \
  --url "<URL>" \
  --registration-token "<TOKEN>" \
  --description "docker-runner" \
  --tag-list "<TAG>" \
  --run-untagged="true" \
  --locked="false" \
  --docker-volumes /var/run/docker.sock:/var/run/docker.sock \
  --docker-volumes /root/.m2/repository \
  --docker-privileged="true" \
  --access-level="not_protected" \
  --docker-pull-policy="if-not-present"

注册完成后,可以在
Admin -> Runners
页面中看到如下图,
Status

online
则正常。

安装流水线插件

通过 Rainbond 开源应用商店部署 Pipeline 应用插件,进入到
平台管理 -> 应用市场 -> 开源应用商店
中搜索
Pipeline
,选择对应的版本进行部署。

安装完成后,需要修改 Pipeline-Backend 服务的配置,进入到
Pipeline 应用内 -> Pipeline-Backend组件内
,修改以下环境变量:

  • RAINBOND_URL:Rainbond 控制台访问地址,例如:
    http://192.168.3.33:7070
  • RAINBOND_TOKEN:Rainbond 控制台的 Token,可以在
    右上角用户 -> 个人中心 -> 访问令牌
    中获取。

修改完成后,更新或重启 Backend 组件生效。

进入到
Pipeline 应用内 -> k8s 资源 -> 编辑 rainbond-pipeline
,修改
pipeline
资源中的
access_urls
配置,修改为
Pipeline-UI
组件的对外访问地址,如下:

apiVersion: rainbond.io/v1alpha1
kind: RBDPlugin
metadata:
  labels:
    plugin.rainbond.io/name: pipeline
  name: pipeline
spec:
  access_urls:
  - https://custom.com
  alias: Pipeline
  author: Talkweb
  description: 该应用插件是基于 GitLab CI/CD 实现,扩展 Rainbond 已有的构建体系。
  icon: https://static.goodrain.com/icon/pipeline.png
  version: 1.0.0

修改完成后,就可以在每个团队视图中看到
流水线
按钮选项了。

部署 RuoYi 项目

将 Gitee 中的
RuoYi
项目 Fork 到私有的 GitLab 中。

修改项目配置文件中的
mysql
连接地址:

# ruoyi-admin/src/main/resources/application-druid.yml
......
spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        druid:
            # 主库数据源
            master:
                url: jdbc:mysql://${MYSQL_HOST}:3306/ry?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
                username: root
                password: root

部署 MySQL

通过 Rainbond 开源应用商店部署 MySQL 即可。部署之后打开 MySQL 对外服务端口,通过本地工具连接到数据库并创建
ry
数据库和初始化 sql 目录下的
quartz.sql

ry_20230223.sql

部署 RuoYi SpringBoot

进入到
团队视图 -> 流水线

1.创建流水线

进入流水线管理,选择 Java Maven 单模块的模版创建。

如果没有 SonarQube 代码扫描步骤可以删除,修改
编译构建物
步骤:

  • 制品目录:ruoyi-admin/target/*.jar

修改
构建镜像
步骤:

  • 脚本命令:

    cp ruoyi-admin/target/*.jar app.jar
    docker login -u ${REPOSITORY_USERNAME} -p ${REPOSITORY_PASSWORD} ${REPOSITORY_URL}
    docker build -t  ${REPOSITORY_URL}/${ORG}/${MODULE}:${DEVOPS_VERSION} .
    docker push ${REPOSITORY_URL}/${ORG}/${MODULE}:${DEVOPS_VERSION}
    

在流水线的变量内,指定 Docker 相关的环境变量用于打包镜像和推送镜像:

  • REPOSITORY_URL:镜像仓库地址,如:registry.cn-hangzhou.aliyuncs.com
  • ORG:镜像仓库组织,例如:goodrain
  • REPOSITORY_USERNAME:镜像仓库用户名
  • REPOSITORY_PASSWORD:镜像仓库密码

2.创建应用服务

  • 服务编码:唯一的
  • 服务名称:自定义
  • 流水线:选择流水线模版
  • 仓库配置:填写仓库地址,如:
    http://gitlab.test.com/root/ruoyi.git
  • 认证配置:可选用户密码或Token

创建应用服务后,可在 GitLab 仓库内看到多了两个文件
Dockerfile

.gitlab-ci.yml
,这是由流水线插件服务自动生成并提交到仓库内。

3.构建服务

进入
代码管理
,应用服务选择
ruoyi
,点击
构建
按钮开始构建。可以在持续集成页面看到构建状态以及步骤,点击步骤可跳转至 GitLab 详情页。

4. 部署后端服务

等待构建完成后,即可在镜像仓库中看到构建的镜像版本,接下来就可以通过该版本进行部署,可选择部署到当前团队下的哪个应用内。

部署完成后,可在部署历史页面看到部署历史,点击部署详情跳转到 Rainbond 组件内。

编辑依赖关系

接下来进入到应用内,切换到编排模式将
ruoyi
服务依赖至 MySQL 服务,并更新 ruoyi 组件。

进入到 ruoyi 组件内 -> 端口,添加 80 端口并打开对外服务,即可通过默认的域名访问到 ruoyi UI。

配置自动构建和自动部署

编辑已经创建的应用服务,打开自动构建和自动部署按钮,下次提交代码时将会自动触发整个流程。

最后

通过流水线插件可以更灵活的扩展构建过程,比如增加代码扫描、构建成功后的消息通知等等。流水线插件也会持续迭代,欢迎大家安装使用!

简介

自从JDK中引入了stream之后,仿佛一切都变得很简单,根据stream提供的各种方法,如map,peek,flatmap等等,让我们的编程变得更美好。

事实上,我也经常在项目中看到有些小伙伴会经常使用peek来进行一些业务逻辑处理。

那么既然JDK文档中说peek方法主要是在调试的情况下使用,那么peek一定存在着某些不为人知的缺点。一起来看看吧。

peek的定义和基本使用

先来看看peek的定义:

    Stream<T> peek(Consumer<? super T> action);

peek方法接受一个Consumer参数,返回一个Stream结果。

而Consumer是一个FunctionalInterface,它需要实现的方法是下面这个:

    void accept(T t);

accept对传入的参数T进行处理,但是并不返回任何结果。

我们先来看下peek的基本使用:

    public static void peekOne(){
        Stream.of(1, 2, 3)
                .peek(e -> log.info(String.valueOf(e)))
                .toList();
    }

运行上面的代码,我们可以得到:

[main] INFO com.flydean.Main - 1
[main] INFO com.flydean.Main - 2
[main] INFO com.flydean.Main - 3

逻辑很简单,就是打印出Stream中的元素而已。

peek的流式处理

peek作为stream的一个方法,当然是流式处理的。接下来我们用一个具体的例子来说明流式处理具体是如何操作的。

    public static void peekForEach(){
        Stream.of(1, 2, 3)
                .peek(e -> log.info(String.valueOf(e)))
                .forEach(e->log.info("forEach"+e));
    }

这一次我们把toList方法替换成了forEach,通过具体的打印日志来看看到底发生了什么。

[main] INFO com.flydean.Main - 1
[main] INFO com.flydean.Main - forEach1
[main] INFO com.flydean.Main - 2
[main] INFO com.flydean.Main - forEach2
[main] INFO com.flydean.Main - 3
[main] INFO com.flydean.Main - forEach3

通过日志,我们可以看出,流式处理的流程是对应流中的每一个元素,分别经历了peek和forEach操作。而不是先把所有的元素都peek过后再进行forEach。

Stream的懒执行策略

之所有会有流式操作,就是因为可能要处理的数据比较多,无法一次性加载到内存中。

所以为了优化stream的链式调用的效率,stream提供了一个懒加载的策略。

什么是懒加载呢?

就是说stream的方法中,除了部分terminal operation之外,其他的都是intermediate operation.

比如count,toList这些就是terminal operation。当接受到这些方法的时候,整个stream链条就要执行了。

而peek和map这些操作就是intermediate operation。

intermediate operation的特点是立即返回,如果最后没有以terminal operation结束,intermediate operation实际上是不会执行的。

我们来看个具体的例子:

    public static void peekLazy(){
        Stream.of(1, 2, 3)
                .peek(e -> log.info(String.valueOf(e)));
    }

运行之后你会发现,什么输出都没有。

这表示peek中的逻辑并没有被调用,所以这种情况大家一定要注意。

peek为什么只被推荐在debug中使用

如果你阅读过peek的文档,你可能会发现peek是只被推荐在debug中使用的,为什么呢?

JDK中的原话是这样说的:

In cases where the stream implementation is able to optimize away the production of some or all the elements (such as with short-circuiting operations like findFirst, or in the example described in count), the action will not be invoked for those elements.

翻译过来的意思就是,因为stream的不同实现对实现方式进行了优化,所以不能够保证peek中的逻辑一定会被调用。

我们再来举个例子:

    public static void peekNotExecute(){
        Stream.of(1, 2, 3)
                .peek(e -> log.info("peekNotExecute"+e))
                .count();
    }

这里的terminal operation是count,表示对stream中的元素进行统计。

因为peek方法中参数是一个Consumer,它不会对stream中元素的个数产生影响,所以最后的运行结果就是3。

peek中的日志输出并没有打印出来,表示peek没有被执行。

所以,我们在使用peek的时候,一定要注意peek方法是否会被优化。要不然就会成为一个隐藏很深的bug。

peek和map的区别

好了,讲到这里,大家应该对peek有了一个全面的认识了。但是stream中还有一个和peek类似的方法叫做map。他们有什么区别呢?

前面我们讲到了peek方法需要的参数是Consumer,而map方法需要的参数是一个Function:

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);

Function也是一个FunctionalInterface,这个接口需要实现下面的方法:

    R apply(T t);

可以看出apply方法实际上是有返回值的,这跟Consumer是不同的。所以一般来说map是用来修改stream中具体元素的。 而peek则没有这个功能。

peek方法接收一个Consumer的入参. 了解λ表达式的应该明白 Consumer的实现类应该只有一个方法,该方法返回类型为void. 它只是对Stream中的元素进行某些操作,但是操作之后的数据并不返回到Stream中,所以Stream中的元素还是原来的元素.

map方法接收一个Function作为入参. Function是有返回值的, 这就表示map对Stream中的元素的操作结果都会返回到Stream中去.

  • 要注意的是,peek对一个对象进行操作的时候,虽然对象不变,但是可以改变对象里面的值。

大家可以运行下面的例子:

    public static void peekUnModified(){
        Stream.of(1, 2, 3)
                .peek(e -> e=e+1)
                .forEach(e->log.info("peek unModified"+e));
    }

    public static void mapModified(){
        Stream.of(1, 2, 3)
                .map(e -> e=e+1)
                .forEach(e->log.info("map modified"+e));
    }

总结

以上就是对peek的总结啦,大家在使用的时候一定要注意存在的诸多陷阱。

本文的例子
https://github.com/ddean2009/learn-java-base-9-to-20/tree/master/peek-and-map/

更多文章请看
www.flydean.com

〇、简介

谈及 RSA 加密算法,我们就需要先了解下这两个专业名词,对称加密和非对称加密。

  • 对称加密:在同一
    密钥
    的加持下,发送方将未加密的
    原文
    ,通过算法加密成
    密文
    ;相对的接收方通过算法将
    密文
    解密出来
    原文
    的过程,就是对称加密算法。

  • 非对称加密:发送发和接收方
    通过不同的密钥加解密
    的过程就是非对称加密。发送方通过
    公钥
    加密后的
    密文
    ,接收方通过
    私钥
    解密密文成
    明文
    。公钥就是公开的,让全部发送方使用,私钥是保密的,不能公开,专门供接收方解密收到的密文,没有私钥的第三方就无法解密密文,从而保证了数据传输的安全性。

非对称加密的代表算法是 RSA 算法,其是
目前最有影响力的公钥加密算法,并且被普遍认为是目前最优秀的公钥方案之一
。本文也将做专门介绍。

RSA 公钥加密算法是 1977 年由 Ron Rivest、Adi Shamirh 和 Len Adleman 在美国麻省理工学院开发的,RSA 取名来自开发他们三者的名字。

RSA 是第一个能同时用于加密和数字签名的算法,
它能够抵抗到目前为止已知的所有密码攻击,已被 ISO 推荐为公钥数据加密标准

RSA 公开密钥
密码体制的原理
是:根据数论,寻求两个大素数比较简单,而将它们的乘积进行因式分解却极其困难,因此可以将乘积公开作为加密密钥。

强大的加密算法也存在一些缺点:

  • 产生密钥很麻烦,受到素数产生技术的限制,因而难以做到一次一密(密钥只能使用一次,永远不对其它消息重复使用)。
  • 分组长度太大,为保证安全性,n 至少也要 600bits 以上,使运算代价很高,尤其是速度较慢,较对称密码算法慢几个数量级;且随着大数分解技术的发展,这个长度还在增加,不利于数据格式的标准化。目前,SET(Secure Electronic Transaction) 协议中要求 CA 采用 2048bits 长的密钥,其他实体使用 1024bits 的密钥。
  • RSA 密钥长度随着保密级别提高,增加很快。

同样的明文经 RSA 公钥加密后的结果,每次都不同。
下面简单说明一下:

不管是使用RSA私钥进行签名还是公钥进行加密,操作中都需要对待处理的数据先进行填充,然后再对填充后的数据进行加密处理。

EB = 00 || BT || PS || 00 || D

  • D: data (指待处理数据,即填充前的原始数据)
  • PS: padding string (填充字符串)
  • BT: block type (数据块类型)
  • EB: encryption block (待加密的数据块,经过填充后结果)
  • ||: 表示连接操作 (X||Y 表示将 X 和 Y 的内容连接到一起)

"填充后数据" = "00" + "数据块类型" + "填充字符串" + "00" + "原始数据"

对私钥处理的数据,BT 取值为 00 或 01:

  • BT 取值为 00 时,PS 为全 00 的字符串;
  • BT 取值为 01 时,PS 为全 FF 的字符串,
    通过填充得到的整数会足够大,可以阻止某些攻击,因此也是推荐的填充方式。

针对公钥处理的数据,BT 取值为 02:

  • 使用伪随机的 16 进制字符串填充 PS,而且每次操作进行填充的伪随机书都是独立的。

可见,针对公钥处理的数据,其
填充内容为伪随机(限制范围的随机数)的 16 进制字符串,每次操作的填充内容都不一样
。因此每次使用公钥加密数据得到的结果就不一样了。

参考: 为什么RSA公钥每次加密得到的结果都不一样? 通俗易懂的对称加密与非对称加密原理浅析

一、C# 语言实现

首先需要安装一个包:
BouncyCastle.NetCore

注意:检验密文是的选项可参考以下值,Hash:SHA-256;MGFHash:SHA-256;填充模式:ENCRYPTION_PKCS1。

// 测试
RsaSecretKey rSASecretKey = SecurityRSA.GenerateRsaSecretKey(1024);
// 密钥示例:(每次结果都不同)
// MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAL8xl2yVjjt0DRzJMhq7WZrOP+8Vivuf+Ut3AxuhKoiD6yQGrgzLLQz7tQ30WuucDgzAIDnhFCyxkydMkrpGpd1sN/d8F2n8VE4zj9Tus2eFYQZeKSvw1fKDWiuy2j2yNOHjyKUtALeX4UNViizRRgK407v84orQk8607UfzfrGtAgMBAAECgYBBt0vy2JzgtozjPgxov8iWuxmilecFggDv/WImFwlFjwI9icY9Q4Cim8mpmDnADg2OOGNbQY/rpMWNlnZAbJQJo+TG/J2n3klWzC5KM5O289faw/EguSl3MChqvunvZZqMfSAcqpAxjj4aZHyWDBhsgJZtZNbKBdn5t2JnGdbVSQJBAP3/P6s/g83jhvahNML2sr+fKBMIq6++1UdX0ZI0GusoT/dLWdSGz0T8i4YvIsHC8a//OVBBUsZr9Vdj6C57EX8CQQDAs49RsDsk5zsj30GeVtPTYr1FJn50keqkrptp5dHd0xBZCaZqCUKCOD2txfl7srNJk0cQUX9bXhA36xTxJ7rTAkAwAeWj1X5xFNc2mGOjkgNZCpkFd/cTYatoL6YRzz1jQxxSLnDNJanZbS5l71TPcKxDyqanj6E4lcEqglypJGO7AkBsDUMzvumrC61xs+ILcwxb32XZvHfzzU4RAYdLnf5Lr+newzZ5BrAwbHDJW9VEszMs8lRKpigPh3L4p+yaPHjZAkACSoguX4h81aZWQz5jMxEWsGrydqz5H5+NCPI0uuaZBGLc9mFclhLgKfXC2eh24lKkvxWmjuM10lA+Bl/GDeWH
// MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/MZdslY47dA0cyTIau1mazj/vFYr7n/lLdwMboSqIg+skBq4Myy0M+7UN9FrrnA4MwCA54RQssZMnTJK6RqXdbDf3fBdp/FROM4/U7rNnhWEGXikr8NXyg1orsto9sjTh48ilLQC3l+FDVYos0UYCuNO7/OKK0JPOtO1H836xrQIDAQAB
string jiamih = SecurityRSA.RsaEncrypt(rSASecretKey.PublicKey, "TestString测试");
// 加密后 Base64 编码结果示例:(每次结果都不同)
// Mync8QBZFLx30wYAOjsiO2+GYdI0pVABDPJ7S/eBSt6+PNVkmSGudEXIDUWBWu4kWwpbTeKpolQCOR+O060eZWD6bFCRovX4AHQAodqGyT/9Q1b0u2YgWhyV1NeaJebm+3QZp9HjMHFzRjoBiGv/v3CEF2I/1SiKz0yfpLGwlwg=
string jiemih = SecurityRSA.RsaDecrypt(rSASecretKey.PrivateKey, jiamih);

/// <summary>
/// RSA 密钥生成及加解密
/// </summary>
public class SecurityRSA
{
    /// <summary>
    /// RSA 加密
    /// </summary>
    /// <param name="xmlpublickey"></param>
    /// <param name="content"></param>
    /// <returns></returns>
    public static string RsaEncrypt(string xmlpublickey, string content)
    {
        string encryptedcontent = string.Empty;
        using (RSACryptoServiceProvider rSACryptoServiceProvider = new RSACryptoServiceProvider())
        {
            rSACryptoServiceProvider.FromXmlString(RSAPublicKeyBase64ToXml(xmlpublickey));
            byte[] encrypteddata = rSACryptoServiceProvider.Encrypt(Encoding.Default.GetBytes(content), false);
            encryptedcontent = Convert.ToBase64String(encrypteddata);
        }
        return encryptedcontent;
    }
    /// <summary>
    /// RSA 解密
    /// </summary>
    /// <param name="xmlprivatekey"></param>
    /// <param name="content"></param>
    /// <returns></returns>
    public static string RsaDecrypt(string xmlprivatekey, string content)
    {
        string decryptedcontent = string.Empty;
        using (RSACryptoServiceProvider rSACryptoServiceProvider = new RSACryptoServiceProvider())
        {
            rSACryptoServiceProvider.FromXmlString(RSAPrivateKeyBase64ToXml(xmlprivatekey));
            byte[] decryptedData = rSACryptoServiceProvider.Decrypt(Convert.FromBase64String(content), false);
            decryptedcontent = Encoding.GetEncoding("UTF-8").GetString(decryptedData);
        }
        return decryptedcontent;
    }
    /// <summary>
    /// 生成 RSA 公钥和私钥
    /// </summary>
    /// <param name="keysize">目前 SET(Secure Electronic Transaction)协议中要求 CA 采用 2048bits 长的密钥,其他实体使用 1024bits 的密钥</param>
    /// <returns></returns>
    public static RsaSecretKey GenerateRsaSecretKey(int keysize)
    {
        RsaSecretKey rSASecretKey = new RsaSecretKey();
        using (RSACryptoServiceProvider rSACryptoServiceProvider = new RSACryptoServiceProvider(keysize))
        {
            rSASecretKey.PrivateKey = RSAPrivateKeyXmlToBase64(rSACryptoServiceProvider.ToXmlString(true));
            rSASecretKey.PublicKey = RSAPublicKeyXmlToBase64(rSACryptoServiceProvider.ToXmlString(false));
        }
        return rSASecretKey;
    }
    /// <summary>
    /// XML 字符串转 Base64 编码的字符串(公钥)
    /// </summary>
    /// <param name="publicKey"></param>
    /// <returns></returns>
    public static string RSAPublicKeyXmlToBase64(string publicKey)
    {
        XmlDocument doc = new XmlDocument();
        doc.LoadXml(publicKey);
        Org.BouncyCastle.Math.BigInteger m = new Org.BouncyCastle.Math.BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Modulus")[0].InnerText));
        Org.BouncyCastle.Math.BigInteger p = new Org.BouncyCastle.Math.BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Exponent")[0].InnerText));
        RsaKeyParameters pub = new RsaKeyParameters(false, m, p);
        SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(pub);
        byte[] serializedPublicBytes = publicKeyInfo.ToAsn1Object().GetDerEncoded();
        return Convert.ToBase64String(serializedPublicBytes);
    }
    /// <summary>
    /// XML 字符串转 Base64 编码的字符串(私钥)
    /// </summary>
    /// <param name="privateKey"></param>
    /// <returns></returns>
    public static string RSAPrivateKeyXmlToBase64(string privateKey)
    {
        XmlDocument doc = new XmlDocument();
        doc.LoadXml(privateKey);
        Org.BouncyCastle.Math.BigInteger m = new Org.BouncyCastle.Math.BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Modulus")[0].InnerText));
        Org.BouncyCastle.Math.BigInteger exp = new Org.BouncyCastle.Math.BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Exponent")[0].InnerText));
        Org.BouncyCastle.Math.BigInteger d = new Org.BouncyCastle.Math.BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("D")[0].InnerText));
        Org.BouncyCastle.Math.BigInteger p = new Org.BouncyCastle.Math.BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("P")[0].InnerText));
        Org.BouncyCastle.Math.BigInteger q = new Org.BouncyCastle.Math.BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Q")[0].InnerText));
        Org.BouncyCastle.Math.BigInteger dp = new Org.BouncyCastle.Math.BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("DP")[0].InnerText));
        Org.BouncyCastle.Math.BigInteger dq = new Org.BouncyCastle.Math.BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("DQ")[0].InnerText));
        Org.BouncyCastle.Math.BigInteger qinv = new Org.BouncyCastle.Math.BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("InverseQ")[0].InnerText));
        RsaPrivateCrtKeyParameters privateKeyParam = new RsaPrivateCrtKeyParameters(m, exp, d, p, q, dp, dq, qinv);
        PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKeyParam);
        byte[] serializedPrivateBytes = privateKeyInfo.ToAsn1Object().GetEncoded();
        return Convert.ToBase64String(serializedPrivateBytes);
    }
    /// <summary>
    /// Base64 编码字符串转 XML 字符串(私钥)
    /// </summary>
    /// <param name="privateKey"></param>
    /// <returns></returns>
    public static string RSAPrivateKeyBase64ToXml(string privateKey)
    {
        RsaPrivateCrtKeyParameters privateKeyParam = (RsaPrivateCrtKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKey));
        return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>",
                             Convert.ToBase64String(privateKeyParam.Modulus.ToByteArrayUnsigned()),
                             Convert.ToBase64String(privateKeyParam.PublicExponent.ToByteArrayUnsigned()),
                             Convert.ToBase64String(privateKeyParam.P.ToByteArrayUnsigned()),
                             Convert.ToBase64String(privateKeyParam.Q.ToByteArrayUnsigned()),
                             Convert.ToBase64String(privateKeyParam.DP.ToByteArrayUnsigned()),
                             Convert.ToBase64String(privateKeyParam.DQ.ToByteArrayUnsigned()),
                             Convert.ToBase64String(privateKeyParam.QInv.ToByteArrayUnsigned()),
                             Convert.ToBase64String(privateKeyParam.Exponent.ToByteArrayUnsigned()));
    }
    /// <summary>
    /// Base64 编码字符串转 XML 字符串(公钥)
    /// </summary>
    /// <param name="publicKey"></param>
    /// <returns></returns>
    public static string RSAPublicKeyBase64ToXml(string publicKey)
    {
        RsaKeyParameters publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publicKey));
        return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent></RSAKeyValue>",
                             Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()),
                             Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned()));
    }
}
/// <summary>
/// RSA 密钥类
/// </summary>
public class RsaSecretKey
{
    public RsaSecretKey(string privatekey = "", string publickey = "")
    {
        PrivateKey = privatekey;
        PublicKey = publickey;
    }
    public string PublicKey { get; set; }
    public string PrivateKey { get; set; }
    public override string ToString()
    {
        return string.Format(
            "PrivateKey: {0}\r\nPublicKey: {1}", PrivateKey, PublicKey);
    }
}

参考:
C#的秘钥跟JAVA的密钥区别

二、js 语言实现

本示例采用引入开源的 js 库:JSEncrypt,来实现 RSA 的加解密。另外暂不建议在前端生成密钥,本示例也无示例。

// 先引入 js 库
<script src="https://cdn.bootcdn.net/ajax/libs/jsencrypt/3.3.2/jsencrypt.min.js"></script>
// npm 方式引入
npm install encryptjs --save-dev

// 调用方法 message() 查看测试结果
function message() {
    var publickey = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC/MZdslY47dA0cyTIau1mazj/vFYr7n/lLdwMboSqIg+skBq4Myy0M+7UN9FrrnA4MwCA54RQssZMnTJK6RqXdbDf3fBdp/FROM4/U7rNnhWEGXikr8NXyg1orsto9sjTh48ilLQC3l+FDVYos0UYCuNO7/OKK0JPOtO1H836xrQIDAQAB';//这个是公钥,建议后端生成
    var data_en = RsaEncrypt(publickey, "TestString测试");
    console.log(data_en); // 输出结果:elHQslM7RM9aewSZHetgAJ4X7VNGcpCa9/xFiKv33+QTXy6Utc6Ca4B502ZO2J3zmmSYzk+YOkh8I8NgQFu+s8rYIy1hQjnCaCJI1xWC47vdEfZN79AbX/bmYb0eyjpCaIptIlrIKRPyPDl/H3D/FtNsqVhIEr7mG9a8u+odnus=
    var privatekey = 'MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAL8xl2yVjjt0DRzJMhq7WZrOP+8Vivuf+Ut3AxuhKoiD6yQGrgzLLQz7tQ30WuucDgzAIDnhFCyxkydMkrpGpd1sN/d8F2n8VE4zj9Tus2eFYQZeKSvw1fKDWiuy2j2yNOHjyKUtALeX4UNViizRRgK407v84orQk8607UfzfrGtAgMBAAECgYBBt0vy2JzgtozjPgxov8iWuxmilecFggDv/WImFwlFjwI9icY9Q4Cim8mpmDnADg2OOGNbQY/rpMWNlnZAbJQJo+TG/J2n3klWzC5KM5O289faw/EguSl3MChqvunvZZqMfSAcqpAxjj4aZHyWDBhsgJZtZNbKBdn5t2JnGdbVSQJBAP3/P6s/g83jhvahNML2sr+fKBMIq6++1UdX0ZI0GusoT/dLWdSGz0T8i4YvIsHC8a//OVBBUsZr9Vdj6C57EX8CQQDAs49RsDsk5zsj30GeVtPTYr1FJn50keqkrptp5dHd0xBZCaZqCUKCOD2txfl7srNJk0cQUX9bXhA36xTxJ7rTAkAwAeWj1X5xFNc2mGOjkgNZCpkFd/cTYatoL6YRzz1jQxxSLnDNJanZbS5l71TPcKxDyqanj6E4lcEqglypJGO7AkBsDUMzvumrC61xs+ILcwxb32XZvHfzzU4RAYdLnf5Lr+newzZ5BrAwbHDJW9VEszMs8lRKpigPh3L4p+yaPHjZAkACSoguX4h81aZWQz5jMxEWsGrydqz5H5+NCPI0uuaZBGLc9mFclhLgKfXC2eh24lKkvxWmjuM10lA+Bl/GDeWH';//这个是私钥,建议后端生成
    var data_de = RsaDecrypt(privatekey, data_en);
    console.log(data_de);
}
// 加密
function RsaEncrypt(publickey, encrypt_content) {
    var encryptpk = new JSEncrypt();
    encryptpk.setPublicKey(publickey);
    let result = encryptpk.encrypt(encrypt_content)
        console.log("RsaEncrypt:", result)
        return result;
}
// 解密
function RsaDecrypt(privatekey, decrypt_content) {
    var encryptpk = new JSEncrypt();
    encryptpk.setPrivateKey(privatekey);
    let result = encryptpk.decrypt(decrypt_content)
        console.log("RsaDecrypt:", result)
        return result;
}