nginx实现keyless解决方案

访客4年前黑客文章1481


简介

当企业把业务迁移到云WAF/CDN边缘节点上,需向云厂商提供业务的私钥安全性不能得到保证,且若业务私钥证书发生变化或频繁修改需要受限于人。风险:一旦服务端的私钥泄露会导致恶意攻击者伪造虚假的服务器和客户端通信,通信内容也存在被劫持和解密的风险。keyless源于clouldflare,采用keyless方案私钥部署在客户自己的服务器,无需向把业务私钥部署在云/CDN边缘节点上。

1611405699_600c1983244e1a36d4bc0.png!small?1611405694125

1611405932_600c1a6cc342367592973.png!small?1611405927816



clouldflare keyless项目地址:https://blog.cloudflare.com/keyless-ssl-the-nitty-gritty-technical-details/

cloudflare keyless开源项目地址:https://github.com/cloudflare/keyless


相关基础知识介绍:

加解密套件知识普及

MAC(Message authentication code):消息认证码

PRF(pseudorandom function):伪随机函数

SHA (Secure Hash Algorithm):安全散列算法

  • 对称密码:

DES:是以64比特的明文为一个单位来进行加密的,密钥长度是64比特

三重DES: 就是将DES重复3次,有3个密钥

AES(Advanced Encryption Standard):是一种新标准的对称密码算法,已取代DES

分组长度128比特,密钥长度128、192、256三种规格

  • 分组密码模式 :

ECB(Electronic CodeBook):将明文分组加密之后的结果将直接成为密文分组

CBC(Cipher Block Chaining):密文分组链接的模式

CFB(Cipher FeedBack):密文反馈模式

OFB(Output-Feedback):输入反馈模式

CTR(CounTeR):计数器模式

GCM(Galois Counter Mode):Galois/计数器模式

  • 填充模式:对当明文长度不为分组长度的整数倍时,需要在最后一个分组中填充一些数据使其填满一个分组长度,攻击者会利用这个利用这个在最后一个分组填充一些数据。
  • 单向散列函数

输入的是消息输出的是散列值,任意长度的消息计算出固定长度的散列值,消息不同散列值也不同

应用:MD4、MD5;SHA-2系列(SHA-256,SHA-384,SHA-512,数字表示计算后的散列值长度)

  • 密钥交换算法

RSA本质上是为了解决密钥配送的问题,密钥配送的是配送的是运算对称密钥的关键信息,并不是对称密钥

RSA:这是一个标准的密钥交换算法,在ClientKeyExchange阶段客户端生成预主秘钥,不支持向前保密,并以服务器公钥加密传送给服务器

DHE_RSA:临时Diffie-Hellman(ephemeral Diffie-Hellman, DHE),支持向前保密,缺点是执行缓慢,DHE是一种秘钥协定算法,进行协商的团体都对密钥生成产生作用,并对公共密钥产生作用

ECDHE_RSA和ECDHE_ECDSA:

临时椭圆曲线Diffe-Hellman(ephemeral elliptic curve Diffie-Hellman, ECDHE)密钥交换建立在椭圆曲线加密的基础之上。执行很快而且提供了向前保密,和DHE类似

过滤了一台设备上一天的数据

加密套件

完整名称

条数

比例

ECDHE-RSA-AES128-SHA256

TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256

590549

89.67%

DHE-RSA-AES256-GCM-SHA384

TLS_DHE_RSA_WITH_AES_256_GCM_SHA384

33802

5.13%

ECDHE-RSA-AES256-GCM-SHA384

TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384

18150

2.76%

DHE-RSA-AES256-SHA256

TLS_DHE_RSA_WITH_AES_256_CBC_SHA256

12845

1.95%

ECDHE-RSA-AES256-SHA

TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA

1462

0.22%

ECDHE-RSA-CHACHA20-PO *** 1305

TLS_ECDHE_RSA_WITH_CHACHA20_PO *** 1305_SHA256

1388

0.21%

AES256-SHA256

TLS_RSA_WITH_AES_256_CBC_SHA256

302

0.05%

ECDHE-RSA-AES128-GCM-SHA256

TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256

36

0.01%

DHE-RSA-AES256-SHA

TLS_DHE_RSA_WITH_AES_256_CBC_SHA

