2024年7月

开心一刻

今天上午,同事群中的刘总私聊我
刘总:你来公司多久了
我:一年了,刘总
刘总:你还年轻,机会还很多,年底了,公司要裁员
刘总语重心长的继续说到:以后我们常联系,无论以后你遇到什么困难,找我,我会尽量帮你!
我:所以了,我是被裁了吗,呵,我爸知道吗?
刘总:知道,今天上午保安部已经出名单了,你爸也在里面
我:我妈知道吗?
刘总:保洁也裁
我:刘总,你做这些事情问过我爷爷吗?
刘总:门卫也裁
我心里咯噔一下,这它妈哒团灭了呀

团灭

安全漏洞

公司的测试部门会定期扫描代码,检测出安全漏洞,导出
Excel
放到群里,各个项目的负责人针对性去修复(升级组件版本),因为某些原因不能修复的,需要给出原因(有些组件版本依赖更高的
JDK
版本,而
JDK
又不能升)。而我负责的项目是基于
Spring Boot 2.7.18
,它依赖的
logback
版本是
1.2.12
,存在安全漏洞
CVE-2023-6378

CVE-2023-6378

我本意是非常拒绝修这玩意的,修的时候得评估影响点,测的时候需要都覆盖到;核心组件的升级不亚于一次重构,开发和测试都得全量测。重点是,修好不算产出,修坏了可是要背锅的,我可是经历过血的教训的:
都说了能不动就别动,非要去调整,出生产事故了吧
,总之还是那句话

能不动就不要动,改好没绩效,改出问题要背锅,吃力不讨好,又不是不能跑

纵使我有万般的不愿,但也不得不修,公司对安全漏洞这一块非常重视,毕竟要给客户留下非常专业的形象。既然避无可避,那就坦然接受,充分评估影响点,做好全面的测试

干就完了

漏洞修复

如何修复,想必大家都知道,剔除掉
spring-boot-starter-logging
依赖,引入新版本依赖,如下所示

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>spring-boot-starter-logging</artifactId>
            <groupId>org.springframework.boot</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
	<groupId>ch.qos.logback</groupId>
	<artifactId>logback-classic</artifactId>
	<version>${logback.version}</version>
</dependency>

升级到哪个版本,就值得仔细斟酌一番了。反正都要升级,那何不升级到最新版?安全漏洞少,甚至暂时没漏洞。那也不是,因为
logback
依赖
JDK
版本,
官方
说明如下

logback与jdk关系

因为项目依赖的
JDK
版本是 8,所以我们将
logback
升级到 1.3 的最新版是最合适的;
logback 1.3.x
依赖的
SLF4J
版本是
2.0.x
,所以最终
pom.xml
调整成如下

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <logback.version>1.3.14</logback.version>
    <slf4j.version>2.0.7</slf4j.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <artifactId>spring-boot-starter-logging</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>${logback.version}</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j.version}</version>
    </dependency>
</dependencies>
logback1.3.14

貌似挺简单的,对吧?编译也不报错,一切都很顺利;一旦你运行,最烦人的
bug
就来了

Exception in thread "main" java.lang.NoClassDefFoundError: org/slf4j/impl/StaticLoggerBinder
	at org.springframework.boot.logging.logback.LogbackLoggingSystem.getLoggerContext(LogbackLoggingSystem.java:304)
	at org.springframework.boot.logging.logback.LogbackLoggingSystem.beforeInitialize(LogbackLoggingSystem.java:118)
	at org.springframework.boot.context.logging.LoggingApplicationListener.onApplicationStartingEvent(LoggingApplicationListener.java:238)
	at org.springframework.boot.context.logging.LoggingApplicationListener.onApplicationEvent(LoggingApplicationListener.java:220)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:178)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:171)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:145)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:133)
	at org.springframework.boot.context.event.EventPublishingRunListener.starting(EventPublishingRunListener.java:79)
	at org.springframework.boot.SpringApplicationRunListeners.lambda$starting$0(SpringApplicationRunListeners.java:56)
	at java.util.ArrayList.forEach(ArrayList.java:1249)
	at org.springframework.boot.SpringApplicationRunListeners.doWithListeners(SpringApplicationRunListeners.java:120)
	at org.springframework.boot.SpringApplicationRunListeners.starting(SpringApplicationRunListeners.java:56)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:299)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1300)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1289)
	at com.qsl.Application.main(Application.java:15)
Caused by: java.lang.ClassNotFoundException: org.slf4j.impl.StaticLoggerBinder
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 17 more

此时,你们会怎么办?本着快速解决
bug
的原则,我们也只能上网查问题

java.lang.ClassNotFoundException: org.slf4j.impl.StaticLoggerBinder

看能不能找到解决办法;可一通查下来,各种尝试,该问题都得不到解决,除非将
logback
版本降到
1.2.x
,可最新的
1.2.13
是有不少安全漏洞的

logback1.2.13漏洞

那没别的办法了,只能去追查问题产生的原因了,找到原因就好对症下药了。问题又来了,如何去查原因了,最直接、最有效的办法就是从异常堆栈信息入手

异常堆栈

鼠标左击
LogbackLoggingSystem.java:304
,然后就来到
spring-boot-2.7.18
的源码

LogbackLoggingSystem_304

这里用到了
StaticLoggerBinder
,在往上滑到
LogbackLoggingSystem

import
部分,
StaticLoggerBinder
的全类路径是

org.slf4j.impl.StaticLoggerBinder

logback 1.2.12
是有这个类的

logback1.2.12_StaticLoggerBinder


logback 1.3.14
不仅没有该类,连
org
包都不存在了

logback1.3.14_StaticLoggerBinder不存在

所以,原因是不是找到了?

spring-boot-2.7.18 依赖 org.slf4j.impl.StaticLoggerBinder,而 logback 1.3.14 没有该类

那如何对症下药了?不仅你们懵,我也懵

懵

调整下思路,这个问题我们肯定不是第一个遇到的,对吧,肯定有人在
spring-boot
的官方提问,我们去搜搜
org/slf4j/impl/StaticLoggerBinder

springboot2.7.x不支持logback1.3.x

点进去
,里面有官方人员给出的答复,我给大家翻译一下;提问者是
LSmyrnaios
,他做了一下背景介绍

logback 1.3.x 基于 Java 8,1.4.x 基于 Java-11,而 Spring Boot 只在 3.x.x 中集成了 logback 1.4.x(基于 Java-11)

Java-8 用户被遗忘了

根据 logback 文档说明,logback 同时维护 1.3.x 和 1.4.x,也就是说,logback 1.3.x 是 '活跃的',Spring Boot 2.7.x 应该集成它

请考虑以下案例:

我有一个Java-8应用程序,使用 logback v.1.3.6,运行没问题

现在,我想将该应用程序集成到 Spring Boot v.2.7.9,运行的时候胞如下错误:

(异常堆栈跟我们遇到的一样,不展示了)

