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/v51. パスワードを「ハッシュ化して保存」する
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 で毎秒数十億回の総当たりが可能
- レインボーテーブルで事前計算済みハッシュと突き合わせ可能
正しいパスワードハッシュ関数の条件:
- 意図的に遅い(CPU/メモリを食う) → 総当たり攻撃を時間的に困難に
- salt 内包(同じパスワードでもハッシュが毎回違う)→ レインボーテーブル無効化
- コスト調整可能 → ハードウェア進化に追従できる
これを満たすのが 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引数目costは 2の何乗回の反復計算を行うか の指数。
cost 1回あたりの目安 用途 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)
}セッションベース認証の流れ
- ログイン成功時に ランダムな session_id を生成
session_id → user_idを Redis などに保存(有効期限付き)- Cookie で session_id をクライアントに送る
- 次回以降のリクエストで Cookie の session_id を読み、Redis 引きでユーザーを特定
session_id の必須要件:
- 推測不能:
crypto/randで256ビット以上のエントロピー- 短く: 100文字以内(Cookie の長さ制限)
- URL セーフ: base64.RawURLEncoding が無難
Cookie 属性の意味と必須化
属性 役割 HttpOnlyJS から読めない → XSS で盗まれない SecureHTTPS でしか送られない → 中間者攻撃を防ぐ SameSite=Laxクロスサイト POST を防ぐ → CSRF対策 Path=/サイト全体に送る MaxAge期限切れで自動削除
HttpOnlyとSecureは本番では絶対必須。これ無しだとブラウザに「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
}ミドルウェアの責務
- Cookie / Authorization ヘッダから認証情報を取り出す
- 検証(セッション引き or 署名検証)
- 失敗なら 401 で打ち切り
- 成功なら 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はデフォルトでexpnbfiatを検証するが、ライブラリのバージョンとオプションを確認。
JWT を localStorage に保存しない
// フロント側: NG localStorage.setItem('jwt', token);XSS で全 JS が読める → トークン盗まれる。
正解:
HttpOnlyCookie に入れる(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 に保存し、取り消し可能(ログアウト、不正検知時)
- リフレッシュトークンは漏れたら危険なので
HttpOnlyCookie に入れる
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 トークン。
練習課題
- ユーザー登録 + ログイン: bcrypt cost=12 でハッシュ化、ログインで検証
- session_id ベース認証: Redis or in-memory map にセッション保存、Cookie で配布
- JWT 認証: HS256 で署名、Authorization ヘッダで送信
- 認証必須エンドポイント:
/api/meをミドルウェアで保護 - ログアウト: セッション削除 + Cookie 削除
httptestでテスト: 認証あり/なしで/api/meの挙動を検証- タイミング攻撃対策: ユーザー不在時もダミー 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 に
HttpOnlySecureSameSite=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. JWTalg=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 属性必須セット
属性 役割 本番必須? HttpOnly JS から読めない(XSS対策) ◎ Secure HTTPS のみ送信 ◎ 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 に入れない、ログアウトできない理由を説明できる
- 認証ミドルウェアの順序と適用範囲を意識できる
さらに深掘りするなら
- OWASP Authentication Cheat Sheet - 必読
- OWASP JWT Cheat Sheet
- 公式: pkg.go.dev/golang.org/x/crypto/bcrypt
- golang-jwt/jwt v5 docs
- 関連レッスン: セキュリティ章: JWT の本質
- 書籍: 『Securing DevOps』 (Manning) 認証章
次のレッスン
3-6 context.Context で context.Context の本質、キャンセル伝搬、WithValue の濫用を扱う。
つながりの予告
- 本章で
context.WithValue(ctx, userIDKey, userID)した値を次章で 正しく取り扱う設計 - ログインのDB問い合わせに
ctx.Done()でタイムアウト - リフレッシュトークン rotation で context によるキャンセル 連携