見出し画像

【Go言語で始めるJWT】HS256 と RS256 の違いを解説!

JWT(JSON Web Token)と聞くと「HS256とRS256」の2種類しかないように思われがちですが、実は他にも ES256(ECDSA 署名)などさまざまな方式があります。

とはいえ、実務で特に多いのが、HS256(共通鍵暗号方式) と RS256(公開鍵暗号方式)。そこで今回は、この2つの違いと、実際のGo言語での実装例を見ていきましょう。



HS256 と RS256 の違い

1.HS256(HMAC + SHA-256)

  • 署名・検証

    • 共通の秘密鍵 を発行側・検証側の両方で使う。

  • 鍵の配置

    • 秘密鍵は バックエンド にのみ置くのが原則。

    • フロントエンドで検証する場合も同じ鍵が必要になるため、漏洩リスクが大幅に上昇(非推奨)。

  • メリット:

    • 実装がシンプル&動作が軽い。

    • サーバー台数が少ない場合に扱いやすい。

  • デメリット

    • 鍵が漏洩すると誰でも署名を偽造できる。

    • 全ての検証サーバーに同じ鍵を配布・管理する必要がある。


2.RS256(RSA + SHA-256)

  • 署名・検証

    • 秘密鍵で署名公開鍵で検証

  • 鍵の配置

    • 秘密鍵 はバックエンド(トークン発行サーバー)のみで管理。

    • 公開鍵 は検証が必要なサーバー、あるいはフロントエンドにも配布可能。

  • メリット

    • 検証サーバーやフロントエンド側に公開鍵だけ配布すればOK。

    • 大規模・分散システムでの鍵管理がしやすい。

  • デメリット

    • 秘密鍵と公開鍵のペア管理・更新がやや煩雑。

    • RSA 署名は共通鍵方式より若干重い場合がある。


Go言語で実装:コード例

本記事では、JWTライブラリとして
github.com/golang-jwt/jwt/v4
を使用します。ターミナルで以下を実行してインストールしましょう。

# bash
go get github.com/golang-jwt/jwt/v4

1.HS256 を使った場合の実装例

(1)秘密鍵(共通鍵)の設定

// go

// HS256 で使う共有鍵(秘密鍵)
var mySecretKey = []byte("mySecretKey")

ここで定義した mySecretKey は、発行時も検証時も同じものを使います。

※ 注意:
 フロントエンドで検証を行う場合は、同じ共通鍵をフロントにも渡す必要があり、漏洩リスクが高くなります。
 もしフロントエンドでどうしても検証を行う必要があるなら、鍵をフロントに渡すリスクを最小化するために、暗号化やアクセス制御の仕組みを導入し、厳重に対策することを強く推奨します。

(2)トークン生成(GenerateTokenHS256)

// go

// HS256 で署名した JWT を生成する
func GenerateTokenHS256() (string, error) {
    
    // 署名対象となるクレームを定義
    claims := jwt.MapClaims{
        "user_id": 12345,
        "exp":     time.Now().Add(time.Hour).Unix(),
        "iat":     time.Now().Unix(),
    }

    // トークンを作成し、HS256で署名方式を指定
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    // 署名には共通の秘密鍵(mySecretKey)を使用
    return token.SignedString(mySecretKey)
}
  • user_id や exp(有効期限)など、独自の情報を含めます。

  • 署名方式に jwt.SigningMethodHS256 を指定して、秘密鍵(共通鍵)で署名します。

(3)トークン検証(ValidateTokenHS256)

// go

// トークンを検証して、問題なければ claims を戻す
func ValidateTokenHS256(tokenString string) (jwt.MapClaims, error) {
    parsedToken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        
        // 署名方式が HS かどうかを念のためチェック(他のアルゴリズムの混在を防ぐ)
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        
        // 検証時も同じ mySecretKey を使う
        return mySecretKey, nil
    })
    if err != nil {
        return nil, err
    }

    // トークンが有効かつ、MapClaimsに変換できるか確認
    if claims, ok := parsedToken.Claims.(jwt.MapClaims); ok && parsedToken.Valid {
        return claims, nil
    }
    return nil, fmt.Errorf("invalid token")
}
  • 発行時と同じ mySecretKey を渡して、トークンが正しく署名されているか検証。

  • 検証が通れば claims から user_id などを取得できます。

【ポイント解説】
HS256は、共通鍵(mySecretKey) を発行時・検証時の両方で使用しているのが特徴。


