JWT
JWT は現代のサーバーサイド通信認証の主流の一つであり、軽量でステートレスな特徴を持っています。
概要
JWT は JSON Web Tokens の略で、JWT の詳細については [jwt.io](JSON Web Token Introduction - jwt.io) をご覧ください。これはオープンで安全、コンパクトな JSON オブジェクトを媒体として、サービス間で情報を伝達する方法です。その特徴は、高いセキュリティ、改ざん防止、低消費です。
構造
RFC 標準では、JWT は以下の 3 つの部分で構成されています:
- Header ヘッダー
- Payload ペイロード
- Signature 署名
そして、各部分はドット . で区切られ、header.payload.signature という形式の文字列になります。これが JWT トークンの標準構造です。次に、各構造の役割について説明します。
TIP
base64 と base64URL は同じエンコーディング方式ではないことに注意してください。後者は Web URL と互換性があり、エスケープ処理が行われています。
ヘッダー
ヘッダーは基本的な情報を宣言するだけで、通常 2 つの部分で構成されています。トークンのタイプと、署名に使用される暗号化アルゴリズムです。例えば以下:
{
"alg": "HS256",
"typ": "JWT"
}上記の情報は、トークンのタイプが JWT で、署名部分に使用される暗号化アルゴリズムが HS256 であることを示しています。そして、JSON オブジェクトを Base64Url で文字列にエンコードします。この文字列が JWT のヘッダーになります。
ペイロード
JWT の 2 番目の部分はペイロード部分で、主にクレーム(claims)部分を含みます。クレーム部分は通常、エンティティに関するデータです。例えばユーザーなど。クレームのタイプは全部で 3 種類あります:
registered:Registered claimsは事前定義されたクレームで、必須ではありませんが使用が推奨されています。例えば:iss(発行者)、exp(有効期限)、aud(対象者)など。public:Public claimsは JWT を使用する人が自由に定義できます。他のクレーム部分との衝突を避けるようにしてください。private claims:この部分のクレームもカスタム定義で、通常はサービス間で情報を共有するために使用されます。
ペイロードの例は以下の通りです:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}この JSON オブジェクトは Base64Url で文字列にエンコードされ、JWT の 2 番目の部分を構成します。
DANGER
ペイロード部分も保護されており改ざん防止がありますが、この部分は公開読み取り可能です。そのため、機密情報を JWT 内に保存しないでください。
署名
エンコードされたヘッダーとエンコードされたペイロード部分を取得した後、ヘッダーで指定された署名アルゴリズムを使用して、これら 2 つの部分の内容と秘密鍵を暗号化して署名します。そのため、JWT の内容が少しでも変化すると、復号時に得られる署名は異なります。また、秘密鍵を使用することで、JWT の発行者を検証することもできます。
sign = HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)例えば以下の例:
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload
{
"alg": "HS256",
"typ": "JWT"
}
Verify Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your secret
)最後に得られる出力は、3 つの base64Url 文字列で構成され、. で区切られた文字列です。以下のようになります:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
cThIIoDvwdueQB468K5xDc5633seEFoqwxjF_xSJyQQ動作原理
認証において、ユーザーが認証情報を使用して正常にログインすると、JSON Web トークンが返されます。トークンは認証情報であるため、セキュリティ問題を防ぐために非常に注意深く扱う必要があります。一般的に、トークンの保持時間は必要な時間を超えないようにしてください。その後、ユーザーが保護されたルートとリソースにアクセスしたい場合、リクエストを送信する際にトークンを携行する必要があります。通常はリクエストヘッダーの Authorization ヘッダー内の Bearer schema です。例えば以下:
Authorization: Bearer <token>サーバーは JWT を受信すると、その有効性を検証します。例えば内容の改ざん、トークンの期限切れなどがあります。検証に合格すれば、リソースに正常にアクセスできます。JWT にはいくつかの基本的な情報を携行できますが、情報は大きくしないことをお勧めします。
JWT ライブラリ
公式リポジトリ:golang-jwt/jwt: Community maintained clone of https://github.com/dgrijalva/jwt-go
公式ドキュメント:jwt package - github.com/golang-jwt/jwt/v4 - Go Packages
このライブラリは、解析と検証、および JWT の生成と署名をサポートしています。現在サポートされている署名アルゴリズムは HMAC SHA、RSA、RSA-PSS、および ECDSA です。ただし、独自のフックを追加することもできます。
インストール
go get -u github.com/golang-jwt/jwt/v4インポート
import "github.com/golang-jwt/jwt/v4"署名アルゴリズムの選択
使用可能な署名アルゴリズムはいくつかあり、使用する前にそれらの違いを理解して、より適切に署名アルゴリズムを選択する必要があります。それらの最大の違いは対称暗号化と非対称暗号化です。
最もシンプルな対称暗号化アルゴリズム HSA は、任意の []byte を有効なキーとして使用できるため、計算速度が少し速いです。プロデューサーとコンシューマーの両方が信頼できる場合、対称暗号化アルゴリズムの効率が最も高くなります。ただし、署名と検証に同じキーを使用するため、検証に使用するキーを簡単に配布することはできません。署名キーも同じであるため、署名が漏洩すると JWT のセキュリティは無意味になります。
非対称暗号化署名方法、例えば RSA は、トークンの署名と検証に異なるキーを使用します。これにより、秘密鍵で署名されたトークンを生成することが可能になり、公開鍵を持つ任何人都にアクセスを許可できます。
異なる署名アルゴリズムには、異なるタイプのキーが必要です。以下に、一般的な署名アルゴリズムのタイプをいくつか示します:
HMAC:対称暗号化、署名と検証にタイプ[]byteの値が必要です。(HS256、HS384、HS512)RSA:非対称暗号化、署名に*rsa.PrivateKeyタイプの値、検証に*rsa.PublicKeyタイプの値が必要です。(RS256、RS384、RS512)ECDSA:非対称暗号化、署名に*ecdsa.PrivateKeyタイプの値、検証に*ecdsa.PublicKeyタイプの値が必要です。(ES256、ES384、ES512)EdDSA:非対称暗号化、署名にed25519.PrivateKeyタイプの値、検証にed25519.PublicKeyタイプの値が必要です。(Ed25519)
例
以下に、JWT の作成と署名、および解析と検証に関するいくつかの例を示します。
type Token struct {
Raw string // 元の Token 文字列、解析開始時にこのフィールドが埋められます
Method SigningMethod // 署名に使用された方法
Header map[string]interface{} // JWT の header 部分
Claims Claims // JWT の payload 部分
Signature string // JWT の署名部分、解析開始時にこのフィールドが埋められます
Valid bool // JWT が有効かどうか
}Token 構造体は JWT トークンを表し、フィールドの使用は JWT がどのように作成/署名または解析/検証されるかに依存します。
type RegisteredClaims struct {
// `iss` (Issuer) クレーム。https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1 を参照
Issuer string `json:"iss,omitempty"`
// `sub` (Subject) クレーム。https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2 を参照
Subject string `json:"sub,omitempty"`
// `aud` (Audience) クレーム。https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3 を参照
Audience ClaimStrings `json:"aud,omitempty"`
// `exp` (Expiration Time) クレーム。https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4 を参照
ExpiresAt *NumericDate `json:"exp,omitempty"`
// `nbf` (Not Before) クレーム。https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5 を参照
NotBefore *NumericDate `json:"nbf,omitempty"`
// `iat` (Issued At) クレーム。https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6 を参照
IssuedAt *NumericDate `json:"iat,omitempty"`
// `jti` (JWT ID) クレーム。https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7 を参照
ID string `json:"jti,omitempty"`
}これはライブラリで提供される事前定義された Claims で、目的に応じて適切に使用できます。
例 1.HMAC の作成と署名
func TestHmac(t *testing.T) {
// hmac のキータイプはバイト配列
secret := []byte("my secret")
// HS256 アルゴリズムを使用、jwt.MapClaims は payload
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"id": 123456,
"name": "jack",
})
fmt.Printf("%+v\n", *token)
// 署名
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}出力:
{Raw: Method:0xc000008150 Header:map[alg:HS256 typ:JWT] Claims:map[id:123456 name:jack] Signature: Valid:false}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.
QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M
<nil>例 2.事前定義された Claims の使用
mySigningKey := []byte("AllYourBase")
// Claims の作成
claims := &jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Unix(1516239022, 0)),
Issuer: "test",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
ss, err := token.SignedString(mySigningKey)
fmt.Printf("%v %v", ss, err)出力:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiJ0ZXN0IiwiZXhwIjoxNTE2MjM5MDIyfQ.
0XN_1Tpp9FszFOonIBpwha0c_SfnNI22DhTnjMshPg8
<nil>例 3.カスタム Claims
type MyClaims struct {
User string `json:"user"`
jwt.RegisteredClaims
}
func TestCustomClaims(t *testing.T) {
// キーの作成
secret := []byte("my secret")
// Claims の作成
claims := MyClaims{
User: "114514",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "Server",
},
}
// Token の作成
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 署名
signedString, err := token.SignedString(secret)
fmt.Println(signedString, err)
}出力:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg1Nzk2LCJuYmYiOjE2NzMwODIxOTYsImlhdCI6MTY3MzA4MjE5Nn0.
PdPXdQBbDuYtE4ENXzoAcrW-dBSxqsufeYXCT5zTwVI
<nil>TIP
カスタム Claims 内に標準 Claims を埋め込む場合は、以下を確認してください:
埋め込まれた標準 Claims はポインタ型ではないこと
ポインタ型の場合、渡す前に適切なメモリを割り当てることをお勧めします。そうしないと panic します。
例 4.HMAC 解析検証 Token
func TestParse(t *testing.T) {
secret := []byte("my secret")
// HS256 アルゴリズムで作成され署名された token を想定
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// token 文字列と検証フック関数を渡す、戻り値は Token 構造体
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// 署名アルゴリズムが一致するか検証
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("一致しない署名アルゴリズム:%s", token.Header["alg"])
}
// 検証キーを返す
return secret, nil
})
if err != nil {
fmt.Println(token, err)
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
fmt.Println(claims)
} else {
fmt.Println(err)
}
}出力:
map[id:123456 name:jack]例 5.エラー処理
func TestProcess(t *testing.T) {
secret := []byte("my secret")
// HS256 アルゴリズムで作成され署名された token を想定
tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzNDU2LCJuYW1lIjoiamFjayJ9.QxLw9NkFgZW3BluyXIofe4efp1IAy61s8b2fe3Eo86M"
// token 文字列と検証フック関数を渡す、戻り値は Token 構造体
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// 署名アルゴリズムが一致するか検証
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("一致しない署名アルゴリズム:%s", token.Header["alg"])
}
// 検証キーを返す
return secret, nil
})
if token.Valid {
fmt.Println("token 有効")
} else if errors.Is(err, jwt.ErrTokenMalformed) {
fmt.Println("渡された文字列は token ですらない...")
} else if errors.Is(err, jwt.ErrTokenExpired) || errors.Is(err, jwt.ErrTokenNotValidYet) {
fmt.Println("token は期限切れまたはまだ有効ではない")
} else {
fmt.Println("token 処理に異常が発生...")
}
}出力:
token 有効例 6.カスタム Claims 解析
Token の作成時にカスタム Claims を使用した場合、解析時に Claims を map ではなくカスタム Claims に直接変換したい場合は、カスタム Claims を渡す必要があります。
func TestCustomClaimsParse(t *testing.T) {
secret := []byte("my secret")
tokenstring := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiMTE0NTE0IiwiaXNzIjoiU2VydmVyIiwiZXhwIjoxNjczMDg4MDU2LCJuYmYiOjE2NzMwODQ0NTYsImlhdCI6MTY3MzA4NDQ1Nn0.T245aoDeL2x19X8_JZde0EmZ2TDyIgr1u3ddKFjQmgw"
token, err := jwt.ParseWithClaims(tokenstring, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return secret, nil
}, jwt.WithValidMethods([]string{"HS256"})) // option を使用して検証
// 型アサーション
if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
fmt.Println(claims)
} else {
fmt.Println(err)
}
}出力:
&{114514 {Server [] 2023-01-07 18:40:56 +0800 CST 2023-01-07 17:40:56 +0800 CST 2023-01-07 17:40:56 +0800 CST }}例 7.RSA の署名と解析
RSA は分散アーキテクチャで使用されることが多いです。大まかなプロセスは以下の通りです:
- 認証センターがキーペアを作成し、秘密鍵を使用して jwt に署名します。jwt はクライアントに返され、公開鍵はビジネスサービスが保持します
- クライアントは jwt を携行してビジネスサービスにリクエストを送信し、ビジネスモジュールは公開鍵を使用して jwt を解析します。認証センターにアクセスする必要はありません
- 認証に成功すると、ビジネス情報が返されます
- 認証に失敗すると、失敗情報が返されます
func TestRsa(t *testing.T) {
// キーペアの作成
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
publicKey := &privateKey.PublicKey
if err != nil {
fmt.Println(err)
return
}
// claims
claims := MyClaims{
User: "114514",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "Server",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
// 秘密鍵で暗号化
signedString, err := token.SignedString(privateKey)
fmt.Println(signedString, err)
// 公開鍵で復号
token, err = jwt.ParseWithClaims(signedString, &MyClaims{}, func(token *jwt.Token) (interface{}, error) {
return publicKey, nil
})
if err != nil {
fmt.Println(err)
} else if claims, ok := token.Claims.(*MyClaims); ok && token.Valid {
fmt.Println(claims)
}
}