看起来像是 Spring Boot 用的 slf4j 1.7.x,但是 logback 1.3.x 用的 slf4j 2.0.x,所以 StaticLoggerBinder 类不见了

所以,你们能够在 Spring Boot >= 2.7.x and < 3 的版本中支持 logback 1.3.x 吗

先谢谢了.

这么看来,这哥们遇到的问题跟我们的一样,提出的诉求也跟我们一样,是不是看到了希望?

官方人员
scottfrederick
给出了回复

scottfrederick回复

翻译过来就是

LSmyrnaios 感谢你的联系。 Spring Boot 2.7.x 依赖 Logback 1.2.x。 已经在
第三方升级政策
中说明过了,我们不会在 2.7.x 的版本中升级 Logback到 1.3.x。正如你提到的,我们不仅仅要升级 Logback 到 1.3.x,还需要将 SLF4J 升级到 2.0.x,这有一个关于我们为什么不在 2.7.x 升级的
讨论
,所以我们做补丁发布

第三方依赖升级说明如下

Third-party dependencies

简单点来说就是:第三方依赖的补丁级别的修复,可以在
Spring Boot
的补丁版本中升级,而第三方依赖的次要或者主要版本的升级,则只能在
Spring Boot
的次要或主要版本中升级。不能在
Spring Boot
的补丁版本中升级第三方依赖的次要或者主要版本。这里的补丁版本可以理解成小版本,也就是
1.2.x
中的
x
,而次要或者重要版本,则是
1.x.x
中的第一个
x
,也就是我们所说的大版本

关于
scottfrederick
,他可不是
Spring Boot
的普通
Contributor
,人家可是榜六大哥

榜六

他说的还是很有权威的;关于他提到的
讨论
,我们后面在看,先把当前的看完。提问者
LSmyrnaios
又说了

LSmyrnaios 补充

翻译过来就是

scottfrederick,我们接着刚刚的讨论,我想问的是,是否有可能在 Spring Boot 的下一个大版本(比如 2.8.0,如果在计划中的话)将 SLF4J 升级到 2.0.x,logback 升级到 1.3.x

这对于大量的 Java 8 用户来说非常重要,他们希望为生产系统提供最新的安全和错误修复

先谢谢了

scottfrederick
说的就很符合我们的期望,我们接着往下看。
wilkinsona
给出了回复

wilkinsona回复

翻译过来就是

目前没有 Spring Boot 2.8 的计划

言简意赅,弦外之音就是
Spring Boot 2.x.x
就是不支持
Logback 1.3.x
,满满的任性感。你们是不是很好奇这任性的哥们是谁?人家可是
Spring Boot
的榜一大哥!!!

榜一大哥

是不是觉得他任性的理所当然了?我们继续往下看,
ASarco
说了一句

ASarco

翻译过来就是

现在的问题是 logback 1.2.12 存在安全漏洞 cve-2023-6378,对于 2.7.x 的煞笔用户却没有一个结论

这哥们表达了自己得愤懑,都直接飙国粹(
SB
)了,那是相当的气愤呀

SB 是 Spring Boot 的简写,并非国粹,大家别被误导了!!!

针对
ASarco
的问题,后面有人给了回复,可以升级
Logback

1.2.13
来修复漏洞
cve-2023-6378
,而我们也没得选了,只能将
Logback

1.3.14
降到
1.2.13
,最终的
pom.xml
如下所示

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	<logback.version>1.2.13</logback.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <artifactId>spring-boot-starter-logging</artifactId>
                <groupId>org.springframework.boot</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>${logback.version}</version>
    </dependency>
</dependencies>

所以,
1.2.13
的安全漏洞仍是存在的,下次扫描出来后我们直接说明如下

修复不了,Spring Boot 2.7.x 官方不打算支持 Logback 1.3.x,除非升级 Spring Boot 到 3.x.x(集成的是 Logback 1.4.x),但同时需要将 JDK 升级到 11

讨论

还记得前面提到的那个
讨论
吗,因为比较长,我挑一些重点给大家翻译下

1、
wilkinsona
提到了
Logback
的一次
commit
,这次提交移除了
StaticLoggerBinder

StaticLoggerBinder移除记录


1.3.0-alpha0
版本就移除了
StaticLoggerBinder
,所以
Spring Boot 2.7.x
不能集成
Logback 1.3.x
的任何一个版本

2、
snicoll
(榜二大哥) 提到了一个很重要的点

snicoll_讨论

Spring Boot

LoggingSystem
是可以禁用或者改变的

-Dorg.springframework.boot.logging.LoggingSystem=none

3、
wilkinsona
提到了
spring.factories
中的
ApplicationListener
,其优先级高于
org.springframework.boot.context.logging.LoggingApplicationListener
,可以用来设置系统属性以响应
ApplicationStartingEvent

4、
wilkinsona
针对
cmuchinsky
提出的

对于 spring boot 2.7,是否有可能更新 ogbackLoggingSystemlogback 来兼容 Logback 1.3 与 1.2,例如反射

给出了回答,他认为这不太可能,支持
Logback 1.4
所需的更改范围太广,无法通过反射并行支持 1.2 和 1.3/1.4

5、
zhaolj214
通过读源代码,找到了一种解决方案

@SpringBootApplication
public class Spring5Application {
    public static void main(String[] args) {
        System.setProperty("org.springframework.boot.logging.LoggingSystem", "none");
        SpringApplication.run(Spring5Application.class, args);
    }
}

至于正确与否,我们下篇再试

6、
wilkinsona
说明了可以自定义日志:
howto.logging

总结下来就是:针对
Spring Boot 2.7.x
,官方不会支持
Logback 1.3.x
,但还是可以通过自定义的方式去支持
Logback 1.3.x
,具体如何自定义,以及效果如何,且听下回分解

总结

  1. Logback
    1.3 依赖 JDK 8,1.4 依赖 JDK 11;
    Spring Boot
    2.7.x 依赖
    Logback 1.2.x
    ,而 3.x.x 依赖
    Logback 1.4.x
    。也就说
    Spring Boot
    跳过了
    Logback 1.3.x
  2. Spring Boot
    官方也给出了答复,根据第三方依赖政策(小版本升级小版本,大版本升级大版本),2.7.x 不会支持
    Logback 1.3.x
    ,而 3.x.x 索性直接支持
    Logback 1.4.x
  3. 非要
    Spring Boot 2.7.x
    支持
    Logback 1.3.x
    也不是不可以,需要调整配置,还存在一些限制,具体细节请看下篇

前言

最近在我的
vue源码交流群
有位面试官分享了一道他的面试题:
vue3的ref是如何实现响应式的?
下面有不少小伙伴回答的是
Proxy

其实这些小伙伴只回答对了一半

wx

  • 当ref接收的是一个对象时确实是依靠
    Proxy
    去实现响应式的。

  • 但是ref还可以接收
    string

    number

    boolean
    这样的原始类型,当是原始类型时,响应式就不是依靠
    Proxy
    去实现的,而是在
    value
    属性的
    getter

    setter
    方法中去实现的响应式。

