마스터하기: 인증(Authentication)과 인가(Authorization) - 안전하고 신뢰할 수 있는 시스템의 핵심 설계

안녕하세요, 10년 경력의 소프트웨어 엔지니어이자 기술 교육자입니다. 오늘은 모든 개발자가 반드시 이해해야 할 보안의 핵심 개념인 '인증(Authentication)'과 '인가(Authorization)'에 대해 깊이 있게 다뤄보려 합니다. 이 두 가지 개념은 서비스의 보안과 신뢰성을 결정하는 가장 기본적인 요소이며, 현대 웹/앱 개발에서 JWT, OAuth 2.0, OpenID Connect와 같은 기술을 이해하는 데 필수적인 배경 지식이 됩니다. 면접 단골 질문이자 실무에서 매일 마주하는 이 중요한 주제를 함께 마스터해 봅시다.
1. 개념 소개: 정의, 탄생 배경, 왜 중요한지

1.1. 정의: '누구인지'와 '무엇을 할 수 있는지'
- 인증 (Authentication): "당신은 누구입니까?" 에 답하는 과정입니다. 사용자가 본인이 주장하는 신원(identity)이 맞는지 검증하는 절차입니다. 예를 들어, 아이디와 비밀번호를 입력하거나, 지문 인식을 통해 로그인하는 과정이 인증에 해당합니다.
- 인가 (Authorization): "당신은 무엇을 할 수 있습니까?" 에 답하는 과정입니다. 인증된 사용자가 특정 리소스(데이터, 기능 등)에 접근하거나 특정 작업을 수행할 수 있는 권한이 있는지 확인하는 절차입니다. 예를 들어, 일반 사용자는 게시글을 읽을 수 있지만, 관리자만 게시글을 삭제할 수 있는 것이 인가에 해당합니다.
이 둘은 마치 공항의 출입국 심사와 같습니다. 출입국 심사관이 여권을 보고 '이 사람이 누구인지' 확인하는 것이 인증이고, 그 사람이 비자 종류에 따라 '이 나라에 입국할 수 있는지, 얼마나 머무를 수 있는지'를 결정하는 것이 인가라고 비유할 수 있습니다.
1.2. 탄생 배경
초기 웹 시스템은 대부분 서버에 사용자 세션 정보를 저장하는 방식으로 인증과 인가를 처리했습니다. 하지만 웹/모바일 클라이언트의 다양화, 마이크로서비스 아키텍처의 확산, 그리고 서드파티 애플리케이션(예: 구글 로그인, 카카오톡 로그인)이 사용자 데이터에 접근해야 하는 필요성이 증가하면서 기존 방식만으로는 한계가 드러나기 시작했습니다.
- 세션 기반의 한계: 서버 확장이 어렵고(세션 동기화 필요), 모바일 앱이나 API 클라이언트에는 적합하지 않으며, CSRF(사이트 간 요청 위조)와 같은 보안 취약점에 노출될 위험이 있습니다.
- 분산 환경의 요구: 여러 서버와 서비스에 걸쳐 사용자 신원을 확인하고 권한을 부여하는 유연하고 확장 가능한 메커니즘이 필요해졌습니다. 이러한 배경 속에서 JWT(JSON Web Token)와 OAuth 2.0, OpenID Connect와 같은 현대적인 토큰 기반 인증/인가 표준들이 등장하게 되었습니다.
1.3. 왜 중요한가?
인증과 인가는 단순한 기능 구현을 넘어 시스템의 근간을 이루는 중요한 요소입니다.
- 보안 강화: 무단 접근과 데이터 유출을 방지하여 사용자의 정보와 서비스의 무결성을 보호합니다.
- 신뢰성 확보: 사용자에게 안전하고 예측 가능한 서비스 경험을 제공하여 서비스에 대한 신뢰를 구축합니다.
- 규제 준수: 개인정보 보호법, GDPR 등 각종 법규 및 보안 표준 준수에 필수적입니다.
- 확장성 및 유연성: 다양한 클라이언트(웹, 모바일, IoT)와 외부 서비스(소셜 로그인, API 연동)를 안전하고 효율적으로 통합할 수 있게 합니다.
2. 핵심 원리 설명

