JWT包中的源码分析【Golang】

前言

最近在学web编程的途中,经过学长提醒,在进行登陆(Login)操作之后,识别是否登陆的标识应该要放入authorization中,正好最近也在学鉴权,就顺便来看看源码了。

正文

1. 代码示例

在进行分析之前,先给予一段JWT生成和解析的代码示例,这样可以随着代码一步一步深入到源码层面来帮助我们进一步理解JWT这一部分知识。

package api

//关于jwt的所有操作都在这里
import (
	"errors"
	"fmt"
	"github.com/dgrijalva/jwt-go"
	"time"
)

var mySigningKey = []byte("Bang dream Girls Band Part!114514Girls Band Cry!114514,It's My Go!!!!!")

func GenerateJWT(username string) (string, error) {
	token := jwt.New(jwt.SigningMethodHS256)
	claims := token.Claims.(jwt.MapClaims)
	claims["username"] = username
	claims["exp"] = time.Now().Add(time.Second * 30).Unix()
	ToKenSting, err := token.SignedString(mySigningKey)
	if err != nil {
		return "", err
	}
	return ToKenSting, nil
}

func VerifyJWT(tokenString string) (string, error) {
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return mySigningKey, nil
	})
	if err != nil {
		return "", err
	}
	if claims, ok1 := token.Claims.(jwt.MapClaims); ok1 && token.Valid {
		username := claims["username"].(string)
		return username, nil
	}
	return "", errors.New("token invalid")
}

2. GenerateJWT

2.1 新建token

	token := jwt.New(jwt.SigningMethodHS256)

在函数开头,我们可以发现,我们使用了jwt包中的new,并传递了一个签名方法来新建了一个token
这个new函数又是如何实现的呢?在这里插入图片描述
我们可以发现,new就是返回了一个token类型的指针,而进一步向下查看NewWithClaims的实现在这里插入图片描述
则是直接返回了Token结构体的指针,其中的参数method.Alg()是直接返回了这个方法的Name:在这里插入图片描述

claims则是这个token的声明,也就是我们接下来需要写入数据的部分,

2.2 签名方法类型

刚刚我们提到了我们在创建token对象的时候需要传递一个签名方法,可以理解为我们的随后会以什么样的方式生成jwt字符串,很多人可能会对这个签名方法是什么东西感到疑惑,没有兴趣了解的可以跳过这一部分。

此处我们以SigningMethodHMAC为例子
在这里插入图片描述
这个所谓的签名方法实际上就是一个结构体,源代码中,我们可以看见,代码为这个结构体创建了256/384/512的实例对象,这就是我们真正可以访问的签名方法了,但是这几个实例对象作为结构体实例的参数又是什么呢?在这里插入图片描述
如图所示,在这个签名方法结构体所在的包中,利用init函数为这些实例对象赋予了值,而RegisterSigningMethod在这里插入图片描述
如图所示,RegisterSigningMethod的作用则是利用哈希表,将签名方法的名字映射到这一结构体的实体对象,相当于将签名方法注册到了实体中,从逻辑上来说,我觉得更美吧。

2.3 Claims

	claims := token.Claims.(jwt.MapClaims)
	claims["username"] = username
	claims["exp"] = time.Now().Add(time.Second * 30).Unix()

这一步,对token中的Claims成员进行类型断言,并作为MapClaims类型赋予claims变量,此处由于Claims是一个接口,并且MapClaims实际上是一个哈希表,并且实现了Claims接口,所以可以转换,并且具有引用的作用,意思是修改claims的值就相当于修改了token.Claims的值。
在这里插入图片描述
这里先不对Raw以及没讲到的成员变量做说明,后面会提到。
在这里插入图片描述
在这里插入图片描述
我们可以看见MapClaims实际上是一个哈希表,而这个类型实现了Valid方法,所以就实现了Claims接口。

2.4 jwt字符串生成

	ToKenSting, err := token.SignedString(mySigningKey)

此处,则是我们生成jwt字符串的一步了
在这里插入图片描述
此处,除了两个需要注意的方法,其他都很简单,下面我们来看看这两个方法具体做了什么事情吧!

func (t *Token) SigningString() (string, error) {
	var err error
	parts := make([]string, 2)
	for i, _ := range parts {
		var jsonValue []byte
		if i == 0 {
			if jsonValue, err = json.Marshal(t.Header); err != nil {
				return "", err
			}
		} else {
			if jsonValue, err = json.Marshal(t.Claims); err != nil {
				return "", err
			}
		}

		parts[i] = EncodeSegment(jsonValue)
	}
	return strings.Join(parts, "."), nil
}

