3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JWT完全解説:原理・特徴・Go言語での実装

Posted at

Group481.png

Leapcell:最適なGolangホスティングのためのサーバーレスプラットフォーム

JWTの詳細解説:原理、形式、特徴、およびGoプロジェクトでの適用

JWTとは

JWTはJSON Web Tokenの略称で、クロスドメイン認証ソリューションです。Webアプリケーションにおいて重要な役割を果たし、安全で便利な認証と情報の転送を可能にします。

JWTを使用することで解決する問題

https___dev-to-uploads.s3.amazonaws.com_uploads_articles_igvye1880i9491jbz525.png

従来のユーザー認証プロセスの限界

従来の認証は、クライアント側のクッキーとサーバー側のセッションに依存しています。これはシングルサーバーアプリケーションにはうまく機能します。しかし、マルチサーバー展開の場合、セッション共有の問題があります。例えば、複数のサーバーが協働する大規模な分散システムでは、各サーバーが独立したセッションを維持しています。ユーザーがサーバー間を切り替えると、ログイン状態が不一致になる可能性があります。同時に、クッキー + セッションを使用した異なるドメイン間のシングルサインオンは実現できません。なぜなら、クッキーはドメインに基づいて設定され、異なるドメインは直接クッキーを共有できないため、マルチビジネスシステムにおける統一的な認証とアクセス制御が制限されるからです。

JWTの利点

JWTはアプリケーションをステートレスにし、セッション共有の必要性を回避します。その構造内にユーザー情報を含んでいます。サーバーはセッションを保存する必要がありません。代わりに、各リクエストに対してJWTの有効性を検証することでユーザーの身元を確認できます。分散システムでは、JWTはサーバーの拡張を容易にし、サーバーの数や分布に影響されません。

JWTの形式

json-web-token.png

正しいJWT形式は以下の通りです:

eyJhbGciOiJIUzI1NiIsInR5c.eyJ1c2VybmFtZaYjiJ9._eCVNYFYnMXwpgGX9Iu412EQSOFuEGl2c

このように、JWT文字列は3つの部分、すなわちヘッダー、ペイロード、および署名で構成され、ドットで接続されています。

ヘッダー

ヘッダーは、トークンの種類と暗号化アルゴリズムの2つの部分で構成されるJSONオブジェクトです。例えば:

{
  "typ": "JWT",// 通常は "JWT"
  "alg": "HS256"// 様々な暗号化アルゴリズムをサポート
}

上記のJSONオブジェクトをBase64URLアルゴリズムを使用して文字列に変換することで、JWTのヘッダー部分を取得できます。JWTエンコードでは標準のBase64ではなくBase64Urlが使用されることに注意する必要があります。これは、Base64で生成される文字列にはURLで使用される3つの特殊記号(+、/、=)が含まれる可能性があるためです。また、トークンをURLで渡すことがあります(例:test.com?token = xxx)。Base64URLアルゴリズムは、Base64アルゴリズムで生成される文字列の基礎上、=を省略し、+を-に置き換え、/を_に置き換えます。これにより、生成された文字列がURLで問題なく渡せるようになります。

ペイロード

JWTのペイロード部分もヘッダーと同様に、実際に必要なデータを格納するためのJSONオブジェクトです。JWT標準では7つのオプションフィールドが提供されており、それぞれ以下の通りです:

  • iss(issuer):発行者。値は大文字と小文字を区別する文字列またはUriです。
  • sub(subject):対象。ユーザーを識別するために使用されます。
  • exp(expiration time):有効期限。
  • aud(audience):対象者。
  • iat(issued at):発行時刻。
  • nbf(not before):JWTが有効でない時刻。
  • jti(JWT ID):識別子。

標準フィールドに加えて、ビジネス要件に応じて独自のフィールドを定義することもできます。例えば:

{
    iss:"admin",// 標準フィールド
    jti:"test",// 標準フィールド
    username:"leapcell",// カスタムフィールド
    "gender":"male",
    "avatar":"https://avatar.leapcell.jpg"
}

上記のJSONオブジェクトをBase64URLアルゴリズムを使用して文字列に変換することで、JWTのペイロード部分を取得できます。

署名