23

0%

AES256-SHA

TLS_RSA_WITH_AES_256_CBC_SHA

1

0%

TLS握手中使用的密码技术


TLS记录协议位于TLS协议的下层,是负责使用对称密码对消息进行加密通信(对消息压缩、加密以及数据的认证)的部分

  • TLS握手协议中使用的密码技术

公钥密码:加密预主秘钥用的

单向散列函数:构成伪随机数生成器

数字签名:验证证书用的(单向散列函数计算公钥密码的散列值,加密后得到)

伪随机书生成器:生成预主秘钥

? ? ? ? ? ? ? ? ? ? ? 生成初始化向量(可以使用对称密码,单向散列函数来构建)?

? ? ? ? ? ? ? ? ? ? ?根据主密钥生成密钥(密码参数)?

  • TLS记录协议中使用的密码技术

对称密码(CBC模式):确保片段的机密性

消息认证码:确保片段的完整性并进行认证(单向散列函数和密钥组合而成,也可以通过对称密码生成,应用单向散列函数计算密钥+消息构成的)

认证加密(AEAD,Authenticated-Encryption with Associated-Data用于关联数据的认证加密):确保片段的完整性和机密性并进行认证?

HTTPS中所用到的密码技术

证书:公钥、数字签名和指纹组合而成,一般讲是基于指纹的数字签名,一堆的东西就认证公钥,为了保证不可否认行、认证、完整性


Keyless原理

总体架构

1611396119_600bf417e29ca5ac46d20.png!small?1611396115042

密钥交换算法类

握手协议

密钥建立

认证

RSA

RSA

RSA

DH

DH

RSA/DSA

client random 是第1个随机数R1(公开),对应wireshark抓包里“Client Hello”的Random

server random 是第2个随机数R2(公开),对应wireshark抓包里“Server Hello”的Random

premaster 是第3个随机数R3(私密),该随机数是由客户端创建,然后客户端用服务端传来的证书对premaster secret进行加密,生成premaster secret用来实际传输,对应抓包里的“Client Key Exchange”

服务端用私钥对premaster secret解密,得到premaster,这样只有客户端和服务端知道premaster

最终,客户端和服务端用公开的随机数R1、R2、双方私密的premaster(R3)组合起来,通过预定的算法生成一个hash值,作为之后的对话密钥(session key)

1611396175_600bf44f68a9fba8c550d.png!small?1611396170646


RSA密钥交换算法主密钥计算

Client Random和Server Random明文传输,中间人可以直接查看,客户端生成于中Premaster Secret后,如果有证书私钥就可以直接通过这三个参数解得主密钥

1611396224_600bf480419a9c2d9e060.png!small?1611396219433

标准RSAkeyless握手方案


工作在:Server端的ChangeCipherSpec阶段


基于DH的完整握手主密钥的计算

从密钥交换流程来说,DH算法和ECDHE一样,二者的主要区别见该页备注里的注意点1~3

client random 是第1个随机数R1(公开),对应wireshark抓包里“Client Hello”的Random ②a、server random 是第2个随机数R2(公开),对应wireshark抓包里“Server Hello”的Random

服务端自己创建一个随机数或者 直接从证书中拿公钥信息(图例是拿公钥信息),记为R3 ,结合上面的两个公开的随机数,通过DH算法算出来服务端DH参数=(R1 * R2 * R3) ,对应wireshark抓包里“Server Key Exchange”的Pubkey。

服务端用私钥,对两个公开随机数R1、R2和服务端的DH参数进行签名,对应wireshark抓包里“Server Key Exchange”的Signature

客户端用证书公钥验证Signature,验证服务端确实拥有私钥后,客户端就创建一个随机数,记为R4,通过DH算法算出来客户端DH参数=(R1 * R2 * R4) ,对应wireshark抓包里“Client Key Exchange”的Pubkey 。 这样,客户端和服务端用对方发来的DH参数,结合各自的私有随机数R3或R4,分别计算得到相同的premaster=(R1 * R2 * R3 * R4) ,且只有客户端和服务端知道premaster 最终,客户端和服务端用公开的随机数1、随机数2、双方私密的premaster组合起来,通过预定的算法生成一个hash值,作为之后的对话密钥(session key)

