Goで学ぶWebSocket：[JWT認証 & 負荷分散設計]

## はじめに

前回の記事では、WebSocketを活用した通知システム、Redisを使ったWebSocketサーバーの構築方法を解説しました。  
今回はさらに発展させて、JWT認証を組み込んだWebSocket実装と 負荷分散を考慮した設計について解説します。

**対象読者**
- WebSocketに認証を組み込みたい方
- 負荷の高いWebSocketアプリを効率的にスケールアウトしたい方
- セキュアなリアルタイム通信を実現したい方

**目次**
1. JWT認証を組み込んだセキュアなWebSocketの実装
   - JWT認証とは？
   - GoでのJWT認証の実装
   - WebSocketへのJWT認証の適用
2. 負荷分散を考慮したWebSocket設計
   - 負荷分散の仕組み
   - WebSocketをスケールアウトする方法
   - Redis & Nginxを使った負荷分散

---

## 1. JWT認証を組み込んだセキュアなWebSocketの実装

#### **1.1 そもそもJWT認証とは？**

**JWT（JSON Web Token）** は、Webアプリケーションで広く使われる認証方式です。

JWTの仕組み：
1. ユーザーがログイン → サーバーが秘密鍵でJWTトークンを発行
2. クライアントがJWTをリクエストヘッダーに追加
3. サーバーがJWTを検証し、認証を通過した場合のみ接続を許可

JWT の例：
```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImV4cCI6MTY3NTAwMDAwMH0.mFkjcYJwGm7SUk8BzL7L5bJxz5XL8yYpXFA0eH0H-lw
```

#### **1.2 GoでのJWT認証の実装**

まず、JWTライブラリをインストールします。
```sh
go get github.com/golang-jwt/jwt/v4
```

**jwt.go**
```go
package main

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

var secretKey = []byte("supersecretkey")

type Claims struct {
    UserID int `json:"userId"`
    jwt.RegisteredClaims
}

func GenerateJWT(userID int) (string, error) {
    claims := Claims{
        UserID: userID,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(secretKey)
}

func ValidateJWT(tokenStr string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        return secretKey, nil
    })
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    return nil, err
}
```

#### **1.3 WebSocketへのJWT認証の適用**

**server.go**
```go
package main

import (
    "fmt"
    "net/http"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool { return true },
}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
    tokenStr := r.URL.Query().Get("token")
    claims, err := ValidateJWT(tokenStr)
    if err != nil {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    fmt.Println("Authenticated user:", claims.UserID)

    conn, _ := upgrader.Upgrade(w, r, nil)
    defer conn.Close()

    for {
        _, msg, err := conn.ReadMessage()
        if err != nil {
            break
        }
        fmt.Printf("Received message: %s\n", msg)
    }
}
```

- WebSocketのリクエストにJWTを含めることで認証されたユーザーのみ接続を許可する
- `ValidateJWT()` でJWTの検証を行い、不正なトークンの場合は`401 Unauthorized`を返す

---

## 2. 負荷分散を考慮したWebSocket設計

#### **2.1 負荷分散の仕組み**

WebSocketの負荷分散では、以下の2つの方式を採用します。
1. Nginxを使ったWebSocket負荷分散
2. Redisを使ったWebSocketのスケールアウト

#### **2.2 WebSocketをスケールアウトする方法（Nginx + Redis）**

**NginxをリバースプロキシとしてWebSocket負荷分散を実装する**

**nginx.conf**
```nginx
http {
    upstream websocket_backend {
        server ws-server1:8080;
        server ws-server2:8080;
    }

    server {
        listen 80;
        location /ws {
            proxy_pass http://websocket_backend;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "Upgrade";
        }
    }
}
```

#### **2.3 Redisを使ったWebSocketセッション共有**

複数のWebSocketサーバーがある場合、**Redis Pub/Subを活用**してメッセージを共有。

**server_redis.go**
```go
rdb.Publish(ctx, "websocket_messages", message)
```

各WebSocketサーバーはRedisのメッセージを受信して全クライアントにブロードキャストされる。

---

## まとめ

| 項目 | 説明 |
|------|------|
| **JWT認証** | WebSocketで認証を行い、セキュアな通信を確立 |
| **Nginx負荷分散** | WebSocketのトラフィックを複数のサーバーに分散 |
| **Redisセッション共有** | 複数サーバー間でメッセージを共有し、スケールアウト可能に |

WebSocketは汎用性も高い仕組みで色々な用途があります！
参考になれば嬉しいです！