署名はJWTの署名です。生成方法は以下の通りです:ヘッダーとペイロードをBase64URLアルゴリズムでエンコードし、ドットで接続し、その後シークレットキー(secretKey)とヘッダーで指定された暗号化方法を使用して暗号化し、最終的に署名を生成します。署名の役割は、JWTが送信中に改ざんされていないことを保証することです。サーバーは署名を検証することでJWTの完全性と信憑性を検証できます。

JWTの特徴

  • セキュリティ推奨事項:JWTの盗難の可能性を防ぐため、できるだけHTTPSプロトコルを使用することが望ましいです。HTTPプロトコルでは、データ送信が平文で行われるため、傍受や改ざんされやすくなります。HTTPSは暗号化された送信を通じてJWTのセキュリティを効果的に保護することができます。
  • 無効化メカニズムの制限:JWTの発行時刻が期限切れになる場合を除き、既に生成されたJWTを無効にする方法はありません。ただし、サーバー側でアルゴリズムを変更する場合を除きます。これは、JWTが発行されると、有効期限内に盗まれると、悪意のある使用が行われる可能性があることを意味します。
  • 機密情報の保存:JWTが暗号化されていない場合、機密情報をその中に保存しないでください。機密情報を保存する必要がある場合は、再度暗号化することが望ましいです。JWT自体はデコードできるため、機密情報を含み、暗号化されていない場合、セキュリティリスクがあります。
  • 有効期限の設定:JWTには短い有効期限を設定することがおすすめです。盗まれた場合でも有効である状態を防ぎ、潜在的な損失を減らすためです。短い有効期限は、JWTが盗まれた後のリスクを減らすことができます。たとえ盗まれても、その有効時間は限られています。
  • ビジネス情報の保存:JWTのペイロードにはビジネス情報も保存できます。これにより、データベースの照会を減らすことができます。例えば、基本的なユーザー情報をペイロードに保存できます。各リクエストが行われるたびに、サーバーはJWTからこの情報を直接取得でき、データベースを再度照会する必要がなくなり、システムのパフォーマンスと応答速度が向上します。

JWTの使用方法

サーバーがJWTを発行した後、それをクライアントに送信します。クライアントがブラウザーの場合は、クッキーやlocalStorageに保存できます。APPの場合は、sqliteデータベースに保存できます。その後、各インターフェイスリクエストでJWTを携帯します。サーバー側に携帯する方法はたくさんあり、クエリ、クッキー、ヘッダー、またはボディなど、データをサーバーに携帯できるあらゆる方法が使用できます。ただし、より標準的な方法は、ヘッダーのAuthorizationを通じてアップロードすることで、以下の形式です:

Authorization: Bearer <token>

このようにHTTPリクエストヘッダーでJWTを渡す方法は、一般的な認証仕様に準拠しており、サーバーが統一的な認証処理を行うのに便利です。

GoプロジェクトでのJWTの使用

JWTの生成

github.com/golang - jwt/jwt ライブラリを使用して、JWTの生成や解析を支援します。NewWithClaims() メソッドを使用してTokenオブジェクトを生成し、その後Tokenオブジェクトのメソッドを使用してJWT文字列を生成できます。例えば:

package main

import (
    "fmt"
    "time"

    "github.com/golang-jwt/jwt"
)

func main() {
    hmacSampleSecret := []byte("123")// シークレットキー、決して漏洩してはいけません
    // Tokenオブジェクトを生成
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
        "foo": "bar",
	"nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
    })
    // jwt文字列を生成
    tokenString, err := token.SignedString(hmacSampleSecret)
    fmt.Println(tokenString, err)
}

また、New() メソッドを使用してTokenオブジェクトを生成し、その後JWT文字列を生成することもできます。例えば:

package main

import (
    "fmt"
    "time"

    "github.com/golang-jwt/jwt"
)

func main() {
    hmacSampleSecret := []byte("123")
    token := jwt.New(jwt.SigningMethodHS256)
    // Newメソッドで作成するとデータを持ち込めないので、token.Claimsに値を代入することでデータを定義できる
    token.Claims = jwt.MapClaims{
	"foo": "bar",
	"nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(),
    }
    tokenString, err := token.SignedString(hmacSampleSecret)
    fmt.Println(tokenString, err)
}

