3-5. 認証 - bcrypt・セッション・JWT

所要時間: 50-60分 コミット内容: ~/learn/go/level3/day05/ にログイン/ログアウト/認証必須エンドポイントの完成形


このレッスンのゴール

  • bcrypt.GenerateFromPassword でパスワードを cost=12 でハッシュ化
  • タイミング攻撃対策(ダミー比較、subtle.ConstantTimeCompare)が打てる
  • crypto/rand で安全な session ID を生成
  • Cookie に HttpOnly / Secure / SameSite を設定
  • JWT で alg 検証と期限チェック
  • リフレッシュトークン rotation を設計できる

なぜ学ぶか - 絶対に失敗してはいけない領域

bcrypt 使えばOKじゃない?」 - NO。**失敗が即ユーザー被害(数百万人のパスワード流出)**になる領域。「平文保存」「SHA256単発」「math/rand」「localStorage に JWT」「alg=none 受け入れ」など、本章で挙げる アンチパターンを1つでも踏めば即事故。本章は「とにかく動くコード」ではなく「事故らない実装」を目指す。

前章とのつながり

3-4_テーブル駆動テストhttptest + 手書きフェイクの技術を 認証フローのテストに応用。2-4_ミドルウェア の認証ミドルウェアを 本格実装 する章。2-5_エラー処理 の「内部情報を漏らさない」設計が 認証メッセージ設計 に直結。

これができると何が嬉しいか

  • OWASP Authentication Cheat Sheet を実装できる
  • 「平文保存」「SHA256単発」を見たら即指摘 できる
  • タイミング攻撃を脳内モデル化 できる
  • JWT vs セッションの選定 が要件ベースで判断できる

ストーリー導入: 「メアド存在判定」を1分のレスポンスで漏らす罠

ログイン失敗時:

  • 存在しないユーザー → 即 1ms で「Invalid credentials」
  • 存在するがパスワード違い → bcrypt 比較で 250ms 後に「Invalid credentials」

応答時間の差で攻撃者がメアド存在判定できる。これがタイミング攻撃。対策は ユーザー不在時もダミー bcrypt 比較 で時間を揃える。本章では「コードを書くたびに攻撃面を考える」習慣を身につける。

セキュリティ章との連動

詳細解説は JWT の本質 で。本章は Go コードでの実装 に重点。


大前提: 認証は「絶対に独自実装で失敗する」領域

認証はバックエンドエンジニアが最も慎重になるべき領域です。なぜなら:

  • 失敗が即座にユーザー被害: パスワード平文保存 → DB漏洩 → 数百万人のメアド+パスワードが流出
  • 暗号は素人実装が必ず壊れる: 「自分でハッシュ関数作ろう」「平文を base64 にすればOK」は事故の入口
  • タイミング攻撃・リプレイ攻撃などの非自明な攻撃面 が存在

このレッスンでは「とにかく動くコード」ではなく、「事故らない実装」 を目指します。Go の標準ライブラリ + 業界標準ライブラリ(golang.org/x/crypto/bcrypt, golang-jwt/jwt)の使い方と、そこに自分で何を足したらダメか を頭に入れる。

セキュリティの章 JWT の本質 と連動するので、可能なら先にそちらも目を通すこと。


セッション①: パスワードとセッション(25-30分)

0. 録画と作業ディレクトリ

mkdir -p ~/log ~/learn/go/level3/day05
cd ~/learn/go/level3/day05
script ~/log/go_level3_day05.log
go mod init example.com/authplay
go get golang.org/x/crypto/bcrypt
go get github.com/golang-jwt/jwt/v5

1. パスワードを「ハッシュ化して保存」する

package auth
 
import "golang.org/x/crypto/bcrypt"
 
func HashPassword(plain string) (string, error) {
    h, err := bcrypt.GenerateFromPassword([]byte(plain), bcrypt.DefaultCost)
    if err != nil {
        return "", err
    }
    return string(h), nil
}
 
func VerifyPassword(hash, plain string) error {
    return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain))
}

パスワードハッシュの本質

「パスワードをハッシュ化する」とは、入力(パスワード)から 逆算できない短い文字列 を生成すること。DB にこのハッシュだけを保存しておけば、DB が漏洩しても元のパスワードは分からない(はず)。