首先是第一部分,创建了一个长度为2的string切片,随后我们对part进行遍历,当遍历到第一个部分时,代码将 Token 结构体中的头部信息(t.Header)序列化为 JSON 格式的字节切片。随后将其存储在parts[0]之中,当遍历到part第二部分则是将Claims部分进行序列化,并存入part[1],最后将这个切片用“.”连接起来。

随后来看看第二部分

func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string, error) {
	if keyBytes, ok := key.([]byte); ok {
		if !m.Hash.Available() {
			return "", ErrHashUnavailable
		}

		hasher := hmac.New(m.Hash.New, keyBytes)
		hasher.Write([]byte(signingString))

		return EncodeSegment(hasher.Sum(nil)), nil
	}

	return "", ErrInvalidKeyType
}

在这里首先对key进行类型断言,由于此处我们传入的是字节切片,所以不会出错,随后再进行判断这个签名方法的hash值是否合法,也就是看这个方法是否是正确的,这样理解便好,随后利用hmac包(对应了签名方法)中的New,并传入我们的Key,新建了一个哈希对象,随后将之前我们在外部生成的签名写入这个哈希对象,最后,通过调用 hasher.Sum(nil) 计算并返回结果,这个结果一般会是一个字符串形式的签名,用于生成最终的 JWT,而关于hash计算和生成其中的源码有点过于跑题了,就不在这里展开讲了。

	return strings.Join([]string{sstr, sig}, "."), nil

在生成签名的最后,将之前生成的字符串利用“.”连接在一起,这一步对于我们后面解析JWT字符串有大用,到这一步,就已经得到我们的JWT字符串了。

3. VerifyJWT

3.1 Parse

	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
		}
		return mySigningKey, nil
	})

我们解析JWT字符串需要创建一个token来存储我们的解析后的结果,而parse函数,就是解析我们的JWT字符串的大杀器了,在这个函数中,我们需要传入原始的JWT字符串和一个匿名函数,用来在Parse函数内部判断解析的方法时候正确。

func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
	return new(Parser).Parse(tokenString, keyFunc)
}

在进入到函数内部之后,我们发现又是一层套娃,但是这一层新建了一个parser解析器,以此来进行调用解析

func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
	return p.ParseWithClaims(tokenString, MapClaims{}, keyFunc)
}

继续进入下一步

func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
	token, parts, err := p.ParseUnverified(tokenString, claims)
	if err != nil {
		return token, err
	}

	// Verify signing method is in the required set
	if p.ValidMethods != nil {
		var signingMethodValid = false
		var alg = token.Method.Alg()
		for _, m := range p.ValidMethods {
			if m == alg {
				signingMethodValid = true
				break
			}
		}
		if !signingMethodValid {
			// signing method is not in the listed set
			return token, NewValidationError(fmt.Sprintf("signing method %v is invalid", alg), ValidationErrorSignatureInvalid)
		}
	}

	// Lookup key
	var key interface{}
	if keyFunc == nil {
		// keyFunc was not provided.  short circuiting validation
		return token, NewValidationError("no Keyfunc was provided.", ValidationErrorUnverifiable)
	}
	if key, err = keyFunc(token); err != nil {
		// keyFunc returned an error
		if ve, ok := err.(*ValidationError); ok {
			return token, ve
		}
		return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable}
	}

	vErr := &ValidationError{}

	// Validate Claims
	if !p.SkipClaimsValidation {
		if err := token.Claims.Valid(); err != nil {

			// If the Claims Valid returned an error, check if it is a validation error,
			// If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set
			if e, ok := err.(*ValidationError); !ok {
				vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid}
			} else {
				vErr = e
			}
		}
	}

	// Perform validation
	token.Signature = parts[2]
	if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil {
		vErr.Inner = err
		vErr.Errors |= ValidationErrorSignatureInvalid
	}

	if vErr.valid() {
		token.Valid = true
		return token, nil
	}

	return token, vErr
}

终于来到了我们的源代码分析环节,

	token, parts, err := p.ParseUnverified(tokenString, claims)
	if err != nil {
		return token, err
	}

首先第一步,是对我们的token进行生成,我们需要进入函数内部来进行分析。

