인증 구조(Authentication Architecture) #세션, 토큰, 세션 하이재킹, RBAC

웹 서비스와 게임 서버는 사용자의 신원을 확인하고, 해당 사용자가 어떤 리소스에 접근할 수 있는지 판단해야 한다. 이때 필요한 핵심 개념이 인증(Authentication)과 인가(Authorization)다.

인증 구조를 설계할 때는 단순히 로그인 기능을 구현하는 것만으로는 부족하다. 로그인 이후 사용자의 신원을 어떻게 유지할 것인지, 인증 정보를 어떻게 보호할 것인지, 다른 사용자의 데이터에 접근하지 못하도록 어떻게 통제할 것인지까지 고려해야 한다.

이 글에서는 다음 개념을 중심으로 인증 구조를 살펴본다.

  • 인증(Authentication)과 인가(Authorization)
  • 세션 기반 인증과 토큰 기반 인증
  • 세션 ID와 세션 하이재킹(Session Hijacking)
  • RBAC(Role-Based Access Control)

인증(Authentication)과 인가(Authorization)

인증과 인가는 서로 다른 목적을 가진 보안 절차다.

인증은 사용자가 누구인지 확인하는 과정이고, 인가는 인증된 사용자가 특정 작업을 수행할 권한이 있는지 확인하는 과정이다.

예를 들어 게임 서버에서 사용자가 로그인에 성공했다면 인증이 완료된 것이다. 하지만 로그인에 성공했다고 해서 다른 사용자의 인벤토리를 조회하거나 운영자 전용 API를 호출할 수 있는 것은 아니다.

인증(Authentication)

인증은 사용자가 주장하는 신원이 실제로 유효한지 검증하는 과정이다.

일반적으로 다음과 같은 방식이 사용된다.

  • 아이디와 비밀번호
  • 세션 쿠키
  • 액세스 토큰(JWT 등)
  • 일회용 인증 코드(OTP)
  • 패스키(Passkey)

아이디와 비밀번호를 사용하는 경우 서버는 사용자가 제출한 비밀번호를 저장된 비밀번호 해시와 비교한다.

비밀번호는 평문으로 저장하지 않고 Argon2id, bcrypt 등의 비밀번호 해싱 알고리즘을 사용해 저장해야 한다. 비밀번호 검증에 성공하면 서버는 세션을 생성하거나 토큰을 발급할 수 있다.

클라이언트
    |
    | 아이디 + 비밀번호
    v
인증 서버
    |
    | 사용자 조회
    | 비밀번호 해시 검증
    v
인증 성공
    |
    | 세션 생성 또는 토큰 발급
    v
인증 정보 전달

인증은 로그인 시점에만 필요한 것이 아니다. 이후 요청에서도 세션이나 토큰을 검증해 요청자가 누구인지 확인해야 한다.

인가(Authorization)

인가는 인증된 사용자가 요청한 작업을 수행할 수 있는지 판단하는 과정이다.

예를 들어 다음과 같은 권한을 생각할 수 있다.

사용자요청결과
일반 사용자자신의 프로필 조회허용
일반 사용자다른 사용자의 비공개 프로필 조회거부
일반 사용자운영자 전용 계정 정지 API 호출거부
운영자계정 정지 API 호출권한 정책에 따라 허용

여기서 중요한 점은 인증에 성공했다는 사실이 모든 작업의 허가를 의미하지 않는다는 것이다.

서버는 인증된 사용자의 식별자와 요청 대상의 소유 관계, 역할, 세부 권한 등을 확인해야 한다.

예를 들어 다음과 같은 API가 있다고 가정한다.

GET /users/1001/inventory

로그인한 사용자의 ID가 1001이라면 자신의 인벤토리를 조회할 수 있다. 하지만 로그인한 사용자의 ID가 2002인데 URL의 사용자 ID가 1001이라면, 별도의 권한이 없는 한 접근을 거부해야 한다.

클라이언트가 전달한 사용자 ID만 믿고 데이터를 반환하면 다른 사용자의 데이터에 접근하는 IDOR(Insecure Direct Object Reference) 취약점이 발생할 수 있다.

