Golang 搭建 WebSocket 应用(四) - jwt 认证

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: Golang 搭建 WebSocket 应用(四) - jwt 认证

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站

在上一篇文章中,我们已经搭建起了基本可用的一个 WebSocket 推送中心,但是有一个比较大的问题是,

我们并没有对进行连接的客户端进行认证,这样就会有一定的风险,如果被恶意攻击,

可能会影响我们的 WebSocket 服务器的正常运作。

本文我们就来把认证这个很关键的功能给补一下,在本文中,我们将会使用 jwt 来对我们的客户端进行认证。

什么是 jwt?

JWTJSON Web Token 的缩写,是一种用于在网络中安全传递信息的开放标准。它是一种紧凑且自包含的方式,用于在各方之间传递信息,通常用于身份验证和授权机制。

JWT 主要由三个部分组成:

  1. 头部(Header): 包含关于令牌的元数据,例如令牌的类型(typ)和签名算法(alg)。头部是一个 JSON 对象,通常会经过 Base64 编码。
  2. 载荷(Payload): 包含要传递的信息,通常包括用户身份信息以及其他声明。载荷也是一个 JSON 对象,同样经过 Base64 编码。
  3. 签名(Signature): 使用头部和载荷以及密钥生成的签名,用于验证令牌的真实性和完整性。签名是对头部和载荷的哈希值进行签名后的结果。

这三个部分通过点号(.)连接起来形成一个字符串,即 JWT。最终的 JWT 结构如下:

header.payload.signature

一个简单的 jwt 例子

jwt 的使用会分为两部分:生成 token,使用 token。本文中将会使用 golang-jwt/jwt 来做 jwt 的验证。

生成 token

生成 token 的操作有以下两步:

  1. 创建一个 token 对象:使用的是 jwt.NewWithClaims 方法,它第一个参数指定了签名算法,这里使用的是 HMAC,第二个参数接受一个 jwt.MapClaims,也就是上面提到的 payload
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    "foo": "bar",
    "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
})
  1. 利用上一步的 token 对象生成成一个 jwt 的签名字符串:使用的是 tokenSignedString 方法,它接受一个 key 作为参数,这个 key 也会用于解析这一步生成的 token 字符串。
// 注意:这里的 secret 在实际使用的时候需要替换为自己的 key
//(一般为一个随机字符串)
tokenString, err := token.SignedString([]byte("secret"))

我们生成 token 的操作一般发生在用户登录成功之后,这个 token 会作为用户后续发起请求的凭证。

使用 token

在用户登录成功拿到 token 之后,会使用这个 token 去从服务器获取其他资源,服务器会解析这个 token 并校验。

使用 token 的步骤如下:

  1. 解析 token:使用的是 jwt.Parse 方法,它第一个参数接受 token 字符串,第二个参数是一个函数(函数的参数就是解析出来的 token 对象,函数返回解密的 key
token, err = jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
    // 返回签名密钥,需要是 []byte 类型的
    return []byte("secret"), nil
})
  1. 使用 token 中的 payloadpayload 也就是我们业务实际使用的数据,其他的东西只是使用 jwt token 这门技术附带的一些东西。

这里说的 payload 实际上就是 token 对象的 Claims 属性,它是一个 map,保存了我们一些业务的数据还有其他一些 jwt 本身的字段。

// claims: map[foo:bar nbf:1.4444784e+09]
fmt.Println("claims:", token.Claims)

在上面这行代码中,foo:bar 是我们的业务数据,而 nbf:1.4444784e+09jwt 本身使用的字段。nbf 表示的是在这个时间之前,这个 token 不应该被处理。

例子

我们把上面的生成 token 的代码和解析 token 的代码放到一起看看效果:

注意:实际使用中这两个操作是分开的。

