jwt教程

未完待續(xù)

JWT是什么?

JWT是JSON Web Token的縮寫,即JSON Web令牌

<a target="_blank">JWT規(guī)范</a>中對其所作的描述是:

JSON Web令牌(JWT)是一種緊湊的、URL安全的方式,用來表示要在雙方之間傳遞的“聲明”。JWT中的聲明被編碼為JSON對象,用作JSON Web簽名(JWS)結(jié)構(gòu)的有效內(nèi)容或JSON Web加密(JWE)結(jié)構(gòu)的明文,使得聲明能夠被:數(shù)字簽名、或利用消息認證碼(MAC)保護完整性、加密。

JWT的聲明(Claims)就是一小段信息,用“鍵-值”對表示。

想要詳細了解<a target="_blank">JSON Web簽名(JWS)</a>和<a target="_blank">JSON Web加密(JWE)</a>,可以自行去IETF的網(wǎng)站查閱規(guī)范,下文中我會簡單的介紹它們。

JWT的構(gòu)成

JWT由三部分組成:

  • Header:頭部,即JOSE Header
  • Claims:聲明,即JWS Paylaod
  • Signature:簽名,即JWS Signature

JWT由這三部分組成,每一部分都是使用base64url編碼的,并使用句點(.)連接起來。這里使用base64url編碼而不是普通的base64,是因為base64編碼會產(chǎn)生+/,這兩個字符在URL中是有特殊意義的,會導(dǎo)致JWT不是URL安全的

下面以<a target="_blank">JWT.io</a>首頁的一個例子介紹JWT的組成。再用Golang通過這些JSON對象生成JWT,最后用<a target="_blank">jwt-go</a>包比對生成的JWT。

JWT標準并沒有規(guī)定必須清除JSON結(jié)構(gòu)中開頭結(jié)尾的空白符和換行,但是為了消除歧義,一般在使用JSON對象時不用換行,并去掉多余的空白符,這會在我們的代碼中有所體現(xiàn)。

為了方便查看,下面展示代碼時使用的都是格式化后的JSON對象。

頭部(JOSE Header)

JSOEJSON Object Signing and Encryption,即JSON對象簽名與加密的縮寫。

{
  "typ": "JWT",
  "alg": "HS256"
}

示例中給出了兩個聲明:

  • typ: (Type)類型。在JOSE Header中這是個可選參數(shù),但這里我們需要指明類型是JWT。
  • alg: (Algorithm)算法,必須是JWS支持的算法,算法列表可以在<a target="_blank">JSON Web算法(JWA)</a>。這里指定算法為HS256

例子中只列舉了兩個聲明,更多的聲明和其具體定義可以到<a target="_blank">JSON Web簽名(JWS)</a>中查看。

Golang代碼:

...
header := []byte(`{
  "typ": "JWT",
  "alg": "HS256"
}`)

buffer := new(bytes.Buffer)
//去掉多余的換行和空白符
json.Compact(buffer, header)
//Base64URL編碼
jwtHeader := base64.URLEncoding.EncodeToString(buffer.Bytes())
//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
fmt.Println(jwtHeader)
...

上述代碼片段會輸出eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9,這就是編碼后的JWT頭部。

聲明(JWT Claims)

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

例子中給的是一個注冊的聲明(sub),和兩個私有的聲明(nameadmin)。

注冊的、公開的、私有的

在一個聲明集當中,一般會有如下注冊的聲明名字

  • iss: (Issuer)簽發(fā)者
  • iat: (Issued At)簽發(fā)時間,用Unix時間戳表示
  • exp: (Expiration Time)過期時間,用Unix時間戳表示
  • aud: (Audience)接收該JWT的一方
  • sub: (Subject)該JWT的主題
  • nbf: (Not Before)不要早于這個時間
  • jti: (JWT ID)用于標識JWT的唯一ID

上面的聲明都是可選的,但是一般都達成共識,
注冊的聲明是在IANA中注冊的,
公開的聲明要保證不引起命名沖突
私有的聲明可以使用

Golang代碼:

...
claims := []byte(`{
    "sub": "1234567890",
    "name": "John Doe",
    "admin": true
}`)

buffer := new(bytes.Buffer)
json.Compact(buffer, claims)
jwtClaims := base64.URLEncoding.EncodeToString(buffer.Bytes())
fmt.Println(jwtClaims)
...

