9.0 KiB
Auth DB Design
Goal
현재 조직도 업무 데이터와 로그인 데이터를 분리한다.
분리 원칙:
- 업무 데이터는 기존
public.members,seat_maps,seat_positions중심으로 유지 - 인증/권한 데이터는 별도
auth스키마로 분리 - 로그인 사용자는 필요할 때만
members.id와 연결
이 방식이면 비밀번호, 세션, 감사로그를 업무 데이터와 분리해서 관리할 수 있고,
엑셀 임포트로 members 가 갱신돼도 인증 체계가 직접 흔들리지 않는다.
Scope
이번 설계의 대상:
- 사용자 계정
- 비밀번호 해시
- 세션
- 역할과 권한
- 로그인 감사로그
- 사용자와 조직 구성원 연결
이번 설계에서 제외:
- SSO 연동
- OAuth/OpenID Connect
- MFA
- 비밀번호 재설정 메일 발송
Recommended Structure
권장 구조는 "같은 PostgreSQL, 다른 스키마" 이다.
- 업무 스키마:
public - 인증 스키마:
auth
초기 운영에서는 DB 인스턴스를 분리하지 않아도 된다. 대신 아래 원칙은 바로 적용한다.
- 애플리케이션 계정도 가능하면 읽기/쓰기 범위를 분리
- 인증 관련 쿼리는
auth.*만 접근 - 업무 API 는
public.*중심으로 접근
Core Tables
auth.users
로그인 가능한 계정의 기준 테이블.
주요 컬럼:
id BIGSERIAL PRIMARY KEYusername TEXT NOT NULL UNIQUEpassword_hash TEXT NOT NULLdisplay_name TEXT NOT NULLemail TEXTstatus TEXT NOT NULL DEFAULT 'active'member_id INTEGER NULL REFERENCES public.members(id) ON DELETE SET NULLlast_login_at TIMESTAMPTZpassword_changed_at TIMESTAMPTZcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
상태값 권장:
activelockeddisabled
원칙:
username는 로그인 식별자member_id는 선택 연결- 구성원이 퇴사하거나 엑셀에서 빠져도 계정 자체는 바로 삭제하지 않음
auth.roles
권한 묶음 정의.
주요 컬럼:
id BIGSERIAL PRIMARY KEYcode TEXT NOT NULL UNIQUEname TEXT NOT NULLdescription TEXT
초기 권장 역할:
super_adminorg_adminviewer
auth.user_roles
사용자와 역할의 다대다 연결.
주요 컬럼:
user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADErole_id BIGINT NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADEcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()PRIMARY KEY (user_id, role_id)
auth.permissions
세분화된 권한 코드 정의.
주요 컬럼:
id BIGSERIAL PRIMARY KEYcode TEXT NOT NULL UNIQUEname TEXT NOT NULLdescription TEXT
초기 권장 권한:
member.readmember.writemember.importseatmap.readseatmap.writephoto.uploadadmin.user.manage
auth.role_permissions
역할과 권한의 다대다 연결.
주요 컬럼:
role_id BIGINT NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADEpermission_id BIGINT NOT NULL REFERENCES auth.permissions(id) ON DELETE CASCADEcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()PRIMARY KEY (role_id, permission_id)
auth.sessions
서버 세션 저장 테이블.
주요 컬럼:
id UUID PRIMARY KEYuser_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADErefresh_token_hash TEXTissued_at TIMESTAMPTZ NOT NULL DEFAULT NOW()expires_at TIMESTAMPTZ NOT NULLrevoked_at TIMESTAMPTZip_address INETuser_agent TEXTcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
원칙:
- 브라우저 쿠키에는 세션 식별자만 저장
- 토큰 자체를 평문으로 DB에 저장하지 않음
- 만료와 강제 로그아웃을 DB에서 통제 가능하게 함
auth.login_audit_logs
로그인 시도와 결과 기록.
주요 컬럼:
id BIGSERIAL PRIMARY KEYusername TEXT NOT NULLuser_id BIGINT NULL REFERENCES auth.users(id) ON DELETE SET NULLsuccess BOOLEAN NOT NULLfailure_reason TEXTip_address INETuser_agent TEXTcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
용도:
- 로그인 실패 추적
- 계정 잠금 기준 판단
- 보안 감사 대응
Relationship To Current members
핵심은 auth.users.member_id -> public.members.id 연결이다.
의미:
- 로그인 계정과 조직도 인원을 분리한다
- 로그인하지 않는 구성원은
members에만 있어도 된다 - 외부 관리자 계정은
member_id없이 운영할 수 있다
권장 규칙:
- 일반 사내 사용자는
employee_id기준으로 계정-구성원 연결 - 엑셀 동기화 시
members.id유지가 중요하므로 이미 반영한 비교 기반 동기화 방식을 유지 member_id연결이 끊긴 계정은 자동 삭제하지 말고 관리자 검토 대상으로 둔다
Login Flow
1. 로그인 요청
입력:
usernamepassword
처리:
auth.users에서username조회status != active이면 거부password_hash검증- 성공 시
auth.sessions생성 auth.login_audit_logs기록
응답 권장:
- 사용자 기본 정보
- 역할 목록
- 권한 목록
- 세션 만료 시각
2. 인증 확인
각 보호 API 요청 시:
- 세션 쿠키 또는 Bearer 토큰 확인
auth.sessions조회- 만료/폐기 여부 확인
- 사용자 상태와 역할 재검증
3. 로그아웃
처리:
- 현재 세션의
revoked_at업데이트 - 클라이언트 쿠키 제거
Authorization Model
초기에는 RBAC 기반으로 충분하다.
권장 역할별 범위:
super_admin
- 사용자 관리
- 권한 관리
- 조직도/사진/자리배치 전체 수정
org_admin
- 조직도 조회/수정
- 엑셀 임포트
- 사진 업로드
- 자리배치 수정
viewer
- 조직도 조회
- 자리배치 조회
API 보호 예시:
GET /api/members:member.readPOST /api/members/import:member.importPOST /api/uploads/profile-photo:photo.uploadPUT /api/seat-maps/{seat_map_id}/layout:seatmap.write- 사용자 관리 API:
admin.user.manage
Password Policy
비밀번호는 평문 저장 금지.
권장:
Argon2id우선- 대안으로
bcrypt
추가 원칙:
- 첫 구현부터 해시 알고리즘 버전 정보 포함
- 비밀번호 변경 시
password_changed_at갱신 - 실패 횟수 기반 잠금은 앱 로직 또는 별도 컬럼으로 확장 가능
Migration Plan
Phase 1
인증 스키마와 기본 테이블만 추가.
작업:
CREATE SCHEMA IF NOT EXISTS authauth.usersauth.rolesauth.user_rolesauth.permissionsauth.role_permissionsauth.sessionsauth.login_audit_logs
이 단계에서는 기존 /api/mock-login 유지 가능.
Phase 2
관리자 1명 이상을 수동 생성하고 실제 로그인 API 추가.
권장 추가 API:
POST /api/auth/loginPOST /api/auth/logoutGET /api/auth/me
Phase 3
기존 프론트엔드의 mock 로그인 제거.
변경 대상:
Phase 4
권한 기반으로 API 보호 적용.
우선순위:
- 쓰기 API 보호
- 업로드 API 보호
- 읽기 API 권한 정리
Recommended SQL Skeleton
CREATE SCHEMA IF NOT EXISTS auth;
CREATE TABLE IF NOT EXISTS auth.users (
id BIGSERIAL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
display_name TEXT NOT NULL,
email TEXT,
status TEXT NOT NULL DEFAULT 'active',
member_id INTEGER NULL REFERENCES public.members(id) ON DELETE SET NULL,
last_login_at TIMESTAMPTZ,
password_changed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS auth.roles (
id BIGSERIAL PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
description TEXT
);
CREATE TABLE IF NOT EXISTS auth.user_roles (
user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
role_id BIGINT NOT NULL REFERENCES auth.roles(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, role_id)
);
CREATE TABLE IF NOT EXISTS auth.sessions (
id UUID PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
refresh_token_hash TEXT,
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Operational Notes
- 엑셀 임포트는 계속
public.members기준으로 처리 - 로그인 계정 생성은 엑셀 업로드와 분리
- 프로필 사진은 현재처럼 파일 저장 +
members.photo_url참조 유지 가능 - 감사로그와 세션은 삭제보다 보존 기간 정책으로 관리
Decision
현재 프로젝트의 권장안은 아래 한 줄로 정리된다.
"로그인 DB는 auth 스키마로 분리하고, 업무 DB는 public 에 유지하며, 두 영역은 auth.users.member_id 로만 연결한다."