Go REST API に JWT認可処理を追加する
はじめに
Go の REST API に、JWTを用いた認可(Authorization)機能を追加しました。
なお、作成したものは以下のレポジトリで公開しています。
ディレクトリ構成
myapp
├── auth/
│ ├── context.go # SetUserID / GetUserID(contextの操作
│ ├── jwt.go # トークン発行・検証(生成・Parse)
│ └── middleware.go # JWTミドルウェア(検証 → contextに保存)
├── config/
│ ├── config.go # 環境変数の設定など
│ └── db.go # DB接続設定など
├── handlers/
│ └── user_handler.go # HTTPハンドラー(リクエスト受け取り → Serviceを呼び出す)
├── models/
│ └── user.go # ドメインモデル定義(User構造体、バリデーションなど)
├── repository/
│ └── user_repository.go # DBアクセス処理(UserのCRUD操作)
├── service/
│ └── user_service.go # ビジネスロジック(バリデーション、トランザクション制御など)
├── router/
│ └── router.go # ハンドラーとルーティングの紐づけ
├── main.go # エントリーポイント(Router起動だけにする)
├── .env
├── .env.sample
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── go.mod
└── go.sum
前提
以下のレポジトのREST APIにJWT認証を追加しました。 このレポジトリをベースにJWTを使用した認可処理を追加します。
なお、Go・postgreSQLの環境をdocker-composeで構築しています。
本記事のゴール
JWT を使った「認証(Authentication)」はすでに導入済みですが、
今回は「認可(Authorization)」=「ログイン中のユーザーがアクセス可能な範囲を制限する処理」を追加します。
ログイン済みのユーザーが、自分自身の情報だけを GET/UPDATE/DELETE できるようにします。
例:
PATCH /users/1 にアクセス
- JWT の sub(subject) が 1 ならOK
- そうでなければ 403 Forbidden
JWT に含めているsub(userID)をContextから取得する
authパッケージで Context にユーザーIDをセットする関数を実装してあります。
package auth
import (
"context"
)
type contextKey string
const UserIDKey contextKey = "userID"
func SetUserID(ctx context.Context, userID int) context.Context {
return context.WithValue(ctx, UserIDKey, userID)
}
Context からユーザーIDを取り出す関数を用意します。
func GetUserID(ctx context.Context) (int, bool) {
v := ctx.Value(UserIDKey)
userID, ok := v.(int)
return userID, ok
}
ハンドラーで認可チェックを追加
GetUserByID や PatchUser で、
URL の ID と JWT の userID が一致しているかを確認します。
GetUserByID を例として実装します。
既存の実装:
func (h *UserHandler) GetUserByID(w http.ResponseWriter, r *http.Request) {
// URLパスからIDを取得
idStr := strings.TrimPrefix(r.URL.Path, "/users/")
if idStr == "" {
respondWithError(w, http.StatusBadRequest, "IDは必要です")
return
}
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
respondWithError(w, http.StatusBadRequest, "IDは数値である必要があります")
return
}
user, err := h.userService.GetUserByID(id)
// 省略
}
以下の流れで認可を行います。
- r.Context() からユーザーIDを取得
- URLパラメータのIDを取得
- 2つか同じか比較
- 違ったら 403 を返す
func (h *UserHandler) GetUserByID(w http.ResponseWriter, r *http.Request) {
userID, ok := auth.GetUserID(r.Context())
if !ok {
respondWithError(w, http.StatusUnauthorized, "ログインが必要です")
return
}
// URLパスからIDを取得
idStr := strings.TrimPrefix(r.URL.Path, "/users/")
if idStr == "" {
respondWithError(w, http.StatusBadRequest, "IDは必要です")
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
respondWithError(w, http.StatusBadRequest, "IDは数値である必要があります")
return
}
// 認可チェック
if id != userID {
respondWithError(w, http.StatusForbidden, "他のユーザーの情報は取得できません")
return
}
// 省略
}
これで、GetUserByID に認可処理を組み込むことができました。
認可処理を共通化
他の操作にも認可処理を組み込もうと思いますが、
実装としては同じことを行うので共通化しておきます。
func authorizeSelf(w http.ResponseWriter, r *http.Request, paramID int) bool {
userID, ok := auth.GetUserID(r.Context())
if !ok {
respondWithError(w, http.StatusUnauthorized, "ログインが必要です")
return false
}
if userID != paramID {
respondWithError(w, http.StatusForbidden, "他のユーザーの情報は取得できません")
return false
}
return true
}
GetUserByID を改善します。
func (h *UserHandler) GetUserByID(w http.ResponseWriter, r *http.Request) {
// URLパスからIDを取得
idStr := strings.TrimPrefix(r.URL.Path, "/users/")
if idStr == "" {
respondWithError(w, http.StatusBadRequest, "IDは必要です")
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
respondWithError(w, http.StatusBadRequest, "IDは数値である必要があります")
return
}
if !authorizeSelf(w, r, id) {
return
}
user, err := h.userService.GetUserByID(id)
// 省略
}
PatchUser、DeleteUser でも同様にauthorizeSelfで認可チェックを行うことができます。
動作確認
/users/:id で動作確認をします。
正常
ログイン
$ curl -X POST -i -d '{"email": "taro@example.com" , "password": "pass"}' localhost:8080/login
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 03 Aug 2025 05:31:19 GMT
Content-Length: 164
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTQyMDI2NzksImlhdCI6MTc1NDE5OTA3OSwiaXNzIjoiIiwic3ViIjoxfQ.q2LM3IuPuRrssquG2EZijpfiik_QGOwCvc2wfZ0jV2c"}
GET リクエスト
$ curl -X GET -i http://localhost:8080/users/1 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTQyMDI2NzksImlhdCI6MTc1NDE5OTA3OSwiaXNzIjoiIiwic3ViIjoxfQ.q2LM3IuPuRrssquG2EZijpfiik_QGOwCvc2wfZ0jV2c"
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 03 Aug 2025 05:34:30 GMT
Content-Length: 101
{"id":1,"name":"山田太郎","email":"taro@example.com","created_at":"2025-08-03T05:26:55.621008Z"}
他人の情報にアクセス
$ curl -X GET -i http://localhost:8080/users/2 \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NTQyMDI2NzksImlhdCI6MTc1NDE5OTA3OSwiaXNzIjoiIiwic3ViIjoxfQ.q2LM3IuPuRrssquG2EZijpfiik_QGOwCvc2wfZ0jV2c"
HTTP/1.1 403 Forbidden
Content-Type: application/json
Date: Sun, 03 Aug 2025 05:34:44 GMT
Content-Length: 64
{"error":"他のユーザーの情報は取得できません"}
トークンなし
$ curl -X GET -i http://localhost:8080/users/1
HTTP/1.1 401 Unauthorized
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Sun, 03 Aug 2025 05:29:54 GMT
Content-Length: 33
Authorization header is required
まとめ
- JWTの
sub(subject)を Context に保存し、ハンドラー内で取得できるようにしました。 - ユーザー自身だけが GET / PATCH / DELETE を実行できるように、認可処理を追加しました。
- 共通関数として
authorizeSelfを定義することで、他のエンドポイントにも簡単に適用可能です。
次回は、ユーザーの「権限(role)」に応じたアクセス制限などを加えたいと思います。
Discussion