JWT介绍
JWT
JSON Web Token (JWT) 是一种开放标准 (RFC 7519),提供一种简洁且自包含的方式,以JSON形式在通信双方间传递信息。这些信息可通过数字签名进行验证,确保其可信度。JWT 可以使用密钥(HMAC)或 RSA 或 ECDSA 的公钥/私钥对进行签名。虽然JWT可以加密以提供私密消息,但我们一般指的是已签名的 tokens。
JWT作用
- 身份验证和授权:JWT常被用来在身份提供者和服务提供者之间传递被认证的用户身份信息,以便于从资源服务器获取资源。这种方式特别适用于分布式站点的登录场景357。此外,JWT还可以包含用户标识、用户角色和权限等信息,进一步实现细粒度的授权控制。
- 信息交换:由于JWT的载荷部分可以自定义,它可以存储任何JSON格式的数据,这使得JWT成为一种便捷的方式来进行信息交换。例如,可以使用JWT来存储用户喜好、配置信息等自定义功能所需的信息。
- 单点登录(SSO):JWT因其小巧和自包含的特性,特别适合用于实现单点登录(SSO)。在单点登录场景中,用户只需进行一次登录操作,便可以在多个服务之间自由切换,而无需重复登录,极大地提高了用户体验。
- API密钥管理:JWT也广泛应用于API密钥管理领域。通过使用JWT作为API请求的一部分,可以有效地管理和控制对API的访问权限。
- 替代传统的Session和Cookie机制:在某些情况下,JWT可以用来替代传统的Session和Cookie机制。与Session和Cookie相比,JWT具有更小的服务器开销和更好的扩展性,因为它直接将用户数据下发给客户端,每次请求时附带发送给服务器。
- 跨域请求(Cross-Origin Requests):JWT可用于跨域请求的身份验证。由于JWT是自包含的,它不依赖于任何服务器端的会话存储,因此非常适合用于跨域API调用。
认证流程
- 前端通过Web将用户名和密码发送至后端,通常采用HttpPost请求。
- 后端在核对用户名和密码后,会将用户ID及其他信息作为JWT payload,并生成一个token返回给前端。
- 前端保存这个token,可以选择将其保存在localStorage或sessionStorage中,退出登录时删除token即可。
- 在后续的每次请求中,前端需要在header中放入token。
- 后端会检查token的有效性,包括签名是否正确,token是否过期等。
- 当后端验证token通过后,就会进行业务处理,并返回相应结果。
JWT的组成:
一个JWT实际上由三个部分组成,它们之间用点(.)分隔:
- 头部(Header):
- 通常包含两个部分:token的类型(typ),这里固定为JWT;以及所使用的签名算法(alg),例如HS256表示使用HMAC和SHA256算法进行签名。
- 例如:
{"alg": "HS256", "typ": "JWT"}
- 载荷(Payload):
- 包含了一系列可以被添加到JWT中的声明(claims)。这些声明可以分为三类:
- 公共声明(Public Claims): 任何JWT都可以包含的声明,如
exp(过期时间)、iat(发行时间)和sub(主题)。 - 注册声明(Registered Claims): 一组预定义的声明,具有特定的语义意义,如
iss(发行者)、aud(接收方)等。 - 私有声明(Private Claims): 由应用程序定义的声明,可以在标准声明之外添加额外的信息。
- 公共声明(Public Claims): 任何JWT都可以包含的声明,如
- 例如:
{"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
- 包含了一系列可以被添加到JWT中的声明(claims)。这些声明可以分为三类:
- 签名(Signature):
- 为了确保JWT的安全性,使用头部中指定的算法和服务器的密钥对头部和载荷进行签名。接收方可以使用相同的密钥和算法来验证签名的有效性。
- 签名的生成通常使用Base64 URL编码的头部和载荷,然后与密钥一起通过签名算法进行加密。
将这三部分编码(Base64 URL编码)并用点连接起来,就形成了一个完整的JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cU5O62AvvewJHsfqh1EJpTbuE7sBqVb64hjSDgZSTrZDRbky3ofGkeXBXBQgTJgzTyTTh0b2tlbg
在这个例子中,JWT由三部分组成,每部分都用.分隔。第一部分是头部,指定了签名算法;第二部分是载荷,包含了用户信息和过期时间等声明;第三部分是签名,用于验证JWT的真实性。
代码实例
1.安装JWT库
安装golang-jwt/jwt库
go get -u [github.com/golang-jwt/jwt](http://github.com/golang-jwt/jwt)
2.创建JWT密钥
import ("github.com/golang-jwt/jwt"
)// 生成JWT密钥
key := jwt.New(jwt.SigningMethodHS256)
3.创建JWT令牌
创建JWT令牌时,你需要定义载荷(Payload)中的声明(Claims),并使用密钥对令牌进行签名
import ("github.com/golang-jwt/jwt""time"
)
//创建JWT令牌
func createToken(claims map[string]interface{})(string,error){//创建JWT令牌token:=jwt.NewWithClaims(key,jwy.MapClaims(claims))//设置过期时间token.Claims.(jwt.MapClaims)["exp"]=time.Now().Add(5*time.Minute).Unix()//签发令牌tokenString, err := token.SignedString()if err != nil {return "", err}return tokenString, nil
}
4.验证JWT令牌
验证JWT令牌时,你需要使用相同的密钥和签名方法来验证签名的有效性,并检查令牌是否过期:
// parseToken 从给定的token字符串中解析出一个有效的jwt.Token实例。
// 函数接受一个tokenString参数,表示待解析的JWT字符串,
// 并返回一个指向jwt.Token的指针以及一个error。
// 如果解析成功,返回的jwt.Token将包含解码后的JWT声明信息,
// 否则返回一个非nil error说明解析失败的原因。
func parseToken(tokenString string)(*jwt.Token,error){
// 使用jwt.ParseWithClaims方法解析tokenString,// 将声明部分解析为jwt.MapClaims类型(一个可索引的声明映射)。// 提供一个自定义的解析密钥验证函数作为第三个参数。tokne,err:=jwt.ParseWithClaims(tokenString,&jwt.MapClaims{},func(tokekn *jwt.Token)(interface{},error){return key,nil})if err!=nil{return nil,error}// 如果token解析成功且有效(即其声明经过验证且未过期等),// 则尝试将其声明部分转换为jwt.MapClaims类型,并检查转换是否成功。// 若成功且token仍处于有效状态(token.Valid == true),则返回解析得到的token及其原始声明。if claims, ok := token.Claims.(*jwt.MapClaims); ok && token.Valid {return token, nil}// 如果上述条件均不满足(即解析或验证过程中出现错误,// 或token已过期、被篡改等导致无效),则返回一个表示无效JWT的预定义错误。return nil, jwt.ErrInvalid
}
此函数的主要功能是接收一个JWT字符串,通过调用jwt.ParseWithClaims方法对其进行解析。在解析过程中,它会使用传入的密钥(key)验证JWT的签名,确保其完整性和有效性。如果解析和验证成功,函数返回解析后的jwt.Token实例以及nil错误。否则,返回nil的jwt.Token指针和相应的错误信息。
5.实现登录和注册逻辑
在后端服务中,实现登录和注册逻辑,并在登录成功后返回JWT令牌:
func login(w http.ResponseWriter, r *http.Request) {// 假设你已经验证了用户的凭据// 创建JWT令牌claims := jwt.MapClaims{"userID": "user-id","exp": time.Now().Add(5 * time.Minute).Unix(),}tokenString, err := createToken(claims)if err != nil {// 处理错误}// 返回JWT令牌w.Header().Set("Content-Type", "application/json")json.NewEncoder(w).Encode(map[string]string{"token": tokenString})
}
6. 保护路由
创建一个中间件来保护你的路由,确保只有携带有效JWT令牌的请求才能访问:
// requireAuth 是一个HTTP中间件函数,用于对请求进行JWT身份验证。
// 它接收一个http.HandlerFunc类型的next参数,代表待执行的下一个HTTP处理器。
// 函数返回一个新的http.HandlerFunc,该处理器在实际执行next之前,
// 会对传入的请求进行JWT令牌验证。若验证失败,则返回HTTP 401 Unauthorized响应。
func requireAuth(next http.HandlerFunc) http.HandlerFunc {// 返回一个匿名http.HandlerFunc,作为实际执行的身份验证中间件。return func(w http.ResponseWriter, r *http.Request) {// 从请求头的"Authorization"字段中提取JWT令牌字符串。tokenString := r.Header.Get("Authorization")// 如果请求头中没有提供"Authorization"字段或其值为空,// 则向客户端发送HTTP 401 Unauthorized响应,并附带错误消息。if tokenString == "" {http.Error(w, "Authorization header is required", http.StatusUnauthorized)return}// 使用parseToken函数解析并验证JWT令牌字符串。// 如果解析或验证过程中发生错误,或者令牌本身无效,// 则向客户端发送HTTP 401 Unauthorized响应,并附带错误消息。token, err := parseToken(tokenString)if err != nil || !token.Valid {http.Error(w, "Invalid token", http.StatusUnauthorized)return}// 如果JWT令牌验证成功,继续执行传入的next处理器(即后续的HTTP处理逻辑)。next(w, r)}
}
此代码实现了HTTP中间件requireAuth,其作用是在接收到HTTP请求时,检查请求头中的Authorization字段是否存在并含有有效的JWT令牌。如果请求缺少Authorization字段或令牌无效,中间件会立即向客户端返回HTTP 401 Unauthorized状态码及相应的错误消息。反之,若令牌验证通过,则允许请求继续传递到下一个HTTP处理器(next参数指定的函数)进行后续处理。这样的设计使得您可以轻松地将requireAuth中间件应用于特定路由或全局,以确保只有持有有效JWT令牌的客户端才能访问受保护的资源。
然后,使用这个中间件来保护你的路由:
http.HandleFunc("/protected", requireAuth(func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Welcome to the protected route!")
}))
实例
在Go语言中实现JWT鉴权的完整应用实例。
package mainimport ("fmt""net/http""time"jwtgo "github.com/golang-jwt/jwt"
)// jwtKey 是用于签署 JWT 令牌的秘密密钥,必须保密。
var jwtKey = []byte("my_secret_key")// User 定义了用户的结构体,包含用户的ID和姓名。
// 这些字段将被编码到 JWT 令牌中。
type User struct {ID int `json:"id"` // 用户的唯一标识符Name string `json:"name"` // 用户的姓名
}// createToken 根据提供的 User 对象创建一个新的 JWT 令牌。
// 它设置了一个 "userID" 声明和一个 5 分钟后过期的时间。
func createToken(user *User) (string, error) {// 使用 HS256 签名方法创建一个新的 JWT 实例。token := jwtgo.New(jwtgo.SigningMethodHS256)// 将 token 的 Claims 设置为 MapClaims 类型,以便我们可以添加自定义声明。claims := token.Claims.(jwtgo.MapClaims)// 添加 "userID" 声明到 JWT 载荷中。claims["userID"] = user.ID// 设置 "exp" 声明为当前时间加上 5 分钟的 Unix 时间戳。claims["exp"] = time.Now().Add(5 * time.Minute).Unix()// 使用 jwtKey 签署令牌,并将其转换为字符串。tokenString, err := token.SignedString(jwtKey)if err != nil {return "", err}return tokenString, nil
}// parseToken 接受一个 JWT 令牌字符串,并使用 jwtKey 验证其签名。
// 如果令牌有效,它将返回解析后的 jwtgo.Token 对象。
func parseToken(tokenString string) (*jwtgo.Token, error) {// 使用 jwtgo.ParseWithClaims 解析 JWT 令牌,并使用 MapClaims 类型作为期望的声明类型。token, err := jwtgo.ParseWithClaims(tokenString, &jwtgo.MapClaims{}, func(token *jwtgo.Token) (interface{}, error) {// 提供 jwtKey 作为密钥来验证令牌的签名。return jwtKey, nil})if err != nil {return nil, err}// 确保解析后的声明是 MapClaims 类型。if _, ok := token.Claims.(*jwtgo.MapClaims); !ok {return nil, fmt.Errorf("invalid claims")}// 检查令牌是否有效(例如,是否已过期)。if !token.Valid {return nil, fmt.Errorf("token is not valid")}return token, nil
}// register 是一个 HTTP 处理函数,用于处理用户注册请求。
// 它创建一个新的 User 对象,并为其生成一个 JWT 令牌。
func register(w http.ResponseWriter, r *http.Request) {// 示例中省略了实际的用户注册逻辑(如保存用户信息到数据库)。// 直接创建一个示例用户和令牌。user := &User{ID: 1, Name: "John Doe"}token, err := createToken(user)if err != nil {http.Error(w, "Error creating token", http.StatusInternalServerError)return}// 设置响应头为 application/json 并将令牌编码为 JSON 返回。w.Header().Set("Content-Type", "application/json")json.NewEncoder(w).Encode(map[string]string{"token": token})
}// login 是一个 HTTP 处理函数,用于处理用户登录请求。
// 它验证用户凭据(示例中省略),并返回一个 JWT 令牌。
func login(w http.ResponseWriter, r *http.Request) {// 示例中省略了实际的用户登录逻辑(如验证用户凭据)。// 直接创建一个示例用户和令牌。user := &User{ID: 1, Name: "John Doe"}token, err := createToken(user)if err != nil {http.Error(w, "Error creating token", http.StatusInternalServerError)return}w.Header().Set("Content-Type", "application/json")json.NewEncoder(w).Encode(map[string]string{"token": token})
}// protected 是一个 HTTP 处理函数,用于处理需要身份验证的请求。
// 它从 HTTP 头中提取 JWT 令牌,并验证其有效性。
func protected(w http.ResponseWriter, r *http.Request) {// 从 "Authorization" HTTP 头中提取 JWT 令牌。tokenString := r.Header.Get("Authorization")if tokenString == "" {http.Error(w, "Unauthorized access", http.StatusUnauthorized)return}// 解析并验证 JWT 令牌。token, err := parseToken(tokenString)if err != nil {http.Error(w, "Unauthorized access", http.StatusUnauthorized)return}// 如果令牌有效,返回成功响应。w.WriteHeader(http.StatusOK)w.Write([]byte("Welcome to the protected route!"))
}func main() {// 设置 HTTP 路由处理函数。http.HandleFunc("/register", register)http.HandleFunc("/login", login)http.HandleFunc("/protected", protected)// 启动 HTTP 服务并监听 8080 端口。http.ListenAndServe(":8080")
}
这个程序实现了一个简单的 JWT 鉴权系统。它提供了用户注册和登录功能,这两个功能都会生成一个 JWT 令牌。此外,它还提供了一个受保护的路由,该路由只允许携带有效 JWT 令牌的请求访问。
相关文章:
JWT介绍
JWT JSON Web Token (JWT) 是一种开放标准 (RFC 7519),提供一种简洁且自包含的方式,以JSON形式在通信双方间传递信息。这些信息可通过数字签名进行验证,确保其可信度。JWT 可以使用密钥(HMAC)或 RSA 或 ECDSA 的公钥/…...
如何实现YOLOv8保存目标检测后的视频文件
首先安装所需的库和依赖项,确保你已经安装了OpenCV和YOLOv8的相关库和依赖项。你可以使用pip或conda来安装它们。 其次加载YOLOv8模型,使用YOLOv8的训练权重文件和配置文件,加载模型并进行初始化。这可以通过使用适当的库函数来完成&…...
LlamaIndex 组件 - Prompts
文章目录 一、关于 Prompts1、概念2、使用模式概览3、示例指南 二、使用模式1、定义自定义提示2、获取和设置自定义提示2.1 常用提示2.2 访问提示2.3 更新提示2.4 修改查询引擎中使用的提示2.5 修改索引构建中使用的提示 3、[高级]高级提示功能3.1 部分格式化3.2 模板变量映射3…...
Github 2024-04-16Python开源项目日报 Top10
根据Github Trendings的统计,今日(2024-04-16统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Python项目10TypeScript项目1Vue项目1系统设计指南 创建周期:2507 天开发语言:Python协议类型:OtherStar数量:241693 个Fork数量:42010 次…...
ElasticSearch nested 字段多关键字搜索,高亮全部匹配关键字的处理
ElasticSearch nested 字段多关键字搜索,高亮全部匹配关键字的处理 环境介绍 ElasticSearch 版本号: 6.7.0 需求说明 用户会传入多个关键字去ES查询ElasticSearch nested 字段 的多个字段,要求在返回的结果中被搜索的字段需要高亮所有匹配的关键字。…...
python_31-32
目录 1.进程 2.同步进程: 3.守护进程: 1.进程 # ### 进程 process import os,time""" # ps -aux 查看进程号 # ps -aux | grep 2784 过滤查找2784这个进程# 强制杀死进程 kill -9 进程号# 获取当前进程号 res os.getpid() print(res)…...
关于机器学习/深度学习的一些事-答知乎问(四)
如何评估和量化深度学习的可解释性问题? 针对深度学习模型,评估指标能够全面衡量模型是否满足可解释性。与分类的评估指标(准确度、精确度和召回率)一样,模型可解释性的评估指标应能从特定角度证明模型的性能。但是&a…...
[spring] Spring Boot REST API - 项目实现
Spring Boot REST API - 项目实现 书接上文 Spring Boot REST API - CRUD 操作,一些和数据库相关联的注解在 [spring] spring jpa - hibernate CRUD 主要的 layer 如下: #mermaid-svg-QE1PR1gyrkz4XIT0 {font-family:"trebuchet ms",verdana…...
ELK之Filebeat实用配置及批量部署(部署200+可用)
跟我之前Zabbix-agent批量部署脚本Linux and Windows(部署300可用)文章的套路一样,在使用该脚本前,请先准备好安装包及配置好安装包的资源下载点,由于我这边是纯内网,所以我就找了一个NAS做了共享目录&…...
用odin实现的资源复制编辑器
用odin实现了一个资源复制编辑器,使用要安装odin,功能是把要复制的资源路径一个个添加设置,点copy能把列表里的资源全部复制,支持目录复制到目录,文件复制到目录,文件复制替换。提升效率,让自己…...
linux监控文件操作行为
linux监控文件操作行为 使用 auditd 系统 auditd 是Linux系统的一个安全和审计系统,它能够跟踪系统上发生的安全相关事件。要使用 auditd 来监控文件,你需要首先确保 auditd 已经安装并且运行在你的系统上。 然后,你可以使用 auditctl 命令…...
单链表接口函数的实现(增删查改)
一、单链表的实现形式以及接口函数的声明 #include<stdio.h> #include<stdlib.h> #include<assert.h> typedef int DataType ;typedef struct SListNode {DataType data;struct SListNode* next; }SLTNODE; void SLTPrint(SLTNODE* phead);//打印链表 SLTNO…...
超低功耗Sub-1G收发芯片DP32RF002 M0内核(G)FSK/OOK 无线收发机的32位SoC芯片
产品概述 DP32RF002是深圳市动能世纪科技有限公司研制的基于ARMCortex-MO内核的超低功耗 高性能的、单片集成(G)FSK/OOK 无线收发机的32位SoC芯片。工作于200 ~960MHz范围内,支持灵活可设的数据包格式,支持自动应答和自动重发功能,支持跳频…...
uniapp_微信小程序_NaN
一、定义 isNaN() 函数用于检查一个值是否为 NaN。它接受一个参数,该参数可以是任何 JavaScript 数据类型,包括数字、字符串、对象等。如果参数是 NaN,或者不能被转换为数字,则 isNaN() 返回 true;否则返回 false。 …...
1043: 利用栈完成后缀表达式的计算
解法: #include<iostream> #include<stack> using namespace std; int main() {char a;stack<int> sk;while (cin >> a && a ! #) {if (a > 0 && a < 9) {sk.push(a - 0);}else {int num2 sk.top();sk.pop();int n…...
初学ELK - elk部署
一、简介 ELK是3个开源软件组合,分别是 Elasticsearch ,Logstash,Kibana Elasticsearch :是个开源分布式搜索引擎,提供搜集、分析、存储数据三大功能。它的特点有:分布式,零配置,自…...
[Java EE] 计算机工作原理与操作系统简明概要
1. 计算机工作原理 1.1 生活中常见的计算机 计算机分为通用计算机和专用计算机,计算机并不单单指的是电脑,还有我们平时使用的手机,ipad,智能手表等终端设备都是计算机.还有我们用户不常见的计算机,比如服务器. 还有许多嵌入式设备(针对特定场景定制的"专用计算机"…...
【尚硅谷】Git与GitLab的企业实战 学习笔记
目录 第1章 Git概述 1. 何为版本控制 2. 为什么需要版本控制 3. 版本控制工具 4. Git简史 5. Git工作机制 6. Git和代码托管中心 第2章 Git安装 第3章 Git常用命令 1. 设置用户签名 1.1 基本语法 1.2 案例实操 2. 初始化本地库 2.1 基本语法 2.2 案例实操 3. 查…...
如何在MobaXterm上使用rz命令
1、首先输入命令和想下载的文件,如下图: 2、按住ctrl鼠标右键,选择如下选项: 上传命令是rz,选择Receive...... 下载命令是sz,选择Send...... 3、我这里是要把Linux上的文件下载到我的本地window磁盘&…...
【计算机考研】408网课汇总+资源分享
408王道的视频就比较通俗易懂 王道的教材非常契合408的大纲,是专门为408大纲而编写的,而教材是方方面面都讲解的透彻。 建议王道为主,网络搜索为辅! 王道中讲解不清楚,看不懂的知识点,可以尝试在网络上进…...
RestClient
什么是RestClient RestClient 是 Elasticsearch 官方提供的 Java 低级 REST 客户端,它允许HTTP与Elasticsearch 集群通信,而无需处理 JSON 序列化/反序列化等底层细节。它是 Elasticsearch Java API 客户端的基础。 RestClient 主要特点 轻量级ÿ…...
深度学习在微纳光子学中的应用
深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向: 逆向设计 通过神经网络快速预测微纳结构的光学响应,替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…...
日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...
黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门 ,你需要做出一个**决定性判断**:恶性还是良性?这种“非黑即白”的抉择,正是**逻辑回归(Logistic Regression)** 的战场&a…...
Day131 | 灵神 | 回溯算法 | 子集型 子集
Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣(LeetCode) 思路: 笔者写过很多次这道题了,不想写题解了,大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
UDP(Echoserver)
网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法:netstat [选项] 功能:查看网络状态 常用选项: n 拒绝显示别名&#…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...
uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...