따라서 인증과 인가는 다음과 같이 분리해 설계해야 한다.

  • 인증: 요청을 보낸 사용자는 누구인가?
  • 인가: 이 사용자는 해당 작업을 수행할 수 있는가?

세션 기반 인증(Session-Based Authentication)

세션 기반 인증은 로그인한 사용자의 인증 상태를 서버가 세션으로 관리하는 방식이다.

서버는 로그인에 성공한 사용자에 대한 세션을 생성하고, 클라이언트에는 해당 세션을 식별할 수 있는 세션 ID를 전달한다.

클라이언트는 이후 요청에 세션 ID를 포함하고, 서버는 이를 사용해 저장된 세션 정보를 조회한다.

동작 원리

1. 클라이언트가 로그인 요청
        |
        v
2. 서버가 사용자 인증
        |
        v
3. 서버가 세션 생성
   sessionId -> userId
        |
        v
4. 클라이언트에 세션 ID 전달
        |
        v
5. 이후 요청에 세션 ID 포함
        |
        v
6. 서버가 세션 조회 및 검증
        |
        v
7. 사용자 식별 후 인가 검사

세션 저장소에는 다음과 같은 정보가 저장될 수 있다.

interface SessionData {
  userId: string;
  role: "user" | "admin";
  createdAt: number;
  expiresAt: number;
}

실제 운영 환경에서는 세션 ID를 키로 사용하고, 위와 같은 데이터를 서버 측 저장소에 보관할 수 있다. 저장소로는 Redis나 데이터베이스 등이 사용된다.

예를 들어 Redis에 다음과 같이 저장할 수 있다.

Key:
session:random-session-id

Value:
{
  "userId": "1001",
  "role": "user"
}

TTL:
1800 seconds

TTL(Time To Live)을 설정하면 지정된 시간이 지난 세션이 자동으로 만료되도록 구성할 수 있다.

세션 기반 인증의 장점

서버에서 인증 상태를 직접 통제할 수 있다.

세션을 삭제하거나 만료시키면 해당 세션 ID를 이용한 후속 요청을 차단할 수 있다. 로그아웃, 강제 로그아웃, 계정 정지 등의 처리를 비교적 명확하게 구현할 수 있다.

또한 클라이언트가 사용자 ID나 권한을 임의로 변경하더라도 서버에 저장된 세션 정보를 기준으로 인증 상태를 판단할 수 있다.

세션 기반 인증의 단점

세션 데이터를 서버 측에서 관리해야 한다.

서버가 여러 대로 확장되면 모든 서버가 동일한 세션 상태를 조회할 수 있어야 한다. 서버별 메모리에만 세션을 저장하면 사용자가 다른 서버로 요청을 보낼 때 세션을 찾지 못할 수 있다.

이를 해결하기 위해 Redis와 같은 중앙 세션 저장소를 사용하거나, 별도의 세션 공유 구조를 구축할 수 있다.

Client
   |
   v
Load Balancer
   |
   +-------- Server A --------+
   |                          |
   +-------- Server B --------+---- Redis
   |                          |
   +-------- Server C --------+

이 구조에서는 어떤 서버가 요청을 처리하더라도 Redis를 통해 동일한 세션 정보를 조회할 수 있다.

단, Redis를 사용한다고 해서 모든 확장성 문제가 자동으로 해결되는 것은 아니다. 연결 수, 저장소 가용성, 네트워크 지연, 만료 정책 등을 함께 고려해야 한다.


토큰 기반 인증(Token-Based Authentication)

토큰 기반 인증은 서버가 발급한 토큰을 클라이언트가 보관하고, 이후 요청에 토큰을 포함하는 방식이다.

대표적인 토큰 형식으로 JWT(JSON Web Token)가 있다.

토큰에는 사용자 식별자, 만료 시간, 발급자 등의 클레임이 포함될 수 있다. 서명된 JWT는 서버가 토큰의 무결성과 발급자를 검증하는 데 사용할 수 있다.

JWT의 구조

JWT는 일반적으로 다음 세 부분으로 구성된다.

Header.Payload.Signature
  • Header: 토큰 유형과 서명 알고리즘 등의 정보
  • Payload: 사용자 식별자, 만료 시간 등의 클레임
  • Signature: 토큰의 무결성을 검증하기 위한 서명

