未完待續(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)
JSOE
是JSON 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
),和兩個私有的聲明(name
和admin
)。
注冊的、公開的、私有的
在一個聲明集當中,一般會有如下注冊的聲明名字:
-
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>