func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) {
	parts = strings.Split(tokenString, ".")
	if len(parts) != 3 {
		return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
	}

	token = &Token{Raw: tokenString}

	// parse Header
	var headerBytes []byte
	if headerBytes, err = DecodeSegment(parts[0]); err != nil {
		if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {
			return token, parts, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed)
		}
		return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
	}
	if err = json.Unmarshal(headerBytes, &token.Header); err != nil {
		return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
	}

	// parse Claims
	var claimBytes []byte
	token.Claims = claims

	if claimBytes, err = DecodeSegment(parts[1]); err != nil {
		return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
	}
	dec := json.NewDecoder(bytes.NewBuffer(claimBytes))
	if p.UseJSONNumber {
		dec.UseNumber()
	}
	// JSON Decode.  Special case for map type to avoid weird pointer behavior
	if c, ok := token.Claims.(MapClaims); ok {
		err = dec.Decode(&c)
	} else {
		err = dec.Decode(&claims)
	}
	// Handle decode error
	if err != nil {
		return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
	}

	// Lookup signature method
	if method, ok := token.Header["alg"].(string); ok {
		if token.Method = GetSigningMethod(method); token.Method == nil {
			return token, parts, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable)
		}
	} else {
		return token, parts, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable)
	}

	return token, parts, nil
}

是不是被吓到了?没关系,我带你来一步一步分析,其实重要的部分并不多。

	parts = strings.Split(tokenString, ".")
	if len(parts) != 3 {
		return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
	}

	token = &Token{Raw: tokenString}

在这里,我们将原始JWT字符串利用我们之前设置的“.”分开,这时候我们便可以进行初步的判断这个接受的jwt字符串是否合法,判断之后,我们先将原始jwt字符串存入新建的token对象的raw成员中,这时候我们就需要提及我们之前没讲的raw成员,他就是我们token对象的原始jwt字符串。

	var headerBytes []byte
	if headerBytes, err = DecodeSegment(parts[0]); err != nil {
		if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {
			return token, parts, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed)
		}
		return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
	}
	if err = json.Unmarshal(headerBytes, &token.Header); err != nil {
		return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
	}

这里看着很复杂,但是实际上抛开大量的if return,真正解析的部分并不多,这一部分就是将我们的jwt中的header部分进行解析,随后判断是否出错而已,随后将header中的信息存入token中。

	// parse Claims
	var claimBytes []byte
	token.Claims = claims

	if claimBytes, err = DecodeSegment(parts[1]); err != nil {
		return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
	}
	dec := json.NewDecoder(bytes.NewBuffer(claimBytes))
	if p.UseJSONNumber {
		dec.UseNumber()
	}
	// JSON Decode.  Special case for map type to avoid weird pointer behavior
	if c, ok := token.Claims.(MapClaims); ok {
		err = dec.Decode(&c)
	} else {
		err = dec.Decode(&claims)
	}
	// Handle decode error
	if err != nil {
		return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
	}

这一步也很简单,就是对我们的claims部分进行解析,然后放入token中就结束了。

	if method, ok := token.Header["alg"].(string); ok {
		if token.Method = GetSigningMethod(method); token.Method == nil {
			return token, parts, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable)
		}
	} else {
		return token, parts, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable)
	}

在最后从header获取方法的名字,判断一下是否合法就可以返回了。

接下来回到上一层源代码

	// Verify signing method is in the required set
	if p.ValidMethods != nil {
		var signingMethodValid = false
		var alg = token.Method.Alg()
		for _, m := range p.ValidMethods {
			if m == alg {
				signingMethodValid = true
				break
			}
		}
		if !signingMethodValid {
			// signing method is not in the listed set
			return token, NewValidationError(fmt.Sprintf("signing method %v is invalid", alg), ValidationErrorSignatureInvalid)
		}
	}

继续下一步,p.ValidMethods是一个切片

type Parser struct {
	ValidMethods         []string // If populated, only these methods will be considered valid
	UseJSONNumber        bool     // Use JSON Number format in JSON decoder
	SkipClaimsValidation bool     // Skip claims validation during token parsing
}

包含允许的签名方法。一个条件判断检查它是否为 nil,如果不为 nil,说明存在需要验证的方法集合,随后遍历这个集合,如果使用的签名方法在这个集合中,便可进行下一步。

	// Lookup key
	var key interface{}
	if keyFunc == nil {
		// keyFunc was not provided.  short circuiting validation
		return token, NewValidationError("no Keyfunc was provided.", ValidationErrorUnverifiable)
	}
	if key, err = keyFunc(token); err != nil {
		// keyFunc returned an error
		if ve, ok := err.(*ValidationError); ok {
			return token, ve
		}
		return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable}
	}

