Programmers

[22일차]웹 인증과 인가: 개념, 방식, 그리고 구현, next(), .env

PARKpatchnotes 2025. 10. 2. 13:23

인증(Authentication)과 인가(Authorization)

1. 인증(Authentication)과 인가(Authorization)의 개념

인증 (Authentication)

인증은 사용자의 신원을 확인하는 절차이다. 즉, 시스템에 접근하려는 주체가 자신이 주장하는 그 사용자가 맞는지 검증하는 과정이다. 가장 일반적인 예는 아이디와 비밀번호를 입력하여 로그인하는 것이다. 인증이 성공적으로 완료되면, 시스템은 해당 사용자가 누구인지 식별할 수 있다.

인가 (Authorization)

인가는 인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지를 확인하는 절차이다. 인증이 "당신은 누구인가?"를 묻는 과정이라면, 인가는 "당신은 무엇을 할 수 있는가?"를 결정하는 과정이다. 인가는 반드시 인증 이후에 수행되며, 사용자의 역할(Role)이나 등급에 따라 접근할 수 있는 데이터나 실행할 수 있는 기능이 달라진다.


2. 웹 인증 방식의 종류

서버가 클라이언트의 인증 상태를 확인하는 방식은 대표적으로 쿠키(Cookie), 세션(Session), 토큰(Token) 세 가지가 있다.

1) 쿠키(Cookie) 인증 방식

쿠키는 서버가 클라이언트의 브라우저에 저장하는 Key-Value 형식의 작은 데이터 조각이다.

동작 방식

  1. 클라이언트가 서버에 최초로 요청을 보낸다.
  2. 서버는 응답 시 Set-Cookie 헤더에 클라이언트에서 저장할 정보를 담아 전송한다.
  3. 클라이언트는 이후 모든 요청에 Cookie 헤더를 통해 저장된 쿠키를 함께 전송한다.
  4. 서버는 쿠키에 담긴 정보를 바탕으로 사용자를 식별한다.

단점

  • 보안 취약성: 쿠키의 정보는 암호화 없이 그대로 전송되므로, 중간에 탈취되거나 변조될 위험이 크다.
  • 용량 제한: 쿠키에 저장할 수 있는 데이터의 양은 제한적이다.
  • 네트워크 부하: 요청마다 쿠키가 전송되므로 쿠키 크기가 커지면 네트워크에 부하를 줄 수 있다.

2) 세션(Session) 인증 방식

세션 방식은 쿠키의 보안 문제를 해결하기 위해, 사용자의 민감한 정보를 서버 측에서 저장하고 관리하는 방식이다.

동작 방식

  1. 사용자가 로그인하면, 서버는 사용자의 인증 정보를 서버 메모리나 데이터베이스에 저장하고, 이 정보를 식별할 고유한 세션 ID를 생성한다.
  2. 서버는 이 세션 ID를 클라이언트에게 전송하여 쿠키에 저장시킨다.
  3. 클라이언트는 이후 요청마다 세션 ID가 담긴 쿠키를 서버에 전송한다.
  4. 서버는 전달받은 세션 ID를 통해 서버에 저장된 사용자 정보를 찾아 인증을 유지한다.

단점

  • 서버 부하: 사용자 수가 증가하면 서버가 관리해야 할 세션 정보도 늘어나 서버에 부하가 발생한다.
  • 확장성 문제: 여러 서버를 사용하는 분산 환경(로드 밸런싱)에서는 세션 정보를 모든 서버가 공유해야 하는 세션 클러스터링 등의 복잡한 설계가 필요하다.
  • 세션 하이재킹: 세션 ID가 탈취되면 공격자가 해당 사용자로 위장하여 요청을 보낼 수 있다.

3) 토큰(Token) 인증 방식

토큰 기반 인증은 서버가 사용자의 인증 정보를 직접 저장하지 않는 무상태(Stateless) 구조를 기반으로 한다. 인증이 완료된 사용자에게 서버가 토큰(Token)을 발급하고, 사용자는 이후 요청 시 이 토큰을 헤더에 포함하여 자신의 신원을 증명한다.

동작 방식

  1. 사용자가 아이디와 비밀번호로 로그인한다.
  2. 서버는 인증이 완료되면, 사용자의 정보와 권한 등을 담은 암호화된 토큰(주로 JWT)을 생성하여 클라이언트에게 발급한다.
  3. 클라이언트는 발급받은 토큰을 로컬 스토리지나 쿠키 등에 저장한다.
  4. 이후 클라이언트는 서버에 API를 요청할 때마다 HTTP 헤더(주로 Authorization 헤더)에 토큰을 담아 전송한다.
  5. 서버는 토큰의 유효성(서명 검증 등)을 확인하고, 유효한 경우 요청을 처리한다. 서버는 토큰 자체에 포함된 정보로 사용자를 식별하므로 별도의 DB 조회가 필요 없다.