func TestHmac(t *testing.T) {
    // part 1:
  // 创建一个新的 token 对象,指定签名方法和你想要包含的 claims
  token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    "foo": "bar",
    "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
  })

  // 生成签名字符串,secret 为签名密钥
  tokenString, err := token.SignedString([]byte("secret"))

  // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJuYmYiOjE3Mjg1NjE2MDB9.9AxzBmYOuOnWfKUul57ATzjQ-sMzbggaoIdDjVzjm2Y, nil
  fmt.Println(tokenString, err)

    // part 2:
  // 解析 token
  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 []byte("secret"), nil
  })
  if err != nil {
    panic(err)
  }

  // header: map[alg:HS256 typ:JWT]
  fmt.Println("header:", token.Header)
  // claims: map[foo:bar nbf:1.4444784e+09]
  fmt.Println("claims:", token.Claims)
  // signature: Nv24hvNy238QMrpHvYw-BxyCp00jbsTqjVgzk81PiYA
  fmt.Println("signature:", base64.RawURLEncoding.EncodeToString(token.Signature))

  if claims, ok := token.Claims.(jwt.MapClaims); ok {
    // bar 1.4444784e+09
    fmt.Println(claims["foo"], claims["nbf"])
  } else {
    panic(err)
  }
}

本文不是讲解 JWT 的文章,关于 JWT 的更多细节可以参考 rfc7519

在消息推送中心 demo 加上 jwt 认证

我们使用 jwt 的目的是为了杜绝一些未知来源的连接,而在我们上一篇文章的实现中,

是先建立起连接,然后再进行 “登录” 操作的,这样就会导致即使是未授权的客户端也可以先进行连接,

这样就会在认证之前就启动了两个协程。

而如果这些连接并不是正常的连接,它们只连接但是不登录,那样就会有很多僵尸连接,这显然不是我们想要的结果。

我们可以在客户端打开连接的时候就去验证客户端的 token,如果 token 校验失败,则直接断开连接,

就可以解决上述问题了,从而避免不必要的开销。

如何在建立连接的时候就认证

要实现这个很简单,我们只需要在连接的 url 后加一个 queryString 即可,如下:

ws = new WebSocket('ws://127.0.0.1:8181/ws?token=123')

然后在 serveWs 中通过 r.FormValue("token") 来获取客户端传递过来的 token

再对其进行认证,认证不通过则拒绝连接。

具体来说,我们的 serveWs 会添加以下几行代码:

jwt := NewJwt(r.FormValue("token"))
err := jwt.Parse()
if err != nil {
    log.Println(fmt.Errorf("jwt parse error: %w", err))
    return
}

在函数入口的地方就进行验证,验证不通过则不进行连接。这里的 jwt 定义如下:

type Jwt struct {
  Secret string
  Token  string
}

func NewJwt(token string) *Jwt {
  return &Jwt{
    Token:  token,
    Secret: os.Getenv("JWT_SECRET"),
  }
}

func (j *Jwt) Parse() error {
  _, err := jwt.Parse(j.Token, 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 []byte(j.Secret), nil
  })

  return err
}

NewJwt 中,我们从 env 中获取 JWT_SECRET,这让我们的配置可以更加灵活。

token 中加上 uid

我们知道了,在 JWT 中的 payload 是可以加入我们的自定义数据的,所以我们的 uid 其实是可以加入到 jwttoken 中的,我们只需要在用户第一次获取 token 的时候加上即可:

token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    "uid": "123",
    "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
})

同样的,在解析 jwt token 的时候,可以从中取出这个 uid

// 第一个返回值是 uid,第二个是 error
func (j *Jwt) Parse() (string, error) {
  token, err := jwt.Parse(j.Token, 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 []byte(j.Secret), nil
  })

  if err != nil {
    return "", err
  }

    // 获取 ui
  if claims, ok := token.Claims.(jwt.MapClaims); ok {
    return claims["uid"].(string), nil
  } else {
    return "", fmt.Errorf("jwt parse error")
  }
}

最终,我们的 serveWs 演化成了如下这样:

func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
    // 解析 jwt token,从中取得 uid
  jwt := NewJwt(r.FormValue("token"))
  uid, err := jwt.Parse()
  if err != nil {
    log.Println(fmt.Errorf("jwt parse error: %w", err))
    return
  }

  conn, err := upgrader.Upgrade(w, r, nil)
  if err != nil {
    log.Println(fmt.Errorf("upgrade error: %w", err))
    return
  }

  client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256), uid: uid}
  client.hub.register <- client

  go client.writePump()
  go client.readPump()
}

这样一来,我们的 readPump 里面就不再需要处理登录消息了,那就暂时先把 readPump 中的逻辑去掉先:

func (c *Client) readPump() {
  defer func() {
    c.hub.unregister <- c
    _ = c.conn.Close()
  }()
  c.conn.SetReadLimit(maxMessageSize)
  c.conn.SetReadDeadline(time.Time{}) // 永不超时
  for {
    // 从客户端接收消息
    _, _, err := c.conn.ReadMessage()
    if err != nil {
      log.Println("readPump error: ", err)
      break
    }
  }
}

register 里的 uid 关联

我们之前是在 readPump 方法中将 uidWebSocket 建立起关联的,但由于我们已经去掉了 readPump

的登录消息处理逻辑。

因此我们需要在 Hubregister 中将 uidWebSocket 建立起关联:

case client := <-h.register:
    h.Lock()
    h.clients[client] = true
    // 建立起 uid 跟 WebSocket 的关联
    h.userClients[client.uid] = client
    h.Unlock()

jti

在此前使用过的一些 jwt 封装中,会有些使用 jwt 规范中的 jti 字段来传输 token 的唯一 ID,

在本文的实现中,uid 也是同样的功能。如果我们之后看到了 jti 这个字段,不要惊讶,其实这个才是规范。

测试

我们将生成 token 的代码中的 foo: bar 这个键值对修改为 uid: 123 之后,得到了如下 token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0NDQ0Nzg0MDAsInVpZCI6IjEyMyJ9.9yJ-ABQGJkdnDqHo-wV-vojQFEQGt-I0dyva1w6EQ7E

我们在 ws 链接后加上这个 token 即可连接成功:

ws = new WebSocket('ws://127.0.0.1:8181/ws?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE0NDQ0Nzg0MDAsInVpZCI6IjEyMyJ9.9yJ-ABQGJkdnDqHo-wV-vojQFEQGt-I0dyva1w6EQ7E')

我们可以看到,ws 连接返回的状态码是 101,这就表明我们的连接成功了。

推送消息

同之前一样的,在控制台执行一个 curl 命令即可推送消息了:

curl "http://localhost:8181/send?uid=123&message=Hello%20World"

总结

本文我们使用 jwt 对我们的 WebSocket 进行了认证。

jwt 认证我们可以已经用过很多次了,但如果还没有用过或者还没有在 go 中用过 jwt 认证的话,

那么本文就是一个很好的入门文章。

最后,再来回顾一下本文的内容:

  • jwt 包含了三部分:头部(Header)、载荷(Payload)、签名(Signature)。
  • 在 go 中可以使用 golang-jwt/jwt 来做 jwt 的验证。
  • 创建 token 分两步:使用 jwt.NewWithClaims 创建 token 对象、使用 SignedString 生成签名字符串。
  • 使用 token 分两步:使用 jwt.Parse 解析 token、使用 token.Claims 获取 payload
  • 在建立连接的时候就进行认证,可以避免非法连接导致的开销。
  • jwt 中可以加入我们的自定义数据,比如 uid,在 jwt.NewWithClaims 中加上即可。