1611396275_600bf4b3cdf317cfc60b1.png!small?1611396271863


Server DH Parameter 是用证书私钥签名的,客户端使用证书公钥就可以验证服务端合法性,相比 RSA 密钥交换,DH 由传递 Premaster Scret 变成了传递 DH 算法所需的 Parameter,然后双方各自算出 Premaster Secret。由于 Premaster Secret 无需交换,中间人就算有私钥也无法获得 Premaster Secret 和 Master Secret。

ServerKeyExchange

基于DH的keyless的完整握手

1611396355_600bf5033a90ed460ea1d.png!small?1611396351125



工作在:Server端的ServerKeyExchange阶段

开源项目做了什么

1611396458_600bf56a473cae682339b.png!small?1611396453411


keyless server安装和配置

存储给定的私钥。

使用加速卡(EXAR)进行解密,签名操作。

状态信息统计。

开源项目地址:https://github.com/cloudflare/keyless

需要进行二次开发,开源版本很多细节处理的不好。

On Centos:

sudo yum install gcc automake libtool
?sudo yum install rpm-build rubybgems ruby-devel # only required for packages
?sudo gem install fpm --no-ri --no-rdoc # only required for packages

安装 make即可,make test测试

参数说明 --port keyless server端的监听端口

? ? ? ? ? --ip keyless server端监听的ip

? ? ? ? ? --server-cert和--server-key签发的证书

? ? ? ? ?--private-key-directory 用户证书对应的私钥存放文件夹

? ? ? ? ? --ca-file生成的根证书

? ? ? ? ? --pid-file pid文件

? ? ? ? ?--num-workers 线程数

? ? ? ? --verbose打印日志

? ? ? --daemon守护进程开启


nginx于keyless server交互?

1611398197_600bfc3567a5d73fd67ff.png!small

在SSL_do_handshake解密和签名处理过程中增加一个keyless状态。

PREPARE REQUEST状态,封装keyless请求报文,然后将状态设置为SEND REQUEST,SSL_do_handshake函数返回,nginx将keyless_connection的wev放到epoll里;

SEND REQUEST状态发送keyless请求,成功后将状态设置为RECEIVE RESPONSE,SSL_do_handshake函数返回,nginx将keyless_connection的rev放到epoll里;

RECEIVE RESPONSE状态读请求,全部读完将状态设置为FINISH;如果未读完数据SSL_do_handshake函数返回,nginx将keyless_connection的rev放到epoll里;

FINISH 继续由openssl原有的逻辑处理。

如果rev和wev超时,则关闭ssl_connection。


nginx 处理https握手

ngx_http_init_connection中recv→handler设置为ngx_http_ssl_handshake,把这个读时间加入到epoll中,重点看handshake这个函数


static void

ngx_http_ssl_handshake(ngx_event_t *rev)

{

...

n=recv(c->fd, (char *) buf, size, MSG_PEEK);

//判断协议

if (n==1) {

if (buf[0] & 0x80 || buf[0]==0x16 ) {

// 获取loc conf和server conf

clcf=ngx_http_get_module_loc_conf(hc->conf_ctx,

ngx_http_core_module);


if (clcf->tcp_nodelay && ngx_tcp_nodelay(c) !=NGX_OK) {

ngx_http_close_connection(c);

return;

}


sscf=ngx_http_get_module_srv_conf(hc->conf_ctx,

ngx_http_ssl_module);

// 调用该函数生成ssl

if (ngx_ssl_create_connection(&sscf->ssl, c, NGX_SSL_BUFFER)

!=NGX_OK)

{

ngx_http_close_connection(c);

return;

}

}

}

...

}



ngx_int_t

ngx_ssl_create_connection(ngx_ssl_t *ssl, ngx_connection_t *c, ngx_uint_t flags)

{

...

sc->session_ctx=ssl->ctx;


sc->connection=SSL_new(ssl->ctx);


if (sc->connection==NULL) {

ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, "SSL_new() failed");

return NGX_ERROR;

}


if (SSL_set_fd(sc->connection, c->fd)==0) {

ngx_ssl_error(NGX_LOG_ALERT, c->log, 0, "SSL_set_fd() failed");

return NGX_ERROR;

}

...

}

之一次收到client hello之后,完成初始化后调用ngx_ssl_handshake,其调用openssl的ssl_do_handshake


