JWT基礎(chǔ)概念
JWT是
json web token
的簡稱其中的 token 是令牌的意思, 其實這個令牌實質(zhì)上是服務(wù)端生成的一段有規(guī)則的字符串
我們看看JWT官方自己對其的定義
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.
我們提煉出重點信息:
- JWT是一個開放的標(biāo)準(zhǔn)
- jwt本身體積比較緊湊,所以傳輸速度比較快
- jwt可以放在URL中傳遞,可以放在請求頭中傳遞,可以放在請求體中傳遞
- jwt的
有效載荷
中包含一些自定義的有效信息,在某些場景中可以避免部分?jǐn)?shù)據(jù)庫查詢
JWT使用場景
-
授權(quán) : jwt最常見的使用場景,用戶端首先登陸成功,從服務(wù)端獲取jwt(令牌),那么用戶后面的所有的請求中都應(yīng)該包含這個令牌,服務(wù)端通過這個令牌判斷允許用戶的權(quán)限和訪問的資源,服務(wù).
基于這樣的特點可以做單點登錄(SingleSignOn易迹,SSO)
基于此也可以做跨域認(rèn)證
信息交換 : 通訊雙方通過jwt可以傳遞信息, 信息都是簽名之后,可以防止偽造
JWT的結(jié)構(gòu)
如下是一個標(biāo)準(zhǔn)的JWT
JWT 是有三部分組成的,每一部分之間通過 .
(點號) 隔開
header 頭部
Payload 載荷
Signature 簽名
那么JWT的格式如下 :
header
.
Payload
.
Signature
頭部
標(biāo)頭是一個json對象通常由兩部分組成:令牌的類型(即JWT)和所使用的簽名算法,例如HMAC SHA256或RSA。
{
"alg": "HS256",
"typ": "JWT"
}
這一部分的json內(nèi)容被 Base64Url 編碼之后成為第一部分
載荷
載荷也是一個json對象,是實際承載傳遞數(shù)據(jù)的部分,JWT提供了7個預(yù)定義好字段可以按需使用
iss (issuer):簽發(fā)人
exp (expiration time):過期時間
sub (subject):主題
aud (audience):受眾
nbf (Not Before):生效時間
iat (Issued At):簽發(fā)時間
jti (JWT ID):編號
除了這些預(yù)定義的字段,我們可以在payload中添加自己的'私貨'
,添加一些自己字段
{
"name":"admin",
"pwd":"123456"
}
有了有效載荷之后再對有效載荷進行 Base64Url 編碼 ,編碼之后的這部分就是 jwt 的第二部分
tips : 盡量不要將很重要和私密的信息放在其中,因為這部分解碼之后是可見的
簽名
簽名就是將前面的編碼之后header
和 編碼之后的 payload
再加上秘鑰secretKey
通過指定的加密算法創(chuàng)建出來的(簽名默認(rèn)算法是 HMAC SHA256)
創(chuàng)建簽名的公式如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secretKey)
通常認(rèn)為簽名的作用是防數(shù)據(jù)被篡改
JWT工作原理
一般而言我們攜帶JWT發(fā)送請求可以放在HTTP請求頭中的
Authorization
中, 格式如下Authorization: Bearer <token>
tips: 我們前面說了jwt 這個令牌其實也可以放在 HTTP的url中,也可以在HTTP請求的請求報文中
當(dāng) token在HTTP的請求頭中的
Authorization
中發(fā)送時可以解決CORS
的問題
Golang使用JWT
模擬使用場景 :
- 新建http服務(wù),提供三個處理接口
/auth
,/home
,/list
/auth
接口處理用戶登陸驗證,并返回 token (jwt)/home
接口處理登陸成功的用戶都能訪問的 home服務(wù)/list
接口處理登陸成功的用戶并且用戶權(quán)限是admin
才能訪問的 list服務(wù)tips : 這里的http服務(wù)和JWT 的 Authorization server(認(rèn)證服務(wù)) 在同一服務(wù)器上,在實際開發(fā)中 Authorization server(認(rèn)證服務(wù)) 可以單獨部署
代碼實現(xiàn)
Golang中有很多關(guān)于jwt的包,我們使用如下包
# 安裝依賴包
go get github.com/dgrijalva/jwt-go
JWT服務(wù)的關(guān)鍵代碼如下
// JWT中的payload中不要放重要數(shù)據(jù),因為這部分?jǐn)?shù)據(jù)通過Base64URL算法能反解出來
type Claims struct {
// 自定義的`私有`數(shù)據(jù),在payload中
UserAccount
// jwt的標(biāo)準(zhǔn)的claims
jwt.StandardClaims
}
// 生成token
func GetToken(name, password, role string) (string, error) {
c := Claims{
UserAccount: UserAccount{
Username: name,
Password: password,
Role: role,
},
StandardClaims: jwt.StandardClaims{
// token過期時間
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
// 簽發(fā)人
Issuer: "captain",
// 主題
Subject: "jwt test",
},
}
// 生成token,默認(rèn)采用HMAC SHA256
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 加上簽名(需要用到秘鑰),生成完整的token
return token.SignedString(SecretKey)
}
// 解析token
func ParseToken(tokenStr string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (i interface{}, err error) {
return SecretKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
此處我們不用框架,直接使用golang一些標(biāo)準(zhǔn)包構(gòu)建一個http服務(wù),并且集成JWT服務(wù)
http服務(wù)(包含JWT服務(wù))端完整代碼
jwt.go
package main
import (
"encoding/json"
"fmt"
"github.com/dgrijalva/jwt-go"
"github.com/pkg/errors"
"log"
"net/http"
"strings"
"time"
)
const (
// 定義token的有效時間
TokenExpireDuration = time.Hour * 1
)
var SecretKey = []byte("123456")
// 請求的賬戶信息
type UserAccount struct {
Username string `json:"username"`
Password string `json:"password"`
Role string `json:"role"`
}
// 響應(yīng)給客戶端的數(shù)據(jù)
type ResponseToClient struct {
Code string `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// JWT中的payload中不要放重要數(shù)據(jù),因為這部分?jǐn)?shù)據(jù)通過Base64URL算法能反解出來
type Claims struct {
// 自定義的`私有`數(shù)據(jù),在payload中
UserAccount
// jwt的標(biāo)準(zhǔn)的claims
jwt.StandardClaims
}
// 生成token
func GetToken(name, password, role string) (string, error) {
c := Claims{
UserAccount: UserAccount{
Username: name,
Password: password,
Role: role,
},
StandardClaims: jwt.StandardClaims{
// token過期時間
ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(),
// 簽發(fā)人
Issuer: "captain",
Subject: "jwt test",
},
}
// 生成token,默認(rèn)采用HMAC SHA256
token := jwt.NewWithClaims(jwt.SigningMethodHS256, c)
// 加上簽名(需要用到秘鑰),生成完整的token
return token.SignedString(SecretKey)
}
// 解析token
func ParseToken(tokenStr string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (i interface{}, err error) {
return SecretKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
func WriteToResponse(w http.ResponseWriter, code, message string, data interface{}) {
var resp ResponseToClient
resp.Code = code
resp.Message = message
resp.Data = data
respJson, _ := json.Marshal(resp)
// 設(shè)置響應(yīng)為json格式
w.Header().Set("Content-Type", "application/json;charset=utf-8")
fmt.Fprintf(w, "%v\n", string(respJson))
}
// 默認(rèn)處理函數(shù)
func defaultFunc(w http.ResponseWriter, r *http.Request) {
}
// 模擬用戶登陸接口,響應(yīng)json數(shù)據(jù)
// 目的是讓客戶端從服務(wù)器端獲取token
// 處理請求邏輯之后,響應(yīng)給客戶的數(shù)據(jù)中包含新生成的token值
func AuthFunc(w http.ResponseWriter, r *http.Request) {
var user UserAccount
// 讀取客戶端請求的類容
buf := make([]byte, 2048)
n, _ := r.Body.Read(buf)
// debug 調(diào)試請求的內(nèi)容
log.Println("json :", string(buf[:n]))
// 將請求json數(shù)據(jù)解析出來
err := json.Unmarshal(buf[:n], &user)
// 如果解析錯誤,給客戶端提示
if err != nil {
WriteToResponse(w, "400", err.Error(), "")
return
}
// debug 調(diào)試解析之后的內(nèi)容
log.Println(user)
// 模擬驗證賬戶登錄的邏輯(賬戶,密碼都正確)
if user.Username == "admin" && user.Password == "123456" {
tokenString, _ := GetToken(user.Username, user.Password, user.Role)
WriteToResponse(w, "200", "success", map[string]string{"token": tokenString})
return
} else {
WriteToResponse(w, "400", "Account error", "")
return
}
// 通過命令行的 CURL 測試
// curl -X POST -H -H "Content-type:application/json" -d '{"username":"admin","password":"123456","role":"admin"}' http://127.0.0.1:8080/auth
}
// 模擬用戶登陸(獲取token)之后再請求某個接口,響應(yīng)json數(shù)據(jù)
// 請求時,在請求的數(shù)據(jù)中包含token
// 包含token的載體可以是請求的url,也可以是請求頭,也可以是請求體
func homeFunc(w http.ResponseWriter, r *http.Request) {
// 此處我們模擬的token包含在請求頭信息中
authorH := r.Header.Get("Authorization")
if authorH == "" {
WriteToResponse(w, "401", "request header Authorization is null", "")
return
}
// 將獲取的Authorization 內(nèi)容通過分割出來
authorArr := strings.SplitN(authorH, " ", 2)
// debug
log.Println(authorArr)
// Authorization的字符串通常是 "Bearer" 開頭(可以理解為固定格式,標(biāo)識使用承載模式),然后一個空格 再加上token的內(nèi)容
// Tips: 請求頭中Authorization的內(nèi)容直接是token也是可以的
if len(authorArr) != 2 || authorArr[0] != "Bearer" {
WriteToResponse(w, "402", "request header Authorization formal error", "")
return
}
// 解析token這個字符串
mc, err := ParseToken(authorArr[1])
if err != nil {
WriteToResponse(w, "403", err.Error(), "")
return
}
// debug
log.Println(mc)
// 請求成功響應(yīng)給客戶端
WriteToResponse(w, "200", "welcome to home", "")
}
func listFunc(w http.ResponseWriter, r *http.Request) {
authorH := r.Header.Get("Authorization")
if authorH == "" {
WriteToResponse(w, "401", "request header Authorization is null", "")
return
}
authorArr := strings.SplitN(authorH, " ", 2)
// debug
log.Println(authorArr)
if len(authorArr) != 2 || authorArr[0] != "Bearer" {
WriteToResponse(w, "402", "request header Authorization formal error", "")
return
}
mc, err := ParseToken(authorArr[1])
if err != nil {
WriteToResponse(w, "403", err.Error(), "")
return
}
// 模擬通過jwt確定權(quán)限的邏輯
// 如果 用戶角色是admin,那么就能訪問該接口,否則就不允許
if mc.Role == "admin" {
WriteToResponse(w, "200", "列表數(shù)據(jù)", "")
return
}
WriteToResponse(w, "404", "無權(quán)限訪問", "")
}
func main() {
http.HandleFunc("/", defaultFunc)
http.HandleFunc("/auth", AuthFunc)
http.HandleFunc("/home", homeFunc)
http.HandleFunc("/list", listFunc)
fmt.Println("start http server and listen 8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal("ListenAndServer err : ", err)
}
}
# 運行jwt服務(wù)
$ go run jwt.go
start http server and listen 8080
服務(wù)測試
測試工具是直接使用命令行下的 curl命令工具進行的測試
tips: 當(dāng)然也可以使用其他的任何能發(fā)送http請求的工具進行測試(包括使用代碼編寫http客戶端)
測試登陸驗證服務(wù)
# 發(fā)送POST并攜帶json數(shù)據(jù)
curl -X POST -v -H "Content-type:application/json" -d '{"username":"admin","password":"123456","role":"edit"}' http://127.0.0.1:8080/auth
# 響應(yīng)數(shù)據(jù)
> POST /auth HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.69.1
> Accept: */*
> Content-type:application/json
> Content-Length: 54
>
} [54 bytes data]
* upload completely sent off: 54 out of 54 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json;charset=utf-8
< Date: Sun, 05 Apr 2020 09:12:56 GMT
< Content-Length: 266
<
// 響應(yīng)的json數(shù)據(jù) 包含了token
{"code":"200","message":"success","data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoiZWRpdCIsImV4cCI6MTU4NjA4NDUzMCwiaXNzIjoiY2FwdGFpbiIsInN1YiI6Imp3dCB0ZXN0In0.VNIk9nI8SStCMCI_QyJ8gLrUbOLNSQgeoVabjQFzMS0"}}
測試訪問 home 服務(wù)
# 無token請求
curl -X POST -H "Content-type:application/json" http://127.0.0.1:8080/home
# 響應(yīng)
{"code":"401","message":"request header Authorization is null","data":""}
# 正確的請求
curl -X POST -H "Content-type:application/json" -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoiZWRpdCIsImV4cCI6MTU4NjA4MTk3NSwiaXNzIjoiY2FwdGFpbiIsInN1YiI6Imp3dCB0ZXN0In0.SJiK_bN7nQHFyRRTjWrNcX4IsuUkFasei21NU4FzI3U" http://127.0.0.1:8080/home
# 響應(yīng)
{"code":"200","message":"welcome to home","data":""}
測試訪問 list 服務(wù)
# 請求
curl -X POST -H "Content-type:application/json" -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiIxMjM0NTYiLCJyb2xlIjoiZWRpdCIsImV4cCI6MTU4NjA4MTk3NSwiaXNzIjoiY2FwdGFpbiIsInN1YiI6Imp3dCB0ZXN0In0.SJiK_bN7nQHFyRRTjWrNcX4IsuUkFasei21NU4FzI3U" http://127.0.0.1:8080/list
# 響應(yīng)
{"code":"404","message":"無權(quán)限訪問","data":""}
參考資料