Ruby 見令如見人,理解 JWT

kevinluo201 · 2021年02月21日 · 最后由 karloku 回复于 2021年02月22日 · 1156 次阅读

Dev.to 链结 https://dev.to/kevinluo201/jwt-27ng

JWT 是 JSON Web Tokens 的缩写。 最近工作上需要用 JWT 来互传资讯,可能年纪渐长...觉得这东西实在使用是满简单的 xD

只是结合多个概念,一开始不是很好懂

纪录下理解的过程

TL;DR

个人观点,不要打我 xD

  • 可把 JWT 当成 JSON
  • JWT 是无法修改的 JSON
  • JWT 跟加密无关

Why?

先不提 JWT 怎麽实做的,先想像班上有一对情侣 Dustin 跟 Suzie...

两人都是 Geek,所以在教室最后一排的 Dustin 要传纸条给第一排的 Suzie 的时候,竟然是用 JSON 的格式!

{
  "from": "Dustin",
  "to": "Suzie",
  "message": "Do you want to have dinner with me tonight?",
  "place": "MacDonald's"
}

不过传纸条的过程中,班上就是会有人硬要打开不是给他的纸条,比如 Will 就是打开纸条并把内容改成

{
  "from": "Dustin",
  "to": "Erica", //  Will 改了
  "message": "Do you want to have dinner with me tonight?",
  "place": "MacDonald's"
}

于是不仅当晚 Dustin 只能孤零零一个人吃大麦克,Suzie 还不理他一个礼拜,悲剧。 其实一切就是他没办法控制發出去的讯息会不会被篡改

后来 Dustin 记取教训,事先跟 Suzie 约定好一个暗号 NeverEndingStory 并用 HMAC SHA-256 这种不可逆的方式製作一串乱码,再把乱码附在要传的讯息后面

// 796e0c718cc2768edfb67a53b0f4fed74b4abbac61baaa68876630d9827714a0

Suzie 打开纸条后即可以用NeverEndingStory 以相同的方式将纸上的讯息转成乱码,再检查是否和纸上附的乱码一致。

可以任意找一个线上的 HMAC SHA256 转换器来验证 Free HMAC-SHA256 Online Generator Tool | Devglan

只要讯息跟算出来的乱码不合,即知道讯息已遭到修改或者不完全。所以这个乱码很像一个签名

JWT 就是可以给 JSON 一个签名,确保讯息没有修任何人动过。变得好像可以宣告一个 const 的 JSON 再传送出去一样

JWT 组成

JWT 其实是一串字串,有 3 个部分,以 . 分开

  • Header: 註明是用何种演算法製作签名的
  • Payload: 就是实际讯息的 JSON
  • Signature: 利用 Header 註明的演算法用 HMAC 方式製作出来的乱码,即签名

可以到 JSON Web Tokens - jwt.io任意製作一个 JWT 左边即为製作出来的 JWT。

我想这时已经有人握紧拳头了...

听你鬼扯! 这个 JWT 看起来根本就只是一串乱码! 什麽 JSON、指定的演算法跑到哪裡去了?

上面的确是漏说了一些细节 😂 Header, Payload 其实是会先经由 base64 去编码 Base64 就是个编辑的方式,可以先简单理解成一个可编码及还原的方法。 如果真的很想知道 base64 是什麽,可参考另一篇文章 base64 介绍

所以红色部色就是 header,紫色部分就是 JSON 这边用 ruby 来做看看

header = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
payload = 'eyJuYW1lIjoiS2V2aW4ifQ'
require 'base64'
puts Base64.decode64(header)
# {"alg":"HS256","typ":"JWT"}
puts Base64.decode64(payload)
# {"name":"Kevin"}

# verify the signature
require 'openssl'
mac = OpenSSL::HMAC.hexdigest("SHA256", 'mySecret', "#{header}.#{payload}")
Base64.urlsafe_encode64(mac).gsub('=', '')  # '=' is just padding
# "IMa4S4W1LMP1xuKVglwBagrHA5wwK9sBu-CVDKudIkg"