ngx_int_t

ngx_ssl_handshake(ngx_connection_t *c)

{

...

n=ngx_ssl_handshake_early_data(c);

n=SSL_do_handshake(c->ssl->connection);

...

}

调用keyless模块的init函数先是获取coremodule的main conf,然后获取到servers,遍历这些servers的上下文中的srv conf配置,然后把sscf→ssl.ctx设置cert_cb为keyless_cert_handler,这个函数在api使用证书的时候会调用。


static ngx_int_t

ngx_http_ssl_keyless_init(ngx_conf_t *cf)

{

...

cmcf=ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);


cscfp=cmcf->servers.elts;


for (s=0; s < cmcf->servers.nelts; s++) {


sscf=cscfp[s]->ctx->srv_conf[ngx_http_ssl_module.ctx_index];

kscf=cscfp[s]->ctx->srv_conf[ngx_http_ssl_keyless_module.ctx_index];

if (sscf->enable==1 && kscf->enable==1) {

//TODO set prev cert callback handler.

kscf->prev_ssl_cert_cb=ngx_http_lua_ssl_cert_handler;

#endif

SSL_CTX_set_cert_cb(sscf->ssl.ctx, ngx_http_ssl_keyless_cert_handler, NULL);


}

...

}

cert handler做了了很多事情,初始化了很多nginx keyless相关的参数,核心在于这个函数新建了一条通向keyserver的连接。


static int

ngx_http_ssl_keyless_cert_handler(ngx_ssl_conn_t *ssl_conn, void *data)

{

...

if (ngx_http_ssl_keyless_get_keyserver_pc(klss) !=NGX_OK) {

ngx_log_error(NGX_LOG_ERR, c->log, 0, "init keyless sess");

}


klss_state->data=klss;

klss_state->state=KEYLESS_STATE_RSA_INIT;


c->klss=klss;

pc=klss->pc;


rc=ngx_event_connect_peer(klss->pc);

pcc=((ngx_peer_connection_t *)klss->pc)->connection;


pcc->data=klss;

pcc->write->handler=ngx_http_ssl_keyless_keyserver_handler;

pcc->read->handler=ngx_http_ssl_keyless_keyserver_handler;

...

}



static ngx_int_t

ngx_http_ssl_keyless_get_keyserver_pc(ngx_http_keyless_sess_t *klss)

{

...

pc->get=ngx_http_ssl_keyless_peer_get;

pc->free=ngx_http_ssl_keyless_peer_free;

...

}


do handshake的时候调用的是openssl的async job的库,相当于新开一个函数栈

ASYNC JOB


int SSL_do_handshake(SSL *s)

{

...

if (SSL_in_init(s) || SSL_in_before(s)) {

if ((s->mode & SSL_MODE_ASYNC) && ASYNC_get_current_job()==NULL) {

struct ssl_async_args args;


args.s=s;


ret=ssl_start_async_job(s, &args, ssl_do_handshake_intern);

} else {

ret=s->handshake_func(s);

}

}

...

}



int ASYNC_start_job(ASYNC_JOB **job, ASYNC_WAIT_CTX *wctx, int *ret,

int (*func)(void *), void *args, size_t size)

{

...

if ((ctx->currjob=async_get_pool_job())==NULL)

return ASYNC_NO_JOBS;

...

}



static ASYNC_JOB *async_get_pool_job(void) {

...

if (job==NULL) {

if ((pool->max_size !=0) && (pool->curr_size >=pool->max_size))

return NULL;


job=async_job_new();

if (job !=NULL) {

if (! async_fibre_makecontext(&job->fibrectx)) {

async_job_free(job);

return NULL;

}

pool->curr_size++;

}

}

...

}

先做初始化get下context,malloc一个stack,这个堆栈创建完成后把函数放进去,使用makecontext来创建一旦调用就会运行该函数,async_start_func本身使用当前job中的func,函数也是传进来的参数


int async_fibre_makecontext(async_fibre *fibre)