本文将通过debug的方式带你搞清楚当ref接收的是对象和原始类型时,分别是如何实现响应式的。注:本文中使用的vue版本为
3.4.19

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

看个demo

还是老套路,我们来搞个demo,
index.vue
文件代码如下:

<template>
  <div>
    <p>count的值为:{{ count }}</p>
    <p>user.count的值为:{{ user.count }}</p>
    <button @click="count++">count++</button>
    <button @click="user.count++">user.count++</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const count = ref(0);

const user = ref({
  count: 0,
});
</script>

在上面的demo中我们有两个ref变量,
count
变量接收的是原始类型,他的值是数字0。

count
变量渲染在template的p标签中,并且在button的click事件中会
count++

user
变量接收的是对象,对象有个
count
属性。

同样
user.count
也渲染在另外一个p标签上,并且在另外一个button的click事件中会
user.count++

接下来我将通过debug的方式带你搞清楚,分别点击
count++

user.count++
按钮时是如何实现响应式的。

开始打断点

第一步从哪里开始下手打断点呢?

既然是要搞清楚ref是如何实现响应式的,那么当然是给ref打断点吖,所以我们的第一个断点是打在
const count = ref(0);
代码处。这行代码是运行时代码,是跑在浏览器中的。

要在浏览器中打断点,需要在浏览器的source面板中打开
index.vue
文件,然后才能给代码打上断点。

那么第二个问题来了,如何在source面板中找到我们这里的
index.vue
文件呢?

很简单,像是在vscode中一样使用
command+p
(windows中应该是control+p)就可以唤起一个输入框。在输入框里面输入
index.vue
,然后点击回车就可以在source面板中打开
index.vue
文件。如下图:
index

然后我们就可以在浏览器中给
const count = ref(0);
处打上断点了。

RefImpl

刷新页面此时断点将会停留在
const count = ref(0);
代码处,让断点走进
ref
函数中。在我们这个场景中简化后的
ref
函数代码如下:

function ref(value) {
  return createRef(value, false);
}

可以看到在
ref
函数中实际是直接调用了
createRef
函数。

接着将断点走进
createRef
函数,在我们这个场景中简化后的
createRef
函数代码如下:

function createRef(rawValue, shallow) {
  return new RefImpl(rawValue, shallow);
}

从上面的代码可以看到实际是调用
RefImpl
类new了一个对象,传入的第一个参数是
rawValue
,也就是ref绑定的变量值,这个值可以是原始类型,也可以是对象、数组等。

接着将断点走进
RefImpl
类中,在我们这个场景中简化后的
RefImpl
类代码如下:

class RefImpl {
  private _value: T
  private _rawValue: T

  constructor(value) {
    this._rawValue = toRaw(value);
    this._value = toReactive(value);
  }
  get value() {
    trackRefValue(this);
    return this._value;
  }
  set value(newVal) {
    newVal = toRaw(newVal);
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal;
      this._value = toReactive(newVal);
      triggerRefValue(this, 4, newVal);
    }
  }
}

从上面的代码可以看到
RefImpl
类由三部分组成:
constructor
构造函数、
value
属性的
getter
方法、
value
属性的
setter
方法。

RefImpl
类的
constructor
构造函数

constructor
构造函数中的代码很简单,如下:

constructor(value) {
  this._rawValue = toRaw(value);
  this._value = toReactive(value);
}

在构造函数中首先会将
toRaw(value)
的值赋值给
_rawValue
属性中,这个
toRaw
函数是vue暴露出来的一个API,他的作用是根据一个 Vue 创建的代理返回其原始对象。因为
ref
函数不光能够接受普通的对象和原始类型,而且还能接受一个ref对象,所以这里需要使用
toRaw(value)
拿到原始值存到
_rawValue
属性中。

接着在构造函数中会执行
toReactive(value)
函数,将其执行结果赋值给
_value
属性。
toReactive
函数看名字你应该也猜出来了,如果接收的value是原始类型,那么就直接返回value。如果接收的value不是原始类型(比如对象),那么就返回一个value转换后的响应式对象。这个
toReactive
函数我们在下面会讲。

_rawValue
属性和
_value
属性都是
RefImpl
类的私有属性,用于在
RefImpl
类中使用的,而暴露出去的也只有
value
属性。

经过
constructor
构造函数的处理后,分别给两个私有属性赋值了:

  • _rawValue
    中存的是ref绑定的值的原始值。

  • 如果ref绑定的是原始类型,比如数字0,那么
    _value
    属性中存的就是数字0。

    如果ref绑定的是一个对象,那么
    _value
    属性中存的就是绑定的对象转换后的响应式对象。

RefImpl
类的
value
属性的
getter
方法

我们接着来看
value
属性的
getter
方法,代码如下:

get value() {
  trackRefValue(this);
  return this._value;
}

当我们对ref的value属性进行读操作时就会走到
getter
方法中。

我们知道template经过编译后会变成render函数,执行render函数会生成虚拟DOM,然后由虚拟DOM生成真实DOM。

在执行render函数期间会对
count
变量进行读操作,所以此时会触发
count
变量的
value
属性对应的
getter
方法。


getter
方法中会调用
trackRefValue
函数进行依赖收集,由于此时是在执行render函数期间,所以收集的依赖就是render函数。

最后在
getter
方法中会return返回
_value
私有属性。

RefImpl
类的
value
属性的
setter
方法

我们接着来看
value
属性的
setter
方法,代码如下:

set value(newVal) {
  newVal = toRaw(newVal);
  if (hasChanged(newVal, this._rawValue)) {
    this._rawValue = newVal;
    this._value = toReactive(newVal);
    triggerRefValue(this, 4, newVal);
  }
}

当我们对ref的value的属性进行写操作时就会走到
setter
方法中,比如点击
count++
按钮,就会对
count
的值进行
+1
,触发写操作走到
setter
方法中。


setter
方法打个断点,点击
count++
按钮,此时断点将会走到
setter
方法中。初始化
count
的值为0,此时点击按钮后新的
count
值为1,所以在
setter
方法中接收的
newVal
的值为1。如下图:
set

从上图中可以看到新的值
newVal
的值为1,旧的值
this._rawValue
的值为0。然后使用
if (hasChanged(newVal, this._rawValue))
判断新的值和旧的值是否相等,
hasChanged
的代码也很简单,如下:

const hasChanged = (value, oldValue) => !Object.is(value, oldValue);

Object.is
方法大家平时可能用的比较少,作用也是判断两个值是否相等。和
==
的区别为
Object.is
不会进行强制转换,其他的区别大家可以参看mdn上的文档。

使用
hasChanged
函数判断到新的值和旧的值不相等时就会走到if语句里面,首先会执行
this._rawValue = newVal
将私有属性
_rawValue
的值更新为最新值。接着就是执行
this._value = toReactive(newVal)
将私有属性
_value
的值更新为最新值。

