Authorizer 오픈소스 인증 시스템 완전 가이드 - 120초 만에 프로덕션 준비 완료
⏱️ 예상 읽기 시간: 23분
서론: 왜 Authorizer인가?
웹 애플리케이션을 개발할 때 가장 복잡하면서도 중요한 부분 중 하나가 바로 인증(Authentication)과 권한 관리(Authorization)입니다. Authorizer는 이러한 문제를 해결하기 위한 완전 오픈소스 솔루션으로, 락인 없이 안전하고 확장 가능한 인증 시스템을 제공합니다.
Authorizer의 핵심 특징
완전한 자유도:
- 🔓 락인 없음: 언제든지 자유롭게 마이그레이션 가능
- 🌐 오픈소스: MIT 라이선스로 완전히 무료 사용
- 🏠 자체 호스팅: 데이터 완전 통제 및 프라이버시 보장
- 🔧 커스터마이징: 소스코드 수정을 통한 무제한 확장
즉시 사용 가능한 기능들:
- 🚀 120초 배포: Railway에서 원클릭 배포
- 🎨 내장 UI: 로그인/회원가입 페이지 기본 제공
- 👑 관리자 패널: 웹 기반 관리 대시보드 내장
- 🔌 GraphQL API: 현대적인 API 설계
고급 인증 기능들:
- 🔐 다중 인증 방식: 이메일/패스워드, 소셜 로그인, 매직 링크
- 📱 2FA 지원: TOTP, SMS OTP 지원
- 🏢 역할 기반 접근 제어: 세밀한 권한 관리
- 🌍 다중 제공자: Google, GitHub, Facebook 등 소셜 로그인
이 가이드에서 배울 내용
- Authorizer 핵심 개념 이해
- Railway 원클릭 배포 (120초)
- Docker를 이용한 로컬/프로덕션 배포
- 관리자 패널 설정 및 환경 구성
- 소셜 로그인 연동 (Google, GitHub 등)
- 2FA 및 매직 링크 설정
- 다양한 프레임워크와의 통합
- 역할 기반 접근 제어 (RBAC) 구현
- 프로덕션 최적화 및 보안 강화
Authorizer 핵심 개념 및 아키텍처
전체 아키텍처 구조
Authorizer 시스템 구성:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 클라이언트 │ │ Authorizer │ │ 데이터베이스 │
│ 앱/웹사이트 │◄──►│ 서버 │◄──►│ PostgreSQL/ │
│ │ │ │ │ MySQL/MongoDB │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ ┌─────────────────┐ │
└──────────────►│ Redis │◄─────────────┘
│ (세션 저장) │
└─────────────────┘
주요 구성 요소:
- Authorizer 서버: GraphQL API 서버 (Go 언어)
- 관리자 대시보드: React 기반 웹 인터페이스
- 내장 로그인 페이지: 기본 제공되는 사용자 인터페이스
- 데이터베이스: PostgreSQL, MySQL, MongoDB, SQLite 지원
- Redis: 세션 및 캐시 저장 (선택적)
지원하는 인증 방식
기본 인증:
- 이메일/패스워드: 전통적인 회원가입/로그인
- 매직 링크: 패스워드 없는 이메일 링크 인증
- OTP: SMS 또는 이메일 기반 일회용 패스워드
소셜 로그인:
- 🔍 Google OAuth2
- 🐙 GitHub OAuth
- 📘 Facebook Login
- 🐦 Twitter OAuth
- 💼 LinkedIn OAuth
- 🍎 Apple Sign In
- 🎮 Discord OAuth
고급 보안:
- 🔐 2FA (Two-Factor Authentication): TOTP, SMS
- 🔄 리프레시 토큰: 자동 토큰 갱신
- 🛡️ 브루트포스 보호: 로그인 시도 제한
- ⏰ 세션 관리: 자동 만료 및 다중 디바이스 관리
Railway 원클릭 배포 (120초 완성)
가장 빠르고 쉬운 방법은 Railway를 이용한 원클릭 배포입니다.
Railway 배포 단계
1단계: Railway 계정 준비
# Railway CLI 설치 (선택적)
npm install -g @railway/cli
# Railway 로그인
railway login
2단계: 원클릭 배포
🚀 Railway에서 배포하기 링크를 클릭하여 즉시 시작
배포 과정:
- Repository 설정: 자동으로 Authorizer 템플릿 로드
- 환경 변수 구성: 기본 설정이 자동으로 적용
- 데이터베이스 생성: PostgreSQL 인스턴스 자동 생성
- 배포 실행: 약 2분 내 완료
3단계: 배포 완료 확인
# 배포 상태 확인
railway status
# 로그 확인
railway logs
# 도메인 확인
railway domain
배포 후 설정:
🌐 URL: https://your-app-name.railway.app
📊 관리자 패널: https://your-app-name.railway.app
🔑 초기 설정: 첫 접속 시 관리자 계정 생성
Docker를 이용한 로컬 및 프로덕션 배포
로컬 개발 환경 설정
docker-compose.yml 파일 생성:
version: '3.8'
services:
authorizer:
image: lakhansamani/authorizer:1.4.4
container_name: authorizer
ports:
- "8080:8080"
environment:
# 기본 설정
PORT: 8080
ADMIN_SECRET: "your-super-secret-admin-key"
COOKIE_NAME: "authorizer"
# 데이터베이스 설정
DATABASE_TYPE: "postgres"
DATABASE_URL: "postgres://authorizer:password@postgres:5432/authorizer"
# Redis 설정 (선택적)
REDIS_URL: "redis://redis:6379"
# JWT 설정
JWT_SECRET: "your-jwt-secret-key"
JWT_ROLE_CLAIM: "role"
# 기타 설정
DISABLE_EMAIL_VERIFICATION: false
DISABLE_MAGIC_LINK_LOGIN: false
DISABLE_SIGN_UP: false
depends_on:
- postgres
- redis
networks:
- authorizer-network
postgres:
image: postgres:15
container_name: authorizer-postgres
environment:
POSTGRES_DB: authorizer
POSTGRES_USER: authorizer
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- authorizer-network
redis:
image: redis:7-alpine
container_name: authorizer-redis
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- authorizer-network
volumes:
postgres_data:
redis_data:
networks:
authorizer-network:
driver: bridge
환경 변수 파일 (.env) 생성:
# 관리자 설정
ADMIN_SECRET=your-super-secret-admin-key-change-this
# 데이터베이스 설정
DATABASE_TYPE=postgres
DATABASE_URL=postgres://authorizer:password@postgres:5432/authorizer
# Redis 설정
REDIS_URL=redis://redis:6379
# JWT 설정
JWT_SECRET=your-jwt-secret-key-minimum-32-characters
JWT_ROLE_CLAIM=role
# 서버 설정
PORT=8080
COOKIE_NAME=authorizer
COOKIE_DOMAIN=localhost
# 이메일 설정 (SMTP)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=your-app-password
# 인증 설정
DISABLE_EMAIL_VERIFICATION=false
DISABLE_MAGIC_LINK_LOGIN=false
DISABLE_SIGN_UP=false
DISABLE_STRONG_PASSWORD=false
# 소셜 로그인 설정 (나중에 설정)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
컨테이너 실행 및 관리
개발 환경 시작:
# 모든 서비스 시작
docker-compose up -d
# 로그 확인
docker-compose logs -f authorizer
# 서비스 상태 확인
docker-compose ps
# 접속 확인
curl http://localhost:8080/health
데이터베이스 초기화 스크립트 (init.sql):
-- 데이터베이스 확장 설치
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 인덱스 최적화
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at);
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
-- 기본 역할 생성 (나중에 관리자 패널에서도 가능)
INSERT INTO roles (id, role, created_at, updated_at)
VALUES
(uuid_generate_v4(), 'admin', NOW(), NOW()),
(uuid_generate_v4(), 'user', NOW(), NOW()),
(uuid_generate_v4(), 'moderator', NOW(), NOW())
ON CONFLICT (role) DO NOTHING;
프로덕션 환경 설정
Nginx 리버스 프록시 설정:
# /etc/nginx/sites-available/authorizer
upstream authorizer {
server 127.0.0.1:8080;
}
server {
listen 80;
server_name auth.yourdomain.com;
# SSL 리다이렉트
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name auth.yourdomain.com;
# SSL 인증서 설정
ssl_certificate /etc/letsencrypt/live/auth.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.yourdomain.com/privkey.pem;
# SSL 보안 설정
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
# 보안 헤더
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
# Authorizer 프록시
location / {
proxy_pass http://authorizer;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket 지원
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 타임아웃 설정
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 헬스체크 엔드포인트
location /health {
proxy_pass http://authorizer/health;
access_log off;
}
}
관리자 패널 설정 및 초기 구성
첫 접속 및 관리자 계정 생성
관리자 계정 설정:
- 브라우저에서 접속:
http://localhost:8080
또는 배포된 도메인 - 관리자 회원가입: 첫 사용자가 자동으로 관리자 권한 획득
- 보안 패스워드: 최소 8자리, 대소문자, 숫자, 특수문자 포함
# 관리자 계정 정보
이메일: admin@yourdomain.com
패스워드: SecureAdminPass123!
역할: admin (자동 할당)
기본 환경 변수 설정
관리자 패널에서 설정 가능한 주요 옵션들:
일반 설정:
{
"APP_NAME": "My App",
"APP_URL": "https://yourapp.com",
"ADMIN_SECRET": "your-admin-secret",
"CUSTOM_ACCESS_TOKEN_SCRIPT": "",
"ACCESS_TOKEN_EXPIRY_TIME": "30m",
"REFRESH_TOKEN_EXPIRY_TIME": "7d"
}
인증 설정:
{
"DISABLE_EMAIL_VERIFICATION": false,
"DISABLE_BASIC_AUTHENTICATION": false,
"DISABLE_MAGIC_LINK_LOGIN": false,
"DISABLE_LOGIN_PAGE": false,
"DISABLE_SIGN_UP": false,
"DISABLE_STRONG_PASSWORD": false,
"ENFORCE_MULTI_FACTOR_AUTHENTICATION": false
}
이메일 설정:
{
"SMTP_HOST": "smtp.gmail.com",
"SMTP_PORT": "587",
"SMTP_USERNAME": "your-email@gmail.com",
"SMTP_PASSWORD": "your-app-password",
"SMTP_LOCAL_NAME": "localhost",
"SENDER_EMAIL": "noreply@yourapp.com",
"SENDER_NAME": "Your App Name"
}
사용자 관리 및 역할 설정
역할 생성 및 관리:
// 관리자 패널에서 역할 생성
const roles = [
{
role: "admin",
description: "Full access to all features",
permissions: ["*"]
},
{
role: "moderator",
description: "Can manage users and content",
permissions: ["user:read", "user:update", "content:*"]
},
{
role: "user",
description: "Standard user access",
permissions: ["profile:read", "profile:update"]
},
{
role: "premium_user",
description: "Premium features access",
permissions: ["profile:*", "premium:read"]
}
];
사용자 일괄 관리:
# CSV 파일을 통한 사용자 일괄 등록
# users.csv 파일 형식:
email,given_name,family_name,role
john@example.com,John,Doe,user
jane@example.com,Jane,Smith,moderator
admin@example.com,Admin,User,admin
소셜 로그인 연동 설정
Google OAuth 설정
Google Cloud Console 설정:
- Google Cloud Console 접속: https://console.cloud.google.com
- 새 프로젝트 생성 또는 기존 프로젝트 선택
- API 및 서비스 > OAuth 동의 화면 설정
OAuth 클라이언트 생성:
# Google Cloud Console에서 설정
OAuth 클라이언트 ID 생성:
- 애플리케이션 유형: 웹 애플리케이션
- 이름: "Your App Authorizer"
- 승인된 자바스크립트 원본:
- http://localhost:8080 (개발용)
- https://auth.yourdomain.com (프로덕션)
- 승인된 리디렉션 URI:
- http://localhost:8080/oauth/google/callback
- https://auth.yourdomain.com/oauth/google/callback
Authorizer 환경 변수 설정:
# Google OAuth 설정
GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your-google-client-secret
GitHub OAuth 설정
GitHub OAuth App 생성:
- GitHub Settings 접속: Settings > Developer settings > OAuth Apps
- New OAuth App 클릭
- 애플리케이션 정보 입력:
Application name: Your App Authorizer
Homepage URL: https://yourapp.com
Authorization callback URL: https://auth.yourdomain.com/oauth/github/callback
환경 변수 설정:
# GitHub OAuth 설정
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
다중 소셜 로그인 설정
모든 소셜 제공자 설정:
# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# GitHub OAuth
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
# Facebook OAuth
FACEBOOK_CLIENT_ID=your-facebook-app-id
FACEBOOK_CLIENT_SECRET=your-facebook-app-secret
# Twitter OAuth
TWITTER_CLIENT_ID=your-twitter-client-id
TWITTER_CLIENT_SECRET=your-twitter-client-secret
# LinkedIn OAuth
LINKEDIN_CLIENT_ID=your-linkedin-client-id
LINKEDIN_CLIENT_SECRET=your-linkedin-client-secret
# Apple Sign In
APPLE_CLIENT_ID=your-apple-client-id
APPLE_CLIENT_SECRET=your-apple-client-secret
2FA 및 매직 링크 고급 설정
TOTP 2FA 설정
2FA 활성화 환경 변수:
# 2FA 설정
ENFORCE_MULTI_FACTOR_AUTHENTICATION=true
TOTP_ISSUER=YourAppName
TOTP_WINDOW=1
사용자별 2FA 설정:
// GraphQL을 통한 2FA 관리
mutation EnableMFA($email: String!) {
enable_mfa(params: { email: $email }) {
message
should_show_qr_code
qr_code_data
backup_codes
}
}
mutation VerifyMFA($email: String!, $otp: String!) {
verify_mfa(params: { email: $email, otp: $otp }) {
message
user {
id
email
is_multi_factor_auth_enabled
}
}
}
SMS OTP 설정
Twilio SMS 설정:
# Twilio SMS 설정
SMS_PROVIDER=twilio
TWILIO_ACCOUNT_SID=your-twilio-account-sid
TWILIO_AUTH_TOKEN=your-twilio-auth-token
TWILIO_PHONE_NUMBER=+1234567890
SMS OTP 사용 플로우:
// SMS OTP 요청
mutation SendOTP($phone_number: String!) {
send_otp(params: { phone_number: $phone_number }) {
message
}
}
// SMS OTP 확인
mutation VerifyOTP($phone_number: String!, $otp: String!) {
verify_otp(params: { phone_number: $phone_number, otp: $otp }) {
message
access_token
refresh_token
user {
id
phone_number
}
}
}
매직 링크 커스터마이징
매직 링크 템플릿 설정:
# 매직 링크 설정
DISABLE_MAGIC_LINK_LOGIN=false
MAGIC_LINK_TOKEN_EXPIRY_TIME=30m
CUSTOM_MAGIC_LINK_URL=https://yourapp.com/auth/magic-link
커스텀 이메일 템플릿:
<!-- 매직 링크 이메일 템플릿 -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Magic Link Login</title>
<style>
.container { max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif; }
.button {
background: #007bff;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 4px;
display: inline-block;
}
</style>
</head>
<body>
<div class="container">
<h2> 로그인</h2>
<p>안녕하세요!</p>
<p>아래 버튼을 클릭하여 에 로그인하세요:</p>
<p>
<a href="" class="button">로그인하기</a>
</p>
<p>이 링크는 분 후에 만료됩니다.</p>
<p>링크를 요청하지 않으셨다면 이 이메일을 무시하세요.</p>
</div>
</body>
</html>
다양한 프레임워크와의 통합
React 애플리케이션 연동
React SDK 설치 및 설정:
# React SDK 설치
npm install @authorizerdev/authorizer-react
# 또는 yarn
yarn add @authorizerdev/authorizer-react
React Provider 설정:
// App.js
import React from 'react';
import { AuthorizerProvider } from '@authorizerdev/authorizer-react';
function App() {
return (
<AuthorizerProvider
config=
>
<div className="App">
<Routes />
</div>
</AuthorizerProvider>
);
}
export default App;
로그인 컴포넌트 구현:
// components/LoginPage.js
import React from 'react';
import { Authorizer } from '@authorizerdev/authorizer-react';
const LoginPage = () => {
return (
<div style=>
<h2>로그인</h2>
<Authorizer />
</div>
);
};
export default LoginPage;
보호된 라우트 구현:
// components/ProtectedRoute.js
import React from 'react';
import { useAuthorizer } from '@authorizerdev/authorizer-react';
import { Navigate } from 'react-router-dom';
const ProtectedRoute = ({ children, requiredRoles = [] }) => {
const { user, loading } = useAuthorizer();
if (loading) {
return <div>로딩 중...</div>;
}
if (!user) {
return <Navigate to="/login" replace />;
}
// 역할 기반 접근 제어
if (requiredRoles.length > 0) {
const userRoles = user.roles || [];
const hasRequiredRole = requiredRoles.some(role =>
userRoles.includes(role)
);
if (!hasRequiredRole) {
return <div>접근 권한이 없습니다.</div>;
}
}
return children;
};
// 사용 예제
const AdminPanel = () => (
<ProtectedRoute requiredRoles={['admin']}>
<div>관리자 전용 페이지</div>
</ProtectedRoute>
);
Vue.js 애플리케이션 연동
Vue SDK 설치 및 설정:
# Vue SDK 설치
npm install @authorizerdev/authorizer-vue
Vue 플러그인 설정:
// main.js
import { createApp } from 'vue';
import { AuthorizerPlugin } from '@authorizerdev/authorizer-vue';
import App from './App.vue';
const app = createApp(App);
app.use(AuthorizerPlugin, {
authorizerURL: 'http://localhost:8080',
redirectURL: window.location.origin,
clientID: 'your-client-id',
});
app.mount('#app');
Vue 컴포넌트에서 사용:
<!-- components/UserProfile.vue -->
<template>
<div>
<div v-if="loading">로딩 중...</div>
<div v-else-if="user">
<h2>환영합니다, {{ user.given_name }}님!</h2>
<p>이메일: {{ user.email }}</p>
<p>역할: {{ user.roles?.join(', ') }}</p>
<button @click="logout">로그아웃</button>
</div>
<div v-else>
<authorizer />
</div>
</div>
</template>
<script>
import { useAuthorizer } from '@authorizerdev/authorizer-vue';
export default {
name: 'UserProfile',
setup() {
const { user, loading, logout } = useAuthorizer();
return {
user,
loading,
logout
};
}
};
</script>
Next.js 애플리케이션 연동
Next.js 서버사이드 인증:
// lib/auth.js
import { AuthorizerProvider } from '@authorizerdev/authorizer-react';
export const authConfig = {
authorizerURL: process.env.NEXT_PUBLIC_AUTHORIZER_URL,
redirectURL: process.env.NEXT_PUBLIC_APP_URL,
clientID: process.env.NEXT_PUBLIC_AUTHORIZER_CLIENT_ID,
};
// API 라우트에서 토큰 검증
export async function verifyToken(token) {
try {
const response = await fetch(`${authConfig.authorizerURL}/graphql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
query: `
query {
profile {
id
email
given_name
family_name
roles
}
}
`,
}),
});
const data = await response.json();
return data.data?.profile || null;
} catch (error) {
console.error('Token verification failed:', error);
return null;
}
}
API 라우트 보호:
// pages/api/protected.js
import { verifyToken } from '../../lib/auth';
export default async function handler(req, res) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Token required' });
}
const user = await verifyToken(token);
if (!user) {
return res.status(401).json({ error: 'Invalid token' });
}
// 역할 기반 접근 제어
if (!user.roles?.includes('admin')) {
return res.status(403).json({ error: 'Admin role required' });
}
res.json({ message: 'Protected data', user });
}
Express.js 백엔드 연동
Express 미들웨어 구현:
// middleware/auth.js
const axios = require('axios');
const authorizerURL = process.env.AUTHORIZER_URL;
async function verifyAuthorizerToken(token) {
try {
const response = await axios.post(`${authorizerURL}/graphql`, {
query: `
query {
profile {
id
email
given_name
family_name
roles
}
}
`,
}, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
return response.data.data?.profile || null;
} catch (error) {
return null;
}
}
// 인증 미들웨어
async function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
const user = await verifyAuthorizerToken(token);
if (!user) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = user;
next();
}
// 역할 기반 권한 미들웨어
function requireRole(roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const userRoles = req.user.roles || [];
const hasRole = roles.some(role => userRoles.includes(role));
if (!hasRole) {
return res.status(403).json({
error: `Required roles: ${roles.join(', ')}`
});
}
next();
};
}
module.exports = { authenticateToken, requireRole };
Express 라우트에서 사용:
// routes/api.js
const express = require('express');
const { authenticateToken, requireRole } = require('../middleware/auth');
const router = express.Router();
// 인증된 사용자만 접근 가능
router.get('/profile', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
// 관리자만 접근 가능
router.get('/admin/users',
authenticateToken,
requireRole(['admin']),
(req, res) => {
res.json({ message: 'Admin only endpoint' });
}
);
// 여러 역할 허용
router.get('/moderator/posts',
authenticateToken,
requireRole(['admin', 'moderator']),
(req, res) => {
res.json({ message: 'Admin or moderator endpoint' });
}
);
module.exports = router;
역할 기반 접근 제어 (RBAC) 고급 구현
계층적 역할 시스템 설계
역할 계층 구조 정의:
// 역할 계층 시스템
const roleHierarchy = {
'super_admin': {
level: 100,
inherits: ['admin', 'moderator', 'user'],
permissions: ['*']
},
'admin': {
level: 80,
inherits: ['moderator', 'user'],
permissions: [
'user:*',
'role:*',
'system:*',
'content:*'
]
},
'moderator': {
level: 60,
inherits: ['user'],
permissions: [
'user:read',
'user:update',
'content:*',
'report:*'
]
},
'premium_user': {
level: 40,
inherits: ['user'],
permissions: [
'profile:*',
'premium:*',
'content:read'
]
},
'user': {
level: 20,
inherits: [],
permissions: [
'profile:read',
'profile:update',
'content:read'
]
},
'guest': {
level: 0,
inherits: [],
permissions: [
'content:read'
]
}
};
권한 기반 미들웨어 시스템
고급 권한 검증 미들웨어:
// middleware/rbac.js
class RBACMiddleware {
constructor(roleHierarchy) {
this.roleHierarchy = roleHierarchy;
}
// 사용자의 모든 권한 계산 (상속 포함)
getUserPermissions(userRoles) {
const allPermissions = new Set();
userRoles.forEach(role => {
const roleInfo = this.roleHierarchy[role];
if (!roleInfo) return;
// 직접 권한 추가
roleInfo.permissions.forEach(permission => {
allPermissions.add(permission);
});
// 상속된 역할의 권한 추가
if (roleInfo.inherits) {
const inheritedPermissions = this.getUserPermissions(roleInfo.inherits);
inheritedPermissions.forEach(permission => {
allPermissions.add(permission);
});
}
});
return Array.from(allPermissions);
}
// 권한 확인
hasPermission(userPermissions, requiredPermission) {
// 와일드카드 권한 확인
if (userPermissions.includes('*')) {
return true;
}
// 정확한 권한 매치
if (userPermissions.includes(requiredPermission)) {
return true;
}
// 패턴 매칭 (예: user:* 권한으로 user:read 허용)
const [resource, action] = requiredPermission.split(':');
const wildcardPermission = `${resource}:*`;
return userPermissions.includes(wildcardPermission);
}
// Express 미들웨어 생성
requirePermission(permission) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const userRoles = req.user.roles || [];
const userPermissions = this.getUserPermissions(userRoles);
if (!this.hasPermission(userPermissions, permission)) {
return res.status(403).json({
error: `Permission required: ${permission}`,
userPermissions: userPermissions // 디버깅용 (프로덕션에서는 제거)
});
}
req.userPermissions = userPermissions;
next();
};
}
// 다중 권한 중 하나라도 있으면 통과
requireAnyPermission(permissions) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const userRoles = req.user.roles || [];
const userPermissions = this.getUserPermissions(userRoles);
const hasAnyPermission = permissions.some(permission =>
this.hasPermission(userPermissions, permission)
);
if (!hasAnyPermission) {
return res.status(403).json({
error: `One of these permissions required: ${permissions.join(', ')}`
});
}
req.userPermissions = userPermissions;
next();
};
}
// 모든 권한이 있어야 통과
requireAllPermissions(permissions) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
const userRoles = req.user.roles || [];
const userPermissions = this.getUserPermissions(userRoles);
const hasAllPermissions = permissions.every(permission =>
this.hasPermission(userPermissions, permission)
);
if (!hasAllPermissions) {
return res.status(403).json({
error: `All of these permissions required: ${permissions.join(', ')}`
});
}
req.userPermissions = userPermissions;
next();
};
}
}
// 인스턴스 생성 및 내보내기
const rbac = new RBACMiddleware(roleHierarchy);
module.exports = rbac;
동적 권한 관리 시스템
GraphQL을 통한 동적 역할 관리:
// GraphQL 쿼리/뮤테이션 예제
const roleManagementQueries = {
// 사용자에게 역할 할당
assignRole: `
mutation AssignRole($user_id: String!, $role: String!) {
assign_role(params: { user_id: $user_id, role: $role }) {
message
user {
id
email
roles
}
}
}
`,
// 사용자에게서 역할 제거
unassignRole: `
mutation UnassignRole($user_id: String!, $role: String!) {
unassign_role(params: { user_id: $user_id, role: $role }) {
message
user {
id
email
roles
}
}
}
`,
// 새 역할 생성
createRole: `
mutation CreateRole($role: String!, $description: String) {
create_role(params: { role: $role, description: $description }) {
message
role {
role
description
created_at
}
}
}
`,
// 모든 사용자 조회 (권한 포함)
getAllUsers: `
query GetUsers($params: UsersQueryInput) {
users(params: $params) {
users {
id
email
given_name
family_name
roles
created_at
email_verified
is_multi_factor_auth_enabled
}
pagination {
total
page
limit
}
}
}
`
};
프로덕션 최적화 및 보안 강화
성능 최적화
Redis 캐싱 전략:
// Redis 설정 최적화
const redis = require('redis');
const redisClient = redis.createClient({
url: process.env.REDIS_URL,
retry_strategy: (options) => {
if (options.error && options.error.code === 'ECONNREFUSED') {
return new Error('Redis server refused connection');
}
if (options.total_retry_time > 1000 * 60 * 60) {
return new Error('Retry time exhausted');
}
if (options.attempt > 10) {
return undefined;
}
return Math.min(options.attempt * 100, 3000);
}
});
// 사용자 세션 캐싱
async function cacheUserSession(userId, sessionData, ttl = 86400) {
const key = `user_session:${userId}`;
await redisClient.setex(key, ttl, JSON.stringify(sessionData));
}
// 권한 캐싱
async function cacheUserPermissions(userId, permissions, ttl = 3600) {
const key = `user_permissions:${userId}`;
await redisClient.setex(key, ttl, JSON.stringify(permissions));
}
데이터베이스 최적화:
-- PostgreSQL 인덱스 최적화
CREATE INDEX CONCURRENTLY idx_users_email_verified ON users(email) WHERE email_verified = true;
CREATE INDEX CONCURRENTLY idx_users_roles ON users USING GIN(roles);
CREATE INDEX CONCURRENTLY idx_sessions_expires_at ON sessions(expires_at) WHERE expires_at > NOW();
CREATE INDEX CONCURRENTLY idx_verification_requests_email ON verification_requests(email, identifier);
-- 파티셔닝 (대용량 데이터 처리)
CREATE TABLE sessions_2025 PARTITION OF sessions
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
-- 통계 업데이트 자동화
CREATE OR REPLACE FUNCTION update_table_stats()
RETURNS void AS $$
BEGIN
ANALYZE users;
ANALYZE sessions;
ANALYZE verification_requests;
END;
$$ LANGUAGE plpgsql;
-- 정기적 통계 업데이트
SELECT cron.schedule('update-stats', '0 2 * * *', 'SELECT update_table_stats();');
고급 보안 설정
보안 헤더 및 CORS 설정:
// 보안 미들웨어 설정
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
// 보안 헤더
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
fontSrc: ["'self'", "https://fonts.gstatic.com"],
imgSrc: ["'self'", "data:", "https:"],
scriptSrc: ["'self'"],
connectSrc: ["'self'", process.env.AUTHORIZER_URL]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// 속도 제한
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 5, // 최대 5회 시도
message: 'Too many authentication attempts, please try again later.',
standardHeaders: true,
legacyHeaders: false,
});
app.use('/auth', authLimiter);
// 일반 API 속도 제한
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: 'Too many requests, please try again later.'
});
app.use('/api', apiLimiter);
환경 변수 보안 강화:
# 프로덕션 보안 설정
ADMIN_SECRET=복잡한-64자리-이상-비밀키
JWT_SECRET=다른-복잡한-64자리-이상-JWT-비밀키
COOKIE_SECURE=true
COOKIE_SAME_SITE=strict
COOKIE_HTTP_ONLY=true
# 세션 보안
SESSION_TIMEOUT=3600
REFRESH_TOKEN_EXPIRY_TIME=7d
ACCESS_TOKEN_EXPIRY_TIME=15m
# 보안 기능 활성화
ENFORCE_MULTI_FACTOR_AUTHENTICATION=true
DISABLE_SIGN_UP=true # 운영환경에서는 초대 전용
DISABLE_STRONG_PASSWORD=false
ENFORCE_EMAIL_VERIFICATION=true
# 브루트포스 보호
MAX_LOGIN_ATTEMPTS=3
LOGIN_LOCKOUT_TIME=30m
# CORS 설정
ALLOWED_ORIGINS=https://yourapp.com,https://admin.yourapp.com
모니터링 및 로깅
Prometheus 메트릭 설정:
// metrics.js
const promClient = require('prom-client');
// 기본 메트릭 수집
promClient.collectDefaultMetrics();
// 커스텀 메트릭 정의
const authenticationAttempts = new promClient.Counter({
name: 'auth_attempts_total',
help: 'Total number of authentication attempts',
labelNames: ['method', 'status']
});
const activeUsers = new promClient.Gauge({
name: 'auth_active_users',
help: 'Number of currently active users'
});
const authenticationDuration = new promClient.Histogram({
name: 'auth_duration_seconds',
help: 'Authentication request duration',
buckets: [0.1, 0.5, 1, 2, 5]
});
// 메트릭 미들웨어
function authMetricsMiddleware(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const method = req.path.includes('login') ? 'login' :
req.path.includes('signup') ? 'signup' : 'other';
const status = res.statusCode < 400 ? 'success' : 'failure';
authenticationAttempts.labels(method, status).inc();
authenticationDuration.observe(duration);
});
next();
}
// 메트릭 엔드포인트
app.get('/metrics', (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.end(promClient.register.metrics());
});
module.exports = {
authMetricsMiddleware,
authenticationAttempts,
activeUsers,
authenticationDuration
};
구조화된 로깅 설정:
// logger.js
const winston = require('winston');
const { ElasticsearchTransport } = require('winston-elasticsearch');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'authorizer-auth' },
transports: [
// 콘솔 출력
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
// 파일 로깅
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'logs/combined.log'
}),
// Elasticsearch (프로덕션)
...(process.env.ELASTICSEARCH_URL ? [
new ElasticsearchTransport({
level: 'info',
clientOpts: { node: process.env.ELASTICSEARCH_URL },
index: 'auth-logs'
})
] : [])
]
});
// 인증 이벤트 로깅
function logAuthEvent(event, userId, metadata = {}) {
logger.info('Authentication event', {
event,
userId,
timestamp: new Date().toISOString(),
ip: metadata.ip,
userAgent: metadata.userAgent,
...metadata
});
}
// 보안 이벤트 로깅
function logSecurityEvent(event, severity, details) {
logger.warn('Security event', {
event,
severity,
details,
timestamp: new Date().toISOString()
});
}
module.exports = { logger, logAuthEvent, logSecurityEvent };
문제 해결 및 FAQ
일반적인 문제들
1. 데이터베이스 연결 문제
# PostgreSQL 연결 확인
psql -h localhost -p 5432 -U authorizer -d authorizer
# Docker 컨테이너 로그 확인
docker-compose logs postgres
# 연결 문자열 형식 확인
DATABASE_URL=postgres://username:password@host:port/database
해결 방법:
- 데이터베이스 서비스가 실행 중인지 확인
- 방화벽 설정 확인
- 인증 정보 정확성 검증
- 네트워크 연결성 테스트
2. 소셜 로그인 실패
// OAuth 설정 디버깅
const debugOAuth = {
google: {
clientId: process.env.GOOGLE_CLIENT_ID,
redirectUri: `${process.env.APP_URL}/oauth/google/callback`,
scopes: ['openid', 'email', 'profile']
}
};
console.log('OAuth Debug Info:', debugOAuth);
일반적인 원인:
- 잘못된 리다이렉트 URI
- 클라이언트 ID/Secret 불일치
- OAuth 앱 설정 누락
- HTTPS 요구사항 미충족
3. JWT 토큰 검증 실패
// JWT 토큰 디버깅
const jwt = require('jsonwebtoken');
function debugToken(token) {
try {
const decoded = jwt.decode(token, { complete: true });
console.log('Token Header:', decoded.header);
console.log('Token Payload:', decoded.payload);
console.log('Token Expiry:', new Date(decoded.payload.exp * 1000));
} catch (error) {
console.error('Token decode error:', error);
}
}
성능 최적화 팁
1. 데이터베이스 쿼리 최적화
-- 느린 쿼리 식별
SELECT query, mean_time, calls, total_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
-- 인덱스 사용량 확인
SELECT schemaname, tablename, attname, n_distinct, correlation
FROM pg_stats
WHERE tablename = 'users';
2. Redis 세션 최적화
// 세션 데이터 압축
const zlib = require('zlib');
async function compressedSetSession(key, data, ttl) {
const compressed = zlib.gzipSync(JSON.stringify(data));
await redisClient.setex(key, ttl, compressed);
}
async function compressedGetSession(key) {
const compressed = await redisClient.get(key);
if (!compressed) return null;
const decompressed = zlib.gunzipSync(compressed);
return JSON.parse(decompressed.toString());
}
3. 캐싱 전략
// 다계층 캐싱
class AuthCache {
constructor() {
this.memoryCache = new Map();
this.maxMemorySize = 1000;
}
async getUser(userId) {
// L1: 메모리 캐시
if (this.memoryCache.has(userId)) {
return this.memoryCache.get(userId);
}
// L2: Redis 캐시
const cached = await redisClient.get(`user:${userId}`);
if (cached) {
const user = JSON.parse(cached);
this.setMemoryCache(userId, user);
return user;
}
// L3: 데이터베이스
const user = await getUserFromDB(userId);
if (user) {
await this.setUser(userId, user);
}
return user;
}
async setUser(userId, user, ttl = 3600) {
// Redis에 저장
await redisClient.setex(`user:${userId}`, ttl, JSON.stringify(user));
// 메모리에 저장
this.setMemoryCache(userId, user);
}
setMemoryCache(key, value) {
if (this.memoryCache.size >= this.maxMemorySize) {
const firstKey = this.memoryCache.keys().next().value;
this.memoryCache.delete(firstKey);
}
this.memoryCache.set(key, value);
}
}
결론: Authorizer로 구축하는 미래의 인증 시스템
Authorizer 도입의 장점
개발 생산성 향상:
- 🚀 빠른 시작: 120초 만에 프로덕션 준비 완료
- 🔧 즉시 사용: 로그인 페이지와 관리 패널 기본 제공
- 📚 풍부한 SDK: 모든 주요 프레임워크 지원
- 🔄 자동 업데이트: 최신 보안 패치 자동 적용
비용 효율성:
- 💰 완전 무료: 오픈소스로 라이선스 비용 절약
- 🏠 자체 호스팅: 사용량에 따른 추가 비용 없음
- ⚡ 자원 최적화: 효율적인 리소스 사용
- 🔒 락인 없음: 언제든지 다른 솔루션으로 이전 가능
보안 및 규정 준수:
- 🛡️ 최신 보안: OAuth2, JWT, 2FA 등 현대적 보안 기준
- 📊 감사 로그: 모든 인증 이벤트 추적 가능
- 🌍 GDPR 준수: 데이터 주권 및 프라이버시 보장
- 🔐 암호화: 저장 및 전송 데이터 암호화
다음 단계 및 확장 방향
단기 목표 (1-3개월):
- 기본 인증 시스템 구축 및 안정화
- 주요 소셜 로그인 연동 (Google, GitHub)
- 역할 기반 접근 제어 구현
- 모니터링 및 로깅 시스템 구축
중기 목표 (3-6개월):
- 고급 보안 기능 활성화 (2FA, 브루트포스 보호)
- 성능 최적화 및 캐싱 구현
- 자동화된 백업 및 재해 복구 계획
- 사용자 분석 및 행동 추적
장기 목표 (6개월+):
- 기업급 기능 추가 (SSO, SAML, LDAP)
- 마이크로서비스 아키텍처 전환
- 글로벌 배포 및 지역별 최적화
- AI 기반 보안 위협 탐지
커뮤니티 및 지원
오픈소스 커뮤니티 참여:
- 📂 GitHub: 기여 및 이슈 리포트
- 💬 디스코드: 개발자 커뮤니티 참여
- 📖 문서화: 공식 문서 개선에 기여
- 🐛 버그 리포트: 발견한 문제 공유
지속적인 학습:
- 📚 정기적인 보안 업데이트 모니터링
- 🔄 새로운 기능 및 개선사항 적용
- 📊 성능 메트릭 분석 및 최적화
- 🛡️ 보안 감사 및 침투 테스트
Authorizer는 단순한 인증 솔루션을 넘어, 개발자와 기업이 안전하고 확장 가능한 인증 시스템을 구축할 수 있는 완전한 플랫폼입니다. 이 가이드를 통해 구축한 시스템을 바탕으로 더욱 안전하고 사용자 친화적인 애플리케이션을 개발하시기 바랍니다.
🚀 지금 바로 시작하세요: Railway에서 120초 배포하고 미래의 인증 시스템을 경험해보세요!