2.RS256 を使った場合の実装例

(1)秘密鍵と公開鍵の用意

  • 事前に openssl コマンドで秘密鍵と公開鍵を作成します。

# bash

# 秘密鍵 (2048ビット)
openssl genrsa -out private_key.pem 2048

# 秘密鍵から公開鍵を作成
openssl rsa -in private_key.pem -pubout -out public_key.pem
  • プログラム内で鍵ファイルを読み込みます。

// go
privateKeyBytes, _ := os.ReadFile("private_key.pem")
privateKey, _ := jwt.ParseRSAPrivateKeyFromPEM(privateKeyBytes)

publicKeyBytes, _ := os.ReadFile("public_key.pem")
publicKey, _ := jwt.ParseRSAPublicKeyFromPEM(publicKeyBytes)

※ 注意:
 実際は、鍵ファイルはアクセス制御やファイル暗号化などで厳重な対策を講じたサーバーに管理しましょう。
 たとえば、クラウド環境であれば AWS KMSHashiCorp Vault などの仕組みを利用し、秘密鍵を安全に保管・管理 することが推奨されます。
 誰でもアクセスできる場所(リポジトリ内で鍵をバージョン管理してしまう、など)に置かないよう、細心の注意を払ってください。

(2)トークン生成(GenerateTokenRS256)

// go

// 秘密鍵で署名してトークンを生成する
func GenerateTokenRS256(privateKey *rsa.PrivateKey) (string, error) {
    
    // 署名対象のクレーム
    claims := jwt.MapClaims{
        "user_id": 12345,
        "exp":     time.Now().Add(time.Hour).Unix(),
        "iat":     time.Now().Unix(),
    }
    
    // RS256を使う
    token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)

  // 秘密鍵で署名してJWT文字列を戻す
    return token.SignedString(privateKey)
}
  • user_id や exp(有効期限)など、独自の情報を含めます。

  • RS256 を指定し、秘密鍵(private key)を使って署名。

  • これにより、公開鍵で検証できるトークンを作ります。

(3)トークン検証(ValidateTokenRS256)

// go

// 公開鍵でトークンを検証し、問題なければ claims を戻す
func ValidateTokenRS256(tokenString string, publicKey *rsa.PublicKey) (jwt.MapClaims, error) {
    parsedToken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
        
        // 署名方式がRSAであることをチェック
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        
        // 公開鍵を戻して、署名検証に使う
        return publicKey, nil
    })
    if err != nil {
        return nil, err
    }

    // トークンの有効性とクレームの型を確認
    if claims, ok := parsedToken.Claims.(jwt.MapClaims); ok && parsedToken.Valid {
        return claims, nil
    }
    return nil, fmt.Errorf("invalid token")
}
  • 公開鍵(public key)を使って署名の正当性を確認。

  • 秘密鍵は不要なので、検証側やフロントエンドにも安全に配布できます。

【ポイント解説】
RS256は、秘密鍵(privateKey)で「署名」し、公開鍵(publicKey)で「検証」する方式。


どのような場面で使い分ける?

HS256 が適している場面

  • サーバー台数が少なく、秘密鍵を厳重に管理しやすい環境。

  • 実装がシンプルでスピード重視の場合。

  • 検証処理を基本的に1つのバックエンドに集中させられる構成(フロントで検証しない)。

RS256 が適している場面

  • 大規模・分散システムやマイクロサービスなど、複数のサービスで検証を行う場合。

  • フロントエンドに公開鍵を渡して署名を検証するニーズがある場合。

  • 秘密鍵をバックエンド専用にして、より強固なセキュリティを確保したいとき。


まとめ

1.HS256

  • 共通鍵方式でシンプル&高速。

  • ただし秘密鍵が漏れると危険度が高い。

  • 小規模・シンプル構成に向いている。

2.RS256

  • 秘密鍵&公開鍵方式でセキュリティが高い。

  • 鍵管理はやや複雑だが、大規模・分散型のシステムにメリット大。

  • フロントでも署名検証したい場合に便利。

いずれも github.com/golang-jwt/jwt/v4 を使えばGo言語で簡単に実装可能です。
運用形態やセキュリティポリシーに合わせ、どこに鍵を置くかを意識しながら選定・設計するとよいでしょう。

ぜひまずは小規模なプロジェクトで試し、運用感をつかんでみてください。


いいなと思ったら応援しよう!