最后就是执行
triggerRefValue
函数触发收集的依赖,前面我们讲过了在执行render函数期间由于对
count
变量进行读操作。触发了
getter
方法,在
getter
方法中将render函数作为依赖进行收集了。

所以此时执行
triggerRefValue
函数时会将收集的依赖全部取出来执行一遍,由于render函数也是被收集的依赖,所以render函数会重新执行。重新执行render函数时从
count
变量中取出的值就是新值1,接着就是生成虚拟DOM,然后将虚拟DOM挂载到真实DOM上,最终在页面上
count
变量绑定的值已经更新为1了。

看到这里你是不是以为关于ref实现响应式已经完啦?

我们来看demo中的第二个例子,
user
对象,回顾一下在template和script中关于
user
对象的代码如下:

<template>
  <div>
    <p>user.count的值为:{{ user.count }}</p>
    <button @click="user.count++">user.count++</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

const user = ref({
  count: 0,
});
</script>

在button按钮的click事件中执行的是:
user.count++
,前面我们讲过了对ref的value属性进行写操作会走到
setter
方法中。但是我们这里ref绑定的是一个对象,点击按钮时也不是对
user.value
属性进行写操作,而是对
user.value.count
属性进行写操作。所以在这里点击按钮不会走到
setter
方法中,当然也不会重新执行收集的依赖。

那么当ref绑定的是对象时,我们改变对象的某个属性时又是怎么做到响应式更新的呢?

这种情况就要用到
Proxy
了,还记得我们前面讲过的
RefImpl
类的
constructor
构造函数吗?代码如下:

class RefImpl {
  private _value: T
  private _rawValue: T

  constructor(value) {
    this._rawValue = toRaw(value);
    this._value = toReactive(value);
  }
}

其实就是这个
toReactive
函数在起作用。

Proxy
实现响应式

还是同样的套路,这次我们给绑定对象的名为
user
的ref打个断点,刷新页面代码停留在断点中。还是和前面的流程一样最终断点走到
RefImpl
类的构造函数中,当代码执行到
this._value = toReactive(value)
时将断点走进
toReactive
函数。代码如下:

const toReactive = (value) => (isObject(value) ? reactive(value) : value);


toReactive
函数中判断了如果当前的
value
是对象,就返回
reactive(value)
,否则就直接返回value。这个
reactive
函数你应该很熟悉,他会返回一个对象的响应式代理。因为
reactive
不接收number这种原始类型,所以这里才会判断
value
是否是对象。

我们接着将断点走进
reactive
函数,看看他是如何返回一个响应式对象的,在我们这个场景中简化后的
reactive
函数代码如下:

function reactive(target) {
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  );
}

从上面的代码可以看到在
reactive
函数中是直接返回了
createReactiveObject
函数的调用,第三个参数是
mutableHandlers
。从名字你可能猜到了,他是一个Proxy对象的处理器对象,后面会讲。

接着将断点走进
createReactiveObject
函数,在我们这个场景中简化后的代码如下:

function createReactiveObject(
  target,
  isReadonly2,
  baseHandlers,
  collectionHandlers,
  proxyMap
) {
  const proxy = new Proxy(target, baseHandlers);
  return proxy;
}

在上面的代码中我们终于看到了大名鼎鼎的
Proxy
了,这里new了一个
Proxy
对象。new的时候传入的第一个参数是
target
,这个
target
就是我们一路传进来的ref绑定的对象。第二个参数为
baseHandlers
,是一个Proxy对象的处理器对象。这个
baseHandlers
是调用
createReactiveObject
时传入的第三个参数,也就是我们前面讲过的
mutableHandlers
对象。

在这里最终将Proxy代理的对象进行返回,我们这个demo中ref绑定的是一个名为
user
的对象,经过前面讲过函数的层层return后,
user.value
的值就是这里return返回的
proxy
对象。

当我们对
user.value
响应式对象的属性进行读操作时,就会触发这里Proxy的get拦截。

当我们对
user.value
响应式对象的属性进行写操作时,就会触发这里Proxy的set拦截。

get

set
拦截的代码就在
mutableHandlers
对象中。

Proxy

set

get
拦截

在源码中使用搜一下
mutableHandlers
对象,看到他的代码是这样的,如下:

const mutableHandlers = new MutableReactiveHandler();

从上面的代码可以看到
mutableHandlers
对象是使用
MutableReactiveHandler
类new出来的一个对象。

我们接着来看
MutableReactiveHandler
类,在我们这个场景中简化后的代码如下:

class MutableReactiveHandler extends BaseReactiveHandler {
  set(target, key, value, receiver) {
    let oldValue = target[key];

    const result = Reflect.set(target, key, value, receiver);
    if (target === toRaw(receiver)) {
      if (hasChanged(value, oldValue)) {
        trigger(target, "set", key, value, oldValue);
      }
    }
    return result;
  }
}

在上面的代码中我们看到了
set
拦截了,但是没有看到
get
拦截。

MutableReactiveHandler
类是继承了
BaseReactiveHandler
类,我们来看看
BaseReactiveHandler
类,在我们这个场景中简化后的
BaseReactiveHandler
类代码如下:

class BaseReactiveHandler {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    track(target, "get", key);
    return res;
  }
}


BaseReactiveHandler
类中我们找到了
get
拦截,当我们对Proxy代理返回的对象的属性进行读操作时就会走到
get
拦截中。

前面讲过了经过层层return后
user.value
的值就是这里的
proxy
响应式对象,而我们在template中使用
user.count
将其渲染到p标签上,在template中读取
user.count
,实际就是在读取
user.value.count
的值。

同样的template经过编译后会变成render函数,执行render函数会生成虚拟DOM,然后将虚拟DOM转换为真实DOM渲染到浏览器上。在执行render函数期间会对
user.value.count
进行读操作,所以会触发
BaseReactiveHandler
这里的
get
拦截。


get
拦截中会执行
track(target, "get", key)
函数,执行后会将当前render函数作为依赖进行收集。到这里依赖收集的部分讲完啦,剩下的就是依赖触发的部分。

我们接着来看
MutableReactiveHandler
,他是继承了
BaseReactiveHandler
。在
BaseReactiveHandler
中有个
get
拦截,而在
MutableReactiveHandler
中有个
set
拦截。

当我们点击
user.count++
按钮时,会对
user.value.count
进行写操作。由于对
count
属性进行了写操作,所以就会走到
set
拦截中,
set
拦截代码如下:

class MutableReactiveHandler extends BaseReactiveHandler {
  set(target, key, value, receiver) {
    let oldValue = target[key];

    const result = Reflect.set(target, key, value, receiver);
    if (target === toRaw(receiver)) {
      if (hasChanged(value, oldValue)) {
        trigger(target, "set", key, value, oldValue);
      }
    }
    return result;
  }
}

我们先来看看
set
拦截接收的4个参数,第一个参数为
target
,也就是我们proxy代理前的原始对象。第二个参数为
key
,进行写操作的属性,在我们这里
key
的值就是字符串
count
。第三个参数是新的属性值。