예를 들어 Payload는 다음과 같은 형태일 수 있다.

{
  "sub": "1001",
  "role": "user",
  "iss": "https://auth.example.com",
  "aud": "game-api",
  "iat": 1792843200,
  "exp": 1792844100
}

각 필드의 의미는 다음과 같다.

필드의미
sub토큰의 주체, 일반적으로 사용자 식별자
role역할 정보
iss토큰 발급자
aud토큰을 사용할 대상으로 지정된 서비스
iat토큰 발급 시각
exp토큰 만료 시각

JWT의 Payload는 일반적으로 Base64URL 인코딩된 데이터이지 암호화된 데이터가 아니다. 따라서 비밀번호, 인증 비밀값, 민감한 개인정보 등을 그대로 넣어서는 안 된다.

토큰 기반 인증의 동작 원리

1. 클라이언트가 로그인 요청
        |
        v
2. 서버가 사용자 인증
        |
        v
3. 서버가 토큰 발급
        |
        v
4. 클라이언트가 토큰 보관
        |
        v
5. 요청 헤더에 토큰 포함
        |
        v
6. 서버가 서명과 만료 시간 검증
        |
        v
7. 사용자 식별 및 인가 검사

일반적인 HTTP API에서는 다음과 같이 요청할 수 있다.

GET /users/me
Authorization: Bearer <access-token>

서버는 토큰의 서명, 만료 시간, 발급자, 대상 서비스 등을 정책에 따라 검증한다. 이후 토큰에서 검증된 사용자 식별자를 얻고 요청에 필요한 권한을 확인한다.

세션 기반 인증과 토큰 기반 인증 비교

비교 항목세션 기반토큰 기반
인증 상태서버 측 세션 저장소토큰 및 검증 정책
요청 시 처리세션 조회 및 검증토큰 검증
강제 로그아웃세션 삭제로 처리 가능별도의 폐기 정책 필요
서버 확장세션 공유 구조 필요검증 구조에 따라 상태 공유 부담 감소
토큰 탈취 대응세션 폐기 가능만료, 폐기 목록, 토큰 회전 등 필요
주요 고려 사항저장소와 세션 관리토큰 수명, 키 관리, 폐기 전략

토큰 기반 인증이 반드시 세션 기반 인증보다 빠르거나 안전한 것은 아니다.

JWT를 사용하더라도 폐기 목록, 사용자 상태 확인, 권한 변경 반영 등을 위해 서버 측 저장소를 조회할 수 있다. 반대로 세션 기반 인증도 Redis 등을 활용해 충분히 확장할 수 있다.

핵심은 특정 기술을 선택하는 것보다 인증 상태의 수명, 폐기 가능성, 운영 복잡성, 보안 요구사항에 맞는 구조를 설계하는 것이다.


세션 ID(Session ID)와 세션 하이재킹(Session Hijacking)

세션 ID는 서버가 특정 세션을 식별하기 위해 사용하는 값이다.

클라이언트는 일반적으로 쿠키에 세션 ID를 보관하고, 브라우저는 이후 요청에 해당 쿠키를 자동으로 포함한다.

예를 들어 다음과 같은 응답을 사용할 수 있다.

Set-Cookie: __Host-session=<opaque-random-value>; Path=/; Secure; HttpOnly; SameSite=Lax

각 옵션은 다음과 같은 목적을 가진다.

옵션목적
SecureHTTPS 연결에서만 쿠키 전송
HttpOnlyJavaScript에서 쿠키에 직접 접근하는 것을 제한
SameSite=Lax일부 교차 사이트 요청에서 쿠키 전송을 제한
Path=/사이트의 전체 경로에서 쿠키 사용
__Host- 접두사지원 브라우저에서 Secure, Path=/ 및 Domain 미지정 조건을 강제

HttpOnly는 JavaScript를 통한 쿠키 탈취 위험을 줄이는 데 도움이 되지만, XSS 자체를 방지하지는 않는다. 공격자가 페이지 내에서 악성 스크립트를 실행할 수 있다면 쿠키를 직접 읽지 못하더라도 사용자의 권한으로 요청을 수행할 수 있다.

