Go Gin 미들웨어에서 JWT 인증으로 API 보안 강화하기
Min-jun Kim
Dev Intern · Leapcell

소개
현대 웹 개발 환경에서 API 보안은 매우 중요합니다. 특히 RESTful API는 단일 페이지 애플리케이션, 모바일 앱 및 마이크로서비스 아키텍처의 근간을 이루는 경우가 많습니다. 이 보안의 중요한 측면은 사용자를 효과적이고 효율적으로 인증하고 권한을 부여하는 것입니다. 바로 여기서 JSON Web Tokens(JWT)가 빛을 발합니다. JWT는 두 당사자 간에 전송될 클레임을 나타내는 간결하고 URL에서 안전한 수단을 제공하며, 인증에 대한 상태 비저장 및 확장 가능한 접근 방식을 제공합니다. 이 문서는 널리 사용되는 Go 웹 프레임워크인 Gin을 활용하여 Gin 미들웨어 내에서 직접 JWT 토큰 발급 및 검증을 구현하는 방법을 자세히 알아보고, Go 애플리케이션 보안을 위한 실용적이고 강력한 솔루션을 제공합니다.
JWT 및 Gin 미들웨어 이해
구현에 들어가기 전에 관련 핵심 개념을 명확히 이해해 봅시다:
- JSON Web Tokens (JWT): JWT는 RFC 7519에 따른 개방형 표준으로, 정보를 JSON 객체로 당사자 간에 안전하게 전송하는 간결하고 자체 포함된 방법을 정의합니다. 이 정보는 디지털 서명이 되어 있으므로 신뢰할 수 있습니다. JWT는 일반적으로 세 부분으로 구성됩니다: 헤더, 페이로드, 서명.
- 헤더: 토큰 유형(JWT) 및 서명 알고리즘(예: HMAC SHA256 또는 RSA)을 포함합니다.
- 페이로드: 클레임을 포함합니다. 클레임은 개체(일반적으로 사용자) 및 추가 데이터에 대한 진술입니다. 표준 클레임에는
iss
(발급자),exp
(만료 시간),sub
(주제) 및aud
(대상)가 포함됩니다. 사용자 정의 클레임을 추가할 수도 있습니다. - 서명: 인코딩된 헤더, 인코딩된 페이로드, 비밀 키 및 헤더에 지정된 알고리즘을 사용하여 디지털 서명합니다. 이 서명은 JWT 발신자를 확인하고 메시지가 변조되지 않았는지 확인하는 데 사용됩니다.
- Gin 미들웨어: Gin의 맥락에서 미들웨어는 최종 요청 핸들러 전후에 실행되는 함수 체인입니다. 미들웨어는 로깅, 인증, 권한 부여, 요청 구문 분석 및 오류 처리와 같은 다양한 작업을 수행할 수 있습니다. 이를 통해 모듈식이고 재사용 가능한 코드를 작성할 수 있으며 여러 라우트에 적용되는 기능을 중앙 집중화할 수 있습니다.
Gin 미들웨어 내에 JWT를 구현하면 여러 가지 이점이 있습니다:
- 중앙 집중식 인증: 여러 라우트 핸들러에 걸쳐 반복을 방지하여 인증 로직을 한 곳에서 처리합니다.
- 상태 비저장: JWT는 상태 비저장 인증을 가능하게 하여 서버 측 세션이 필요 없게 하고 확장성을 향상시킵니다.
- 분리된 로직: 인증 및 권한 부여가 비즈니스 로직과 분리되어 코드를 더 깔끔하고 유지 관리하기 쉽게 만듭니다.
Gin 미들웨어에 JWT 구현
구현은 사용자 로그인 성공 시 JWT 발급과 후속 보호된 API 요청에 대한 JWT 검증의 두 가지 주요 단계로 구성됩니다.
1. 프로젝트 설정 및 종속성
먼저 새 Go 모듈을 초기화하고 필요한 종속성을 설치합니다:
go mod init your-module-name go get github.com/gin-gonic/gin go get github.com/golang-jwt/jwt/v5
2. JWT 클레임 정의
jwt.RegisteredClaims
를 임베딩하고 특정 사용자 정보를 추가하는 사용자 정의 Claims
구조체를 만들 것입니다.
package main import "github.com/golang-jwt/jwt/v5" // MyCustomClaims defines the JWT claims structure type MyCustomClaims struct { Username string `json:"username"` jwt.RegisteredClaims }
3. JWT 비밀 키
토큰 서명 및 검증에는 비밀 키가 필요합니다. 프로덕션 환경에서는 강력하고 무작위로 생성된 키를 사용해야 하며, 환경 변수 등으로 안전하게 저장해야 합니다.
// Replace with a strong, securely stored secret in production var jwtSecret = []byte("supersecretkeythatshouldbesecretandlong")
4. JWT 토큰 발급
이 함수는 사용자 로그인 성공 후 호출됩니다. 사용자 이름을 받아 서명된 JWT를 생성합니다.
package main import ( t"time" "github.com/golang-jwt/jwt/v5" ) // GenerateToken generates a new JWT token for a given username func GenerateToken(username string) (string, error) { expirationTime := time.Now().Add(24 * time.Hour) // Token valid for 24 hours claims := &MyCustomClaims{ Username: username, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expirationTime), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), Issuer: "your-app-issuer", Subject: username, ID: "", // Optional: unique identifier for the token Audience: []string{"your-app-audience"}, }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenString, err := token.SignedString(jwtSecret) if err != nil { return "", err } return tokenString, nil }
로그인 라우트에서의 예시 사용법:
package main import ( "net/http" "github.com/gin-gonic/gin" ) // LoginInput defines the expected input for a login request type LoginInput struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } // LoginHandler handles user authentication and token issuance func LoginHandler(c *gin.Context) { var input LoginInput if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // In a real application, you'd verify credentials against a database if input.Username == "user" && input.Password == "password" { // Dummy credentials token, err := GenerateToken(input.Username) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"}) return } c.JSON(http.StatusOK, gin.H{"token": token}) } else { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"}) } }
5. JWT 검증 미들웨어
이것은 인증 시스템의 핵심입니다. 이 Gin 미들웨어는 보호된 라우트에 대한 요청을 가로채 JWT를 추출하고, 서명을 확인하며, 클레임을 구문 분석합니다.
package main import ( "fmt" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" ) // AuthMiddleware is a Gin middleware to verify JWT tokens func AuthMiddleware() 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 } // Expected format: Bearer <token> headerParts := strings.Split(authHeader, " ") if len(headerParts) != 2 || headerParts[0] != "Bearer" { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"}) c.Abort() return } tokenString := headerParts[1] token, err := jwt.ParseWithClaims(tokenString, &MyCustomClaims{}, func(token *jwt.Token) (interface{}, error) { // Validate the signing method if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) } return jwtSecret, nil }) if err != nil { // Handle various JWT errors if ve, ok := err.(*jwt.ValidationError); ok { if ve.Errors&jwt.ValidationErrorMalformed != 0 { c.JSON(http.StatusUnauthorized, gin.H{"error": "That's not even a token"}) c.Abort() return } else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 { c.JSON(http.StatusUnauthorized, gin.H{"error": "Token is either expired or not active yet"}) c.Abort() return } else { c.JSON(http.StatusUnauthorized, gin.H{"error": "Couldn't handle this token"}) c.Abort() return } } c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) c.Abort() return } if claims, ok := token.Claims.(*MyCustomClaims); ok && token.Valid { // Token is valid, store claims in context for downstream handlers c.Set("username", claims.Username) c.Next() // Proceed to the next handler } else { c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"}) c.Abort() } } }
6. Gin 라우트와 미들웨어 통합
마지막으로 Gin 라우터와 AuthMiddleware
를 통합합니다.
package main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.Default() // Public route for login r.POST("/login", LoginHandler) // Protected routes that require JWT authentication protected := r.Group("/api") protected.Use(AuthMiddleware()) // Apply the authentication middleware { protected.GET("/profile", func(c *gin.Context) { // Access username from context after successful authentication username, _ := c.Get("username") c.JSON(http.StatusOK, gin.H{"message": "Welcome to your profile, " + username.(string)}) }) protected.GET("/dashboard", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "This is your dashboard!"}) }) } r.Run(":8080") // Listen and serve on 0.0.0.0:8080 }
애플케이션 시나리오
Gin 미들웨어와 함께하는 이 JWT 인증 패턴은 다음과 같은 경우에 이상적입니다:
- RESTful API: 웹 및 모바일 애플리케이션 엔드포인트를 보호합니다.
- 마이크로서비스: 서비스 간 요청을 인증합니다.
- 단일 페이지 애플리케이션 (SPA): React, Angular 또는 Vue.js와 같은 프론트엔드 프레임워크에 대한 상태 비저장 인증 메커니즘을 제공합니다.
- 게이트웨이: API 게이트웨이의 경우 특정 서비스로 요청을 라우팅하기 전의 초기 인증 계층 역할을 할 수 있습니다.
결론
Gin 미들웨어를 사용하여 JWT 발급 및 검증을 구현하는 것은 Go 애플리케이션에서 사용자를 인증하고 API 엔드포인트를 보호하는 강력하고 확장 가능하며 안전한 방법을 제공합니다. 인증 로직을 중앙 집중화하고 JWT의 자체 포함 특성을 활용함으로써 강력하고 성능이 뛰어난 웹 서비스를 구축할 수 있습니다. 궁극적으로 이 접근 방식은 개발자가 안전하고 효율적인 Go 기반 API 백엔드를 자신 있게 구축할 수 있도록 지원합니다.