未完待續(xù)
JWT是什么挺据?
JWT是JSON Web Token的縮寫取具,即JSON Web令牌。
<a target="_blank">JWT規(guī)范</a>中對其所作的描述是:
JSON Web令牌(JWT)是一種緊湊的扁耐、URL安全的方式暇检,用來表示要在雙方之間傳遞的“聲明”。JWT中的聲明被編碼為JSON對象婉称,用作JSON Web簽名(JWS)結構的有效內容或JSON Web加密(JWE)結構的明文块仆,使得聲明能夠被:數字簽名、或利用消息認證碼(MAC)保護完整性王暗、加密悔据。
JWT的聲明(Claims)就是一小段信息,用“鍵-值”對表示俗壹。
想要詳細了解<a target="_blank">JSON Web簽名(JWS)</a>和<a target="_blank">JSON Web加密(JWE)</a>科汗,可以自行去IETF的網站查閱規(guī)范,下文中我會簡單的介紹它們绷雏。
JWT的構成
JWT由三部分組成:
-
Header
:頭部头滔,即JOSE Header
-
Claims
:聲明,即JWS Paylaod
-
Signature
:簽名涎显,即JWS Signature
JWT由這三部分組成坤检,每一部分都是使用base64url編碼的,并使用句點(.
)連接起來期吓。這里使用base64url編碼而不是普通的base64早歇,是因為base64編碼會產生+
和/
,這兩個字符在URL中是有特殊意義的讨勤,會導致JWT不是URL安全的箭跳。
下面以<a target="_blank">JWT.io</a>首頁的一個例子介紹JWT的組成。再用Golang通過這些JSON對象生成JWT潭千,最后用<a target="_blank">jwt-go</a>包比對生成的JWT衅码。
JWT標準并沒有規(guī)定必須清除JSON結構中開頭結尾的空白符和換行,但是為了消除歧義脊岳,一般在使用JSON對象時不用換行逝段,并去掉多余的空白符垛玻,這會在我們的代碼中有所體現。
為了方便查看奶躯,下面展示代碼時使用的都是格式化后的JSON對象帚桩。
頭部(JOSE Header)
JSOE
是JSON Object Signing and Encryption
,即JSON對象簽名與加密
的縮寫嘹黔。
{
"typ": "JWT",
"alg": "HS256"
}
示例中給出了兩個聲明:
-
typ
: (Type)類型账嚎。在JOSE Header中這是個可選參數,但這里我們需要指明類型是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"`
}
//實現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編碼
一些可以應用JWT的案例
注意:下面的例子設計并不完善袄秩,甚至存在漏洞阵翎。這里僅僅是展示JWT的用途逢并。不要將例子直接用于生產環(huán)境。
驗證用戶
簽發(fā)JWT
1.客戶端發(fā)送帶有用戶名郭卫、密碼的表單到服務器砍聊;
2.服務器驗證用戶名密碼后,將user_id
作為JWT Claims中的一個聲明贰军,生成JWT玻蝌;
3.將簽發(fā)的JWT作為cookies的內容發(fā)送給用戶。
這里要注意词疼,JWT作為cookies的一部分俯树,本質上還是cookies,所以還是要遵循一般的安全原則贰盗,防止XSS等攻擊手段许饿。
驗證請求
1.客戶端發(fā)送帶有JWT的請求到服務器;
2.服務器從JWT中提取信息童太;
3.驗證JWT是否合法(簽名是否正確米辐、令牌是否過期、請求時間在nbf
之前還是之后书释、簽發(fā)人是否被接受翘贮、服務器是否是真正的接受者等);
4.從聲明中取出user_id
和session的區(qū)別
session需要在服務器中存儲標記用戶的信息爆惧,比如session_id
狸页,而JWT則需要。
JWT在服務器端需要一定量的計算扯再,而session方式一般不需要芍耘。
在分布式系統中,使用Session的方式熄阻,需要在多臺服務器之間session id
斋竞,增加了服務器的內存和IO壓力。而JWT方式則免去了同步的麻煩秃殉。因為用戶的狀態(tài)已經存儲在客戶端中了坝初,雖然增加了一些計算開銷,但是與IO開銷比起來钾军,還是要好很多的鳄袍。
單點登錄
Set-Cookie: jwt=header.claims.signature; HttpOnly; max-age=980000; domain=.yourdomain.com
我們將域名設置為頂級域名(域名前要加.
),這樣yourdomain.com
和*.yourdomain.com
都能接收這個cookies了吏恭。
免登陸退訂訂閱郵件功能
我們的郵箱中經常會收到一些訂閱郵件拗小,有一些
一些有用的鏈接
<a target="_blank">JWT.io</a>
<a target="_blank">Using JSON Web Tokens with Node.js</a>