Files
a301_server/internal/auth/service.go
tolelom d6abac3f0a
All checks were successful
Server CI/CD / deploy (push) Successful in 1m17s
feat: JWT 검증 엔드포인트 추가 (POST /api/auth/verify)
게임 서버가 클라이언트로부터 받은 JWT를 웹 서버에 전달하면,
서명 검증 + Redis 세션 확인 후 userId와 username을 응답한다.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 13:13:26 +09:00

135 lines
3.7 KiB
Go

package auth
import (
"context"
"fmt"
"time"
"a301_server/pkg/config"
"github.com/golang-jwt/jwt/v5"
"github.com/redis/go-redis/v9"
"golang.org/x/crypto/bcrypt"
)
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
Role string `json:"role"`
jwt.RegisteredClaims
}
type Service struct {
repo *Repository
rdb *redis.Client
}
func NewService(repo *Repository, rdb *redis.Client) *Service {
return &Service{repo: repo, rdb: rdb}
}
func (s *Service) Login(username, password string) (string, *User, error) {
user, err := s.repo.FindByUsername(username)
if err != nil {
return "", nil, fmt.Errorf("아이디 또는 비밀번호가 올바르지 않습니다")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return "", nil, fmt.Errorf("아이디 또는 비밀번호가 올바르지 않습니다")
}
expiry := time.Duration(config.C.JWTExpiryHours) * time.Hour
claims := &Claims{
UserID: user.ID,
Username: user.Username,
Role: string(user.Role),
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiry)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err := token.SignedString([]byte(config.C.JWTSecret))
if err != nil {
return "", nil, fmt.Errorf("토큰 생성에 실패했습니다")
}
// Redis에 세션 저장 (1계정 1세션)
key := fmt.Sprintf("session:%d", user.ID)
s.rdb.Set(context.Background(), key, tokenStr, expiry)
return tokenStr, user, nil
}
func (s *Service) Logout(userID uint) {
key := fmt.Sprintf("session:%d", userID)
s.rdb.Del(context.Background(), key)
}
func (s *Service) GetAllUsers() ([]User, error) {
return s.repo.FindAll()
}
func (s *Service) UpdateRole(id string, role Role) error {
return s.repo.UpdateRole(id, role)
}
func (s *Service) DeleteUser(id string) error {
return s.repo.Delete(id)
}
func (s *Service) Register(username, password string) error {
if _, err := s.repo.FindByUsername(username); err == nil {
return fmt.Errorf("이미 사용 중인 아이디입니다")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return fmt.Errorf("비밀번호 처리에 실패했습니다")
}
return s.repo.Create(&User{
Username: username,
PasswordHash: string(hash),
Role: RoleUser,
})
}
// 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 // 이미 존재하면 스킵
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
return s.repo.Create(&User{
Username: username,
PasswordHash: string(hash),
Role: RoleAdmin,
})
}