From d6abac3f0a02bf68062c1017ce152ed767197da6 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Wed, 4 Mar 2026 13:13:26 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20JWT=20=EA=B2=80=EC=A6=9D=20=EC=97=94?= =?UTF-8?q?=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94=EA=B0=80=20(PO?= =?UTF-8?q?ST=20/api/auth/verify)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 게임 서버가 클라이언트로부터 받은 JWT를 웹 서버에 전달하면, 서명 검증 + Redis 세션 확인 후 userId와 username을 응답한다. Co-Authored-By: Claude Sonnet 4.6 --- internal/auth/handler.go | 19 +++++++++++++++++++ internal/auth/service.go | 26 ++++++++++++++++++++++++++ routes/routes.go | 1 + 3 files changed, 46 insertions(+) diff --git a/internal/auth/handler.go b/internal/auth/handler.go index 7fbcba2..ba18f4c 100644 --- a/internal/auth/handler.go +++ b/internal/auth/handler.go @@ -81,6 +81,25 @@ func (h *Handler) UpdateRole(c *fiber.Ctx) error { return c.JSON(fiber.Map{"message": "권한이 변경되었습니다"}) } +func (h *Handler) VerifyToken(c *fiber.Ctx) error { + var req struct { + Token string `json:"token"` + } + if err := c.BodyParser(&req); err != nil || req.Token == "" { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "token 필드가 필요합니다"}) + } + + userID, username, err := h.svc.VerifyToken(req.Token) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": err.Error()}) + } + + return c.JSON(fiber.Map{ + "userId": userID, + "username": username, + }) +} + func (h *Handler) DeleteUser(c *fiber.Ctx) error { if err := h.svc.DeleteUser(c.Params("id")); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "유저 삭제에 실패했습니다"}) diff --git a/internal/auth/service.go b/internal/auth/service.go index 6389d63..5cd6053 100644 --- a/internal/auth/service.go +++ b/internal/auth/service.go @@ -92,6 +92,32 @@ func (s *Service) Register(username, password string) error { }) } +// VerifyToken validates a JWT and its Redis session, returning (userID, username, error). +func (s *Service) VerifyToken(tokenStr string) (uint, string, error) { + token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) { + if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method") + } + return []byte(config.C.JWTSecret), nil + }) + if err != nil || !token.Valid { + return 0, "", fmt.Errorf("유효하지 않은 토큰입니다") + } + + claims, ok := token.Claims.(*Claims) + if !ok { + return 0, "", fmt.Errorf("토큰 파싱 실패") + } + + key := fmt.Sprintf("session:%d", claims.UserID) + stored, err := s.rdb.Get(context.Background(), key).Result() + if err != nil || stored != tokenStr { + return 0, "", fmt.Errorf("만료되었거나 로그아웃된 세션입니다") + } + + return claims.UserID, claims.Username, nil +} + func (s *Service) EnsureAdmin(username, password string) error { if _, err := s.repo.FindByUsername(username); err == nil { return nil // 이미 존재하면 스킵 diff --git a/routes/routes.go b/routes/routes.go index 85ee43e..10f8a19 100644 --- a/routes/routes.go +++ b/routes/routes.go @@ -21,6 +21,7 @@ func Register( a.Post("/register", authH.Register) a.Post("/login", authH.Login) a.Post("/logout", middleware.Auth, authH.Logout) + a.Post("/verify", authH.VerifyToken) // Users (admin only) u := api.Group("/users", middleware.Auth, middleware.AdminOnly)