第四个参数
receiver
一般情况下是Proxy返回的代理响应式对象。这里为什么会说是一般是呢?看一下MDN上面的解释你应该就能明白了:

假设有一段代码执行
obj.name = "jen"

obj
不是一个 proxy,且自身不含
name
属性,但是它的原型链上有一个 proxy,那么,那个 proxy 的
set()
处理器会被调用,而此时,
obj
会作为 receiver 参数传进来。

接着来看
set
拦截函数中的内容,首先
let oldValue = target[key]
拿到旧的属性值,然后使用
Reflect.set(target, key, value, receiver)


Proxy
中一般都是搭配
Reflect
进行使用,在
Proxy

get
拦截中使用
Reflect.get
,在
Proxy

set
拦截中使用
Reflect.set

这样做有几个好处,在set拦截中我们要return一个布尔值表示属性赋值是否成功。如果使用传统的
obj[key] = value
的形式我们是不知道赋值是否成功的,而使用
Reflect.set
会返回一个结果表示给对象的属性赋值是否成功。在set拦截中直接将
Reflect.set
的结果进行return即可。

还有一个好处是如果不搭配使用可能会出现
this
指向不对的问题。

前面我们讲过了
receiver
可能不是Proxy返回的代理响应式对象,所以这里需要使用
if (target === toRaw(receiver))
进行判断。

接着就是使用
if (hasChanged(value, oldValue))
进行判断新的值和旧的值是否相等,如果不相等就执行
trigger(target, "set", key, value, oldValue)

这个
trigger
函数就是用于依赖触发,会将收集的依赖全部取出来执行一遍,由于render函数也是被收集的依赖,所以render函数会重新执行。重新执行render函数时从
user.value.count
属性中取出的值就是新值1,接着就是生成虚拟DOM,然后将虚拟DOM挂载到真实DOM上,最终在页面上
user.value.count
属性绑定的值已经更新为1了。

这就是当ref绑定的是一个对象时,是如何使用Proxy去实现响应式的过程。

看到这里有的小伙伴可能会有一个疑问,为什么ref使用
RefImpl
类去实现,而不是统一使用
Proxy
去代理一个拥有
value
属性的普通对象呢?比如下面这种:

const proxy = new Proxy(
  {
    value: target,
  },
  baseHandlers
);

如果是上面这样做那么就不需要使用
RefImpl
类了,全部统一成Proxy去使用响应式了。

但是上面的做法有个问题,就是使用者可以使用
delete proxy.value

proxy
对象的
value
属性给删除了。而使用
RefImpl
类的方式去实现就不能使用
delete
的方法去将
value
属性给删除了。

总结

这篇文章我们讲了
ref
是如何实现响应式的,主要分为两种情况:ref接收的是number这种原始类型、ref接收的是对象这种非原始类型。

  • 当ref接收的是number这种原始类型时是依靠
    RefImpl
    类的
    value
    属性的
    getter

    setter
    方法中去实现的响应式。

    当我们对ref的value属性进行读操作时会触发value的
    getter
    方法进行依赖收集。

    当我们对ref的value属性进行写操作时会进行依赖触发,重新执行render函数,达到响应式的目的。

  • 当ref接收的是对象这种非原始类型时,会调用
    reactive
    方法将ref的value属性转换成一个由
    Proxy
    实现的响应式对象。

    当我们对ref的value属性对象的某个属性进行读操作时会触发
    Proxy
    的get拦截进行依赖收集。

    当我们对ref的value属性对象的某个属性进行写操作时会触发
    Proxy
    的set拦截进行依赖触发,然后重新执行render函数,达到响应式的目的。

最后我们讲了为什么ref不统一使用
Proxy
去代理一个有
value
属性的普通对象去实现响应式,而是要多搞个
RefImpl
类。

因为如果使用
Proxy
去代理的有value属性的普通的对象,可以使用
delete proxy.value

proxy
对象的
value
属性给删除了。而使用
RefImpl
类的方式去实现就不能使用
delete
的方法去将
value
属性给删除了。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

大家好,我是码农先森。

在这个大家都崇尚高性能的时代,程序员的谈笑间句句都离不开高性能,仿佛嘴角边不挂着「高性能」三个字都会显得自己很 Low,其中众所皆知的 Nginx 就是高性能的代表。有些朋友可能连什么是高性能都不一定理解,其实高性能就是单位时间内能处理更多的客户端请求,如果要问具体能处理多少请求,这个就要结合软硬件条件来评估了,感兴趣的朋友可以在定性的条件下使用压力测试工具对自己的程序进行测试。

大家都知道 PHP-FPM 是 PHP 的进程管理器,每一次来自 Ngixn 转发过来的客户端请求,都会交由一个 PHP-FPM 子进程进行处理,在同一时刻一个子进程只能处理一个客户端请求,如果想要同一时刻能处理多个请求,那么就需要启动多个子进程,当遇到秒杀抢购这种瞬间大量请求的场景时,PHP-FPM 对请求处理的模式显然无法满足需求。在这种情况下,我们只能使用 Workerman 或 Swoole 这种 PHP 的高性能通信框架,来解决类似特殊场景下的并发问题,不过这次我分享的内容主要是 Workerman。

如标题所提到的 Workerman 立命之本,那什么是其立命之本呢?我认为是 IO 多路复用的 epoll 利器,epoll 是高性能程序的根基,解决 C10K 问题的尚方宝剑。接下来我会剖析 epoll 在 Workerman 源码中的使用,不过在这之前我们需要先学习下 PHP 中如何将 Socket 与 Event 结合使用的案例。这里的 Event 可以理解为是对 epoll 的高度封装,底层采用的就是 epoll 利器。

看了这段代码,有助于你理解 Workerman 源码,因为这段代码就是提炼了 Workerman 对事件循环的实现原理。stream_socket_server 函数把创建、绑定、监听一并实现了,让代码显得更加简洁,不像之前的 socket_create、socket_bind、socket_listen 搞了三个步骤略显繁琐。因为使用了事件循环,所以需要对 Socket 设置成非阻塞模式,只有当有读或写的通知时才会调用相应的回调函数。还有一点需要额外注意的,需要针对客户端 Socket 创建的 Event 需要定义成静态变量或全局变量,不然无法持久化连接到内存,会造成客户端无法建立连接传输数据,我看到网上很多人都踩到了这个坑上。最后启动事件循环 EventLoop 自此开启了 Socket 监听和事件循环双操作。

<?php

// 创建 TCP 服务器套接字
$server = stream_socket_server("tcp://0.0.0.0:8080", $errno, $error);
echo "正在监听 8080 端口...". PHP_EOL; 

// 设置为非阻塞,在 $server 对象没有数据可以读取或写入时不会阻塞其执行
stream_set_blocking($server, 0);

// 创建事件基础对象
$event_base = new EventBase();

