인증 vs 인가 — 로그인은 시작일 뿐
Authentication(너는 누구냐?)과 Authorization(너 뭐 할 수 있냐?)을 분리해서 설계.
이 둘을 섞으면 '로그인된 유저는 무조건 다 할 수 있음' 같은 사고가 난다. 관리자 페이지 뚫림, 남의 데이터 조회(IDOR) 같은 사고는 전부 인가 누락. 인증은 구현이 쉽지만, 인가 설계가 진짜 어렵다.
비유하자면 회사 건물에 들어가는 과정이다. 인증(Authentication)은 "이 사원증이 진짜 당신 건가?"(본인 확인). 인가(Authorization)는 "당신 직급으로 이 회의실에 들어가도 됩니까?"(권한 확인). 두 단계가 분리되어야 건물 보안이 성립한다.
| 구분 | 인증 (AuthN) | 인가 (AuthZ) |
|---|---|---|
| 질문 | 너는 누구냐? | 너 뭐 할 수 있냐? |
| 예시 | 로그인, 얼굴인식, OTP | "관리자만 삭제 가능" |
| 시점 | 세션 시작 1회 | 매 요청마다 |
| 실패 시 HTTP | 401 Unauthorized | 403 Forbidden |
| 저장 위치 | 세션 스토어·토큰 | DB 권한 테이블·RBAC |
인가를 '무슨 기준으로' 판단하느냐에 따라 모델이 나뉜다. 앱 복잡도에 맞춰 고른다.
| 모델 | 기준 | 예시 |
|---|---|---|
| RBAC (Role-Based) | 역할 | admin·editor·viewer |
| ABAC (Attribute-Based) | 속성 조합 | "본인이 작성한 글만 삭제" |
| ACL (Access Control List) | 리소스별 허용 목록 | 파일별 공유 상대 지정 |
| ReBAC (Relationship-Based) | 관계 그래프 | Google Drive 폴더 공유 |
// ❌ 위험: 로그인만 확인, 인가 누락 — IDOR 취약점
app.delete("/api/posts/:id", async (req, res) => {
if (!req.user) return res.status(401).end();
await db.deletePost(req.params.id); // 남의 글도 삭제 가능!
res.json({ ok: true });
});
// ✅ 안전: 소유권 체크 (인가)
app.delete("/api/posts/:id", async (req, res) => {
if (!req.user) return res.status(401).end();
const post = await db.getPost(req.params.id);
if (!post) return res.status(404).end();
// 인가: 본인 or 관리자만 삭제 허용
if (post.authorId !== req.user.id && req.user.role !== "admin") {
return res.status(403).end();
}
await db.deletePost(req.params.id);
res.json({ ok: true });
});IDOR(Insecure Direct Object Reference)은 OWASP 단골. AI가 만든 CRUD API는 대부분 인가를 깜빡한다 — Claude에게 항상 '소유권 체크 추가'를 명시적으로 요구하자.
Supabase·Firebase는 DB 자체에 인가 정책(RLS, Security Rules)을 박을 수 있다. 앱 코드가 실수로 빼먹어도 DB가 거부. 이중 방어로 강력 추천.
-- Supabase Row Level Security (RLS)
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- "본인이 작성한 글만 삭제 가능" 정책
CREATE POLICY "users can delete own posts"
ON posts FOR DELETE
USING (auth.uid() = author_id);
-- "모두 읽기 가능, 쓰기는 로그인 유저만"
CREATE POLICY "public read" ON posts FOR SELECT USING (true);
CREATE POLICY "auth write" ON posts FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);RLS는 DB 레벨 인가. 앱 코드에서 깜빡해도 DB가 거부한다. Supabase 쓰는 바이브코더는 거의 필수.
바이브코더 단골 사고 3종. ① 로그인 = 모든 권한 오해 → 인가 체크 누락. ② 클라이언트 측 역할 체크만 → 브라우저 콘솔에서 우회 가능. ③ 프론트에서 admin UI만 숨김 → URL 직접 입력으로 접근. 모든 인가 판단은 반드시 서버/DB에서.
"너는 누구냐?"를 판단하는 단계와 "뭐 할 수 있냐?"를 판단하는 단계의 영어 용어를 각각 쓰시오.
로그인은 됐지만 해당 작업 권한이 없을 때 서버가 돌려주는 HTTP 상태 코드는?
프론트엔드에서 admin 메뉴를 숨겼다면 비인가 접근이 방어된다.