Golang使用JWT進(jìn)行認(rèn)證和加密的示例詳解
最近看了一個(gè)名為go-auth的庫(kù),它將JWT作為HTTP cookie對(duì)用戶(hù)進(jìn)行驗(yàn)證,但這個(gè)例子中缺少了對(duì)JWT的保護(hù),由此進(jìn)行了一些針對(duì)JWX的研究。
下面描述來(lái)自golang-jwt的官方描述:
概述
JWT是一個(gè)簽名的JSON對(duì)象,通常用作Oauth2的Bearer token,JWT包括三個(gè)用.分割的部分:前兩部分為使用base64url編碼的JSON對(duì)象,最后一部分是簽名。第一部分稱(chēng)為header,包含用于驗(yàn)證最后一部分簽名所需的信息,如使用的簽名方式和使用的密鑰等,中間的部分是程序最關(guān)心的部分,稱(chēng)為Claim, RFC 7519定義了相關(guān)的字段,當(dāng)然也可以添加自己的字段。
簽名 vs 加密
一個(gè)token是一個(gè)簽名的json對(duì)象,涉及兩方面的內(nèi)容:
- token的創(chuàng)建者擁有簽名的secret
- 數(shù)據(jù)一旦被簽名就不能修改
需要注意的是,JWT并不支持加密,因此任何人都能讀取token 的內(nèi)容。如果需要加密數(shù)據(jù),可以使用配套的規(guī)范--JWE,可以參考這兩個(gè)庫(kù):lestrrat-go/jwx和golang-jwt/jwe。
選擇簽名方法
簽名方法有很多種,在使用前可能需要花時(shí)間挑選合適的簽名方法,主要考量點(diǎn)為:對(duì)稱(chēng)和非對(duì)稱(chēng)。
對(duì)稱(chēng)簽名方法,如HSA,只需要一個(gè)secret即可,這也是最簡(jiǎn)單的簽名方法,可以使用任何[]byte作為有效的secret。對(duì)稱(chēng)加密的計(jì)算速度也相對(duì)快一些。當(dāng)token的生產(chǎn)者和消費(fèi)者都可信的前提下,可以考慮使用對(duì)稱(chēng)加密。由于對(duì)稱(chēng)加密使用相同的secret進(jìn)行token的簽名和驗(yàn)證,因此不能輕易將密鑰分發(fā)出去。
非對(duì)稱(chēng)簽名,如RSA,則使用了不同的密鑰進(jìn)行簽名和token驗(yàn)證,因此可以使用私鑰生成token,并允許消費(fèi)者使用公鑰進(jìn)行驗(yàn)證。
JWT和OAuth
這里提一下,OAuth和JWT并不是一回事,一個(gè)JWT token只是一個(gè)簡(jiǎn)單的被簽名的JSON對(duì)象,可以用在所需要的地方,最常見(jiàn)的方式是用在OAuth2認(rèn)證中。
下面描述了二者是如何交互的:
- OAuth是一種允許身份提供者與用戶(hù)登錄的服務(wù)分離的協(xié)議。例如,你可以使用Facebook登陸不同的服務(wù)(Yelp、Spotify等),此時(shí)用的就是OAuth。
- OAuth定義了幾種傳遞身份驗(yàn)證數(shù)據(jù)的選項(xiàng),其中流行的一種方式稱(chēng)為"bearer token",一個(gè)bearer token就是一個(gè)只能被已認(rèn)證的用戶(hù)持有的簡(jiǎn)單字符串,即通過(guò)提供該token來(lái)進(jìn)行身份認(rèn)證。從這里可以看出JWT可以作為一種bearer token。
- 由于bearer token用于認(rèn)證,因此私密性很重要,這也是為什么通常會(huì)通過(guò)SSL來(lái)使用bearer token。
JWT的用法
從上面的官方描述中可以看到JWT其實(shí)就是一個(gè)字符串,其分為三段:header,主要指定簽名方法;claim,用于提供用戶(hù)身份數(shù)據(jù);signature,使用header中指定的簽名方法進(jìn)行簽名,簽名時(shí)主要使用了三個(gè)基礎(chǔ)數(shù)據(jù):
1.簽名密鑰:在對(duì)稱(chēng)簽名(如HMAC)中作為哈希數(shù)據(jù)的一部分,在非對(duì)稱(chēng)簽名(如ECDSA)中則作為私鑰。在JWT的簽名和驗(yàn)證過(guò)程中都需要使用到密鑰。
2.JWT的過(guò)期時(shí)間:JWT有一個(gè)過(guò)期時(shí)間。在用戶(hù)登陸服務(wù)器之后,服務(wù)器會(huì)給客戶(hù)端返回JWT,當(dāng)客戶(hù)端服務(wù)服務(wù)端時(shí)會(huì)將JWT傳遞給服務(wù)端,服務(wù)端除了需要驗(yàn)證客戶(hù)端的簽名之外還需要驗(yàn)證該token是否過(guò)期,JWT的過(guò)期時(shí)間數(shù)據(jù)位于claims中。
3.claim:主要包含了JWT相關(guān)的信息,用戶(hù)可以擴(kuò)展自己的claim信息。簽名方法會(huì)使用這部分信息進(jìn)行簽名。如下是標(biāo)準(zhǔn)的claims,可以看到這部分信息其實(shí)與SSL證書(shū)中的字段雷同。
type StandardClaims struct {
Audience string `json:"aud,omitempty"`
ExpiresAt int64 `json:"exp,omitempty"`
Id string `json:"jti,omitempty"`
IssuedAt int64 `json:"iat,omitempty"`
Issuer string `json:"iss,omitempty"`
NotBefore int64 `json:"nbf,omitempty"`
Subject string `json:"sub,omitempty"`
}
另外需要注意的是,JWT是使用明文交互的,其中claim中包含了用戶(hù)的敏感信息,因此需要使用JWE進(jìn)行加密。
在了解JWT之前可以看下幾個(gè)重要的術(shù)語(yǔ):
JWS(SignedJWT):經(jīng)過(guò)簽名的jwt,為三段式結(jié)構(gòu):header、claims、signature
JWA:簽名算法,即 header中的alg字段值。
JWE(EncryptedJWT):用于加密payload,如JWT,主要字段如下:
const ( AgreementPartyUInfoKey = "apu" #(Algorithm) Header Parameter AgreementPartyVInfoKey = "apv" AlgorithmKey = "alg" #(Algorithm) Header Parameter CompressionKey = "zip" #(Compression Algorithm) Header Parameter ContentEncryptionKey = "enc" #(Encryption Algorithm) Header Parameter ContentTypeKey = "cty" #(Content Type) Header Parameter CriticalKey = "crit" #(Critical) Header Parameter EphemeralPublicKeyKey = "epk" JWKKey = "jwk" #(JSON Web Key) Header Parameter JWKSetURLKey = "jku" #(JWK Set URL) Header Parameter KeyIDKey = "kid" #(Key ID) Header Parameter TypeKey = "typ" #(Type) Header Parameter X509CertChainKey = "x5c" #(X.509 Certificate Chain) Header Parameter X509CertThumbprintKey = "x5t" #(X.509 Certificate SHA-1 Thumbprint) Header Parameter X509CertThumbprintS256Key = "x5t#S256" #(X.509 Certificate SHA-256 Thumbprint) Header Parameter X509URLKey = "x5u" #(X.509 URL) Header Parameter )
JWK:是一個(gè)JSON數(shù)據(jù)結(jié)構(gòu),用于JWS的簽名驗(yàn)證以及JWE的加解密,主要字段如下:
const ( KeyTypeKey = "kty" #(Key Type) Parameter KeyUsageKey = "use" #(Public Key Use) Parameter KeyOpsKey = "key_ops" #(Key Operations) Parameter AlgorithmKey = "alg" #(Algorithm) Parameter KeyIDKey = "kid" #(Key ID) Parameter X509URLKey = "x5u" #(X.509 URL) Parameter X509CertChainKey = "x5c" #(X.509 Certificate Chain) Parameter X509CertThumbprintKey = "x5t" #(X.509 Certificate SHA-1 Thumbprint) Parameter X509CertThumbprintS256Key = "x5t#S256" #(X.509 Certificate SHA-256 Thumbprint) Parameter )
例子
lestrrat-go庫(kù)中給出了很多例子。在使用該庫(kù)之前簡(jiǎn)單看下主要的函數(shù):
jwt.NewBuilder:創(chuàng)建一個(gè)表示JWT 的結(jié)構(gòu)體(也可以使用jwt.New創(chuàng)建):
type stdToken struct {
mu *sync.RWMutex
dc DecodeCtx // per-object context for decoding
options TokenOptionSet // per-object option
audience types.StringList // https://tools.ietf.org/html/rfc7519#section-4.1.3
expiration *types.NumericDate // https://tools.ietf.org/html/rfc7519#section-4.1.4
issuedAt *types.NumericDate // https://tools.ietf.org/html/rfc7519#section-4.1.6
issuer *string // https://tools.ietf.org/html/rfc7519#section-4.1.1
jwtID *string // https://tools.ietf.org/html/rfc7519#section-4.1.7
notBefore *types.NumericDate // https://tools.ietf.org/html/rfc7519#section-4.1.5
subject *string // https://tools.ietf.org/html/rfc7519#section-4.1.2
privateClaims map[string]interface{} //用戶(hù)自定義的claims
}
jwt.Sign:用于對(duì)JWT 進(jìn)行簽名,輸入為表示JWT元素的stdToken,輸出為[]byte
jwt.Parse:將簽名的token解析為stdToken,輸入為jwt.Sign的輸出。
jws.sign:使用字符串來(lái)創(chuàng)建JWS消息,入?yún)?code>[]byte。與jwt.Sign的不同點(diǎn)在于,前者的入?yún)⑹?code>stdToken標(biāo)準(zhǔn)結(jié)構(gòu)體,而后者是任意字符串。
jws.parse:對(duì)編碼的JWS消息進(jìn)行解碼,輸出結(jié)構(gòu)如下:
type Message struct {
dc DecodeCtx
payload []byte
signatures []*Signature
b64 bool // true if payload should be base64 encoded
}
jwe.Encrypt:加密payload
jwe.Decrypt:解密payload
下面看下如何生成JWT,以及如何結(jié)合使用JWE和JWK對(duì)其進(jìn)行加密。
Example 1
下面jwt使用對(duì)稱(chēng)方式進(jìn)行簽名/解析,jwe使用非對(duì)稱(chēng)方式進(jìn)行加解密
package main
import (
"crypto/rand"
"crypto/rsa"
"fmt"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwe"
"github.com/lestrrat-go/jwx/v2/jwk"
"github.com/lestrrat-go/jwx/v2/jws"
"github.com/lestrrat-go/jwx/v2/jwt"
"time"
)
func main() {
// 創(chuàng)建一個(gè)jwt token結(jié)構(gòu)體
tok, err := jwt.NewBuilder().
Issuer(`github.com/lestrrat-go/jwx`).
IssuedAt(time.Now()).
Build()
if err != nil {
fmt.Printf("failed to build token: %s\n", err)
return
}
//創(chuàng)建對(duì)稱(chēng)簽名的key
key, err := jwk.FromRaw([]byte(`abracadabra`))
if err != nil {
fmt.Printf(`failed to create new symmetric key: %s`, err)
return
}
key.Set(jws.KeyIDKey, `secret-key`)
//使用HS256對(duì)稱(chēng)簽名方式進(jìn)行簽名,生成JWS
signed, err := jwt.Sign(tok, jwt.WithKey(jwa.HS256, key))
//下面使用jwe對(duì)JWS進(jìn)行加密,使用的是非對(duì)稱(chēng)加密方式
//首先生成RSA密鑰對(duì)
rawprivkey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
fmt.Printf("failed to create raw private key: %s\n", err)
return
}
//提取私鑰,用于解密
privkey, err := jwk.FromRaw(rawprivkey)
if err != nil {
fmt.Printf("failed to create private key: %s\n", err)
return
}
//提取公鑰,用于加密
pubkey, err := privkey.PublicKey()
if err != nil {
fmt.Printf("failed to create public key:%s\n", err)
return
}
//使用公鑰加密JWS
encrypted, err := jwe.Encrypt(signed, jwe.WithKey(jwa.RSA_OAEP, pubkey))
if err != nil {
fmt.Printf("failed to encrypt payload: %s\n", err)
return
}
//使用私鑰解密出JWS
decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA_OAEP, privkey))
if err != nil {
fmt.Printf("failed to decrypt payload: %s\n", err)
return
}
//使用對(duì)稱(chēng)簽名方式解析出token 結(jié)構(gòu)體
parsedTok, err := jwt.Parse(decrypted, jwt.WithKey(jwa.HS256, key), jwt.WithValidate(true))
if err != nil {
fmt.Println("failed to parse signed token")
return
}
fmt.Println(parsedTok)
}
Example 2
下面使用非對(duì)稱(chēng)方式進(jìn)行簽名/解析:
package main
import (
"crypto/rand"
"crypto/rsa"
"fmt"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
)
func main() {
//創(chuàng)建RSA密鑰對(duì)
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
fmt.Printf("failed to generate private key: %s\n", err)
return
}
var payload []byte
{ // 創(chuàng)建JWT payload
token := jwt.New()
token.Set(`foo`, `bar`)
//使用RSA私鑰進(jìn)行簽名
payload, err = jwt.Sign(token, jwt.WithKey(jwa.RS256, privKey))
if err != nil {
fmt.Printf("failed to generate signed payload: %s\n", err)
return
}
}
{ // 使用RSA公鑰進(jìn)行解析
token, err := jwt.Parse(
payload,
jwt.WithValidate(true),
jwt.WithKey(jwa.RS256, &privKey.PublicKey),
)
if err != nil {
fmt.Printf("failed to parse JWT token: %s\n", err)
return
}
fmt.Println(token)
}
}
Example 3
上面使用的JWK是使用代碼生成的,也可以加載本地文件(jwk.ReadFile)或通過(guò)JSU的方式從網(wǎng)絡(luò)上拉取所需的JWK(jwk.Fetch)。
lestrrat-go的官方文檔中給出了很多指導(dǎo)。
{
v, err := jwk.ReadFile(`private-key.pem`, jwk.WithPEM(true))
if err != nil {
// handle error
}
}
{
v, err := jwk.ReadFile(`public-key.pem`, jwk.WithPEM(true))
if err != nil {
// handle error
}
}
srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{
"keys": [
{"kty":"EC",
"crv":"P-256",
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"use":"enc",
"kid":"1"},
{"kty":"RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"alg":"RS256",
"kid":"2011-04-29"}
]
}`)
}))
defer srv.Close()
set, err := jwk.Fetch(
context.Background(),
srv.URL,
// This is necessary because httptest.Server is using a custom certificate
jwk.WithHTTPClient(srv.Client()),
)
if err != nil {
fmt.Printf("failed to fetch JWKS: %s\n", err)
return
}到此這篇關(guān)于Golang使用JWT進(jìn)行認(rèn)證和加密的示例詳解的文章就介紹到這了,更多相關(guān)Golang JWT認(rèn)證加密內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang 檢查網(wǎng)絡(luò)狀態(tài)是否正常的方法
今天小編就為大家分享一篇golang 檢查網(wǎng)絡(luò)狀態(tài)是否正常的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-07-07
Golang內(nèi)存對(duì)齊的規(guī)則及實(shí)現(xiàn)
本文介紹了Golang內(nèi)存對(duì)齊的規(guī)則及實(shí)現(xiàn),通過(guò)合理的內(nèi)存對(duì)齊,可以提高程序的執(zhí)行效率和性能,通過(guò)對(duì)本文的閱讀,讀者可以更好地理解Golang內(nèi)存對(duì)齊的原理和技巧,并應(yīng)用于實(shí)際編程中2023-08-08
Go中regexp包常見(jiàn)的正則表達(dá)式操作
本文主要介紹了Go中regexp包常見(jiàn)的正則表達(dá)式操作,包括匹配、查找、替換和分割字符串等,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02
詳解golang執(zhí)行Linux shell命令完整場(chǎng)景下的使用方法
本文主要介紹了golang執(zhí)行Linux shell命令完整場(chǎng)景下的使用方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
Go語(yǔ)言并發(fā)之Select多路選擇操作符用法詳解
Go?語(yǔ)言借用多路復(fù)用的概念,提供了?select?關(guān)鍵字,用于多路監(jiān)聽(tīng)多個(gè)通道,本文就來(lái)和大家聊聊Go語(yǔ)言中Select多路選擇操作符的具體用法,希望對(duì)大家有所幫助2023-06-06
Golang實(shí)現(xiàn)自己的orm框架實(shí)例探索
這篇文章主要為大家介紹了Golang實(shí)現(xiàn)自己的orm框架實(shí)例探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-01-01
Golang微服務(wù)框架Kratos實(shí)現(xiàn)Kafka消息隊(duì)列的方法
消息隊(duì)列是大型分布式系統(tǒng)不可缺少的中間件,也是高并發(fā)系統(tǒng)的基石中間件,所以掌握好消息隊(duì)列MQ就變得極其重要,在本文當(dāng)中,您將了解到:什么是消息隊(duì)列?什么是Kafka?怎樣在微服務(wù)框架Kratos當(dāng)中應(yīng)用Kafka進(jìn)行業(yè)務(wù)開(kāi)發(fā),需要的朋友可以參考下2023-09-09
golang中使用proto3協(xié)議導(dǎo)致的空值字段不顯示的問(wèn)題處理方案
這篇文章主要介紹了golang中使用proto3協(xié)議導(dǎo)致的空值字段不顯示的問(wèn)題處理方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10