또한 SameSite는 CSRF 위험을 줄이는 방어 수단이지만, 모든 CSRF 공격을 자동으로 방지한다고 볼 수는 없다.

세션 하이재킹이란?

세션 하이재킹은 공격자가 다른 사용자의 유효한 세션 ID를 탈취해 해당 사용자로 위장하는 공격이다.

세션 ID가 유효하고 서버가 이를 정상적인 인증 정보로 받아들인다면, 공격자는 사용자의 비밀번호를 몰라도 해당 세션의 권한을 악용할 수 있다.

즉, 세션 ID는 단순한 식별자가 아니다. 유효한 세션 ID 자체가 인증 수단으로 작동하기 때문에 비밀 정보처럼 보호해야 한다.

주요 공격 경로

네트워크 도청

HTTPS를 사용하지 않으면 네트워크 구간에서 세션 쿠키가 노출될 위험이 커진다.

대응 방법은 HTTPS 적용, 안전한 TLS 설정, HTTP에서 HTTPS로의 강제 전환이다.

XSS(Cross-Site Scripting)

공격자가 웹 페이지에서 악성 JavaScript를 실행하면 세션 정보를 노리거나 사용자의 권한으로 요청을 수행할 수 있다.

대응 방법은 출력 인코딩, 안전한 DOM API 사용, 입력 처리, CSP(Content Security Policy) 적용 등이다. HttpOnly 쿠키도 중요한 방어 수단이지만 XSS에 대한 완전한 해결책은 아니다.

세션 고정(Session Fixation)

공격자가 특정 세션 ID를 사용자가 로그인한 뒤에도 유지하도록 유도하는 공격이다.

로그인 이전에 공격자가 알고 있던 세션 ID가 로그인 이후에도 유효하면 공격자가 인증된 세션을 악용할 수 있다.

대응 방법은 로그인 성공 및 권한 상승 시 세션 ID를 재발급하는 것이다.

악성 코드 및 브라우저 확장 프로그램

사용자 기기에서 실행되는 악성 코드나 권한이 과도한 확장 프로그램이 인증 정보를 탈취할 수 있다.

서버 측 방어만으로 모든 클라이언트 환경의 위험을 제거할 수는 없으므로 기기 보안과 최소 권한 원칙도 고려해야 한다.

세션 하이재킹 방어 전략

충분한 무작위성을 가진 세션 ID 생성

세션 ID는 예측할 수 없어야 한다. 암호학적으로 안전한 난수 생성기를 사용하고 충분한 엔트로피를 확보해야 한다.

직접 구현하기보다 검증된 세션 관리 라이브러리를 사용하는 편이 안전하다.

로그인 성공 시 세션 ID 재발급

로그인 이전의 세션 ID가 인증 이후에도 유지되지 않도록 한다. 이를 세션 ID 회전(Session Rotation)이라고 한다.

세션 만료 정책 적용

유휴 시간이 길어지거나 절대 유효 시간이 지나면 세션을 만료시킨다. 민감한 작업에는 재인증을 요구할 수도 있다.

로그아웃 시 서버 측 세션 무효화

클라이언트에서 쿠키만 삭제해서는 충분하지 않을 수 있다. 서버 저장소에서도 해당 세션을 무효화해야 한다.

민감한 요청에 대한 추가 검증

비밀번호 변경, 결제 수단 변경, 계정 삭제 등 중요한 작업은 단순히 유효한 세션이 있다는 이유만으로 허용하지 않고 재인증이나 추가 인증을 요구할 수 있다.


RBAC(Role-Based Access Control)

RBAC는 역할(Role)을 중심으로 사용자에게 권한을 부여하는 접근 제어 모델이다.

사용자마다 개별 권한을 직접 부여하는 대신 역할에 권한을 연결하고, 사용자에게 역할을 할당한다.

예를 들어 게임 서비스에서는 다음과 같은 역할을 정의할 수 있다.

  • user: 일반 사용자
  • moderator: 신고 및 채팅 관리 담당자
  • admin: 운영 관리 담당자

