마이크로서비스 아키텍처에서 인증 시스템은 중요한 요소입니다. 여러 서비스에 걸쳐 일관된 인증 메커니즘이 필요하며, 특히 GraphQL API에서는 특정 작업에 따라 인증 요구사항이 달라질 수 있습니다. 이 글에서는 Go 언어로 구현한 GraphQL API용 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 요청을 처리하는 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))
},
)
}
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 쿼리를 파싱하여 작업 유형을 판별하는 기능입니다. 이를 통해 인증이 필요한 작업과 그렇지 않은 작업을 자동으로 구분합니다.
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(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")
}