GraphQL을 인증하는 JWT 미들웨어를 만들어보자 (golang)

dasd412·2025년 5월 5일
0

golang

목록 보기
5/5

들어가며

마이크로서비스 아키텍처에서 인증 시스템은 중요한 요소입니다. 여러 서비스에 걸쳐 일관된 인증 메커니즘이 필요하며, 특히 GraphQL API에서는 특정 작업에 따라 인증 요구사항이 달라질 수 있습니다. 이 글에서는 Go 언어로 구현한 GraphQL API용 JWT 미들웨어의 핵심 구조와 동작 방식을 살펴보겠습니다.


JWT 미들웨어 구조

미들웨어는 JWT 토큰 검증에 필요한 비밀 키를 관리하는 간단한 구조로 설계되었습니다.

// 인증이 필요 없는 GraphQL 작업 목록
var publicOperations = map[string]bool{
    "signUp":            true,
    "signIn":            true,
    "refreshToken":      true,
    "generateTestToken": true,
    "__schema":          true, // GraphQL 스키마 조회용
}

type JWTMiddleware struct {
    accessSecret  []byte
    refreshSecret []byte
}

func NewJWTMiddleware(config appconfig.Config) *JWTMiddleware {
    return &JWTMiddleware{
        accessSecret:  []byte(config.JwtSecretKey),
        refreshSecret: []byte(config.JwtRefreshSecretKey),
    }
}

HTTP 요청 처리 과정

미들웨어의 핵심 로직은 들어오는 HTTP 요청을 처리하는 Middleware 함수에 있습니다. 이 미들웨어는 요청을 분석하여 공개 작업인지 확인하고, 비공개 작업일 경우에만 JWT 토큰을 검증합니다. 이를 통해 로그인이나 회원가입과 같은 공개 작업에서는 불필요한 인증 검사를 생략할 수 있습니다.

func (m *JWTMiddleware) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(
        func(w http.ResponseWriter, r *http.Request) {
            // 1. 공개 작업 여부 확인
            isPublic, err := isPublicOperation(r)
            if err != nil {
                apperror.SetErrorResponse(w, r.Context(), &apperror.AppError{
                    StatusCode: http.StatusInternalServerError,
                    Message:    err.Error(),
                })
                return
            }

            // 2. 공개 작업이면 인증 검사 건너뛰기
            if isPublic {
                next.ServeHTTP(w, r)
                return
            }

            // 3. JWT 토큰에서 클레임 추출
            claims, err := m.extractClaimsFromHeader(r)
            if err != nil {
                apperror.SetErrorResponse(w, r.Context(), &apperror.AppError{
                    StatusCode: http.StatusUnauthorized,
                    Message:    "invalid token claims",
                })
                return
            }

            // 4. 인증 정보를 컨텍스트에 추가
            ctx, err := WithAuthContext(claims, r)
            if err != nil {
                apperror.SetErrorResponse(w, r.Context(), &apperror.AppError{
                    StatusCode: http.StatusUnauthorized,
                    Message:    "failed to make auth context",
                })
            }

            // 5. 다음 핸들러 호출
            next.ServeHTTP(w, r.WithContext(ctx))
        },
    )
}

JWT 클레임 추출

Authorization 헤더에서 JWT 토큰을 추출하고 검증하는 과정입니다.

func (m *JWTMiddleware) extractClaimsFromHeader(r *http.Request) (jwt.MapClaims, error) {
    // 1. Authorization 헤더 가져오기
    authHeader := r.Header.Get("Authorization")
    if authHeader == "" {
        return nil, errors.New("authorization header missing")
    }

    // 2. Bearer 접두사 제거
    tokenString := strings.TrimPrefix(authHeader, "Bearer ")

    // 3. JWT 토큰 검증
    token, err := ValidateJwt(tokenString, false, m.accessSecret, m.refreshSecret)
    if err != nil {
        return nil, errors.New("invalid token")
    }

    // 4. 클레임 추출
    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok {
        return nil, errors.New("invalid token claims")
    }

    return claims, nil
}

GraphQL 쿼리 분석

이 미들웨어의 특징적인 부분은 GraphQL 쿼리를 파싱하여 작업 유형을 판별하는 기능입니다. 이를 통해 인증이 필요한 작업과 그렇지 않은 작업을 자동으로 구분합니다.

func isPublicOperation(r *http.Request) (bool, error) {
    var query string
    var operationName string

    // 1. POST 요청의 JSON 본문에서 GraphQL 쿼리 추출
    if r.Method == http.MethodPost {
        if r.Header.Get("Content-Type") == "application/json" {
            // 요청 본문 읽기
            bodyBytes, err := io.ReadAll(r.Body)
            if err != nil {
                return false, err
            }

            // 요청 본문 재설정 (다음 핸들러에서 사용할 수 있도록)
            r.Body = io.NopCloser(bytes.NewReader(bodyBytes))

            // JSON 파싱
            var body struct {
                Query         string `json:"query"`
                OperationName string `json:"operationName"`
            }

            if err := json.Unmarshal(bodyBytes, &body); err == nil {
                query = body.Query
                operationName = body.OperationName
            }
        }
    }

    // 2. GraphQL 쿼리에서 필드 이름 추출
    if query != "" {
        fieldName, err := extractGraphQLFieldName(query, operationName)
        if err != nil {
            return false, nil
        }

        // 3. 공개 작업 여부 확인
        return publicOperations[fieldName], nil
    }

    return false, nil
}

GraphQL AST 파싱

GraphQL 쿼리의 AST(Abstract Syntax Tree)를 분석하여 실제 작업 이름을 추출하는 함수입니다.

// GraphQL 쿼리에서 주 필드 이름을 추출합니다.
// 예: "query GetUser { user(id: "9") { ... } }" -> "user"
func extractGraphQLFieldName(query string, operationName string) (string, error) {
    // 1. GraphQL 쿼리 파싱
    doc, err := parser.Parse(parser.ParseParams{
        Source: query,
    })
    if err != nil {
        return "", err
    }

    // 2. 작업 정의와 필드 이름 추출
    for _, definition := range doc.Definitions {
        if operation, ok := definition.(*ast.OperationDefinition); ok {
            if operation.Name != nil && operation.Name.Value == operationName {
                if len(operation.SelectionSet.Selections) > 0 {
                    if field, ok := operation.SelectionSet.Selections[0].(*ast.Field); ok {
                        return field.Name.Value, nil
                    }
                }
            }
        }
    }

    return "", errors.New("field name not found")
}

profile
시스템 아키텍쳐 설계에 관심이 많은 백엔드 개발자입니다. (Go/Python/MSA/graphql/Spring)

0개의 댓글