这一步很简单,就是调用我们传入的匿名函数,看看我们传入的签名方法是否和实际解析出来的的方法一致,如果一致,就进行下一步。

	// Validate Claims
	if !p.SkipClaimsValidation {
		if err := token.Claims.Valid(); err != nil {

			// If the Claims Valid returned an error, check if it is a validation error,
			// If it was another error type, create a ValidationError with a generic ClaimsInvalid flag set
			if e, ok := err.(*ValidationError); !ok {
				vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid}
			} else {
				vErr = e
			}
		}
	}

p.SkipClaimsValidation就是一个bool值,这一步判断是否要跳过Claims的有效性验证,如果有效,进行下一步验证。

	// Perform validation
	token.Signature = parts[2]
	if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil {
		vErr.Inner = err
		vErr.Errors |= ValidationErrorSignatureInvalid
	}

	if vErr.valid() {
		token.Valid = true
		return token, nil
	}

	return token, vErr
}

此处又要提到我们之前提到的token结构体中的signature成员了,此处将jwt中的签名部分赋值给signatrue(签名)
这一部分用于确保 JWT 的完整性和身份验证。签名是通过将前两个部分的编码后结果与密钥进行哈希计算而生成的。
此处直接调用token.Method.Verify(…)进行验证便好。

最后调用 valid 方法检查当前的验证错误状态。如果没有错误(即 vErr 是有效的),则将 token.Valid 设置为 true,直接返回就行了。

3.2 提取信息

最后,回到我们的VerifyJWT函数

	if claims, ok1 := token.Claims.(jwt.MapClaims); ok1 && token.Valid {
		username := claims["username"].(string)
		return username, nil
	}
	return "", errors.New("token invalid")

判断错误,提取信息就好了。

那么,关于源码的分析就到这里结束了,如果有什么问题,请留言~

结语

其实看源码的过程还是蛮有趣的,主要是可以感受着自己在一步一步变强的感觉(也说不准,代码的乐趣也很多),关于JWT的源码分析就到这里结束了,读完这篇文章,你应该对JWT有更深的了解了~
强者应该也不会到这里来吧…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/945456.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Airbnb/Booking 系统设计(high level architecture)

原文地址 CodeKarle: Airbnb System Design | Booking.com System Design B站搜 “Airbnb System Design” 有视频版本 需求: 功能性需求 系统用户包括商家和客人。 Hotel - 商家(拥有hotel的人) onboarding - 商家可以入住系统。 update…

windows系统安装完Anaconda之后怎么激活自己的虚拟环境并打开jupyter

1.在win主菜单中找到Anaconda安装文件夹并打开终端 文件夹内有所有安装后的Anaconda的应用软件和终端窗口启动窗口 点击Anaconda Prompt(Anaconda)就会打开类似cmd的命令终端窗口,默认打开的路径是用户名下的路径 2.激活虚拟环境 使用命令…

C++进阶(三)--多态

目录 一、多态的基本概念 1.什么是多态 二、多态的定义及实现 1.虚函数 2.虚函数的重写 3.虚函数重写的⼀些其他问题 协变(了解) 析构函数的重写 C11 override和final 4.重载、覆盖(重写)、隐藏(重定义)的对比 三、抽象类 1.纯虚函数 2.接口继承和实现继承 四、多…

2025经典的软件测试面试题(答案+文档)

🍅 点击文末小卡片,免费获取软件测试全套资料,资料在手,涨薪更快 以下是软件测试相关的面试题及答案,希望对各位能有帮助! 1、测试分为哪几个阶段? 一般来说分为5个阶段:单元测试、集成测试…

海南省首套数据资产化系列团体标准正式发布

近日,首套数据资产化系列团体标准正式发布。本次系列涵盖《数据资产 数据治理规范》、《数据资产数据质量评价规范》、《数据资产 数据评估定价办法》和《数据资产 入表流程规范化标准》四项团体标准,通过海南省人工智能学会面向行业发布,自2…

突发!GitLab(国际版)将停止对中国区用户提供 GitLab.com 账号服务

消息称: 目前,为了更加严格的遵循中国网络数据安全管理的相关要求,GitLab SaaS(国际版)已逐步停止向国内用户提供服务与支持,国内用户亦无法注册或使用 GitLab SaaS(国际版)。自您的…

LLaVA模型讲解与总结

系列文章目录 第一章:LoRA微调系列笔记 第二章:Llama系列关键知识总结 第三章:LLaVA模型讲解与总结 文章目录 系列文章目录LLaVA(Large Language and Vision Assistant)Q: 这篇论文试图解决什么问题?Q: 论…

【linux内核分析-存储】EXT4源码分析之“创建文件”原理