ただし、単純な MD5 や SHA-256 は 不適切。理由:

  • 高速すぎて、攻撃者が GPU で毎秒数十億回の総当たりが可能
  • レインボーテーブルで事前計算済みハッシュと突き合わせ可能

正しいパスワードハッシュ関数の条件:

  1. 意図的に遅い(CPU/メモリを食う) → 総当たり攻撃を時間的に困難に
  2. salt 内包(同じパスワードでもハッシュが毎回違う)→ レインボーテーブル無効化
  3. コスト調整可能 → ハードウェア進化に追従できる

これを満たすのが bcrypt / scrypt / argon2。Go 標準周辺で一番使われているのが golang.org/x/crypto/bcrypt

2. bcrypt のコスト設計

hash, _ := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost) // 10
hash, _ := bcrypt.GenerateFromPassword([]byte(pw), 12)                 // 推奨範囲

cost パラメータの意味

bcrypt.GenerateFromPassword の2引数目 cost2の何乗回の反復計算を行うか の指数。

cost1回あたりの目安用途
4〜1msテスト用
10〜80msデフォルト、最低限の本番
12〜250ms推奨(2026 現在)
14〜1秒重要システム

指針: ユーザーがログインで待たされる時間 + サーバーCPU負荷のバランス。100-300ms が目安。ハードウェアが速くなるたびに上げる(5年に1回見直し)。

アンチパターン: cost=4 で本番運用

bcrypt.GenerateFromPassword([]byte(pw), 4) // 〜1ms

攻撃者が DB を盗んで cost=4 で総当たりすると、1秒に数千パスワード試行できる。8文字英数字なら1ヶ月以内に半分が解読される計算。

テストでは cost=4 が有用(テストを高速化)、本番では絶対に12以上bcrypt.DefaultCost (= 10)でも 2026年水準では若干弱め。

bcrypt は salt を内部で持つ

bcrypt.GenerateFromPassword が返すハッシュ文字列にはこんな構造がある:

$2a$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
 └┬┘ └┬┘ └─────┬──────┘ └──────────┬──────────────────┘
ハッシュ識別子 cost  salt(22文字)           ハッシュ本体(31文字)

このため、ユーザーごとに salt カラムを別途持つ必要がない。CompareHashAndPassword は salt と cost をハッシュから読み取って、平文を同じ手順でハッシュ化 → 比較 する。

3. ログイン処理の組み立て

type Credentials struct {
    Email    string
    Password string
}
 
type AuthService struct {
    Users UserRepository
}
 
func (s *AuthService) Login(ctx context.Context, c Credentials) (*User, error) {
    u, err := s.Users.GetByEmail(ctx, c.Email)
    if err != nil {
        // 「ユーザーが存在しません」と返してはダメ(後述)
        // ダミー比較でタイミング攻撃対策
        _ = bcrypt.CompareHashAndPassword([]byte("$2a$12$............................."), []byte(c.Password))
        return nil, ErrInvalidCredentials
    }
    if err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(c.Password)); err != nil {
        return nil, ErrInvalidCredentials
    }
    return u, nil
}

エラーメッセージで存在の有無を漏らさない

// NG
if userNotFound { return ErrUserNotFound }
if wrongPassword { return ErrWrongPassword }

攻撃者がメアド総当たりすると 「どのメールが登録済みか」が分かる。「メアドはバレてもOK」というシステムなら良いが、医療系・金融系では情報漏洩。

正解: ユーザーがいなくてもパスワードが違っても「Invalid email or password」の同じエラーを返す。

タイミング攻撃を意識する

「ユーザーがいない時は bcrypt 比較をスキップ」と書くと、応答時間で「存在する/しない」が判別できる

  • 存在する: bcrypt 計算で 250ms
  • 存在しない: 即座に 1ms

攻撃者は応答時間を測って「これは存在するアカウント」と特定できる。

対策: ユーザー不在時もダミーハッシュで bcrypt 比較する(上のコード例)。または、subtle.ConstantTimeCompare を使う(短い文字列比較の場合)。

import "crypto/subtle"
 
if subtle.ConstantTimeCompare([]byte(token1), []byte(token2)) != 1 {
    return ErrUnauthorized
}

== で文字列比較すると、Go は早期終了する。ConstantTimeCompare必ず一定時間 で比較する。トークン検証で必須。

4. セッションベース認証(Cookie + Redis)

// セッション ID を生成
import (
    "crypto/rand"
    "encoding/base64"
)
 