可能会疑惑怎麽可以直接把 payload 还原成 JSON,那 JWT 裡的资料不就大辣辣地秀出,这样不是不安全

没错...因为 JWT 好像变成一串乱码,容易误会它很安全,其实它跟加密完全没有关係 xD

JWT 主要在乎资料是否被篡改,Signature 是否一致而已

所以别把敏感的资讯放在裡面。

我们可以先看实际使用 JWT 的程式码

Demo

因为我主要用 ruby,这边利用 ruby-jwt 这个 gem 来 demo。不过几乎所有的语言都有实作 jwt,可在 JSON Web Tokens - jwt.io 查找相关资源。

require 'jwt'

payload = {
  first_name: 'Kevin',
  last_name: 'Luo'
}

secret = "my secret"

token = JWT.encode payload, secret, 'HS256'
# "eyJhbGciOiJIUzI1NiJ9.eyJmaXJzdF9uYW1lIjoiS2V2aW4iLCJsYXN0X25hbWUiOiJMdW8ifQ.dZJnejsQ9cWs1hyOvCAij_Q4k87vfbQpeBIjgqYCrgs"

decoded_token = JWT.decode token, secret, true, { algorithm: 'HS256' }
# [{"first_name"=>"Kevin", "last_name"=>"Luo"}, {"alg"=>"HS256"}]

此外,JWT 的格式 RFC 其实有约定一些参数可以设定,不过端看程式有没有做对应的处理, 一个常用的是「到期时间」 exp ,设定一个到期时间给 JWT, 假使真的到期,decode 时即丢出JWT::ExpiredSignature 这个 Exception

require 'jwt'
exp = Time.now.to_i + 3600 # 1 hour
exp_payload = { data: payload, exp: exp }

token = JWT.encode exp_payload, secret, 'HS256'
# "eyJhbGciOiJIUzI1NiJ9.eyJkYXRhIjp7ImZpcnN0X25hbWUiOiJLZXZpbiIsImxhc3RfbmFtZSI6Ikx1byJ9LCJleHAiOjE2MTM4ODU4MjF9.1_NIKXDnBVz1G6Li7_CZbcDwIk5AFaOsreK7BFDS13Q" 

begin
  decoded_token = JWT.decode token, secret, true, { algorithm: HS256 }
  # [{"data"=>{"first_name"=>"Kevin", "last_name"=>"Luo"}, "exp"=>1613885821}, {"alg"=>"HS256"}] 
rescue JWT::ExpiredSignature
  # 过期
end

应用

JWT 除了可以让 2 台有共同 secret 的电脑可互传确认不会被篡改的资料外, 最常见的情境应该就是后端 API 使用者登入后發给前端的 session token 了吧 会用 API 的 Request 的 header 中裡的Authorization Bearer [TOKEN]来判断来源是否可以取用该 API

我觉得用 JWT 当 session token 应该有 2 个优点:

  • 由于 JWT 有不会被篡改特性,server 收到 token 后,可在裡面直接取用资料,比如说 user_id 之类的。所以才说见令如见人
  • 不同使用者 JWT 因为 user_id 不同,必长得不同,而不用是检查碰撞。如果 token 是随机产生,我们还得去检查是否有碰撞 (就是 2 个使用者运气好,用到相同的乱数字串)

目前就只用过这 2 种应用,不知道还有什麽特别的用途囉?

JWT 的分享到此囉 : )

个人经验:使用 JWT 的时候,最好要声明 exp 时间,这样可以保证时效性;另外再处理好有效期内的唯一性,避免重放攻击。这样整体就比较安全靠谱了

kevinluo201 Web 的摩斯密码,Base64 介绍 提及了此话题。 02月22日 18:33

ActiveSupport::MessageVerifier 提供的也是类似的功能。当时看 JWT 怎么看怎么觉得眼熟。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号