上記の例では、JWTのペイロード内のデータは jwt.MapClaims データ構造を通じて定義されています。jwt.MapClaims を使用する他に、独自の構造体を使用することもできます。ただし、この構造体は以下のインターフェイスを実装する必要があります:

type Claims interface {
    Valid() error
}

以下は独自のデータ構造を実装する例です:

package main

import (
	"fmt"
	"github.com/golang-jwt/jwt"
)

type CustomerClaims struct {
    Username string `json:"username"`
    Gender   string `json:"gender"`
    Avatar   string `json:"avatar"`
    Email    string `json:"email"`
}

func (c CustomerClaims) Valid() error {
return nil
}

func main() {
    // シークレットキー
    hmacSampleSecret := []byte("123")
    token := jwt.New(jwt.SigningMethodHS256)
    token.Claims = CustomerClaims{
        Username: "Leapcell",
	Gender:   "male",
	Avatar:   "https://avatar.leapcell.jpg",
	Email:    "admin@test.org",
    }
    tokenString, err := token.SignedString(hmacSampleSecret)
    fmt.Println(tokenString, err)
}

独自の構造体でJWT標準で定義されているフィールドを使用したい場合は、以下のようにすることができます:

type CustomerClaims struct {
    *jwt.StandardClaims// 標準フィールド
    Username string `json:"username"`
    Gender   string `json:"gender"`
    Avatar   string `json:"avatar"`
    Email    string `json:"email"`
}

JWTの解析

解析は生成の逆操作です。トークンを解析して、そのヘッダー、ペイロードを取得し、署名を通じてデータが改ざんされていないかを検証します。以下が具体的な実装です:

package main

import (
	"fmt"

	"github.com/golang-jwt/jwt"
)

type CustomerClaims struct {
	Username string `json:"username"`
	Gender   string `json:"gender"`
	Avatar   string `json:"avatar"`
	Email    string `json:"email"`
	jwt.StandardClaims
}

func main() {
	var hmacSampleSecret = []byte("111")
        // 前の例で生成されたトークン
	tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IuWwj-aYjiIsImdlbmRlciI6IueUtyIsImF2YXRhciI6Imh0dHBzOi8vMS5qcGciLCJlbWFpbCI6InRlc3RAMTYzLmNvbSJ9.mJlWv5lblREwgnP6wWg-P75VC1FqQTs8iOdOzX6Efqk"

	token, err := jwt.ParseWithClaims(tokenString, &CustomerClaims{}, func(t *jwt.Token) (interface{}, error) {
		return hmacSampleSecret, nil
	})

	if err!= nil {
		fmt.Println(err)
		return
	}
	claims := token.Claims.(*CustomerClaims)
	fmt.Println(claims)
}

GinプロジェクトでのJWTの使用

Ginフレームワークでは、一般的にミドルウェアを通じてログイン認証を実装します。github.com/appleboy/gin - jwt ライブラリは github.com/golang - jwt/jwt の実装を統合し、対応するミドルウェアとコントローラを定義してくれます。以下が具体的な例です:

package main

import (
    "log"
    "net/http"
    "time"

    jwt "github.com/appleboy/gin-jwt/v2"
    "github.com/gin-gonic/gin"
)

// ログイン時のユーザー名とパスワードを受け取るために使用
type login struct {
    Username string `form:"username" json:"username" binding:"required"`
    Password string `form:"password" json:"password" binding:"required"`
}

var identityKey = "id"

// jwtのペイロード内のデータ
type User struct {
    UserName  string
    FirstName string
    LastName  string
}

