GoミドルウェアとJWT認証
ミドルウェアとは、Goのマイクロサービスにおける「パイプライン」のことです。認証、レート制限、ロギング、リカバリなど、各横断的関心事項は、それぞれミドルウェアの一要素となります。
すべてのAPIエンドポイントでJWT認証の検証、アクティビティのログ記録、またはレート制限の適用が必要な場合、ミドルウェアパターンを利用すれば、コードを一度記述するだけで、すべてのエンドポイントに適用することができます。
1. 学習内容
- JWTの生成と検証
- アクセストークン + リフレッシュトークン(デュアルトークン)
- RBAC(ロールベースのアクセス制御)
- トークンバケット方式によるスループット制御
- リカバリ / ロギング / CORS ミドルウェア
- カスケード型ミドルウェアパイプライン
2. バックエンドエンジニアの実話
(1) 課題:各ルーターでトークンを手動で確認する必要があること。私は3回も確認し忘れてしまった。
チャーリーはAPI Gatewayチームに所属しており、すべてのエンドポイントの認証を担当しています:
「上司から、すべてのAPIでJWTトークンの検証を行うよう指示があった。そこで、各ハンドラーに15行のトークン検証コードをコピー&ペーストした。デプロイから2日目、
/admin/usersへの認証処理を追加し忘れていたため、誰でもユーザーを直接削除できてしまった。CTOは『セキュリティ監査で重大な脆弱性が発見された』と言った。」
// 悪いコード:各ハンドラで手動検証
func deleteUser(w http.ResponseWriter, r *http.Request) {
// Token 検証の書き忘れ!重大なセキュリティ脆弱性
userID := r.PathValue("id")
deleteUserFromDB(userID)
}
func updateOrder(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
// 毎回 15 行の検証コードをコピペ
if !validateToken(token) {
http.Error(w, "unauthorized", 401)
return
}
// ビジネスロジック...
}
(2) Goソリューション:認証ミドルウェア
// 良いコード:認証ミドルウェア、一度実装すればどこでも有効
func AuthMiddleware(jwtSecret 文字列) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := extractToken(r)
claims, err := validateJWT(token, jwtSecret)
if err != nil {
writeError(w, http.StatusUnauthorized, "invalid token")
return
}
// ユーザー情報を Context に注入
ctx := context.WithValue(r.Context(), "ユーザー", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// 登録は Middleware を 1 つ追加するだけ
mux.Handle("POST /api/orders", AuthMiddleware(secret)(orderHandler))
(3) メリット:手動とミドルウェアの比較
| 次元 | 手動チェック | ミドルウェアパターン |
|---|---|---|
| 漏れのリスク | 新しいルートを追加し忘れるリスクがある | ゼロ |
| コードの重複 | ハンドラあたり15行 | 0行(ハンドラはビジネスロジックにのみ焦点を当てている) |
| ロジックの変更 | 20個のハンドラの変更 | 1個のミドルウェアの変更 |
| ユニットテスト | 各ハンドラの認証をテストする | ミドルウェアは1回だけテストする |
3. JWT認証
▶ サンプル:JWTの生成と検証
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
)
// ---------- JWT 手動実装(サードパーティライブラリ非依存)----------
type JWTClaims struct {
UserID int `json:"user_id"`
Role string `json:"role"`
Permissions []string `json:"permissions"`
ExpiresAt int64 `json:"exp"`
}
type JWTHeader struct {
Alg string `json:"alg"`
Typ string `json:"typ"`
}
func base64Encode(data []byte) string {
return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=")
}
func base64Decode(s string) ([]byte, error) {
// padding を補完
switch len(s) % 4 {
case 2:
s += "=="
case 3:
s += "="
}
return base64.URLEncoding.DecodeString(s)
}
func createJWT(claims JWTClaims, secret string) (string, error) {
header := JWTHeader{Alg: "HS256", Typ: "JWT"}
headerJSON, _ := json.Marshal(header)
claimsJSON, _ := json.Marshal(claims)
headerEnc := base64Encode(headerJSON)
claimsEnc := base64Encode(claimsJSON)
// 署名
message := headerEnc + "." + claimsEnc
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
signature := base64Encode(mac.Sum(nil))
return message + "." + signature, nil
}
func validateJWT(token string, secret string) (*JWTClaims, error) {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid token format")
}
// 署名の検証
message := parts[0] + "." + parts[1]
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
expectedSig := base64Encode(mac.Sum(nil))
if !hmac.Equal([]byte(parts[2]), []byte(expectedSig)) {
return nil, fmt.Errorf("invalid signature")
}
// claims を解析
claimsJSON, err := base64Decode(parts[1])
if err != nil {
return nil, err
}
var claims JWTClaims
if err := json.Unmarshal(claimsJSON, &claims); err != nil {
return nil, err
}
// 有効期限の確認
if time.Now().Unix() > claims.ExpiresAt {
return nil, fmt.Errorf("token expired")
}
return &claims, nil
}
func main() {
secret := "my-secret-key"
// Token を生成
claims := JWTClaims{
UserID: 1,
Role: "admin",
ExpiresAt: time.Now().Add(1 * time.Hour).Unix(),
}
token, _ := createJWT(claims, secret)
fmt.Printf("JWT: %s\n", token)
// Token を検証
validated, err := validateJWT(token, secret)
if err != nil {
fmt.Printf("検証に失敗しました: %v\n", err)
} else {
fmt.Printf("認証に成功しました: user=%d, role=%s\n", validated.UserID, validated.Role)
}
}
github.com/golang-jwt/jwt/v5) を使用してください。上記のコードは、JWTの仕組みを説明することのみを目的としています。サードパーティ製のライブラリは、トークンの種類、鍵のローテーション、標準クレームの検証など、より多くのエッジケースに対応しています。
4. アクセストークン + リフレッシュトークン
▶ サンプル:2要素認証
package main
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
)
// ---------- Token サービス ----------
type TokenService struct {
secret string
refreshTTL time.Duration
// refresh token を保存(本番環境は Redis)
refreshTokens map[string]int // token → userID
mu sync.RWMutex
}
func NewTokenService(secret string) *TokenService {
return &TokenService{
secret: secret,
refreshTTL: 7 * 24 * time.Hour,
refreshTokens: make(map[string]int),
}
}
func (s *TokenService) GenerateTokens(userID int, role string) (accessToken, refreshToken string, err error) {
// Access Token(15 分で期限切れ)
accessClaims := JWTClaims{
UserID: userID,
Role: role,
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(),
}
accessToken, err = createJWT(accessClaims, s.secret)
if err != nil {
return "", "", err
}
// Refresh Token(ランダム文字列、7 日で期限切れ)
bytes := make([]byte, 32)
rand.Read(bytes)
refreshToken = hex.EncodeToString(bytes)
s.mu.Lock()
s.refreshTokens[refreshToken] = userID
s.mu.Unlock()
return accessToken, refreshToken, nil
}
func (s *TokenService) RefreshAccessToken(refreshToken string) (string, error) {
s.mu.RLock()
userID, exists := s.refreshTokens[refreshToken]
s.mu.RUnlock()
if !exists {
return "", fmt.Errorf("invalid refresh token")
}
// 新しい Access Token を生成
claims := JWTClaims{
UserID: userID,
Role: "user",
ExpiresAt: time.Now().Add(15 * time.Minute).Unix(),
}
return createJWT(claims, s.secret)
}
func (s *TokenService) RevokeRefreshToken(refreshToken string) {
s.mu.Lock()
delete(s.refreshTokens, refreshToken)
s.mu.Unlock()
}
// ---------- Auth Handler ----------
type AuthHandler struct {
tokenSvc *TokenService
}
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type tokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
}
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
var req loginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
// ユーザー名とパスワードの検証(本番環境は bcrypt)
if req.Username != "admin" || req.Password != "password" {
writeError(w, http.StatusUnauthorized, "invalid credentials")
return
}
accessToken, refreshToken, err := h.tokenSvc.GenerateTokens(1, "admin")
if err != nil {
writeError(w, http.StatusInternalServerError, "token generation failed")
return
}
writeJSON(w, http.StatusOK, tokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
TokenType: "Bearer",
ExpiresIn: 900, // 15 分
})
}
func (h *AuthHandler) Refresh(w http.ResponseWriter, r *http.Request) {
var req struct {
RefreshToken string `json:"refresh_token"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
accessToken, err := h.tokenSvc.RefreshAccessToken(req.RefreshToken)
if err != nil {
writeError(w, http.StatusUnauthorized, "invalid refresh token")
return
}
writeJSON(w, http.StatusOK, map[string]string{"access_token": accessToken})
}
sequenceDiagram
participant Client
participant API as API Gateway
participant Auth as Auth Service
Client->>API: POST /login (username, password)
API->>Auth: 認証情報の確認
Auth-->>API: access_token (15min) + refresh_token (7d)
API-->>Client: 2つのトークンに戻る
Client->>API: GET /orders (Bearer access_token)
API->>API: 検証 access_token
API-->>Client: 200 OK
Client->>API: GET /orders (access_token 期限切れ)
API-->>Client: 401 Unauthorized
Client->>API: POST /refresh (refresh_token)
API->>Auth: 検証 refresh_token
Auth-->>API: 新しい access_token
API-->>Client: トップへ戻る access_token
5. RBAC 権限ミドルウェア
▶ サンプル:RBACの実装
パッケージ main
import (
"context"
"encoding/json"
"net/http"
)
type UserRole 文字列
const (
RoleAdmin UserRole = "admin"
RoleUser UserRole = "ユーザー"
RoleGuest UserRole = "guest"
)
// 権限の定義
var rolePermissions = map[UserRole][]文字列{
RoleAdmin: {"read:users", "write:users", "削除:users", "read:orders", "write:orders"},
RoleUser: {"read:orders", "write:orders"},
RoleGuest: {"read:products"},
}
type Claims struct {
UserID int
Role UserRole
}
// Context から認証情報を取得
func GetClaims(r *http.Request) *Claims {
claims, _ := r.Context().Value("claims").(*Claims)
return claims
}
// RBAC ミドルウェア
func RequirePermission(permission 文字列) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims := GetClaims(r)
if claims == nil {
writeError(w, http.StatusUnauthorized, "not authenticated")
return
}
permissions, exists := rolePermissions[claims.Role]
if !exists {
writeError(w, http.StatusForbidden, "no permissions defined for role")
return
}
// 権限の確認
hasPermission := false
for _, p := range permissions {
if p == permission {
hasPermission = true
break
}
}
if !hasPermission {
writeError(w, http.StatusForbidden, "insufficient permissions")
return
}
next.ServeHTTP(w, r)
})
}
}
// ---------- ルーティング登録 ----------
func setupRoutes() http.Handler {
mux := http.NewServeMux()
// 公開エンドポイント
mux.HandleFunc("POST /login", loginHandler)
// 認証が必要なエンドポイント
mux.Handle("GET /api/orders", RequirePermission("read:orders")(authMiddleware(http.HandlerFunc(listOrders))))
mux.Handle("POST /api/orders", RequirePermission("write:orders")(authMiddleware(http.HandlerFunc(createOrder))))
mux.Handle("DELETE /api/users/{id}", RequirePermission("削除:users")(authMiddleware(http.HandlerFunc(deleteUser))))
return mux
}
(1) ミドルウェア・パイプライン
Request → Auth Middleware → RBAC Middleware → Handler
| |
検証 JWT Token 確認 Role 権限
注入 Claims ユーザーには権限があります?
| レベル | ミドルウェア | 担当業務 |
|---|---|---|
| 1 | リカバリ | パニックをキャッチし、500を返す |
| 2 | ロギング | ロギングリクエストの所要時間 |
| 3 | CORS | クロスオリジンリクエストの処理 |
| 4 | 認証 | JWTの検証、ユーザー情報の挿入 |
| 5 | RBAC | ロールの権限を確認 |
| 6 | レート制限 | トークンバケット方式によるスロットリング |
| 7 | ハンドラー | ビジネスロジック |
6. トークンバケット方式によるスループット制御
▶ サンプル:トークンバケットの実装
package main
import (
"net/http"
"sync"
"time"
)
type TokenBucket struct {
mu sync.Mutex
tokens float64
maxTokens float64
refillRate float64
lastRefill time.Time
}
func NewTokenBucket(rate float64, burst int) *TokenBucket {
return &TokenBucket{
tokens: float64(burst),
maxTokens: float64(burst),
refillRate: rate,
lastRefill: time.Now(),
}
}
func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()
// Refill tokens
now := time.Now()
elapsed := now.Sub(tb.lastRefill).Seconds()
tb.tokens = min(tb.tokens+elapsed*tb.refillRate, tb.maxTokens)
tb.lastRefill = now
// Take a token
if tb.tokens >= 1 {
tb.tokens--
return true
}
return false
}
func min(a, b float64) float64 {
if a < b {
return a
}
return b
}
// IP レートリミッター
type IPRateLimiter struct {
mu sync.RWMutex
buckets map[string]*TokenBucket
rate float64
burst int
}
func NewIPRateLimiter(rate float64, burst int) *IPRateLimiter {
return &IPRateLimiter{
buckets: make(map[string]*TokenBucket),
rate: rate,
burst: burst,
}
}
func (rl *IPRateLimiter) GetBucket(ip string) *TokenBucket {
rl.mu.Lock()
defer rl.mu.Unlock()
bucket, exists := rl.buckets[ip]
if !exists {
bucket = NewTokenBucket(rl.rate, rl.burst)
rl.buckets[ip] = bucket
}
return bucket
}
// RateLimiterMiddleware
func RateLimiterMiddleware(rate float64, burst int) Middleware {
limiter := NewIPRateLimiter(rate, burst)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
bucket := limiter.GetBucket(ip)
if !bucket.Allow() {
w.Header().Set("Retry-After", "1")
writeError(w, http.StatusTooManyRequests, "rate limit exceeded")
return
}
next.ServeHTTP(w, r)
})
}
}
7. 完全な例:API Gateway ミドルウェア・パイプライン
// api_gateway.go
パッケージ main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
// ---------- 型定義 ----------
type Middleware func(http.Handler) http.Handler
type Claims struct {
UserID int
Role 文字列
}
// ---------- ミドルウェア ----------
// Recovery
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
writeError(w, http.StatusInternalServerError, "internal エラー")
}
}()
next.ServeHTTP(w, r)
})
}
// Logging
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
})
}
// CORS
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// Auth(JWT 認証)
func Auth(secret 文字列) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
writeError(w, http.StatusUnauthorized, "missing token")
return
}
token := authHeader[7:]
claims, err := validateJWT(token, secret)
if err != nil {
writeError(w, http.StatusUnauthorized, err.Error())
return
}
ctx := context.WithValue(r.Context(), "claims", &Claims{
UserID: claims.UserID,
Role: claims.Role,
})
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// RBAC
func RequireRole(roles ...文字列) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := r.Context().Value("claims").(*Claims)
if !ok {
writeError(w, http.StatusUnauthorized, "not authenticated")
return
}
for _, role := range roles {
if claims.Role == role {
next.ServeHTTP(w, r)
return
}
}
writeError(w, http.StatusForbidden, "insufficient permissions")
})
}
}
// Rate Limiter
var rateLimiter = NewIPRateLimiter(10, 20) // 10 req/s, burst 20
func RateLimit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
bucket := rateLimiter.GetBucket(ip)
if !bucket.Allow() {
writeError(w, http.StatusTooManyRequests, "rate limit exceeded")
return
}
next.ServeHTTP(w, r)
})
}
// ---------- Chain ----------
func Chain(h http.Handler, mws ...Middleware) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
// ---------- Handlers ----------
type OrderHandler struct{}
func (h *OrderHandler) ListOrders(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[文字列]文字列{"orders": "list"})
}
func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusCreated, map[文字列]文字列{"順序": "created"})
}
type AdminHandler struct{}
func (h *AdminHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[文字列]文字列{"deleted": "ユーザー"})
}
// ---------- ユーティリティ関数 ----------
func writeJSON(w http.ResponseWriter, ステータス int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(ステータス)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, ステータス int, msg 文字列) {
writeJSON(w, ステータス, map[文字列]文字列{"エラー": msg})
}
// ---------- Main ----------
func main() {
jwtSecret := "super-secret-key"
orders := &OrderHandler{}
mux := http.NewServeMux()
// 公開エンドポイント
mux.HandleFunc("POST /login", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[文字列]文字列{"token": "login-implementation"})
})
// 認証 + レート制限が適用されるエンドポイント
mux.Handle("GET /api/orders", Chain(
http.HandlerFunc(orders.ListOrders),
Auth(jwtSecret),
RateLimit,
))
mux.Handle("POST /api/orders", Chain(
http.HandlerFunc(orders.CreateOrder),
Auth(jwtSecret),
RateLimit,
))
// 管理者用エンドポイント(Auth + RBAC + RateLimit)
mux.Handle("DELETE /api/users/{id}", Chain(
http.HandlerFunc((&AdminHandler{}).DeleteUser),
Auth(jwtSecret),
RequireRole("admin"),
RateLimit,
))
// グローバルミドルウェア(Recovery → Logging → CORS → ルート)
app := Chain(mux, Recovery, Logging, CORS)
サーバー := &http.Server{Addr: ":8080", Handler: app}
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("閉じている...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
サーバー.Shutdown(ctx)
}()
log.Println("API ゲートウェイの起動は :8080")
if err := サーバー.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}
❓ よくある質問
github.com/golang-jwt/jwt/v5の使用が推奨されています。アクセストークンの有効期間は短く(15分)、リフレッシュトークンの有効期間は長く(7日間)、サーバー上に保存されます。rolePermissions[role] = []permission + ミドルウェアによる確認 permission in rolePermissions[claims.Role]。debug.Stack() はスタックトレースを出力します。ログレベルは ERROR を使用してください。本番環境では、スタックトレースをクライアントに返さず、サーバー上でのみログに記録してください。Chain(h, A, B, C) → リクエストは A → B → C → Handler → C → B → A の順を通過します。登録順序は、最外層から最内層へと進みます。Recovery は最外層にあり(どの層から投げられたパニックも捕捉します)、Handler は最内層にあります(ビジネスロジック)。📖 まとめ
- JWT:ヘッダー + ペイロード + 署名、
golang-jwt/jwt/v5 - トークン2つ:アクセス(15分)+リフレッシュ(7日間)
- 認証ミドルウェア:トークンを検証し、クレームをコンテキストに注入する
- RBAC:ロールと権限のマッピング。ミドルウェアが権限をチェックする
- トークンバケット方式によるスループット制御:トラフィックの急増を許容しつつ、平均流量を制御する
- リカバリ・ミドルウェア:パニックを捕捉し、500を返す
- ミドルウェアの順序:リカバリ → ロギング → CORS → 認証 → RBAC → レート制限 → ハンドラー
📝 練習問題
-
基本演習(難易度 ⭐):簡単な認証ミドルウェアを実装してください。
/api/protectedというエンドポイントを作成し、リクエストヘッダーにAuthorization: Bearer token123が含まれていることを条件とします。トークンが有効な場合は{"message": "access granted"}を返し、そうでない場合は401エラーを返してください。 -
上級問題(難易度 ⭐⭐):完全なJWT認証システムを実装してください。要件:(1) 登録用の
POST /register(パスワードはbcryptで暗号化);(2) ログイン用のPOST /login(access_token + refresh_token を返す); (3) すべての/api/*エンドポイントにおいて access_token を検証する Auth ミドルウェア;(4)refresh_tokenを新しいaccess_tokenと交換するためのPOST /refresh;(5)refresh_tokenを失効させるためのPOST /logout。 -
課題(難易度:⭐⭐⭐):RBACとレート制限を備えたブログ管理APIを実装してください。要件:(1) 3つのロール:admin、editor、reader;(2) admin:すべての権限;(3) editor:自身の投稿の作成、編集、削除;(4) reader:読み取り専用アクセス; (5) ロールごとに異なるレート制限を設定すること;(6) すべてのエンドポイントは、以下のミドルウェアパイプラインを経由すること:Recovery → Logging → Auth → RBAC → RateLimit → Handler;(7)
-raceを使用して、並行処理の安全性を検証すること。



