backend

Go 인증/인가 구현하기: JWT 기반 인증 미들웨어 설계

mirabo01 2026. 2. 3. 22:30

Gin으로 API 서버를 옮겼다면,
다음으로 거의 반드시 등장하는 요구사항이 인증(Authentication)과 인가(Authorization) 다.

이번 챕터에서는
세션 기반이 아니라 JWT(Json Web Token) 를 사용하는 방식으로

  • 인증 흐름이 어떻게 구성되는지
  • Gin 미들웨어로 어떻게 분리하는지
  • 실무에서 자주 실수하는 포인트는 무엇인지

를 차분하게 정리한다.

“완벽한 보안 구현”보다는
API 서버에서 반복적으로 쓰이는 기본 설계를 이해하는 게 목표다.


인증과 인가는 구분해서 생각해야 한다

먼저 용어부터 정리하는 게 좋다.

  • 인증(Authentication): 너 누구냐
  • 인가(Authorization): 이 작업 해도 되냐

JWT는 주로

  • 인증 결과를 토큰으로 전달하고
  • 이후 요청에서 인가 판단의 근거로 사용

하는 데 쓰인다.


왜 JWT를 사용하는가

JWT가 많이 쓰이는 이유는 비교적 명확하다.

  • 서버가 세션 상태를 들고 있지 않아도 된다
  • API 서버 / 모바일 / 프론트엔드 분리 구조에 적합
  • 토큰 하나로 인증 정보 전달 가능

특히 Go + API 서버 조합에서는
상태 없는(stateless) 구조와 잘 맞는다.


JWT 인증의 전체 흐름

JWT 기반 인증 흐름은 보통 다음과 같다.

  1. 사용자 로그인 (ID / 비밀번호)
  2. 서버에서 JWT 생성
  3. 클라이언트가 토큰 저장
  4. 이후 요청마다 토큰 전송
  5. 서버에서 토큰 검증 후 요청 처리

[이미지: JWT 인증 전체 흐름]

이 흐름을 코드 구조로 옮기는 게 이번 챕터의 핵심이다.


토큰을 어디에 담아 보낼까

API 서버에서는 보통
Authorization 헤더를 사용한다.

Authorization: Bearer <JWT_TOKEN>

이 방식의 장점은

  • HTTP 표준에 가깝고
  • 프록시, 미들웨어와 잘 어울리며
  • 쿠키보다 의도가 명확하다

그래서 이 글에서도
Authorization 헤더 기준으로 설명한다.


JWT에 담을 정보는 최소한으로

JWT payload에는 여러 정보를 담을 수 있지만,
실무 기준으로는 다음 정도가 적당하다.

  • 사용자 ID
  • 토큰 만료 시간(exp)
  • 발급자(iss)
{
  "user_id": 42,
  "exp": 1700000000
}

⚠️ 주의할 점

  • 민감 정보(비밀번호, 권한 전체 목록)는 넣지 않는다
  • JWT는 암호화가 아니라 서명이다
  • 토큰 내용은 누구나 디코딩할 수 있다

JWT 생성 위치는 어디가 좋을까

JWT 생성은 보통 다음 위치 중 하나다.

  • 인증용 service
  • 인증 전용 패키지

중요한 기준은 이것이다.

handler에서 직접 JWT를 만들지 않는다

handler → service → token 생성

이 흐름을 지키면
테스트와 유지보수가 훨씬 쉬워진다.


로그인 API 예시 흐름

POST /login
  1. handler에서 요청 파싱
  2. service에서 사용자 검증
  3. JWT 생성
  4. 토큰 응답
type LoginResponse struct {
    Token string `json:"token"`
}

이 API는
JWT를 발급하는 유일한 진입점이 된다.


Gin 인증 미들웨어 기본 형태

이제 핵심인 인증 미들웨어를 보자.

func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        auth := c.GetHeader("Authorization")
        if auth == "" {
            c.AbortWithStatus(401)
            return
        }

        // 토큰 파싱 및 검증
        userID, err := parseToken(auth)
        if err != nil {
            c.AbortWithStatus(401)
            return
        }

        c.Set("userID", userID)
        c.Next()
    }
}

이 코드의 역할은 명확하다.

  • 토큰이 없으면 차단
  • 토큰이 유효하지 않으면 차단
  • 유효하면 사용자 정보 저장 후 다음으로 진행

[이미지: Gin 인증 미들웨어 동작 흐름]


인증 정보는 context에 담는다

미들웨어에서 인증이 끝났다면,
그 결과를 handler로 전달해야 한다.

Gin에서는 Context를 사용한다.

c.Set("userID", userID)

handler에서는 이렇게 꺼낸다.

userID := c.GetInt("userID")

⚠️ 주의할 점

  • service 계층으로 *gin.Context를 넘기지 않는다
  • handler에서 필요한 값만 추출해서 전달한다

이 기준이 무너지면
프레임워크 의존이 빠르게 퍼진다.


인증이 필요한 API에만 적용하기

Gin에서는 미들웨어 적용 범위를 쉽게 제어할 수 있다.

api := r.Group("/api")
api.Use(AuthMiddleware())
{
    api.GET("/users/me", handler.Me)
}
  • 로그인, 회원가입 API → 미들웨어 미적용
  • 보호된 API → 미들웨어 적용

이 구조가 명확하면
“이 API는 인증이 필요한가?”를 한눈에 파악할 수 있다.


인가(Authorization)는 어디서 처리할까

인가 판단은 보통 다음 기준으로 나뉜다.

  • 단순 권한 체크 → handler
  • 비즈니스 규칙 기반 → service

예를 들어

  • “본인 정보만 조회 가능”
  • “관리자만 삭제 가능”

같은 규칙은
service 계층에서 판단하는 게 자연스럽다.

JWT는
인가 판단을 위한 힌트를 제공할 뿐,
모든 결정을 대신해주지는 않는다.


JWT 사용 시 자주 나오는 실수

⚠️ 실무에서 정말 자주 보는 실수들

  • 토큰 만료(exp) 체크 누락
  • HTTPS 없이 토큰 전달
  • 로그에 토큰 전체 출력
  • JWT를 세션처럼 사용하는 설계

JWT는 편리하지만,
한 번 유출되면 만료 전까지는 무효화가 어렵다는 특성이 있다.
그래서 만료 시간 설정은 사실상 필수다.


이 구조의 장점

이 챕터에서 만든 구조의 장점은 다음과 같다.

  • 인증 로직이 미들웨어에 집중됨
  • handler / service 구조 유지
  • 테스트 시 인증 미들웨어만 분리 가능

즉,
보안 로직이 코드 전체에 흩어지지 않는다.


정리

JWT 기반 인증의 핵심은
토큰 자체보다 구조와 책임 분리다.

  • 로그인 API에서만 토큰 발급
  • 인증은 미들웨어에서 처리
  • 인가는 service 중심으로 판단
  • Gin Context는 handler까지만 사용

이 구조를 잡아두면,
이후에

  • 권한(Role) 추가
  • Refresh Token 도입
  • OAuth 연동

같은 확장도 훨씬 수월해진다.

다음 챕터로는

  • PostgreSQL 연동 + migration 설계
  • API 에러 응답 규약 통일하기
  • 실무용 로그 / 에러 / 트레이싱 정리

중 하나로 이어가면 흐름이 좋다.