{

fibre->env_init=0;

if (getcontext(&fibre->fibre)==0) { //初始化当前ucontext

fibre->fibre.uc_stack.ss_sp=OPENSSL_malloc(STACKSIZE);

if (fibre->fibre.uc_stack.ss_sp !=NULL) {

fibre->fibre.uc_stack.ss_size=STACKSIZE;

fibre->fibre.uc_link=NULL;

makecontext(&fibre->fibre, async_start_func, 0);

return 1;

}

} else {

fibre->fibre.uc_stack.ss_sp=NULL;

}

return 0;

}

Pause?job最关键的是swapcontext,这个在func中一旦被调用的话,就可以立即切换栈信息,切回start_job的主函数,根据job→status=ASYNC_JOB_PAUSING来返回


int ASYNC_pause_job(void)

{

...

if (!async_fibre_swapcontext(&job->fibrectx,

&ctx->dispatcher, 1)) {

ASYNCerr(ASYNC_F_ASYNC_PAUSE_JOB, ASYNC_R_FAILED_TO_SWAP_CONTEXT);

return 0;

}

...

}

切回主函数之后,因为start?job是for死循环,所以会根据job的状态进行返回


int ASYNC_start_job(ASYNC_JOB **job, ASYNC_WAIT_CTX *wctx, int *ret,

int (*func)(void *), void *args, size_t size)