上述代碼片段會輸出eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9,這就是編碼后的JWT聲明。

簽名(Signature)

按照頭部中指定的,我們要使用HS256算法對上面的編碼后的字符串進行簽名。
頭部和聲明用.號連接起來:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

我們要做的就是對這個字符串進行簽名。

Golang代碼:

...
//eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
s := strings.Join([]string{jwtHeader, jwtClaims}, ".")
//HS256算法,key是"secret"
mac := hmac.New(sha256.New, []byte("secret"))
mac.Write([]byte(s))
expectedMAC := mac.Sum(nil)
//TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
signature := strings.TrimRight(base64.URLEncoding.EncodeToString(expectedMAC), "=")
fmt.Println(signature)
...

上述代碼輸出TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ,這就是這個JWT的簽名。

將頭部、聲明、簽名用.號連在一起就得到了我們要的JWT。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

驗證

...
//定義
type MyCustomClaims struct {
    Sub   string `json:"sub"`
    Name  string `json:"name"`
    Admin bool   `json:"admin"`
}
//實現(xiàn)Claims接口
func (m MyCustomClaims) Valid() error {
    return nil
}

mySigningKey := []byte("secret")

claims2 := MyCustomClaims{
    "1234567890",
    "John Doe",
    true,
}


token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims2)
ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v\n", ss, err)
if ss == s {
    fmt.Println("OK")
}
...
// Encode JWT specific base64url encoding with padding stripped
func EncodeSegment(seg []byte) string {
    return strings.TrimRight(base64.URLEncoding.EncodeToString(seg), "=")
}

// Decode JWT specific base64url encoding with padding stripped
func DecodeSegment(seg string) ([]byte, error) {
    if l := len(seg) % 4; l > 0 {
        seg += strings.Repeat("=", 4-l)
    }
    return base64.URLEncoding.DecodeString(seg)
}

不安全的JWT

簽名為空的JWT

創(chuàng)建JWT

按一下步驟創(chuàng)建:

對UTF-8的八進制序列進行Base64url編碼

一些可以應(yīng)用JWT的案例

注意:下面的例子設(shè)計并不完善,甚至存在漏洞。這里僅僅是展示JWT的用途。不要將例子直接用于生產(chǎn)環(huán)境。

驗證用戶

簽發(fā)JWT

1.客戶端發(fā)送帶有用戶名、密碼的表單到服務(wù)器;
2.服務(wù)器驗證用戶名密碼后,將user_id作為JWT Claims中的一個聲明,生成JWT;
3.將簽發(fā)的JWT作為cookies的內(nèi)容發(fā)送給用戶。

這里要注意,JWT作為cookies的一部分,本質(zhì)上還是cookies,所以還是要遵循一般的安全原則,防止XSS等攻擊手段。

驗證請求

1.客戶端發(fā)送帶有JWT的請求到服務(wù)器;
2.服務(wù)器從JWT中提取信息;
3.驗證JWT是否合法(簽名是否正確、令牌是否過期、請求時間在nbf之前還是之后、簽發(fā)人是否被接受、服務(wù)器是否是真正的接受者等);
4.從聲明中取出user_id

和session的區(qū)別

session需要在服務(wù)器中存儲標記用戶的信息,比如session_id,而JWT則需要。

JWT在服務(wù)器端需要一定量的計算,而session方式一般不需要。

在分布式系統(tǒng)中,使用Session的方式,需要在多臺服務(wù)器之間session id,增加了服務(wù)器的內(nèi)存和IO壓力。而JWT方式則免去了同步的麻煩。因為用戶的狀態(tài)已經(jīng)存儲在客戶端中了,雖然增加了一些計算開銷,但是與IO開銷比起來,還是要好很多的。

單點登錄

Set-Cookie: jwt=header.claims.signature; HttpOnly; max-age=980000; domain=.yourdomain.com

我們將域名設(shè)置為頂級域名(域名前要加.),這樣yourdomain.com*.yourdomain.com都能接收這個cookies了。

免登陸退訂訂閱郵件功能

我們的郵箱中經(jīng)常會收到一些訂閱郵件,有一些

一些有用的鏈接

<a target="_blank">JWT.io</a>
<a target="_blank">Using JSON Web Tokens with Node.js</a>

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容