feat: JWT 검증 엔드포인트 추가 (POST /api/auth/verify)
All checks were successful
Server CI/CD / deploy (push) Successful in 1m17s

게임 서버가 클라이언트로부터 받은 JWT를 웹 서버에 전달하면,
서명 검증 + Redis 세션 확인 후 userId와 username을 응답한다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 13:13:26 +09:00
parent 2996e0fa0f
commit d6abac3f0a
3 changed files with 46 additions and 0 deletions

View File

@@ -81,6 +81,25 @@ func (h *Handler) UpdateRole(c *fiber.Ctx) error {
return c.JSON(fiber.Map{"message": "권한이 변경되었습니다"}) 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 { func (h *Handler) DeleteUser(c *fiber.Ctx) error {
if err := h.svc.DeleteUser(c.Params("id")); err != nil { if err := h.svc.DeleteUser(c.Params("id")); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "유저 삭제에 실패했습니다"}) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "유저 삭제에 실패했습니다"})

View File

@@ -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 { func (s *Service) EnsureAdmin(username, password string) error {
if _, err := s.repo.FindByUsername(username); err == nil { if _, err := s.repo.FindByUsername(username); err == nil {
return nil // 이미 존재하면 스킵 return nil // 이미 존재하면 스킵

View File

@@ -21,6 +21,7 @@ func Register(
a.Post("/register", authH.Register) a.Post("/register", authH.Register)
a.Post("/login", authH.Login) a.Post("/login", authH.Login)
a.Post("/logout", middleware.Auth, authH.Logout) a.Post("/logout", middleware.Auth, authH.Logout)
a.Post("/verify", authH.VerifyToken)
// Users (admin only) // Users (admin only)
u := api.Group("/users", middleware.Auth, middleware.AdminOnly) u := api.Group("/users", middleware.Auth, middleware.AdminOnly)