권한은 다음과 같이 정의할 수 있다.

  • profile:read: 프로필 조회
  • profile:update: 프로필 수정
  • report:manage: 신고 관리
  • user:ban: 사용자 정지

역할과 권한을 분리하면 새로운 역할을 추가하거나 권한 정책을 변경하기 쉽다.

RBAC의 기본 구조

User
  |
  | 역할 할당
  v
Role
  |
  | 권한 연결
  v
Permission
  |
  v
API 또는 리소스 접근 허용 여부

예를 들어 다음과 같이 설계할 수 있다.

역할프로필 조회신고 관리사용자 정지
user허용거부거부
moderator허용허용정책에 따라 제한
admin허용허용허용

실제 서비스에서는 역할별 권한을 명시적으로 정의해야 한다. moderator가 사용자를 정지할 수 있는지와 같은 정책을 암묵적으로 처리하면 안 된다.

역할과 권한의 분리

사용자에게 admin이라는 문자열이 있다는 이유만으로 모든 작업을 허용하는 구조는 간단하지만, 권한이 세분화될수록 관리하기 어렵다.

역할과 권한을 별도로 관리하면 다음과 같은 구조를 사용할 수 있다.

type Role = "user" | "moderator" | "admin";

type Permission =
  | "profile:read"
  | "profile:update"
  | "report:manage"
  | "user:ban";

const rolePermissions: Record<Role, Permission[]> = {
  user: [
    "profile:read",
    "profile:update",
  ],

  moderator: [
    "profile:read",
    "report:manage",
  ],

  admin: [
    "profile:read",
    "profile:update",
    "report:manage",
    "user:ban",
  ],
};

이 예제는 역할과 권한의 관계를 설명하기 위한 기본 구조다. 실제 운영 환경에서는 권한을 코드에 고정하는 대신 데이터베이스나 중앙 정책 설정으로 관리할 수도 있다.

Express에서 RBAC 적용하기

다음은 인증 미들웨어가 검증된 사용자 정보를 req.user에 설정했다고 가정한 권한 검사 예제다.

import type {
  Request,
  Response,
  NextFunction,
} from "express";

type Role = "user" | "moderator" | "admin";

type Permission =
  | "profile:read"
  | "profile:update"
  | "report:manage"
  | "user:ban";

interface AuthenticatedUser {
  id: string;
  role: Role;
}

declare global {
  namespace Express {
    interface Request {
      user?: AuthenticatedUser;
    }
  }
}

const rolePermissions: Record<Role, Permission[]> = {
  user: [
    "profile:read",
    "profile:update",
  ],

  moderator: [
    "profile:read",
    "report:manage",
  ],

  admin: [
    "profile:read",
    "profile:update",
    "report:manage",
    "user:ban",
  ],
};

function requirePermission(permission: Permission) {
  return (
    req: Request,
    res: Response,
    next: NextFunction,
  ) => {
    const user = req.user;

    if (!user) {
      return res.status(401).json({
        code: "UNAUTHENTICATED",
        message: "인증이 필요합니다.",
      });
    }

    const permissions = rolePermissions[user.role];

    if (!permissions?.includes(permission)) {
      return res.status(403).json({
        code: "FORBIDDEN",
        message: "해당 작업을 수행할 권한이 없습니다.",
      });
    }

    next();
  };
}

라우트에는 다음과 같이 적용할 수 있다.

app.get(
  "/admin/reports",
  authenticate,
  requirePermission("report:manage"),
  getReports,
);

app.post(
  "/admin/users/:userId/ban",
  authenticate,
  requirePermission("user:ban"),
  banUser,
);

여기서 authenticate는 토큰이나 세션을 검증하고 req.user를 설정하는 별도의 미들웨어다. 이 예제는 해당 미들웨어의 구현을 생략했다.

HTTP 상태 코드도 구분해야 한다.

  • 401 Unauthorized: 인증되지 않았거나 유효한 인증 정보가 없는 경우
  • 403 Forbidden: 인증은 되었지만 해당 작업을 수행할 권한이 없는 경우

RBAC 미들웨어는 역할에 연결된 권한을 검사한다. 하지만 이것만으로 모든 인가 문제가 해결되지는 않는다.