// 建立事件监听服务端 Socket 可读事件
$event = new Event($event_base, $server, Event::READ | Event::PERSIST, function ($server) use ($event_base) {
    // 获取新的连接,由于设置了非阻塞模式,那么这里即使没有新的连接,也不会一直阻塞在这
    $client = @stream_socket_accept($server, 0);
    if ($client) {
        echo "客户端(" . $client . ")连接建立". PHP_EOL; 

        // 针对客户端过来的连接,也要设置成非阻塞模式
        stream_set_blocking($client, 0);

        // 客户端连接创建监听可读事件
        // 这里需要特别注意:客户端事件需要定义成静态变量或全局变量
        static $client_event;
        $client_event = new Event($event_base, $client, Event::READ | Event::PERSIST, function ($client) {
            // 从客户端连接中读取数据,每次只读取 1024 字节数据
            $buffer = fread($client, 1024);

            // 如果没有读取到数据或者客户端已经不是资源句柄,则关闭客户端连接
            if ($buffer == false || !is_resource($client)) {
                // 关闭客户端连接
                fclose($client);
                echo "客户端(" . $client . ")连接关闭" . PHP_EOL; 
                return;
            }
            echo "收到客户端(" . $client . ")数据: $buffer" . PHP_EOL;

            // 回写数据给客户端
            $msg = "HTTP/1.0 200 OK\r\nContent-Length: 10\r\n\r\nServerOK\r\n";
            fwrite($client, $msg);
        }, $client);
        $client_event->add();
    }
}, $server);

// 添加事件
$event->add();

// 执行事件循环
$event_base->loop();

使用 CURL 工具访问
http://127.0.0.1:8080
便能正确返回结果 ServerOK 这表明事件循环可以进入正常运行状态。

[manongsen@root php_event]$ curl -i http://127.0.0.1:8080
HTTP/1.0 200 OK
Content-Length: 10

ServerOK

看懂了上面那段代码之后,接下来的内容就会更顺利了。下面这段代码是引至 Workerman 的示例,通过 Worker 类构造了一个 HTTP 服务。onMessage 参数定义了一个回调函数,当有事件通知时,会回调到此处,之后就是用户自行实现后续的处理逻辑了。runAll 函数会整体启动整个服务,其中包括进程的创建、事件的循环等。

<?php

// 引用 Worker 类
use Workerman\Worker;

// 自动加载 Composer
require_once __DIR__ . '/vendor/autoload.php';

// 定义 HTTP 服务并监听 8081 端口
$http_worker = new Worker('http://0.0.0.0:8081');

// 定义回调函数
$http_worker->onMessage = function ($connection, $request) {
    //$request->get();
    //$request->post();
    //$request->header();
    //$request->cookie();
    //$request->session();
    //$request->uri();
    //$request->path();
    //$request->method();

    // Send data to client
    $connection->send("Hello World");
};

// 启动服务
Worker::runAll();

在 Worker.php 文件的 2367 行,使用 stream_socket_server 函数创建了服务端 Socket 并且绑定、监听了 8081 端口。

// workerman/Worker.php:2367
$this->_mainSocket = \stream_socket_server($local_socket, $errno, $errmsg, $flags, $this->_context);

在 Worker.php 文件的 2394 行,使用 stream_set_blocking 函数将 服务端 Socket 设置成非阻塞模式。

// workerman/Worker.php:2394
\stream_set_blocking($this->_mainSocket, false);

在 Worker.php 文件的 2417 行,将服务端的 _mainSocket 添加到事件循序中,并且设置回调函数为 acceptConnection 。

// workerman/Worker.php:2417
static::$globalEvent->add($this->_mainSocket, EventInterface::EV_READ, array($this, 'acceptConnection'));

在 Worker.php 文件的 2561 行,使用 stream_socket_accept 接收到来自客户端的连接 $new_socket ,其中这个操作是在 acceptConnection 回到函数中所进行的。

// workerman/Worker.php:2561
$new_socket = \stream_socket_accept($socket, 0, $remote_address);

在 TcpConnection.php 文件的 285 行,使用 stream_set_blocking 函数将客户端的 _socket 设置成非阻塞模式,这里的 _socket 和上面的 new_socket 是同一个。

// workerman/Connection/TcpConnection.php:285
\stream_set_blocking($this->_socket, 0);

在 TcpConnection.php 文件的 290 行,将客户端的 _socket 添加到事件循环中,并且设置其的回调函数为 baseRead 。

// workerman/Connection/TcpConnection.php:290
Worker::$globalEvent->add($this->_socket, EventInterface::EV_READ, array($this, 'baseRead'));

在 Worker.php 文件的 1638 行,启动事件循环。

// workerman/Worker.php:1638
static::$globalEvent->loop();

启动事件循环后,当有客户端连接时便可以读取数据了。因此在 TcpConnection.php 文件的 583 行,使用 fread 函数读取客户端 $socket 的数据。

// workerman/Connection/TcpConnection.php:583
$buffer = @\fread($socket, self::READ_BUFFER_SIZE);

在 TcpConnection.php 文件的 647 行,使用 parser::decode 函数将上面读取到的 buffer 数据解析成 $request 对象,还有 $this 表示的是 $connection 对象,这个 $this->onMessage 是最开始用户自定义的回调函数。最终通过 call_user_func 函数,将 $connection、$request 参数回调到 onMessage 方法。

// workerman/Connection/TcpConnection.php:647
\call_user_func($this->onMessage, $this, $parser::decode($one_request_buffer, $this));

最后我们使用 CURL 工具调用一下
http://127.0.0.1:8081
通过返回的数据,可以看出正确的回调到了 onMessage 函数。

[manongsen@root workerman]$ curl -i http://127.0.0.1:8081
HTTP/1.1 200 OK
Server: workerman
Connection: keep-alive
Content-Type: text/html;charset=utf-8
Content-Length: 13

Hello World

看到这里相信你已经对 Workerman 源码中的事件循环有些了解了,如果有时间最好能够实践下最开始的那段案例代码,然后再结合着看 Workerman 的源代码会颇有收获。Workerman 的高性能是站在了巨人 epoll 的肩膀上来实现,没有了 epoll 则啥也不是。这里再重申一下 PHP 中的 Event 是对 epoll 的封装,epoll 是 Linux 的底层技术。我们在日常的编程中是不会直接接触到 epoll 的,最后回归一下主题 epoll 技术才是 Workerman 的立命之本。

感谢大家阅读,个人观点仅供参考,欢迎在评论区发表不同观点。


欢迎关注、分享、点赞、收藏、在看,我是微信公众号「码农先森」作者。

一、说明

在大数据处理和分析中
Apache Kafka
已经成为了一个核心组件。然而在生产环境中部署
Kafka
时,安全性是一个必须要考虑的重要因素。
SASL
(简单认证与安全层)和
SCRAM
(基于密码的认证机制的盐化挑战响应认证机制)提供了一种方法来增强
Kafka
集群的安全性。

本文将从零开始部署
ZooKeeper