func newSessionID() (string, error) {
    b := make([]byte, 32) // 256ビット
    if _, err := rand.Read(b); err != nil {
        return "", err
    }
    return base64.RawURLEncoding.EncodeToString(b), nil
}
// ログインハンドラ
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
    var c Credentials
    if err := json.NewDecoder(r.Body).Decode(&c); err != nil {
        http.Error(w, "bad request", http.StatusBadRequest)
        return
    }
    u, err := h.Svc.Login(r.Context(), c)
    if err != nil {
        http.Error(w, "invalid credentials", http.StatusUnauthorized)
        return
    }
 
    sid, _ := newSessionID()
    if err := h.Sessions.Save(r.Context(), sid, u.ID, 24*time.Hour); err != nil {
        http.Error(w, "session error", http.StatusInternalServerError)
        return
    }
    http.SetCookie(w, &http.Cookie{
        Name:     "session_id",
        Value:    sid,
        Path:     "/",
        HttpOnly: true,                    // JS から読めない
        Secure:   true,                    // HTTPS のみ
        SameSite: http.SameSiteLaxMode,    // CSRF 対策
        MaxAge:   int(24 * time.Hour / time.Second),
    })
    w.WriteHeader(http.StatusNoContent)
}

セッションベース認証の流れ

  1. ログイン成功時に ランダムな session_id を生成
  2. session_id → user_id を Redis などに保存(有効期限付き)
  3. Cookie で session_id をクライアントに送る
  4. 次回以降のリクエストで Cookie の session_id を読み、Redis 引きでユーザーを特定

session_id の必須要件:

  • 推測不能: crypto/rand で256ビット以上のエントロピー
  • 短く: 100文字以内(Cookie の長さ制限)
  • URL セーフ: base64.RawURLEncoding が無難

Cookie 属性の意味と必須化

属性役割
HttpOnlyJS から読めない → XSS で盗まれない
SecureHTTPS でしか送られない → 中間者攻撃を防ぐ
SameSite=Laxクロスサイト POST を防ぐ → CSRF対策
Path=/サイト全体に送る
MaxAge期限切れで自動削除

HttpOnlySecure は本番では絶対必須。これ無しだとブラウザに「DevTools で確認 → トークン丸見え」「HTTP盗聴で取得」される。

アンチパターン: math/rand で session ID 生成

import "math/rand"
 
rand.Seed(time.Now().UnixNano())
sid := fmt.Sprintf("%d", rand.Int63()) // NG

math/rand暗号学的乱数ではない。シードが分かれば全部予測可能。session_id・トークン・パスワードリセットコードなど セキュリティ用途は必ず crypto/rand

5. 認証必須ミドルウェア

type contextKey string
 
const userIDKey contextKey = "userID"
 
func (h *AuthHandler) RequireLogin(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        c, err := r.Cookie("session_id")
        if err != nil {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        userID, err := h.Sessions.Lookup(r.Context(), c.Value)
        if err != nil {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), userIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}
 
// 取り出し側
func UserIDFromContext(ctx context.Context) (int64, bool) {
    v, ok := ctx.Value(userIDKey).(int64)
    return v, ok
}

ミドルウェアの責務

  1. Cookie / Authorization ヘッダから認証情報を取り出す
  2. 検証(セッション引き or 署名検証)
  3. 失敗なら 401 で打ち切り
  4. 成功なら user_id を context に注入 して下流へ

後続ハンドラは UserIDFromContext(r.Context()) でユーザー特定。ハンドラが「誰がリクエストしているか」を知るためのチャネルは context

アンチパターン: ミドルウェアの順序ミス

mux := http.NewServeMux()
mux.HandleFunc("/api/profile", h.Profile)       // 認証必須にすべき
mux.HandleFunc("/api/login", h.Login)           // これは認証不要
 
server := h.RequireLogin(h.Logging(mux))        // 全部に認証適用される

/api/login まで認証必須になって、ログイン不能になる事故。

正解: ミドルウェアを ルートごとに適用 する設計(chi.With(middleware) などのルーター機能を使う)か、ミドルウェアの内側で パスを判定 して認証を skip する。


セッション②: JWT と本番設計(25-30分)

6. JWT の実装

import (
    "github.com/golang-jwt/jwt/v5"
    "time"
)
 
type Claims struct {
    UserID int64 `json:"uid"`
    jwt.RegisteredClaims
}
 
func (s *AuthService) IssueJWT(u *User) (string, error) {
    claims := Claims{
        UserID: u.ID,
        RegisteredClaims: jwt.RegisteredClaims{
            Issuer:    "myapp",
            Subject:   fmt.Sprintf("%d", u.ID),
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            NotBefore: jwt.NewNumericDate(time.Now()),
        },
    }
    tok := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return tok.SignedString(s.JWTSecret)
}
 
func (s *AuthService) ParseJWT(tokenStr string) (*Claims, error) {
    claims := &Claims{}
    tok, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) {
        // 署名アルゴリズム検証(重要)
        if t.Method.Alg() != jwt.SigningMethodHS256.Alg() {
            return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
        }
        return s.JWTSecret, nil
    })
    if err != nil || !tok.Valid {
        return nil, ErrInvalidToken
    }
    return claims, nil
}