장점

  • 무상태(Stateless) 및 확장성: 서버가 사용자 상태를 저장하지 않으므로, 서버 부하가 줄고 분산 시스템 환경에서도 확장성이 뛰어나다.
  • 보안성: 토큰은 서명(Signature)을 통해 위변조 여부를 검증할 수 있다.
  • 다양한 환경 지원: 웹뿐만 아니라 모바일 애플리케이션 등 세션을 사용하기 어려운 환경에서도 유연하게 적용할 수 있다.

3. next() 함수와 미들웨어

next()는 Express.js와 같은 Node.js 웹 프레임워크의 핵심 개념인 미들웨어(Middleware)에서 사용되는 함수이다. 미들웨어는 요청과 응답 사이클 중간에서 특정 기능을 수행하는 함수이며, next()는 이 미들웨어 체인에서 다음 미들웨어로 제어를 전달하는 역할을 한다.

동작 원리

  • 제어 흐름 전달: 하나의 미들웨어가 자신의 역할을 마친 후, next()를 호출하면 Express는 그 다음으로 등록된 미들웨어에게 요청(req)과 응답(res) 객체를 전달한다. 만약 next()가 호출되지 않으면, 요청-응답 사이클은 해당 미들웨어에서 멈추게 된다.
  • 에러 처리: next() 함수에 인자(주로 에러 객체)를 전달하면, Express는 일반적인 미들웨어 체인을 건너뛰고 곧바로 에러 처리 미들웨어로 제어를 넘긴다. 에러 처리 미들웨어는 (err, req, res, next)와 같이 4개의 인자를 갖는 특별한 형태의 함수이다.

사용 예시

1) 일반 미들웨어 체인