Kafka
并通过配置
SASL/SCRAM

ACL
(访问控制列表)来增强
Kafka
的安全性。

二、Kafka 的安全机制

kafka 社区在
0.9.0.0
版本正式添加了安全特性,可以满足各种安全性的要求,包括:

  1. Kafka 与 ZooKeeper 之间的安全通信;
  2. Kafka 集群 ZooKeeper 之间的安全通信;
  3. 客户端与服务端之间的安全通信;
  4. 消息级别的权限控制,可以控制客户端(生产者或消费者)的读写操作权限。

认证方式 引入版本 适用场景
SSL 0.9.0 SSL做信道加密比较多,SSL认证不如SASL所以一般都会使用SSL来做通信加密。
SASL/GSSAPI 0.9.9 主要是给 Kerberos 使用的。如果你的公司已经做了 Kerberos 认证(比如使用 Active Directory),那么使用 GSSAPI 是最方便的了。因为你不需要额外地搭建 Kerberos,只要让你们的 Kerberos 管理员给每个 Broker 和要访问 Kafka 集群的操作系统用户申请 principal 就好了。
SASL/PLAIN 0.10.2 简单的用户名密码认证,通常与SSL结合使用,对于小公司来说,没必要搭建公司级别的Kerberos,使用它就比较合适。
SASL/SCRAM 0.10.2 PLAIN的加强版本,支持动态的用户增减。
Deleation Token 1.1 Delegation Token 是在 1.1 版本引入的,它是一种轻量级的认证机制,主要目的是补充现有的 SASL 或 SSL 认证。如果要使用 Delegation Token,你需要先配置好 SASL 认证,然后再利用 Kafka 提供的 API 去获取对应的 Delegation Token。这样 Broker 和客户端在做认证的时候,可以直接使用这个 token,不用每次都去 KDC 获取对应的 ticket(Kerberos 认证)或传输 Keystore 文件(SSL 认证)。
SASL/OAUTHBEARER 2.0 OAuth 2框架的集成。

三、环境和软件准备


Apache Kafka 官网
下载对应版本的 Kafka 并解压到你选择的目录。

确保已经安装 Java 才能运行 Kafka,可以通过运行
java -version
来检查 Java 环境。

四、部署 Zookeeper

使用 Kafka
内置
的 Zookeeper

4.1. 启用 SASL 认证

进入 config 目录,修改
zookeeper.properties
配置文件增加以下内容:

authProvider.1=org.apache.zookeeper.server.auth.SASLAuthenticationProvider
jaasLoginRenew=3600000
requireClientAuthScheme=sasl
zookeeper.sasl.client=true

4.2. 配置 JAAS

在 config 目录下创建
zk_jaas.conf
文件,内容如下:

Server {
    org.apache.zookeeper.server.auth.DigestLoginModule required
    username="admin"
    password="admin"
    user_admin="admin"
    user_zkclient="zkclient";
};

其作用是创建了一个 Server 节点,其中

  • org.apache.zookeeper.server.auth.DigestLoginModule required
    是认证逻辑的处理类;
  • username、password
    是zookeeper之间通讯的用户名和密码;
  • user_admin="admin"
    的结构是 user_[username]=[password] 定义 kafka-broker(zookeeper客户端)连接到 zookeeper 时用的用户名和密码。

注意:Server 内部最后一行的
;
和 } 后的
;
不能缺少!

4.3. 修改启动文件

进入 bin 目录,修改
zookeeper-server-start.sh
文件;


export KAFKA_HEAP_OPTS=
配置项的参数后添加
JAAS
的配置:

export KAFKA_HEAP_OPTS="-Xmx512M -Xms512M -Djava.security.auth.login.config=../config/zk_jaas.conf"

4.4. 启动 Zookeeper

执行命令:
./zookeeper-server-start.sh -daemon ../config/zookeeper.properties

-daemon 参数配置后台运行

4.5. 测试

可以从官网
Apache ZooKeeper
下载对应版本的 ZooKeeper 并解压;

添加
JAAS
配置,在 confi 目录下创建
zk_client_jaas.conf
文件:

Client{
    org.apache.zookeeper.server.auth.DigestLoginModule required
    username="zkclient"
    password="zkclient";
};

修改 bin 目录下的
zkCli.sh
文件,在启动命令中增加 JAAS 的配置:

"$JAVA" "-Dzookeeper.log.dir=${ZOO_LOG_DIR}" "-Dzookeeper.root.logger=${ZOO_LOG4J_PROP}" "-Dzookeeper.log.file=${ZOO_LOG_FILE}" \
     -cp "$CLASSPATH" $CLIENT_JVMFLAGS $JVMFLAGS \
     "-Djava.security.auth.login.config=../conf/zk_client_jaas.conf" \
     org.apache.zookeeper.ZooKeeperMain "$@"

执行
zkCli.sh
连接本机已经启动好的
ZooKeeper

进入 Kafka 的 log 目录,查看内置 zk 的日志
zookeeper.out
显示以下内容:

INFO adding SASL authorization for authorizationID: zkclient (org.apache.zookeeper.server.ZooKeeperServer)

代表 ZooKeeper 的 SASL 认证已经配置成功。

五、部署 Kafka

5.1. 配置 Kafka Broker

进入 config 目录,修改
server.properties
配置文件增加以下内容:

listeners=SASL_PLAINTEXT://:9092
advertised.listeners=SASL_PLAINTEXT://localhost:9092
security.inter.broker.protocol=SASL_PLAINTEXT
sasl.mechanism.inter.broker.protocol=SCRAM-SHA-256
sasl.enabled.mechanisms=SCRAM-SHA-256
authorizer.class.name=kafka.security.authorizer.AclAuthorizer
allow.everyone.if.no.acl.found=false
super.users=User:admin
  • authorizer.class.name
    开启 ACL 授权机制并指定实现类;
  • allow.everyone.if.no.acl.found
    如果没有找到ACL(访问控制列表)配置,是否允许任何操作;这里设置为
    false
    指除了超级管理员,其他用户必须配置 ACL 才能访问资源;
  • super.users
    超级管理员,无需配置 ACL 拥有所有权限的用户。

5.2. 配置 JAAS

在 config 目录下创建
kafka_server_jaas.conf
文件,内容如下:

KafkaServer {
    org.apache.kafka.common.security.scram.ScramLoginModule required
    username="admin"
    password="admin";
};

Client{
    org.apache.kafka.common.security.plain.PlainLoginModule required
    username="zkclient"
    password="zkclient";
};
  • KafkaServer
    中的
    username,password
    用于 Kafka 集群 Broker 节点之间通信用的账号密码;
  • KafkaServer
    中的
    user_test="test"
    用于 Kafka 客户端(producer,consumer)连接broker时,用该配置下user_[username]=[password]结构配置的账号密码登录;
  • Client
    用于 broker 和 zookeeper 之间的认证,对应 zk_jaas.conf 中的 【user_zkclient="zkclient"】 配置;
  • user_admin="admin"
    的结构是 user_[username]=[password] 定义 kafka-broker(zookeeper客户端)连接到 zookeeper 时用的用户名和密码。