JWT の本質

JSON Web Token は「自己完結型のトークン」。3つのパートを . で繋いだ文字列:

{Header}.{Payload}.{Signature}
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsImV4cCI6...}.signature_here
  • Header: 署名アルゴリズム(HS256, RS256 など)
  • Payload: ユーザーID, 期限, ロールなどの情報(base64 デコードで読める。暗号化されていない
  • Signature: Header + Payload の HMAC または RSA 署名

メリット: サーバー側に「session を引く」処理が不要。分散システムで便利 デメリット: 一度発行すると 取り消しできない(期限切れまで有効)

JWT セキュリティの3大事故

事故1: alg=none を受け入れる

tok, _ := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
    return s.JWTSecret, nil // ← アルゴリズム検証してない
})

攻撃者が {"alg":"none"} の偽トークンを送ると、ライブラリによっては署名検証をスキップして通る。alg を必ず検証

事故2: HS256 と RS256 の取り違え RS256(公開鍵で検証)のサーバーが、攻撃者から送られた HS256 トークンを「秘密鍵で署名検証」してしまう。HS256 の鍵 = 公開鍵で偽造可能。ライブラリで明示的にアルゴリズム指定

事故3: 期限検証忘れ exp クレームを設定しても、検証側がチェックしないと意味なし。golang-jwt/jwt v5 はデフォルトで exp nbf iat を検証するが、ライブラリのバージョンとオプションを確認

JWT を localStorage に保存しない

// フロント側: NG
localStorage.setItem('jwt', token);

XSS で全 JS が読める → トークン盗まれる。

正解:

  • HttpOnly Cookie に入れる(JS から読めない)
  • もしくはメモリにだけ持つ(ページリロードで消える→リフレッシュ)

SPA で「Cookie じゃなくて Authorization ヘッダにしたい」場合は、メモリ + リフレッシュトークンで運用。

7. JWT 認証ミドルウェア