目录
相关文章
|
2月前
|
SQL Java 测试技术
在Spring boot中 使用JWT和过滤器实现登录认证
在Spring boot中 使用JWT和过滤器实现登录认证
178 0
|
15天前
|
JSON 安全 数据安全/隐私保护
Python认证新风尚:OAuth遇上JWT,安全界的时尚Icon👗
【10月更文挑战第2天】在当今互联网世界中,数据安全与隐私保护日益重要。Python 作为广泛应用于 Web 开发的语言,其认证机制也不断进化。OAuth 2.0 和 JSON Web Tokens (JWT) 成为当前最热门的安全认证方案,不仅保障数据安全传输,还简化了用户认证流程。本文将介绍 Python 如何结合 OAuth 2.0 和 JWT 打造安全高效的认证体系。
29 3
|
19天前
|
前端开发 JavaScript UED
探索Python Django中的WebSocket集成:为前后端分离应用添加实时通信功能
通过在Django项目中集成Channels和WebSocket,我们能够为前后端分离的应用添加实时通信功能,实现诸如在线聊天、实时数据更新等交互式场景。这不仅增强了应用的功能性,也提升了用户体验。随着实时Web应用的日益普及,掌握Django Channels和WebSocket的集成将为开发者开启新的可能性,推动Web应用的发展迈向更高层次的实时性和交互性。
43 1
|
27天前
|
算法 安全 测试技术
golang 栈数据结构的实现和应用
本文详细介绍了“栈”这一数据结构的特点,并用Golang实现栈。栈是一种FILO(First In Last Out,即先进后出或后进先出)的数据结构。文章展示了如何用slice和链表来实现栈,并通过golang benchmark测试了二者的性能差异。此外,还提供了几个使用栈结构解决的实际算法问题示例,如有效的括号匹配等。
golang 栈数据结构的实现和应用
|
1月前
|
安全 Java 数据安全/隐私保护
|
1月前
|
JavaScript 前端开发 UED
WebSocket在Python Web开发中的革新应用:解锁实时通信的新可能
在快速发展的Web应用领域中,实时通信已成为许多现代应用不可或缺的功能。传统的HTTP请求/响应模式在处理实时数据时显得力不从心,而WebSocket技术的出现,为Python Web开发带来了革命性的变化,它允许服务器与客户端之间建立持久的连接,从而实现了数据的即时传输与交换。本文将通过问题解答的形式,深入探讨WebSocket在Python Web开发中的革新应用及其实现方法。
36 3
|
7天前
|
中间件 Go 数据处理
应用golang的管道-过滤器架构风格
【10月更文挑战第1天】本文介绍了一种面向数据流的软件架构设计模式——管道-过滤器(Pipe and Filter),并通过Go语言的Gin框架实现了一个Web应用示例。该模式通过将数据处理流程分解为一系列独立的组件(过滤器),并利用管道连接这些组件,实现了模块化、可扩展性和高效的分布式处理。文中详细讲解了Gin框架的基本使用、中间件的应用以及性能优化方法,展示了如何构建高性能的Web服务。
23 0
|
10天前
|
消息中间件 网络协议 安全
C# 一分钟浅谈:WebSocket 协议应用
【10月更文挑战第6天】在过去的一年中,我参与了一个基于 WebSocket 的实时通信系统项目,该项目不仅提升了工作效率,还改善了用户体验。本文将分享在 C# 中应用 WebSocket 协议的经验和心得,包括基础概念、C# 实现示例、常见问题及解决方案等内容,希望能为广大开发者提供参考。
48 0
|
1月前
|
存储 监控 Go
面向OpenTelemetry的Golang应用无侵入插桩技术
文章主要讲述了阿里云 ARMS 团队与程序语言与编译器团队合作研发的面向OpenTelemetry的Golang应用无侵入插桩技术解决方案,旨在解决Golang应用监控的挑战。
|
1月前
|
JSON 安全 数据库
Python安全性大升级:OAuth与JWT,让你的应用穿上防弹衣🛡️
【9月更文挑战第6天】在数字世界中,每个应用都面临着安全威胁。作为Python开发者,构建强大的系统至关重要。OAuth和JWT为我们提供了坚实的安全保障。OAuth作为一种授权机制,让用户无需向第三方应用暴露敏感信息;JWT则通过自包含的信息传输,增强了安全性并提高了系统性能。利用Python生态中的这些工具,我们可以更好地保护用户数据,守护他们的信任与期待。下面是一个使用PyJWT生成和验证JWT的示例代码:(示例代码同上)通过这些技术,我们的应用能够更加稳健地在数字海洋中航行。
26 3