抖音v1版(以消息接口为例)

194 阅读6分钟

本篇为青训营大项目笔记,用于查漏补缺,尚未整理供食用。

注:下文的第二版指的是第一个版本合代码之后的第二版即v1版,而原始仓库的v2版本应为添加grpc微服务架构的版本,本文未讨论。

阅读源码架构

与第一版v0相比,第二版v1有以下区别:

  1. controller控制层
  • Init.go

      1. 第二版使用了嵌套的路由组来组织和注册路由,在这个版本中,所有的路由都在同一个Init函数中注册,并使用middleware.Jwt.MiddlewareFunc()来为特定的路由组添加JWT鉴权中间件。
      1. 第一版则更模块化,它将每个功能模块的路由注册分离到其自己的函数中,如RegMessageActionInit函数只调用了其他的注册函数,如RegExample,每个注册函数都直接为特定的路由注册了处理函数,而没有使用路由组
  • middleware即jwt中间件

    • 这里分为初始化jwt中间件及jwt鉴权流程中所需要用到的工具,比如定义claims结构体,生成token,从token中获取用户 ID等
  • ctlModel和ctlFunc

    • 自定义结构体均统一放置在controller/ctlModel下,同时针对某几个结构体的类型进行变更,比如ID由之前的unit改为int64
      • 原消息接口的response结构体
      • service服务层定义的结构体
    • 使用controller/ctlFunc包中的函数统一进行响应
      • response.go => controller/ctlFunc
      • resp的结构体统一定义在controller/ctlModel
  • message.go

    • controller中的messageAction.go聊天操作及messageChatList.go聊天记录将函数Action和Chat集合到一个文件controller/message.go中,并对逻辑进行了优化。
  1. dal数据层

    GetMessage方法进行了优化,提供preMsgTime写入日志,使用了更健全更简洁的SQL查询语句,将错误处理删除?还是转移到哪里?

  2. service服务层

    定义的结构体提取到controller/ctlModel,由于ID由之前的unit改为int64,这里在convertMessagesToChatResponse方法对ID进行了int64(msg.ID)的类型转换。

JWT

JWT 是一个开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息作为 JSON 对象。这种信息可以被验证和信任,因为它是数字签名的。

type Token struct {
	Raw       string                 // The raw token.  Populated when you Parse a token
	Method    SigningMethod          // The signing method used or to be used
	Header    map[string]interface{} // The first segment of the token
	Claims    Claims                 // The second segment of the token
	Signature string                 // The third segment of the token.  Populated when you Parse a token
	Valid     bool                   // Is the token valid?  Populated when you Parse/Verify a token
}

controller/middleware/jwtutil.go

  1. 初始化 jwtKey

    • jwtUtilInit 函数从全局配置中获取 JWT 密钥并初始化 jwtKey 变量。
  2. 定义 Claims 结构体

    • Claims 结构体扩展了 JWT 的标准声明,并添加了一个 UserId 字段,用于存储用户 ID。
  3. 生成 Token

    • ReleaseToken 函数接收一个用户 ID,并使用该 ID 生成一个新的 JWT。此函数返回生成的 token 或任何错误。
  4. 获取用户 ID

    • GetUserID 函数从上下文中提取用户 ID。如果 JWT 中间件未在路由中使用,或者上下文中不存在用户 ID,该函数将记录一个错误并返回 0。
  5. 解析 Token

    • ParseToken 函数接收一个 token 字符串,并尝试使用 JWT 密钥来解析它。该函数返回解析的 token、声明和任何错误。
  6. 从 Token 中获取用户 ID

    • GetUserIDFromToken 函数解析 token 并从中提取用户 ID。

生成 Token

ReleaseToken 函数接收一个用户 ID,并使用该 ID 生成一个新的 JWT。此函数返回生成的 token 或任何错误。

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 这里的生成逻辑是由JWT 软件包 - github.com/dgrijalva/jwt-go - Go 软件包提供的

func NewWithClaims(method SigningMethod, claims Claims) *Token