func main() {

    // Ginのミドルウェアを定義
    authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
	Realm:            "test zone",          // 識別子
	SigningAlgorithm: "HS256",              // 暗号化アルゴリズム
	Key:              []byte("secret key"), // シークレットキー
	Timeout:          time.Hour,
	MaxRefresh:       time.Hour,   // 最大のリフレッシュ拡張時間
	IdentityKey:      identityKey, // クッキーのidを指定
	PayloadFunc: func(data interface{}) jwt.MapClaims { // ペイロード、返されるjwtのペイロード内のデータを定義できる
            if v, ok := data.(*User); ok {
		return jwt.MapClaims{
                    identityKey: v.UserName,
		}
            }
            return jwt.MapClaims{}
	},
	IdentityHandler: func(c *gin.Context) interface{} {
            claims := jwt.ExtractClaims(c)
            return &User{
                UserName: claims[identityKey].(string),
            }
	},
	Authenticator: Authenticator, // ログイン検証のロジックをここに記述できる
	Authorizator: func(data interface{}, c *gin.Context) bool { // ユーザーがトークンを通じて制限されたインターフェイスにアクセスするとき、このロジックが実行される
            if v, ok := data.(*User); ok && v.UserName == "admin" {
		return true
            }

            return false
	},
	Unauthorized: func(c *gin.Context, code int, message string) { // エラーが発生したときのレスポンス
		c.JSON(code, gin.H{
                    "code":    code,
                    "message": message,
		})
	},
		// トークンを取得する場所を指定。形式は:"<ソース>:<名前>"。複数ある場合はコンマで区切る
	TokenLookup:   "header: Authorization, query: token, cookie: jwt",
	TokenHeadName: "Bearer",
	TimeFunc:      time.Now,
})

    if err!= nil {
        log.Fatal("JWT Error:" + err.Error())
    }
    r := gin.Default()
    // ログインインターフェイス
    r.POST("/login", authMiddleware.LoginHandler)
    auth := r.Group("/auth")
    // ログアウト
    auth.POST("/logout", authMiddleware.LogoutHandler)
    // トークンを更新し、有効期限を延長する
    auth.POST("/refresh_token", authMiddleware.RefreshHandler)
    auth.Use(authMiddleware.MiddlewareFunc()) // ミドルウェアを適用
    {
        auth.GET("/hello", helloHandler)
    }

    if err := http.ListenAndServe(":8005", r); err!= nil {
	log.Fatal(err)
    }
}

func Authenticator(c *gin.Context) (interface{}, error) {
	var loginVals login
	if err := c.ShouldBind(&loginVals); err!= nil {
		return "", jwt.ErrMissingLoginValues
	}
	userID := loginVals.Username
	password := loginVals.Password

	if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
		return &User{
			UserName:  userID,
			LastName:  "Leapcell",
			FirstName: "Admin",
		}, nil
	}

	return nil, jwt.ErrFailedAuthentication
}

// /helloルートを処理するコントローラ
func helloHandler(c *gin.Context) {
	claims := jwt.ExtractClaims(c)
	user, _ := c.Get(identityKey)
	c.JSON(200, gin.H{
		"userID":   claims[identityKey],
		"userName": user.(*User).UserName,
		"text":     "Hello World.",
	})
}

サーバーを実行した後、curlコマンドを使用してログインリクエストを送信します。例えば:

curl http://localhost:8005/login -d "username=admin&password=admin"

レスポンス結果としてトークンが返されます。例えば:

{"code":200,"expire":"2021-12-16T17:33:39+08:00","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2Mzk2NDcyMTksImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTYzOTY0MzYxOX0.HITgUPDqli-RrO2zN_PfS4mISWc6l6eA_v8VOjlPonI"}

Leapcell:最適なGolangホスティングのためのサーバーレスプラットフォーム

barndpic.png

最後に、Golangサービスの展開に最適なプラットフォームを1つ紹介します:Leapcell

1. 多言語対応

  • JavaScript、Python、Go、またはRustで開発可能。

2. 無料で無制限のプロジェクトを展開

  • 使用量に応じて課金 — リクエストがなければ料金はかかりません。

3. 圧倒的なコスト効率

  • 使い捨て課金方式で、アイドル時の課金はありません。
  • 例:25ドルで平均応答時間60msで694万回のリクエストをサポート。

4. シンプルな開発者体験

  • 直感的なUIで簡単なセットアップ。
  • 完全自動化されたCI/CDパイプラインとGitOpsの統合。
  • アクション可能な洞察のためのリアルタイムメトリックとログ。

5. 簡単なスケーラビリティと高性能

  • 高い同時実行数を簡単に処理するための自動スケーリング。
  • オペレーションオーバーヘッドはゼロ — 構築に集中できます。

Frame3-withpadding2x.png

ドキュメントで詳細を確認!

LeapcellのTwitter:https://x.com/LeapcellHQ

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?