JWT(JSON Web Token)是一個很是輕巧的規範。這個規範容許咱們使用JWT在用戶和服務器之間傳遞安全可靠的信息。
一個JWT由3個部分組成:頭部(header)、載荷(payload)、簽名(signature)。
這三個部分又是由一個分隔符「.」 分割開的。git
用戶說明簽名的加密算法等,大概以下:github
{
"typ": "JWT",
"alg": "HS256"
}
payload 結構是一個json或者說是map對象
目前有一個相對標準的payload格式web
固然你也能夠不用這些字段,能夠本身隨意定義。redis
簽名是由頭部和荷載加上一串祕鑰,通過頭部聲明的加密算法加密獲得的。由於這個祕鑰只有服務端知道,可是這個祕鑰一旦泄漏了後果是很嚴重的。算法
通常使用方法,則是在登陸的時候生成一個token返回到客戶端。客戶端則能夠放到header或者cookie中。每次請求數據的時候帶上這個token,而服務端則去驗證token是否正確,由於jwt中的祕鑰只有服務器知道一旦這個token被別人修改過及時修改過再使用base64編碼替換也是能夠被發現的,由於簽名是把header和payload加起來再麼祕鑰加密的。以下圖:
這樣作有幾個好處:數據庫
可是也帶來了一些問題:json
簡單的用Gin實現一個http服務端, 一個login接口若是帳號密碼正確,則爲客戶端添加cookie。
第二個接口則是請求數據接口,經過auth中間件來驗證cookie中的token是否爲以前服務端發出去的那個token,這個只有服務端能驗證,由於服務端擁有祕鑰。
這個是最簡單的實現,沒有加上上面說的redis驗證。數組
package main import ( "fmt" "github.com/dgrijalva/jwt-go" "github.com/gin-gonic/gin" "github.com/gomodule/redigo/redis" "time" ) const ( SecretKey = "I have login" ) var redisCoon redis.Conn func main() { router := gin.Default() router.GET("/login", loginHandler) router.Use(authMiddleware) router.GET("/getData", getData) router.Run(":2323") } //驗證token中間件 func authMiddleware(ctx *gin.Context) { //從cookie中獲取token if tokenStr, err := ctx.Cookie("token"); err == nil { //獲取驗證以後的結果 token, err := parseToken(tokenStr) if err != nil { ctx.JSON(200, "token verify error") } //若是驗證結果是false直接返回token錯誤我 若是成功則繼續下一個handler if token.Valid { ctx.Next() } else { ctx.JSON(200, "token verify error") ctx.Abort() } } else { ctx.JSON(200, "no token") ctx.Abort() } } func getData(ctx *gin.Context) { ctx.JSON(200, "data") } func loginHandler(ctx *gin.Context) { user := ctx.Query("user") pwd := ctx.Query("pwd") if user == "peter" && pwd == "pwd" { token := CreateToken(user, pwd) //ctx.Header("Authorization", token) ctx.SetCookie("token", token, 10, "/", "localhost", false, true) ctx.JSON(200, "ok") } else { ctx.JSON(200, "user is not exit") } } func parseToken(s string) (*jwt.Token, error) { fn := func(token *jwt.Token) (interface{}, error) { return []byte(SecretKey), nil } return jwt.Parse(s, fn) } //建立token func CreateToken(user, pwd string) string { token := jwt.New(jwt.SigningMethodHS256) claims := make(jwt.MapClaims) claims["user"] = user // 這邊的pwd 不該該放到claims 荷載中不該該有機密的數據 claims["pwd"] = pwd token.Claims = claims if tokenString, err := token.SignedString([]byte(SecretKey)); err == nil { return tokenString } else { return "" } }
其實源碼邏輯挺簡單的,就是把上述流程簡單的實現。安全
其中的SigningMethod接口是主要簽名的方法,在jwt中有幾個預置的簽名方法。
其實若是咱們本身寫一個類而且實現了這個接口,其實也是能夠自定義簽名方法。服務器
// token結構 type Token struct { Raw string // 保存原始token解析的時候保存 Method SigningMethod // 保存簽名方法 目前庫裏有HMAC RSA ECDSA Header map[string]interface{} // jwt中的頭部 Claims Claims // jwt中第二部分荷載,Claims是一個藉口 Signature string // jwt中的第三部分 簽名 Valid bool // 記錄token是否正確 } type Claims interface { Valid() error } // 簽名方法 全部的簽名方法都會實現這個接口 // 具體能夠參考https://github.com/dgrijalva/jwt-go/blob/master/hmac.go type SigningMethod interface { // 驗證token的簽名,若是有限返回nil Verify(signingString, signature string, key interface{}) error // 簽名方法 接受頭部和荷載編碼事後的字符串和簽名祕鑰 // 在hmac中key必須是Key must be []byte // 在rsa中key 必須是*rsa.PrivateKey 對象 Sign(signingString string, key interface{}) (string, error) // 返回加密方法的名字 好比'HS256' Alg() string } // 新建token func New(method SigningMethod) *Token { return NewWithClaims(method, MapClaims{}) } func NewWithClaims(method SigningMethod, claims Claims) *Token { // 組成token return &Token{ Header: map[string]interface{}{ "typ": "JWT", "alg": method.Alg(), }, Claims: claims, Method: method, } }
建立簽名的邏輯很清晰,下面的註釋中已經很清楚了。
// 傳入 key 返回token或者error func (t *Token) SignedString(key interface{}) (string, error) { var sig, sstr string var err error // 生成jwt的前兩部分string if sstr, err = t.SigningString(); err != nil { return "", err } // 根據不一樣的簽名method 生成簽名字符串 if sig, err = t.Method.Sign(sstr, key); err != nil { return "", err } return strings.Join([]string{sstr, sig}, "."), nil } // 生成jwt的頭部和荷載的string func (t *Token) SigningString() (string, error) { var err error parts := make([]string, 2) // 建立一個字符串數組 for i, _ := range parts { var jsonValue []byte if i == 0 { // 把header部分轉成[]byte if jsonValue, err = json.Marshal(t.Header); err != nil { return "", err } } else { // 把荷載部分部轉成[]byte if jsonValue, err = json.Marshal(t.Claims); err != nil { return "", err } } // 爲簽名編碼 parts[i] = EncodeSegment(jsonValue) } // 用'.'號拼接兩部分而後返回 return strings.Join(parts, "."), nil }
有了建立token,就必定有驗證token。這個操做通常在服務端的中間件完成。在上面的例子中也能夠看到。
// 解析方法的回調函數 方法返回祕鑰 能夠根據不一樣的判斷返回不一樣的祕鑰 type Keyfunc func(*Token) (interface{}, error) func Parse(tokenString string, keyFunc Keyfunc) (*Token, error) { return new(Parser).Parse(tokenString, keyFunc) } func ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { return new(Parser).ParseWithClaims(tokenString, claims, keyFunc) } func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { // 解析tokenstring 根據'.' 風格以後用base64反編碼以後組成 token對象 token, parts, err := p.ParseUnverified(tokenString, claims) if err != nil { return token, err } // 判斷parse裏的validmethods 是否爲空 不爲空則循環調用 if p.ValidMethods != nil { var signingMethodValid = false var alg = token.Method.Alg() for _, m := range p.ValidMethods { if m == alg { signingMethodValid = true break } } if !signingMethodValid { // signing method is not in the listed set return token, NewValidationError(fmt.Sprintf("signing method %v is invalid", alg), ValidationErrorSignatureInvalid) } } // 調用keyfunc 返回祕鑰 方法從以前的調用注入的方法 var key interface{} if keyFunc == nil { // keyFunc was not provided. short circuiting validation return token, NewValidationError("no Keyfunc was provided.", ValidationErrorUnverifiable) } if key, err = keyFunc(token); err != nil { // keyFunc returned an error if ve, ok := err.(*ValidationError); ok { return token, ve } return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable} } vErr := &ValidationError{} // 判斷是否須要驗證claims if !p.SkipClaimsValidation { // valid 方法中會判斷 過時時間、簽發人、生效時間 若是沒有這3個字段則不判斷 if err := token.Claims.Valid(); err != nil { if e, ok := err.(*ValidationError); !ok { vErr = &ValidationError{Inner: err, Errors: ValidationErrorClaimsInvalid} } else { vErr = e } } } // 驗證jwt中第三部分 簽名 調用的是簽名方法定義的verify方法 token.Signature = parts[2] if err = token.Method.Verify(strings.Join(parts[0:2], "."), token.Signature, key); err != nil { vErr.Inner = err vErr.Errors |= ValidationErrorSignatureInvalid } // 設置valid字段 if vErr.valid() { token.Valid = true return token, nil } return token, vErr }
上面的源碼,只是主要的流程。jwt中還有不少代碼上面兵沒有列出來,好比rsa,ecdsa的具體實現、claims.go裏面也有不少邏輯的判斷。有興趣的話能夠再深刻研究。