Gin API에 JWT 인증으로 강화하기
Ethan Miller
Product Engineer · Leapcell

소개
현대 웹 개발의 활기찬 환경에서 API는 수많은 애플리케이션의 백본 역할을 하며 데이터 교환 및 서비스 통합을 촉진합니다. 이러한 API가 점차 중요해짐에 따라 보안을 보장하고 민감한 리소스에 대한 접근을 제어하는 것이 무엇보다 중요합니다. 무단 액세스는 데이터 유출, 서비스 중단 및 사용자 신뢰의 상당한 침식으로 이어질 수 있습니다. 여기서 인증 메커니즘이 디지털 출입문지기 역할을 하며 등장합니다. 다양한 인증 전략 중에서 JSON 웹 토큰(JWT)은 특히 상태 비저장 API의 경우 매우 인기 있고 효율적인 선택으로 부상했습니다.
이 글에서는 Gin 기반 API에 JWT 인증을 미들웨어로 통합하는 과정을 안내하여 강력한 보안 계층을 제공하고 실질적인 구현을 보여줍니다.
핵심 개념 및 구현
코드로 들어가기 전에 JWT 인증을 이해하는 데 기본적인 몇 가지 용어를 간략하게 정의해 보겠습니다.
- JSON 웹 토큰(JWT): 두 당사자 간에 전송될 클레임을 나타내는 간결하고 URL에 안전한 수단입니다. JWT의 클레임은 JSON 객체로 인코딩되고 디지털 서명되어 무결성을 보장합니다. JWT는 일반적으로 세 부분으로 구성됩니다.
- 헤더: 토큰 유형(JWT) 및 서명 알고리즘(예: HS256)과 같은 토큰에 대한 메타데이터를 포함합니다.
- 페이로드: 엔터티(일반적으로 사용자)에 대한 진술 및 추가 데이터를 포함하는 클레임을 포함합니다. 일반적인 클레임에는
iss
(발급자),exp
(만료 시간),sub
(주제) 및 사용자 지정 애플리케이션별 클레임이 포함됩니다. - 서명: 인코딩된 헤더, 인코딩된 페이로드, 비밀 키 및 헤더에 지정된 알고리즘을 가져와 서명하여 생성됩니다. 서명은 JWT 발신자가 주장하는 사람인지, 그리고 메시지가 변경되지 않았는지 확인하는 데 사용됩니다.
- 인증: 사용자 또는 시스템의 신원을 확인하는 프로세스입니다. JWT를 사용하면 사용자가 자격 증명(예: 사용자 이름 및 비밀번호)을 제공하고 성공적으로 확인되면 서버가 JWT를 발급합니다.
- 권한 부여: 인증된 사용자가 수행할 수 있는 작업을 결정하는 프로세스입니다. 사용자가 유효한 JWT를 제시하면 애플리케이션은 토큰 내의 클레임을 사용하여 특정 리소스 또는 기능에 대한 액세스 권한이 있는지 결정할 수 있습니다.
- 미들웨어: Gin과 같은 웹 프레임워크의 맥락에서 미들웨어는 들어오는 요청과 최종 핸들러 함수 사이에 배치되는 함수입니다. 로깅, 오류 처리, 그리고 우리 주제에 매우 중요한 인증 및 권한 부여와 같은 다양한 작업을 수행할 수 있습니다.
JWT 인증의 원칙
클라이언트가 보호된 Gin API 엔드포인트에 액세스하려고 할 때 워크플로는 일반적으로 다음과 같은 단계를 따릅니다.
- 로그인: 클라이언트는 서버의 인증 엔드포인트에 자격 증명(예: 사용자 이름 및 비밀번호)을 보냅니다.
- 토큰 발급: 자격 증명이 유효하면 서버는 사용자별 클레임과 서명이 포함된 JWT를 생성한 다음 이 JWT를 클라이언트로 다시 보냅니다.
- 후속 요청: 보호된 엔드포인트에 대한 모든 후속 요청에 대해 클라이언트는 일반적으로 "Bearer"로 접두사가 붙은
Authorization
헤더에 JWT를 포함합니다. - 토큰 검증: Gin API의 JWT 미들웨어는 요청을 가로챕니다. JWT를 추출하고, 비밀 키를 사용하여 서명을 검증하고, 클레임(예: 만료 시간)을 유효성을 검사합니다.
- 액세스 세분화: 토큰이 유효하면 미들웨어는 토큰에서 사용자 정보를 추출하여 요청 컨텍스트에 첨부하여 후속 핸들러가 이 정보를 권한 부여 결정에 사용할 수 있도록 할 수 있습니다. 토큰이 유효하지 않거나 누락된 경우 미들웨어는 요청을 거부합니다.
Gin을 사용한 구현
Gin API에 대한 JWT 인증 미들웨어를 구현하는 실제 예제를 살펴보겠습니다. 토큰을 생성하는 방법과 이를 검증하는 미들웨어가 필요합니다.
먼저 JWT 클레임 구조를 정의하겠습니다.
package main import ( "github.com/golang-jwt/jwt/v5" time "time" ) // Claims는 JWT의 클레임 구조를 나타냅니다 type Claims struct { Username string `json:"username"` jwt.RegisteredClaims } // JWT 서명 및 검증을 위한 비밀 키. 실제 애플리케이션에서는 이 키를 안전하게 // (예: 환경 변수) 저장해야 하며 하드코딩해서는 안 됩니다. var jwtSecret = []byte("supersecretkeythatshouldbeprotected") // GenerateToken은 주어진 사용자 이름에 대한 새 JWT를 생성합니다 func GenerateToken(username string) (string, error) { expirationTime := time.Now().Add(24 * time.Hour) // 토큰 유효 시간 24시간 claims := &Claims{ Username: username, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) ttokenString, err := token.SignedString(jwtSecret) if err != nil { return "", err } return tokenString, nil }
이제 Gin 미들웨어를 만들어 보겠습니다.
package main import ( "fmt" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) // AuthRequiredMiddleware는 JWT를 사용하여 요청을 인증하는 Gin 미들웨어입니다 func AuthRequiredMiddleware() gin.HandlerFunc { return func(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"}) c.Abort() return } parts := strings.Split(authHeader, " ") if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"}) c.Abort() return } tokenString := parts[1] claims := &Claims{} token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return jwtSecret, nil }) if err != nil { if err == jwt.ErrSignatureInvalid { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token signature"}) c.Abort() return } c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized: " + err.Error()}) c.Abort() return } if !token.Valid { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) c.Abort() return } // 컨텍스트에 사용자 정보를 저장하여 후속 핸들러에서 사용 c.Set("username", claims.Username) c.Next() // 다음 핸들러로 진행 } }
이제 이것을 Gin 애플리케이션에 통합해 보겠습니다.
package main import ( "net/http" "github.com/gin-gonic/gin" ) func main() { router := gin.Default() // 사용자 로그인(JWT 생성)을 위한 공개 엔드포인트 router.POST("/login", func(c *gin.Context) { var loginRequest struct { Username string `json:"username"` Password string `json:"password"` // 단순화를 위해 비밀번호를 확인하지 않습니다 } if err := c.ShouldBindJSON(&loginRequest); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 실제 애플리케이션에서는 데이터베이스에 대해 사용자 이름과 비밀번호를 확인하게 됩니다. // 이 예제에서는 토큰 생성을 위해 모든 입력을 '유효'하다고 가정합니다. if loginRequest.Username == "" || loginRequest.Password == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Username and password are required"}) return } tokenString, err := GenerateToken(loginRequest.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return } c.JSON(http.StatusOK, gin.H{"token": tokenString}) }) // 보호된 엔드포인트 그룹 protected := router.Group("/api") protected.Use(AuthRequiredMiddleware()) // 인증 미들웨어 적용 { protected.GET("/profile", func(c *gin.Context) { // 미들웨어에서 설정한 대로 컨텍스트에서 사용자 이름에 액세스 username, exists := c.Get("username") if !exists { c.JSON(http.StatusInternalServerError, gin.H{"error": "Username not found in context"}) return } c.JSON(http.StatusOK, gin.H{"message": "Welcome to your profile", "username": username}) }) protected.POST("/data", func(c *gin.Context) { username, _ := c.Get("username") c.JSON(http.StatusOK, gin.H{"message": "Data received successfully by", "user": username}) }) } router.Run(":8080") }
애플리케이션 시나리오
JWT 인증은 다음에서 매우 적합합니다.
- 단일 페이지 애플리케이션(SPA): 클라이언트는 JWT를 저장하고 백엔드에 대한 각 요청과 함께 보냅니다.
- 모바일 애플리케이션: SPA와 유사하게 모바일 클라이언트는 JWT를 저장하고 보낼 수 있습니다.
- 마이크로서비스 아키텍처: JWT는 중앙 집중식 세션 저장소가 필요 없이 인증된 사용자 컨텍스트를 전달하는 서비스 간에 전달될 수 있습니다.
- API 게이트웨이: API 게이트웨이는 백엔드 서비스로 요청을 전달하기 전에 가장자리에서 JWT를 검증할 수 있습니다.
보안 고려 사항
JWT는 훌륭한 보안 이점을 제공하지만 잠재적인 취약점과 모범 사례를 숙지하는 것이 중요합니다.
- 비밀 키 관리:
jwtSecret
은 매우 중요합니다. 강력하고 무작위로 생성된 문자열이어야 하며 기밀로 유지되어야 합니다. 프로덕션에서는 하드코딩하지 말고 환경 변수나 비밀 관리 서비스를 사용하십시오. - 토큰 만료: 항상 JWT(
exp
클레임)에 대한 합리적인 만료 시간을 설정하십시오. 토큰이 손상된 경우 무단 사용 기간을 줄입니다. - 토큰 취소: JWT는 상태 비저장입니다. 만료 전에 취소하는 것은 어려울 수 있습니다. 만료 시간이 짧고 새로 고침 토큰과 함께 사용되는 취소된 토큰 블랙리스트를 유지하는 것과 같은 전략을 사용할 수 있습니다.
- HTTPS/SSL/TLS: 맨인 더 로컬 공격으로 암호화되지 않은 토큰이 가로채지는 것을 방지하기 위해 항상 보안 연결(HTTPS)을 통해 JWT를 전송하십시오.
- 스토리지: 클라이언트 측에서는 JWT를 안전하게 저장하는 것이 중요합니다. HTTP 전용 쿠키는 XSS 공격을 완화하는 데 도움이 될 수 있지만 API 전용 시나리오에는 항상 실용적이지는 않습니다. 로컬 스토리지/세션 스토리지는 주의해서 사용해야 하며 XSS와 같은 보안 취약점은 저장된 토큰을 손상시킬 수 있습니다.
결론
Gin API에 JWT 인증 미들웨어를 구현하면 엔드포인트를 보호하고 사용자 액세스를 관리하는 강력하고 효율적인 방법을 제공합니다. JWT의 핵심 개념을 이해하고, 미들웨어를 세심하게 설계하고, 보안 모범 사례를 준수함으로써 안전하고 확장 가능한 API 서비스를 구축할 수 있습니다. JWT는 상태 비저장, 안전 및 쉽게 배포 가능한 인증 메커니즘을 구축할 수 있게 하여 현대 웹 위협에 맞서 API를 복원력 있게 만듭니다. API를 보호하고 애플리케이션을 강화하십시오.