EXT4源码分析之“文件创建”原理,详细的介绍文件创建的核心流程,并对EXT4中关于文件创建的关键函数进行了分析。 文章目录 0.前言1.创建文件概览1.1关键流程1.2 关键步骤说明 2.源码分析2.1 入口函数ext4_create2.2 分配inode关键函数 ext4_new_inode_st…

自学记录鸿蒙 API 13:骨骼点检测应用Core Vision Skeleton Detection

骨骼点检测技术是一项强大的AI能力,能够从图片中识别出人体的关键骨骼点位置,如头部、肩部、手肘等。这些信息在人体姿态分析、动作捕捉、健身指导等场景中有着广泛应用。我决定深入学习HarmonyOS Next最新版本API 13中的Skeleton Detection API&#xf…

使用ArcGIS Pro自带的Notebook计算多个遥感指数

在之前的分享中,我们介绍了如何使用ArcPy将GEE下载的遥感影像转为单波段文件。基于前面创建的单波段文件,我们可以一次性计算多种遥感指数,例如NDVI、EVI、NDSI等。我这里直接在ArcGIS Pro中自带的Notebook进行的运行。如下图所示&#xff0c…

XGPT用户帮助手册

文章目录 2024 更新日志2024.12.272024.12.29 摘要 本文详细介绍了XGPT软件的功能及发展历程。XGPT是一款融合了当前最先进人工智能技术的多模态智能软件,专为国内用户优化设计。除了强大的智能问答功能外,XGPT还结合日常办公和科学研究的需求&#xff0…

面试提问:Redis为什么快?

Redis为什么快? 引言 Redis是一个高性能的开源内存数据库,以其快速的读写速度和丰富的数据结构支持而闻名。本文将探讨Redis快速处理数据的原因,帮助大家更好地理解Redis的内部机制和性能优化技术。 目录 完全基于内存高效的内存数据结构…

强化学习——贝尔曼公式

文章目录 前言一、Return的重要性二、State Value三、贝尔曼公式总结 前言 一、Return的重要性 在不同策略下,最终得到的return都会有所不同。因此,return可以用来评估策略。 return的计算公式在基础概念中已经给出,通过包含 γ {\gamma} γ与…

使用MFC编写一个paddleclas预测软件

目录 写作目的 环境准备 下载编译环境 解压预编译库 准备训练文件 模型文件 图像文件 路径整理 准备预测代码 创建预测应用 新建mfc应用 拷贝文档 配置环境 界面布局 添加回cpp文件 修改函数 报错1解决 报错2未解决 修改infer代码 修改MFCPaddleClasDlg.cp…

CSS特效032:2025庆新春,孔明灯向上旋转飘移效果

CSS常用示例100专栏目录 本专栏记录的是经常使用的CSS示例与技巧,主要包含CSS布局,CSS特效,CSS花边信息三部分内容。其中CSS布局主要是列出一些常用的CSS布局信息点,CSS特效主要是一些动画示例,CSS花边是描述了一些CSS…

3D云展厅对文物保护有什么意义?

在文化遗产保护领域,3D云展厅技术的应用正成为一股新兴力量,它不仅改变了文物展示的方式,也为文物保护工作带来了深远的影响。 下面,由【圆桌3D云展厅平台】为大家介绍一下3D云展厅对文物保护意义的详细探讨。 1. 减少物理接触&a…

spring入门程序

安装eclipse https://blog.csdn.net/qq_36437991/article/details/131644570 新建maven项目 安装依赖包 pom.xml <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation&quo…

vue 修改vant样式NoticeBar中的图标,不用插槽可以直接用图片

使用文档中是可以直接使用图片链接的 :left-icon"require(../../assets/newImages/noticeImg.png)" <html> .... <NoticeBarmode""color"#C6C6C6"background""v-if"global_info.site_bulletin":left-icon"r…

MySQL数据导出导出的三种办法(1316)

数据导入导出 基本概述 目前常用的有3中数据导入与导出方法&#xff1a; 使用mysqldump工具&#xff1a; 优点&#xff1a; 简单易用&#xff0c;只需一条命令即可完成数据导出。可以导出表结构和数据&#xff0c;方便完整备份。支持过滤条件&#xff0c;可以选择导出部分数据…

【亲测有效】k8s分布式集群安装部署

1.实验环境准备 准备三台centos7虚拟机&#xff0c;用来部署k8s集群&#xff1a; master&#xff08;hadoop1&#xff0c;192.168.229.111&#xff09;配置&#xff1a; 操作系统&#xff1a;centos7.3以及更高版本都可以配置&#xff1a;4核cpu&#xff0c;4G内存&#xff…