// 1. 요청 로깅 미들웨어
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url} - ${new Date()}`);
  next(); // 다음 미들웨어로 제어 전달
});

// 2. 인증 체크 미들웨어
app.use('/private', (req, res, next) => {
  if (isUserAuthenticated(req)) {
    next(); // 인증 성공, 다음 미들웨어로 전달
  } else {
    res.status(401).send('인증이 필요합니다.'); // 인증 실패, 여기서 응답 종료
  }
});

// 3. 라우트 핸들러
app.get('/private/data', (req, res) => {
  res.json({ data: '비밀 데이터' });
});

2) 에러 처리

app.get('/user/:id', (req, res, next) => {
  const user = findUserById(req.params.id);
  if (!user) {
    const err = new Error('사용자를 찾을 수 없습니다.');
    err.status = 404;
    return next(err); // 에러 처리 미들웨어로 제어 전달
  }
  res.json(user);
});

// 에러 처리 미들웨어 (반드시 다른 미들웨어들보다 뒤에 위치해야 함)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).send(err.message || '서버에서 오류가 발생했습니다.');
});

next() 함수를 통해 Express는 모듈화되고 재사용 가능한 미들웨어를 구성하여, 복잡한 서버 로직을 체계적으로 관리할 수 있게 된다.


4. JWT (JSON Web Token)

JWT는 인증에 필요한 정보들을 암호화시킨 JSON 객체를 사용하여 웹 표준(RFC 7519)으로서 규정된 토큰이다. 토큰 자체에 사용자의 권한 정보나 서비스를 사용하는 데 필요한 정보들을 포함하는 자가 수용적(Self-Contained)인 특징을 가진다.

1) JWT의 구조

JWT는 .을 구분자로 하여 헤더(Header), 페이로드(Payload), 서명(Signature) 세 부분으로 구성된다.

헤더 (Header)

토큰의 유형(typ)과 서명에 사용할 해시 알고리즘(alg) 정보를 담는다.

{
  "alg": "HS256",
  "typ": "JWT"
}

페이로드 (Payload)

토큰에 담을 정보의 조각들인 클레임(Claim)을 포함한다.

  • Registered claims: iss(발행자), exp(만료 시간), sub(주제) 등 미리 정의된 클레임.
  • Public claims: 충돌 방지를 위해 URI 형식으로 이름을 짓는, 공개용 정보를 위한 클레임.
  • Private claims: 서버와 클라이언트 간 협의 하에 사용하는 비공개 정보.
{
  "sub": "user123",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622,
  "role": "admin"
}

주의: 페이로드는 Base64로 인코딩될 뿐, 암호화되는 것이 아니다. 따라서 누구나 디코딩하여 내용을 확인할 수 있으므로, 비밀번호와 같은 민감한 정보는 절대 포함해서는 안 된다.

서명 (Signature)

서명은 토큰의 무결성을 보장하는 가장 중요한 부분이다. 인코딩된 헤더페이로드.으로 연결한 문자열을, 서버만 알고 있는 비밀키(Secret Key)를 사용하여 헤더에 명시된 알고리즘으로 암호화하여 생성한다.
서버는 토큰을 받으면, 동일한 방식으로 서명을 다시 생성하여 전달받은 서명과 일치하는지 검증한다.

2) JWT 인증 절차와 Access/Refresh Token

JWT 토큰 탈취의 위험을 줄이기 위해, 유효기간이 짧은 Access Token과 유효기간이 긴 Refresh Token을 함께 사용한다.

  • Access Token: 실제 API 요청에 사용되는 토큰. 유효기간을 짧게(예: 1시간) 설정한다.
  • Refresh Token: Access Token이 만료되었을 때 새 Access Token을 발급받기 위한 토큰. 유효기간을 길게(예: 2주) 설정하며, 서버 DB에 저장하여 관리한다.

동작 원리

  1. 로그인: 로그인 성공 시, 서버는 Access TokenRefresh Token을 모두 발급한다. Refresh Token은 DB에 저장하고, 두 토큰을 클라이언트에게 전달한다.
  2. API 요청: 클라이언트는 Authorization 헤더에 Access Token을 담아 요청한다.
  3. Access Token 만료: 토큰이 만료되면 서버는 401 Unauthorized 에러를 반환한다.
  4. 토큰 재발급: 클라이언트는 Refresh Token을 서버로 보내 새 Access Token을 요청한다.
  5. Refresh Token 검증: 서버는 DB에 저장된 Refresh Token과 비교하여 유효성을 검증하고, 성공 시 새 Access Token을 발급한다.
  6. 로그아웃: 서버는 DB에서 해당 사용자의 Refresh Token을 삭제하여 더 이상 재발급이 불가능하도록 처리한다.

5. 쿠키와 403 Forbidden 에러 (HTTP vs HTTPS)

403 Forbidden 에러는 서버가 요청을 이해했지만, 권한이 없기 때문에 거절했음을 의미한다. 쿠키 사용 시 이 에러는 주로 SameSiteSecure 속성 설정과 관련하여 발생한다.

SameSite 속성

다른 도메인에서 온 요청(Cross-Site Request)에 쿠키를 전송할지 여부를 결정하는 보안 속성이다.

  • Strict: 동일한 사이트에서 온 요청에만 쿠키를 전송한다.
  • Lax (기본값): 일부 최상위 탐색(예: 링크 클릭)에서는 Cross-Site 요청에도 쿠키를 전송한다.
  • None: 모든 Cross-Site 요청에 쿠키를 전송한다. 단, 이 경우 반드시 Secure 속성을 함께 설정해야 한다.

Secure 속성

이 속성이 설정된 쿠키는 HTTPS 프로토콜을 통해서만 전송된다.

발생 시나리오

서버가 SameSite=None; Secure 속성의 인증 쿠키를 발급했는데, 클라이언트가 HTTP 환경에서 API를 요청하는 경우:

  1. 브라우저는 Secure 속성 때문에 HTTP 요청에 쿠키를 포함시키지 않는다.
  2. 서버는 인증 정보가 없는 요청을 받게 되므로, 사용자를 미인증 상태로 간주한다.
  3. 해당 리소스가 인증된 사용자에게만 허용된 경우, 서버는 "권한 없음"을 의미하는 403 Forbidden 에러를 반환할 수 있다.

따라서 Secure 속성을 사용하는 서비스는 반드시 전체 통신 구간을 HTTPS로 구성해야 한다.


6. .env 환경 변수

.env 파일은 프로젝트의 환경 변수(Environment Variables)를 텍스트 형식으로 관리하는 파일이다. 코드와 설정을 분리하여 보안과 관리 용이성을 높이는 데 사용된다.

사용 목적

  • 보안: 데이터베이스 접속 정보, API 키, JWT 비밀키 등 민감한 정보를 코드로부터 분리하여 저장한다. .gitignore.env 파일을 추가하여 Git 저장소에 민감 정보가 올라가지 않도록 방지하는 것이 필수이다.
  • 설정 관리: 개발, 테스트, 운영 등 각 환경에 따라 달라지는 설정 값을 환경별 .env 파일(.env.development, .env.production 등)을 통해 쉽게 관리할 수 있다.
  • 이식성: 다른 개발자가 프로젝트를 설정할 때, 필요한 환경 변수의 목록을 담은 .env.example 파일을 제공하여 쉽게 환경을 구성할 수 있도록 돕는다.

사용 예시 (Node.js)

dotenv 라이브러리를 사용하여 .env 파일의 변수들을 process.env 객체로 로드할 수 있다.

.env 파일:

DB_HOST=localhost
DB_USER=root
DB_PASS=secret_password
JWT_SECRET=this_is_a_very_secret_key

애플리케이션 코드:

require('dotenv').config(); // .env 파일의 변수들을 process.env로 로드

const dbConfig = {
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS
};

const jwtSecret = process.env.JWT_SECRET;

이처럼 .env를 사용하면 코드의 유연성과 보안성을 크게 향상시킬 수 있다.

 

 

출처: Access Token & Refresh Token 원리 - feat. JWT
출처: JWT 설명 및 인증 구조