예를 들어 일반 사용자가 자신의 프로필을 수정하는 작업은 허용하더라도, 다른 사용자의 프로필을 수정하려는 요청은 별도의 소유권 검사가 필요하다.

즉, 역할 기반 권한 검사와 리소스 단위 권한 검사를 함께 적용해야 한다.


세션, 토큰, RBAC를 함께 사용하는 인증 구조

실제 서비스에서는 인증과 인가를 각각 분리된 단계로 처리한다.

Client
  |
  | 요청 + 세션 쿠키 또는 액세스 토큰
  v
Authentication Middleware
  |
  | 세션 또는 토큰 검증
  | 사용자 식별
  v
Authenticated User
  |
  v
Authorization Middleware
  |
  | 역할 및 권한 확인
  v
Resource Ownership Check
  |
  | 대상 리소스에 대한 접근 권한 확인
  v
Application Service
  |
  v
Database / Redis

이 구조에서 각 계층의 책임은 다음과 같다.

Authentication Middleware

인증 정보를 검증하고 요청의 사용자를 식별한다.

Authorization Middleware

사용자의 역할과 권한을 확인한다.

Resource Ownership Check

사용자가 요청한 리소스에 접근할 수 있는지 확인한다.

Application Service

검증된 요청을 바탕으로 실제 비즈니스 로직을 수행한다.

이러한 책임 분리는 인증 로직과 비즈니스 로직이 뒤섞이는 것을 방지하고, 권한 정책을 일관되게 적용하는 데 도움이 된다.


게임 서버에서 고려할 사항

게임 서버는 일반 웹 서비스와 마찬가지로 인증과 인가를 적용하지만, 실시간 연결과 게임 상태까지 고려해야 한다.

연결 인증과 게임 권한은 별개다

WebSocket이나 Colyseus 기반 서버에서 연결이 인증되었다고 해서 모든 Room이나 메시지에 접근할 수 있는 것은 아니다.

서버는 연결 시 사용자를 인증하고, Room 입장 시 해당 Room에 대한 접근 권한을 확인하며, 메시지를 처리할 때도 필요한 권한과 상태를 검증해야 한다.

클라이언트가 전달하는 사용자 ID를 신뢰하지 않는다

클라이언트가 다음과 같은 메시지를 보낸다고 가정한다.

{
  "userId": "1001",
  "itemId": "sword-01",
  "quantity": 1
}

서버가 메시지의 userId만 믿고 아이템을 지급하면 공격자가 다른 사용자 ID를 입력해 타인의 계정에 영향을 줄 수 있다.

서버는 인증된 연결에 연결된 사용자 식별자를 기준으로 요청자를 판단해야 한다. 또한 아이템 소유권, 수량, 거래 가능 여부 등 게임 규칙을 서버 측에서 검증해야 한다.

토큰 만료와 재접속

실시간 연결은 일반 HTTP 요청보다 오래 유지될 수 있다. 따라서 연결을 맺을 때만 토큰을 검증하고 이후에는 아무런 확인도 하지 않는 구조는 권한 변경이나 계정 정지에 대응하기 어려울 수 있다.

서비스의 보안 요구사항에 따라 토큰 만료 시 연결을 종료하거나 재인증을 요구하고, 계정 정지 또는 세션 폐기 시 활성 연결에도 해당 상태를 반영해야 한다.

인증과 게임 상태 변경의 분리

인증에 성공했다는 것은 사용자의 신원이 확인되었다는 뜻이지, 게임 내 모든 행동이 유효하다는 뜻이 아니다.

예를 들어 사용자가 인증되었더라도 다음 조건은 별도로 검증해야 한다.

  • 충분한 게임 재화를 보유하고 있는가?
  • 해당 아이템을 실제로 소유하고 있는가?
  • 현재 게임 상태에서 해당 행동이 가능한가?
  • 동일한 거래 요청이 중복 처리되지 않았는가?

인증은 요청자의 신원을 보장하고, 인가는 접근 가능 범위를 제한한다. 게임 규칙 검증은 요청한 행동이 유효한지 판단한다. 이 세 가지를 분리해야 서버 권한 기반의 안전한 게임 로직을 구축할 수 있다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다