func (h *AuthHandler) RequireJWT(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authz := r.Header.Get("Authorization")
        if !strings.HasPrefix(authz, "Bearer ") {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        tokenStr := strings.TrimPrefix(authz, "Bearer ")
 
        claims, err := h.Svc.ParseJWT(tokenStr)
        if err != nil {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := context.WithValue(r.Context(), userIDKey, claims.UserID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

8. リフレッシュトークンの設計

リフレッシュトークンの役割

JWT を短命(15分など)にしておく → セキュリティ向上。ただし15分ごとに再ログインは UX が悪い。

解決策: 「アクセストークン(15分、JWT)+ リフレッシュトークン(7日、DB保存)」のペア。

  • アクセストークン期限切れ → リフレッシュトークンで新しいペアを発行
  • リフレッシュトークンは DB に保存し、取り消し可能(ログアウト、不正検知時)
  • リフレッシュトークンは漏れたら危険なので HttpOnly Cookie に入れる
type RefreshToken struct {
    Token     string
    UserID    int64
    ExpiresAt time.Time
    Revoked   bool
}
 
func (s *AuthService) Refresh(ctx context.Context, refreshTok string) (newAccess string, newRefresh string, err error) {
    rt, err := s.RefreshRepo.Get(ctx, refreshTok)
    if err != nil || rt.Revoked || time.Now().After(rt.ExpiresAt) {
        return "", "", ErrInvalidToken
    }
    // 古い refresh を無効化(rotation)
    if err := s.RefreshRepo.Revoke(ctx, refreshTok); err != nil {
        return "", "", err
    }
    user, _ := s.Users.GetByID(ctx, rt.UserID)
    newAccess, _ = s.IssueJWT(user)
    newRefresh = newSessionToken()
    _ = s.RefreshRepo.Save(ctx, newRefresh, user.ID, 7*24*time.Hour)
    return newAccess, newRefresh, nil
}

リフレッシュトークン rotation

リフレッシュトークンを使うたびに 古いものを無効化して新しいものを発行。これを「rotation」と呼ぶ。

意義: 漏洩したトークンが使われると、正規ユーザーのリフレッシュも無効化される → 異常検知できる。OAuth 2.1 で推奨されている方式。

9. ログアウト処理

func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
    c, err := r.Cookie("session_id")
    if err == nil {
        _ = h.Sessions.Delete(r.Context(), c.Value) // セッション削除
    }
    // Cookie を空にして MaxAge=-1(ブラウザに削除指示)
    http.SetCookie(w, &http.Cookie{
        Name:     "session_id",
        Value:    "",
        Path:     "/",
        HttpOnly: true,
        Secure:   true,
        MaxAge:   -1,
    })
    w.WriteHeader(http.StatusNoContent)
}

JWT は「ログアウト」できない

JWT は自己完結なので、サーバー側で「無効化」する仕組みが標準では無い。

解決策:

  • リフレッシュトークンだけ DB 保存 → そっちを revoke。アクセストークンは短命なので耐えられる
  • JWT ID(jti クレーム)+ blocklist。ステートフル化するので JWT のメリットが減る
  • 「ログアウトはユーザーへの UI 上の演出」と割り切る運用もある(古いトークンは期限切れまで動く)

10. アカウントロックアウト

func (s *AuthService) Login(ctx context.Context, c Credentials) (*User, error) {
    failed, _ := s.Failures.Count(ctx, c.Email, 15*time.Minute)
    if failed >= 5 {
        return nil, ErrTooManyAttempts
    }
 
    u, err := s.Users.GetByEmail(ctx, c.Email)
    if err != nil || bcrypt.CompareHashAndPassword(...) != nil {
        _ = s.Failures.Record(ctx, c.Email)
        return nil, ErrInvalidCredentials
    }
    _ = s.Failures.Clear(ctx, c.Email)
    return u, nil
}

ブルートフォース攻撃への防御

「同じメールに対して 15分以内に 5回失敗したらロック」。Redis でカウンタを持つのが定番。

ただし「ロックアウト」自体が DoS の入口にもなる(攻撃者が他人のアカウントを意図的にロックさせる)。ロック期間を短く(15分程度)ロック中もエラーメッセージを工夫(「しばらくしてから」のような)。

11. アンチパターン総まとめ

パスワード平文保存

言わずもがな。ハッシュ化必須。SHA-256 単発も NG(前述)。

パスワード長制限が短い

「6文字以上」みたいな制限。8文字以上、推奨14文字以上

ただし上限を 50文字とかに 絞らない。bcrypt の入力上限は 72バイトなので、それを超える長さは事前に切り詰めるか拒否する設計に。

レスポンスにパスワードハッシュが混入

json.NewEncoder(w).Encode(u) // u にハッシュフィールドが含まれている

User 構造体に PasswordHash を含んだまま JSON エンコードすると、レスポンスに "PasswordHash":"$2a$..." が混入。API レスポンス用に PasswordHash を含まない別構造体 を作る。

type UserResponse struct {
    ID    int64
    Email string
    Name  string
}

平文パスワードをログに出力

log.Printf("login attempt: %+v", credentials) // パスワードがログに残る

ログは長期保管されることが多い → 漏洩リスクが平文保存と同等に。機密フィールドの構造体に String() メソッドを実装 して、ログ出力時にマスクする。

CSRF 対策なし(Cookie 認証時)

セッション Cookie だけだと、悪意あるサイトから POST /transfer のような操作を仕掛けられる。SameSite=Lax + 必要に応じて CSRF トークン


練習課題

  1. ユーザー登録 + ログイン: bcrypt cost=12 でハッシュ化、ログインで検証
  2. session_id ベース認証: Redis or in-memory map にセッション保存、Cookie で配布
  3. JWT 認証: HS256 で署名、Authorization ヘッダで送信
  4. 認証必須エンドポイント: /api/me をミドルウェアで保護
  5. ログアウト: セッション削除 + Cookie 削除
  6. httptestでテスト: 認証あり/なしで /api/me の挙動を検証
  7. タイミング攻撃対策: ユーザー不在時もダミー bcrypt 比較

締め: git で証跡を残す

cd ~/learn/go/level3/day05
git add .
git commit -m "feat(go): bcrypt + セッション + JWT で認証を実装"

チェックリスト

  • bcrypt.GenerateFromPassword で cost=12 でハッシュ化した
  • bcrypt.CompareHashAndPassword で検証した
  • エラーメッセージで存在の有無を漏らさない設計
  • ユーザー不在時もダミーハッシュ比較(タイミング攻撃対策)
  • session_id を crypto/rand で生成(math/rand 不使用)
  • Cookie に HttpOnly Secure SameSite=Lax を設定
  • JWT で alg を明示検証
  • JWT 期限(exp)と署名アルゴリズムを設定
  • 認証ミドルウェアの順序を確認(ログインルートは認証不要)
  • レスポンス用構造体で PasswordHash を除外

アンチパターン集 - 即事故の地雷10選

認証実装の地雷

1. パスワード平文保存 - 言わずもがな、即漏洩リスク 2. SHA256/MD5 単発でハッシュ化 - GPU で秒間数億回総当たり可能、bcrypt/argon2 必須 3. bcrypt cost=4 で本番 - 1秒に数千試行、推奨 12+ 4. メール存在をエラーメッセージで漏らす - 「Invalid credentials」で統一 5. ユーザー不在時に bcrypt 比較スキップ - タイミング攻撃で存在判定可能 6. math/rand で session ID 生成 - シードで予測可能、crypto/rand 必須 7. Cookie に HttpOnly / Secure / SameSite なし - XSS / 中間者 / CSRF 直撃 8. JWT alg=none を受け入れる - 偽造可能、必ず alg 検証 9. JWT を localStorage に保存 - XSS で全読める、HttpOnly Cookie 必須 10. レスポンスに PasswordHash 含む - User struct そのまま Marshal で漏洩

対比表で違いを明確化

セッション vs JWT

観点セッション (Cookie + Redis)JWT
状態サーバー側で保持自己完結
取り消し即時可能期限切れまで有効
分散システムRedis 共有が必要不要
容量短い(32バイト)長い(数百〜数千バイト)
用途通常の Web アプリ分散・モバイル

パスワードハッシュアルゴリズム

アルゴリズム評価用途
MD5 / SHA-1×(壊滅的)旧データ移行時の照合のみ
SHA-256 単発×使うな
bcrypt標準的、Go で最も使われる
scryptメモリハードネス
argon2id最新推奨、メモリ + CPU

Cookie 属性必須セット

属性役割本番必須?
HttpOnlyJS から読めない(XSS対策)
SecureHTTPS のみ送信
SameSite=Laxクロスサイト POST 防止(CSRF)
MaxAge自動失効

詰まった時のチートシート

やりたいこと書き方
パスワードハッシュbcrypt.GenerateFromPassword([]byte(pw), 12)
パスワード検証bcrypt.CompareHashAndPassword(hash, []byte(pw))
暗号学的乱数crypto/rand.Read(buf)
一定時間比較subtle.ConstantTimeCompare(a, b) == 1
Cookie 設定http.SetCookie(w, &http.Cookie{HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode})
JWT 発行jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(secret)
JWT 検証jwt.ParseWithClaims(str, &claims, keyFunc) + alg チェック
context にユーザーcontext.WithValue(ctx, userIDKey, userID)

「実務OK」基準

  • 「平文保存」「SHA256単発」「math/rand」を見たら即座に NG と分かる
  • bcrypt の cost を本番で12以上にできる
  • タイミング攻撃の概念を語れる: 応答時間で情報漏洩
  • Cookie 属性(HttpOnly, Secure, SameSite)の役割を全部説明できる
  • JWT を localStorage に入れない、ログアウトできない理由を説明できる
  • 認証ミドルウェアの順序と適用範囲を意識できる

さらに深掘りするなら


次のレッスン

3-6 context.Contextcontext.Context の本質、キャンセル伝搬、WithValue の濫用を扱う。

つながりの予告

  • 本章で context.WithValue(ctx, userIDKey, userID) した値を次章で 正しく取り扱う設計
  • ログインのDB問い合わせに ctx.Done() でタイムアウト
  • リフレッシュトークン rotation で context によるキャンセル 連携