type Token struct {
	Raw       string                 // The raw token.  Populated when you Parse a token
	Method    SigningMethod          // The signing method used or to be used
	Header    map[string]interface{} // The first segment of the token
	Claims    Claims                 // The second segment of the token
	Signature string                 // The third segment of the token.  Populated when you Parse a token
	Valid     bool                   // Is the token valid?  Populated when you Parse/Verify a token
}

jwt.NewWithClaimsgithub.com/dgrijalva/jwt-go 包中用于创建一个新的 JWT(JSON Web Tokens)的方法。这个方法接受一个签名方法(如 jwt.SigningMethodHS256)和一组声明(如 claims)作为参数,并返回一个未签名的 token。

让我们深入了解这里的每个参数:

  1. SigningMethodHS256:

    • 这是 HMAC 使用 SHA-256 哈希算法的签名方法。
    • JWT 支持多种签名方法,其中 HS256(HMAC 使用 SHA-256)是最常用的一种。
    • 使用 HMAC 进行签名意味着你需要一个秘密密钥(在您的代码中,这个密钥是 jwtKey)。
    • 当你创建一个新的 JWT 时,它会使用这个秘密密钥和 HS256 算法对 JWT 的头部和有效载荷进行签名。签名的结果是 JWT 的第三部分,即签名部分。
  2. claims:

    • 声明是 JWT 的有效载荷。它是一个键值对的集合,可以包含任何你想在 token 中传递的信息。
    • 在您的代码中,claims 包括一个 UserId 和一些标准声明,如 ExpiresAtIssuedAtIssuerSubject
    • 这些声明将被编码为 JSON,并成为 JWT 的第二部分,即有效载荷部分。

如何生成 token:

  1. 首先,JWT 的头部和有效载荷部分分别被 Base64Url 编码。
  2. 这两个编码后的字符串通过一个点号 (.) 连接在一起,形成一个未签名的 token。
  3. 然后,使用提供的签名方法(HS256)和秘密密钥对未签名的 token 进行签名,生成签名。
  4. 最后,签名被添加到未签名的 token 后面,再次通过一个点号 (.) 连接,形成完整的 JWT。

当验证 JWT 时,可以使用相同的密钥和签名方法对 token 进行签名,并与提供的签名进行比较,以验证 token 的完整性和真实性。

controller/middleware/jwtmiddleware.go

使用 github.com/hertz-contrib/jwt 的第三方库来实现 JWT 功能。

从 token 提取用户信息

IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
			claims := jwt.ExtractClaims(ctx, c)
			return claims[identityKey]
		},

认证使用md5

password := encrypts.Md5(reqObj.Password + global.Config.PasswordSalt)

登陆成功后为向 token 中添加自定义负载信息PayloadFunc 登录校验成功,将token返回给前端 LoginResponse使用ctlModel和ctlFunc构造resp 鉴权Authorizator单一角色 不设权限校验

设置 jwt 校验流程发生错误时响应所包含的错误信息HTTPStatusMessageFunc 调用ctlFunc构造jwt 验证流程失败的响应函数Unauthorized

GLOBAL/CONFIG 新增

  1. AliOSS 结构体:

    • 这个结构体可能是用于配置阿里云的对象存储服务 (OSS)。以下是该结构体中字段的简要描述:

      • Bucket: OSS 的存储空间名称。
      • Endpoint: OSS 的服务入口地址。
      • AccessKeyId: 访问OSS的API的ID。
      • AccessKeySecret: 访问OSS的API的密钥。
    • 这些字段通常用于SDK或API调用,以连接和与OSS服务进行交互。

  2. PasswordSalt 字段:

    • 这个字段是一个字符串,可能是用于加密或哈希密码的盐。使用盐可以增加哈希函数的复杂性,并使每个用户的哈希密码唯一,即使他们的实际密码相同。

待解决的问题

  1. utils中的conv应用场景?
  2. 数据流向变化待整理。
  3. 现在调用的jwt-go有新版本,为什么选旧版本?以及为什么初始化jwt要选用hertz框架的jwt包?除了这个包之外还有哪些技术选型的选项?选择hertz框架的jwt包比其他选项好在哪里?

参考资料

JWT 软件包 - github.com/dgrijalva/jwt-go - Go 软件包 hertz-contrib/jwt: JWT middleware for Hertz (github.com)