JWT簡介
JWT,JSON Web Token。作為現在常見的 Authorization Solution,大部分的網站會在用戶認證身份後,給予一個 JWT token 作為身份證明,之後在向網站 API 存取資源時,就可以用 JWT 進行需要認證身份的操作了。
好處
- 對服務提供者來說,因為驗證過的 token 內容是可以被相信的,一來可以減少對 DB 拿資料的部分負擔,二來也可以比較容易實現 Stateless。
- 對網路傳輸來說,相比於舊時代常用的 SAML (Security Assertion Markup Language),JWT 的內容短上很多,所以容易在網路中傳遞。
- 對工程師來說,JSON 很容易在各種語言中用 Map 的方式實作,所以操作起來也相對 XML 格式方便很多。
重要
JWT 的資訊不帶有保密性,只能被驗證是否被竄改而已,所以不要把機密資訊(例如密碼)放在 JWT 裡面!
JWT解析
JWT有好幾種實作方式,大部分我們現在看到的格式都是採用 JWS (JSON Web Signature) (RFC7515) 實現的,也是 JWT.io 網頁中選用的方法。JWS 格式分為 Header, Payload, Signature 三個部分:
Header
通常只會帶兩個值:alg(Algorithm) 和 typ(Type)。
- alg: 註明等等 Signature 部分用的 Hash 演算法,目前都用 SHA 家族的方法比較普遍。
- typ: 這沒有懸念,填 JWT 上去就對了。
{
"alg": "HS256",
"typ": "JWT"
}
填完之後,把這邊的資料先做 base64encoding,轉成 HeaderString 待用。
Payload
主要用來放資料的地方,標準文件裡把這邊的資訊叫做 Claims,只要用 Key-Value 的方式填上去,基本上想放什麼都可以。
比較需要注意的是,有一些當初 JWT 標準制訂時被保留的關鍵字,如果誤用了很容易造成誤會。例如 iss(Issuer), exp(Expiration Time), iat (Issued At), sub (Subject)等等,有興趣的話可以看這篇文件。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
這邊也一樣,填完之後做 base64encoding,轉成 PayloadString 待用。
Signature
簽名部分是 JWT 的關鍵所在,也是 JWT 包含的資料可以被信任的原因。
簽名就是:
- 前面經過 base64encoding 準備好的 HeaderString 和 PayloadString
- 外加只有自己知道的 Secret
- 以選定的加密法(例如 SHA256)作加密
所生出來的一串無法被還原的亂碼。
這個亂碼的生成因為涉及了先前在 Header 和 Payload 填入的資料,所以如果有人偷偷改了裡面的資料,就沒辦法生成一樣的簽名,就沒辦法通過驗證。
而所謂的驗證就是,當伺服器收到 JWT 時,利用 Header 和 Payload 和自己的 Secret 重新生成一次 Signature,如果是相同的就代表驗證成功囉!
Golang JWT 實作
有了 JWT 的基礎概念後,接下來我們就用 Golang 實作看看吧。以下我們會用 github.com/dgrijalva/jwt-go 這個 library 來實作基本的 User JWT:
前置作業
如果需要多塞一些資訊的話,可以製作一個自己的 struct 來並嵌入 library 裡基本的 struct :
type DemoTokenClaims struct {
Email string
jwt.StandardClaims
}
但如果沒什麼特別需求,只需要塞 ID 之類的話,就直接用 jwt.StandardClaims 吧。
製作 JWT
如同前面介紹的, JWT 其實就是把資訊放進 JSON 裡面,再經過 base64encoding 與 Signature 組合的結果而已,所以製作 JWT 也只有以下三步驟:
- 填入資訊
- 編碼,製作token
- 簽名
做好之後就可以把 Token 傳回去了,請看以下範例:
func CreateDemoToken(id, email string, secret []byte) string {// 1. 填入資訊
now := time.Now()
claims := new(DemoTokenClaims)
claims.Id = id
claims.Email = email
claims.Issuer = "demoProject"
claims.IssuedAt = now.Unix()
claims.ExpiresAt = now.Add(30 * 24 * time.Hour).Unix()// 2. 編碼,製作token
demoToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)// 3. 簽名
tokenString, _ := demoToken.SignedString(secret)return tokenString
}
驗證 Token
當我們收到 JWT 時,需要先確認資訊有沒有被竄改,同樣地,利用這包 jwt library 可以很輕鬆的完成:
func ValidDemoToken(demo string, secret []byte) (*DemoTokenClaims, error) {// base64decoding + 驗證 + 回傳 raw token
token, err := jwt.ParseWithClaims(demo, &DemoTokenClaims{}, func(token *jwt.Token) (interface{}, error) {
return secret, nil
})if err != nil {
return nil, errors.New("invalid")
}// 從 raw token 中取回資訊
if claims, ok := token.Claims.(*DemoTokenClaims); ok && token.Valid {
return claims, nil
}return nil, errors.New("invalid")
}
完成驗證並取回資訊後,就可以繼續後續的動作囉。
參考資料
RFC7519
JSON Web Token (JWT) draft-ietf-oauth-json-web-token-32
Introduction to JSON Web Tokens
Go dev — package jwt
不要用JWT替代session管理(上):全面了解Token,JWT,OAuth,SAML,SSO