2.1. 토큰 기반 인증: JWT (JSON Web Token)
JWT는 웹 표준(RFC 7519)으로, 클라이언트와 서버 간에 정보를 안전하게 주고받기 위한 경량의 토큰입니다. 서버에 사용자 세션 상태를 저장하지 않는 Stateless 특징을 가지며, 분산 시스템에서 매우 유용합니다.
JWT의 구조: JWT는 .으로 구분된 세 부분으로 구성됩니다.
Header.Payload.Signature
- Header (헤더): 토큰의 타입(JWT)과 서명에 사용된 알고리즘(예: HS256, RS256)을 포함합니다.
{ "alg": "HS256", "typ": "JWT" } - Payload (페이로드): 실제 전달하려는 정보(클레임, Claims)를 담습니다. 사용자 ID, 권한, 토큰 만료 시간 등 필요한 정보를 자유롭게 포함할 수 있습니다. 하지만 민감한 정보는 담지 않아야 합니다. JWT는 암호화(Encryption)가 아니라 서명(Signature)으로 위변조를 방지하는 것이 주 목적이기 때문에, 디코딩하면 누구나 내용을 볼 수 있습니다.
{ "sub": "1234567890", "name": "John Doe", "admin": true, "exp": 1678886400 // 만료 시간 (Unix Time) } - Signature (서명): 인코딩된 헤더와 페이로드를 서버의 **비밀 키(Secret Key)**로 서명한 값입니다. 이 서명을 통해 토큰이 위변조되지 않았음을 검증합니다. 비밀 키를 아는 서버만이 유효한 토큰을 생성할 수 있으며, 이 키가 외부에 노출되면 안 됩니다.
JWT 인증 흐름:
- 사용자 로그인: 클라이언트가 사용자 ID와 비밀번호를 인증 서버에 전송합니다.
- JWT 발급: 인증 서버는 사용자 정보를 확인한 후, 고유한 비밀 키로 서명된 JWT를 생성하여 클라이언트에 응답합니다.
- 토큰 저장: 클라이언트는 받은 JWT를 로컬 저장소(localStorage, sessionStorage, cookie 등)에 저장합니다.
- 리소스 요청: 클라이언트는 보호된 리소스(API)를 요청할 때마다 HTTP 헤더의
Authorization필드에 JWT를 포함하여 전송합니다. (예:Authorization: Bearer <JWT>) - 토큰 검증: 리소스 서버는 받은 JWT의 서명을 비밀 키로 검증하여 위변조 여부와 만료 시간을 확인합니다.
- 인가 처리: JWT의 페이로드에 담긴 사용자 정보(예: 역할)를 바탕으로 해당 리소스에 접근 권한이 있는지 인가를 처리하고, 결과를 클라이언트에 반환합니다.
다이어그램: JWT 인증 흐름
sequenceDiagram
participant C as 클라이언트
participant AS as 인증 서버
participant RS as 리소스 서버
C->>AS: 1. 로그인 요청 (ID/PW)
AS->>AS: 2. 사용자 정보 확인
AS-->>C: 3. JWT 발급 (Access Token)
C->>C: 4. JWT 저장
C->>RS: 5. 리소스 요청 (JWT 포함)
RS->>RS: 6. JWT 서명 및 만료 시간 검증
RS->>RS: 7. 인가 처리 (JWT 페이로드 기반)
RS-->>C: 8. 리소스 응답
2.2. 권한 위임 표준: OAuth 2.0
OAuth 2.0은 사용자의 비밀번호를 직접 공유하지 않고, 서드파티 애플리케이션이 특정 리소스에 접근할 수 있는 권한을 안전하게 위임하는 표준 프로토콜입니다. '소셜 로그인'이나 '다른 앱으로 로그인' 기능을 구현할 때 주로 사용됩니다.
주요 역할:
- Resource Owner (자원 소유자): 보호된 리소스에 접근 권한을 가진 사용자 (예: 페이스북 사용자).
- Client (클라이언트): 자원 소유자를 대신하여 리소스 서버에 접근하려는 서드파티 애플리케이션 (예: 인스타그램).
- Authorization Server (인증 서버): 자원 소유자를 인증하고, 클라이언트에게 Access Token을 발급하는 서버 (예: 페이스북 로그인 서버).
- Resource Server (리소스 서버): 보호된 리소스를 호스팅하는 서버 (예: 페이스북 프로필 API).
OAuth 2.0 Authorization Code Flow (가장 일반적이고 안전한 흐름):
- 권한 요청: 클라이언트(앱)가 자원 소유자(사용자)에게 특정 리소스(예: 프로필 정보)에 대한 접근 권한을 요청합니다.
- 사용자 동의: 클라이언트는 자원 소유자를 인증 서버의 로그인/동의 페이지로 리다이렉트합니다. 자원 소유자는 로그인 후 클라이언트에게 권한을 부여할지 동의합니다.
- 인가 코드 발급: 자원 소유자가 동의하면, 인증 서버는 클라이언트에게 '인가 코드(Authorization Code)'를 발급하여 클라이언트의 미리 등록된
redirect_uri로 리다이렉트합니다. - Access Token 요청: 클라이언트는 받은 인가 코드를 인증 서버에 직접 전송하여 Access Token을 요청합니다. (이때 클라이언트 ID와 Secret도 함께 전송하여 클라이언트 자신을 인증합니다.)
- Access Token 발급: 인증 서버는 인가 코드와 클라이언트 정보를 검증한 후, Access Token(그리고 Refresh Token, ID Token 등)을 클라이언트에게 발급합니다.
- 리소스 접근: 클라이언트는 Access Token을 사용하여 리소스 서버에 보호된 리소스를 요청합니다.
- 리소스 제공: 리소스 서버는 Access Token을 검증한 후, 요청된 리소스를 클라이언트에 제공합니다.
다이어그램: OAuth 2.0 Authorization Code Flow
graph TD
subgraph Resource Owner (사용자)
A[1. 앱 사용] --> B(2. 권한 요청 동의)
end
subgraph Client (서드파티 앱)
C[1. 앱에서 로그인 버튼 클릭] --> D{인증 서버로 리다이렉트}
E[3. 인가 코드 수신] --> F(4. Access Token 요청)
G[5. Access Token 수신] --> H(6. 리소스 요청)
I[7. 리소스 수신]
end
subgraph Authorization Server (인증 서버)
J[2. 로그인 및 동의 페이지 제공] --> K(3. 인가 코드 발급)
L[4. 인가 코드 및 클라이언트 인증] --> M(5. Access Token 발급)
end
subgraph Resource Server (리소스 서버)
N[6. Access Token 검증] --> O(7. 리소스 제공)
end
C --> J
J --> B
B --> K
K --> E
F --> L
L --> G
H --> N
N --> I
2.3. 인증 레이어: OpenID Connect (OIDC)
OpenID Connect (OIDC)는 OAuth 2.0 프레임워크 위에 구축된 인증(Authentication) 레이어입니다. OAuth 2.0이 '권한 위임'에 초점을 맞춘 반면, OIDC는 '사용자 신원 확인'에 초점을 맞춥니다.
OIDC는 ID Token이라는 JWT 형태의 토큰을 추가로 발급하여 사용자의 신원 정보를 클라이언트에게 안전하게 전달합니다. 이 ID Token은 누가 로그인했는지, 언제 로그인했는지 등 기본적인 사용자 프로필 정보를 담고 있어, 클라이언트가 사용자를 인증하고 식별하는 데 사용됩니다.
간단히 말해, OAuth 2.0은 "이 앱이 내 사진 앨범에 접근할 수 있게 해줘"라는 권한을 위임하는 것이고, OpenID Connect는 여기에 "내가 누구인지 이 앱에 알려줘"라는 인증 기능을 추가한 것입니다.
3. 코드 예제 2개
3.1. Python (Flask): JWT 생성 및 검증 예제
PyJWT 라이브러리를 사용하여 JWT를 생성하고, Flask 애플리케이션에서 보호된 라우트에 대한 JWT 검증 미들웨어를 구현하는 예제입니다.
# app.py
from flask import Flask, request, jsonify
import jwt
import datetime
from functools import wraps
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_super_secret_key_that_should_be_strong_and_kept_secret' # 실제 환경에서는 환경 변수 등으로 관리
# JWT 검증 데코레이터
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = None
# Authorization 헤더에서 토큰 추출 (Bearer <token>)
if 'Authorization' in request.headers:
token = request.headers['Authorization'].split(" ")[1]
if not token:
return jsonify({'message': 'Token is missing!'}), 401
try:
# 토큰 디코딩 및 검증
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=["HS256"])
# 여기서는 decoded_user_info를 request 객체에 저장하여 다음 함수에서 사용 가능하게 함
request.decoded_user_info = data
except jwt.ExpiredSignatureError:
return jsonify({'message': 'Token has expired!'}), 401
except jwt.InvalidTokenError:
return jsonify({'message': 'Token is invalid!'}), 401
return f(*args, **kwargs)
return decorated
@app.route('/login', methods=['POST'])
def login():
auth = request.json
if not auth or not auth.get('username') or not auth.get('password'):
return jsonify({'message': 'Missing username or password!'}), 400
# 실제 환경에서는 데이터베이스에서 사용자 검증
if auth.get('username') == 'testuser' and auth.get('password') == 'password123':
# 페이로드 생성
payload = {
'user_id': 'testuser_id_123',
'username': auth.get('username'),
'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30) # 30분 후 만료
}
# JWT 생성
token = jwt.encode(payload, app.config['SECRET_KEY'], algorithm="HS256")
return jsonify({'token': token})
return jsonify({'message': 'Invalid credentials!'}), 401
@app.route('/protected', methods=['GET'])
@token_required
def protected_route():
# token_required 데코레이터를 통해 검증된 사용자 정보 사용
user_info = request.decoded_user_info
return jsonify({
'message': 'This is a protected route!',
'user': user_info.get('username'),
'user_id': user_info.get('user_id')
})
if __name__ == '__main__':
app.run(debug=True)
사용법:
pip install Flask PyJWTpython app.py실행- 로그인 요청:
POST /loginBody:{"username": "testuser", "password": "password123"}응답:{"token": "eyJ..."} - 보호된 리소스 요청:
GET /protectedHeaders:Authorization: Bearer <로그인에서 받은 토큰>
3.2. JavaScript (Node.js/Express): JWT 미들웨어 예제
jsonwebtoken 라이브러리를 사용하여 JWT를 생성하고, Express 애플리케이션에서 보호된 라우트에 대한 JWT 검증 미들웨어를 구현하는 예제입니다.
// server.js
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const port = 3000;
app.use(express.json()); // JSON 요청 본문 파싱
const SECRET_KEY = 'your_super_secret_key_that_should_be_strong_and_kept_secret'; // 실제 환경에서는 환경 변수 등으로 관리
// JWT 검증 미들웨어
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer <token>
if (token == null) {
return res.sendStatus(401); // 토큰 없음
}
jwt.verify(token, SECRET_KEY, (err, user) => {
if (err) {
return res.sendStatus(403); // 유효하지 않거나 만료된 토큰
}
req.user = user; // 검증된 사용자 정보를 request 객체에 저장
next(); // 다음 미들웨어 또는 라우트 핸들러로 이동
});
};
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ message: 'Missing username or password!' });
}
// 실제 환경에서는 데이터베이스에서 사용자 검증
if (username === 'testuser' && password === 'password123') {
const user = {
id: 'testuser_id_123',
name: username
};
// JWT 생성 (30분 후 만료)
const accessToken = jwt.sign(user, SECRET_KEY, { expiresIn: '30m' });
return res.json({ accessToken: accessToken });
}
res.status(401).json({ message: 'Invalid credentials!' });
});
app.get('/protected', authenticateToken, (req, res) => {
// authenticateToken 미들웨어를 통해 검증된 사용자 정보 사용
res.json({
message: 'This is a protected route!',
user: req.user.name,
user_id: req.user.id
});
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
사용법:
npm init -ynpm install express jsonwebtokennode server.js실행- 로그인 요청:
POST /loginBody:{"username": "testuser", "password": "password123"}응답:{"accessToken": "eyJ..."} - 보호된 리소스 요청:
GET /protectedHeaders:Authorization: Bearer <로그인에서 받은 토큰>
4. 실무 적용 사례
- 마이크로서비스 아키텍처: 여러 서비스가 분산되어 있는 환경에서 JWT는 각 서비스가 사용자 인증 상태를 공유하지 않고 독립적으로 토큰을 검증할 수 있게 하여 확장성을 높입니다.
- 서드파티 로그인/싱글 사인온 (SSO): Google, Facebook, Apple 등으로 로그인하는 기능은 모두 OAuth 2.0과 OpenID Connect 표준을 기반으로 합니다. 사용자는 각 서비스에 직접 비밀번호를 제공하지 않고도 안전하게 로그인하고, 관련 정보를 공유할 수 있습니다. 기업 내부 시스템에서는 OpenID Connect를 활용하여 SSO를 구현하기도 합니다.
- API 보안: 모바일 앱이나 SPA(Single Page Application)가 백엔드 API에 접근할 때, JWT를 사용하여 클라이언트의 신원을 인증하고 특정 API에 대한 접근 권한을 제어합니다.
- 권한 기반 기능 제어: 특정 사용자 역할(예: 관리자, 일반 사용자, 게스트)에 따라 웹 페이지의 특정 버튼을 활성화하거나 비활성화하고, API 응답에서 특정 데이터를 포함하거나 제외하는 등의 인가 로직을 구현합니다.
5. 자주 하는 실수와 해결법
- JWT 시크릿 키(Secret Key) 노출: JWT 서명에 사용되는 비밀 키가 노출되면 공격자가 위조된 토큰을 생성하여 시스템에 침입할 수 있습니다.
- 해결법: 비밀 키는 환경 변수, AWS Secrets Manager, HashiCorp Vault 등 안전한 키 관리 시스템에 보관하고, 코드에 하드코딩하지 않습니다. 주기적으로 키를 변경하는 것도 좋습니다.
- JWT 만료 시간 너무 길게 설정: 만료 시간이 너무 길면 토큰이 탈취되었을 때 공격자가 시스템에 접근할 수 있는 시간이 길어집니다.
- 해결법: Access Token의 만료 시간은 짧게(예: 15~30분) 설정하고, Refresh Token을 사용하여 Access Token이 만료될 때마다 새로운 Access Token을 발급받는 방식을 사용합니다. Refresh Token은 더 긴 만료 시간을 가질 수 있으며, 더 엄격한 보안(예: 한 번만 사용 가능, 특정 IP에서만 사용 가능)을 적용할 수 있습니다.
- JWT 페이로드에 민감 정보 포함: JWT는 서명되어 위변조를 막을 수는 있지만, 디코딩하면 누구나 페이로드 내용을 볼 수 있습니다. 주민등록번호, 신용카드 정보 등 민감한 정보를 페이로드에 직접 담아서는 안 됩니다.
- 해결법: 페이로드에는 사용자 ID나 역할 등 식별 및 인가에 필요한 최소한의 정보만 담습니다. 민감한 정보는 별도의 암호화된 채널을 통해 전달하거나, 서버에서 직접 조회하도록 설계합니다.
- 인가 로직 누락 또는 불충분: 인증(로그인)은 성공했지만, 특정 리소스에 대한 접근 권한(인가)을 제대로 확인하지 않아 권한 없는 사용자가 중요한 데이터에 접근하거나 기능을 실행할 수 있습니다. (예: 다른 사용자의 프로필 정보를 수정하거나, 관리자 기능에 접근)
- 해결법: 모든 보호된 API 엔드포인트와 중요한 기능에 대해 사용자 역할, 소유권,