{

...

for (;;) {

if (ctx->currjob !=NULL) {

if (ctx->currjob->status==ASYNC_JOB_PAUSING) {

*job=ctx->currjob;

ctx->currjob->status=ASYNC_JOB_PAUSED;

ctx->currjob=NULL;

return ASYNC_PAUSE;

}

if (ctx->currjob->status==ASYNC_JOB_PAUSED) {

ctx->currjob=*job;

if (!async_fibre_swapcontext(&ctx->dispatcher,

&ctx->currjob->fibrectx, 1)) {

ASYNCerr(ASYNC_F_ASYNC_START_JOB,

ASYNC_R_FAILED_TO_SWAP_CONTEXT);

goto err;

}

continue;

}

...

}



static int ssl_start_async_job(SSL *s, struct ssl_async_args *args,

int (*func) (void *))

{

...

switch (ASYNC_start_job(&s->job, s->waitctx, &ret, func, args,

sizeof(struct ssl_async_args))) {

case ASYNC_ERR:

s->rwstate=SSL_NOTHING;

SSLerr(SSL_F_SSL_START_ASYNC_JOB, SSL_R_FAILED_TO_INIT_ASYNC);

return -1;

case ASYNC_PAUSE:

s->rwstate=SSL_ASYNC_PAUSED;

return -1;

}

...

}

返回的这个状态码,在nginx里面接到就是SSL_ERROR_WANT_ASYNC

关键就是调用async_pause_job交还给nginx来做keyless处理,以及将与keyserver的状态调整为presend。


int kls_rsa_private_decrypt(int flen, const unsigned char *from, unsigned char *to, RSA *rsa, int padding)

{

...

waitctx=ASYNC_get_wait_ctx(job);

ASYNC_WAIT_CTX_get_fd(waitctx, (void *)waitctx, &fd, (void *)&klss_state);

if (klss_state->state==KEYLESS_STATE_RSA_INIT) {

klss_state->is_rsa_decrypt=1;

dec_ctx=&klss_state->dec_ctx;

dec_ctx->from=from;

dec_ctx->to=to;

dec_ctx->flen=flen;

dec_ctx->padding=padding;

klss_state->state=KEYLESS_STATE_RSA_PRE_SEND;

ASYNC_pause_job();

}

...

}

这里处理完交还给nginx,之后nginx 就可以做原本由openssl实现的加解密。

重写engine重写两个关键函数


static int bind_helper(ENGINE *e)

{

kls_rsa_meth=RSA_meth_new("keyless rsa method", RSA_METHOD_FLAG_NO_CHECK);

RSA_meth_set_sign(kls_rsa_meth, kls_rsa_sign);

RSA_meth_set_priv_dec(kls_rsa_meth, kls_rsa_private_decrypt);



if (!ENGINE_set_id(e, engine_keyless_id) ||

!ENGINE_set_name(e, engine_keyless_name) ||

!ENGINE_set_RSA(e, kls_rsa_meth)) {


return 0;

}


return 1;

}


static ENGINE *ENGINE_keyless(void)

{

ENGINE *e=ENGINE_new();

if (e==NULL)

return NULL;

if (!bind_helper(e)) {

ENGINE_free(e);

return NULL;

}

return e;

}


void engine_load_keyless_int(void)

{

ENGINE *e=ENGINE_keyless();

if (e==NULL) {

return;

}


ENGINE_add(e);

ENGINE_free(e);

ERR_clear_error();


return;

}

重写的两个函数是sign和priv_dec


struct rsa_meth_st {

int (*rsa_priv_dec) (int flen, const unsigned char *from,

unsigned char *to, RSA *rsa, int padding);

int (*rsa_sign) (int type,

const unsigned char *m, unsigned int m_length,

unsigned char *sigret, unsigned int *siglen,

const RSA *rsa);

}


int kls_rsa_sign(int type, const unsigned char *m, unsigned int m_length, unsigned char *sigret, unsigned int *siglen, const RSA *rsa)

{

...

waitctx=ASYNC_get_wait_ctx(job);

ASYNC_WAIT_CTX_get_fd(waitctx, (void *)waitctx, &fd, (void *)&klss_state);


if (klss_state->state==KEYLESS_STATE_RSA_INIT) {

klss_state->is_rsa_sign=1;

sign_ctx=&klss_state->sign_ctx;

sign_ctx=&klss_state->sign_ctx;

sign_ctx->type=type;

sign_ctx->m=m;

sign_ctx->m_length=m_length;

klss_state->state=KEYLESS_STATE_RSA_PRE_SEND;

ASYNC_pause_job();

}

...

}

调用pause时最重要逻辑是async_fibre_swapcontext,这个函数是用于切换的核心,同时进行初始化操作,把函数放到新开辟栈里运行。


static ossl_inline int async_fibre_swapcontext(async_fibre *o, async_fibre *n, int r)

{

o->env_init=1;


if (!r || !_setjmp(o->env)) {

if (n->env_init)

_longjmp(n->env, 1);

else

setcontext(&n->fibre);

}


return 1;

}


相关文章

Real World CTF 2021 DBaaSadge Writeup

Real World CTF 2021 DBaaSadge Writeup

昨日刚打的RWCTF赛事,感觉题目是十分非常好的,最少这一自然环境下,postgre是绝大多数Web参赛选手的薄弱点,圈里也没什么自动化技术检测工具,因而写这篇WP還是必须的。 因为绝大多数重要的技...

斗鱼皇帝黑客知乎(斗鱼c皇)

斗鱼皇帝黑客知乎(斗鱼c皇)

斗鱼超级皇帝18万主播拿多少 1、万快币主播拿2万快币。主播在平台收益的计算方法是,打赏金额需要扣税20%,打赏的快币需要主播和平台平均分,剩下的就是主播实际的收入了。18万快币减去扣税后,和平台均分...

车辆车损险是什么意思(车损险赔偿范围有哪些

车辆车损险是什么意思(车损险赔偿范围有哪些

最通俗点讲,车损险就是赔你自己的修车钱,如果你没有买,当车辆出现事故的时候需要你自己掏腰包修理,如果购买车损险由保险公司掏钱给你修车。 我们看一下官方的解释: 险期间内,被保险人或其允许的驾驶...

11月金牛座运势2020-2020年11月金牛座运势完整版

11月金牛座运势2020-2020年11月金牛座运势完整版

天蝎座是一个深受五星好评的十二星座,很多人都期待自身能有一个天蝎座的盆友,天蝎座的小伙伴们十分会照料人,不论是哪一个十二星座,大伙儿针对自身的运程還是特别关注的,今年十一月立刻就需要到来,天蝎座在今年...

养狗需要准备什么用品(饲养狗狗必备用品)

养狗需要准备什么用品(饲养狗狗必备用品)

养狗并非易事,想要养好更是难上加难,一个闪亮的狗狗背后,总有着一个或者一个以上的辛勤主人。那么在当个好主人之前,我们应该准备那些东西呢?   1、狗笼,或者航空箱。 狗笼不仅仅是防止狗狗乱跑...

黑客接单入侵赌博网站_被骗后找 黑客 追损的想法不靠谱

在JavaScript中,你的方法是运用名为web worker的东西。 这些web worker与你在其他言语中运用的线程略有不同。 默许状况下,它们不同享内存。 实践上,有一个Envoy含糊测验方...