5.3. 修改启动文件

进入 bin 目录,修改
kafka-server-start.sh
文件;


export KAFKA_HEAP_OPTS=
配置项的参数后添加
JAAS
的配置:

export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G -Djava.security.auth.login.config=../config/kafka_server_jaas.conf"

5.4. 创建 SCRAM 用户

在启动 Kafka 之前需要先创建好用户,在 bin 目录下执行以下内容:

分别创建
admin
(超级管理员) 和
test
(客户端用户)

./kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'SCRAM-SHA-256=[password=admin]' --entity-type users --entity-name admin

./kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'SCRAM-SHA-256=[password=test]' --entity-type users --entity-name test

SASL/SCRAM
认证的用户信息是动态创建存储在 ZooKeeper 中, 由于上面的配置
kafka_server_jaas.conf
中 Broker 之间的通信是通过
admin
用户的,如果该用户不存在会
启动报错

5.5. 启动 Kafka

执行命令:
./kafka-server-start.sh -daemon ../config/server.properties

-daemon 参数配置后台运行

六、验证 SASL/SCRAM 鉴权

6.1. 客户端认证配置

6.1.1. 管理员配置

进入 config 目录创建
cmd.properties
内容如下:

security.protocol=SASL_PLAINTEXT
sasl.mechanism=SCRAM-SHA-256
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="admin" password="admin";

配置认证的类型以及登录逻辑的处理类和用户,使用超级管理员 admin

注意
最后的
;
是必须加上的。

6.1.2. 生产者配置

修改 config 目录下的
producer.properties
增加以下内容:

security.protocol=SASL_PLAINTEXT
sasl.mechanism=SCRAM-SHA-256
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="admin" password="admin";

生产者也使用超级管理员 admin 来发送消息。

6.1.3. 消费者配置

修改 config 目录下的
consumer.properties
增加以下内容:

security.protocol=SASL_PLAINTEXT
sasl.mechanism=SCRAM-SHA-256
sasl.jaas.config=org.apache.kafka.common.security.scram.ScramLoginModule required username="test" password="test";

消费者使用 test 用户来接收消息。

6.2. 创建topic

在 bin 目录下执行以下命令:

./kafka-topics.sh --bootstrap-server localhost:9092 --create --topic test --partitions 1 --replication-factor 1 --command-config ../config/cmd.properties
  • bootstrap-server
    配置 Kafka 服务端的地址
  • topic
    指定topic名称
  • command-config
    指定命令的认证配置,这里使用上面创建的
    管理员配置

创建成功后可以通过以下命令查看存在的 topic 清单:

./kafka-topics.sh --bootstrap-server localhost:9092 --list --command-config ../config/cmd.properties

6.3. 创建消费者

6.3.1. 执行 kafka-console-consumer

在 bin 目录下执行以下命令:

./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --consumer.config ../config/consumer.properties

执行命令后会发现以下
报错
信息:

ERROR Error processing message, terminating consumer process:  (kafka.tools.ConsoleConsumer$)
org.apache.kafka.common.errors.SaslAuthenticationException: Authentication failed during authentication due to invalid credentials with SASL mechanism SCRAM-SHA-256
Processed a total of 0 messages

Authentication failed
认证失败,由于消费者的认证使用的是 test 用户,而该用户还未配置任何 ACL 权限。

6.3.2. 配置用户 ACL 权限

Kafka 的 ACL (Access Control Lists) 允许你定义哪些用户可以访问哪些主题,并且可以执行哪些操作(如读、写、创建、删除等)。

执行以下命令:

./kafka-acls.sh --authorizer-properties zookeeper.connect=localhost:2181 --add --allow-principal User:test --operation Read --topic test --group test-consumer-group

为 test 用户在资源
topic[test]
下分配只读权限

执行成功,可以通过以下命令查看资源所分配的所有 ACL 清单:

./kafka-acls.sh --bootstrap-server localhost:9092 --topic test --list --command-config ../config/cmd.properties

重新创建消费者:

./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --consumer.config ../config/consumer.properties

执行成功后该 shell 窗口会一直阻塞等待消息。

6.4. 创建生产者

新开一个 shell 窗口
在 bin 目录下执行以下命令:

./kafka-console-producer.sh --bootstrap-server localhost:9092 --topic test --producer.config ../config/producer.properties

由于生产者的认证使用的是 admin 为
超级管理员
所以无需配置 ACL 权限。

执行成功后会出现
>
符号,输入内容之后,切换到
消费者
窗口就可以看到了。

一键语法错误增强工具

欢迎使用我最近开源的使用一键语法错误增强工具,该工具可以进行14种语法错误的增强,不同行业可以根据自己的数据进行错误替换,来训练自己的语法和拼写模型,希望推动行业文本纠错的发展,欢迎Star,14种错误如下所示:

每种错误类型,对应的使用方法,如下所示:

环境的安装

pip install ChineseErrorCorrector

不同类型的数据增强

1.缺字漏字

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.lack_word("小明住在北京"))

# 输出:小明在北京

2.错别字错误

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.wrong_word("小明住在北京"))
# 输出:小明住在北鲸

3.缺少标点

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.lack_char("小明住在北京,热爱NLP。"))
# 输出:小明住在北京热爱NLP。

4.错用标点

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.wrong_char("小明住在北京"))
# 输出:小明住在北京。热爱NLP。

5.主语不明

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.unknow_sub("小明住在北京"))
# 输出:住在北京

6.谓语残缺

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.unknow_pred("小明住在北京"))
# 输出:小明在北京

7.宾语残缺

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.lack_obj("小明住在北京,热爱NLP。"))
# 输出:小明住在北京,热爱。

8.其他成分残缺

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.lack_others("小明住在北京,热爱NLP。"))
# 输出:小明住北京,热爱NLP。

9.虚词多余

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.red_fun("小明住在北京,热爱NLP。"))
# 输出:小明所住的在北京,热爱NLP。

10.其他成分多余

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.red_component("小明住在北京,热爱NLP。"))
# 输出:小明住在北京,热爱NLP。,看着

11.主语多余

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.red_sub("小明住在北京,热爱NLP。"))
# 输出:小明住在北京,小明热爱NLP。

12.语序不当

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.wrong_sentence_order("小明住在北京,热爱NLP。"))
# 输出:热爱NLP。,小明住在北京

13.动宾搭配不当

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.wrong_ver_obj("小明住在北京,热爱NLP。"))
# 输出:None ,即无法进行此类错误的增强

14.其他搭配不当

from ChineseErrorCorrector.dat import GrammarErrorDat

cged_tool = GrammarErrorDat()
print(cged_tool.other_wrong("小明住在北京,热爱NLP。"))
# 输出:None, 即无法进行此类错误的增强

代码地址:
https://github.com/TW-NLP/ChineseErrorCorrector