인증과 인가는 로그인이라는 하나의 화면 뒤에 나란히 숨어 있어 자주 한 덩어리로 취급된다. 그러나 이 둘은 서로 다른 질문에 답하는 별개의 절차다. 제출한 아이디와 비밀번호를 확인해 문을 열어 주는 일과, 문을 통과한 사람이 어디까지 들어가도 되는지를 정하는 일은 성격이 전혀 다르다. 로그인 기능을 붙였다는 사실만으로 보안의 절반이 끝났다고 여기는 착각은 대개 이 둘을 뭉뚱그린 데서 온다. 신원을 확인하는 절차와 권한을 확인하는 절차가 흐려지는 순간, 문을 통과한 사람이 남의 자료까지 손대는 빈틈이 열린다.


이번 편은 인증과 인가가 왜 다른 질문인지, 웹이 이 둘을 상태 코드로 어떻게 나누는지, 순서가 왜 정해져 있는지, 그리고 매 요청마다 반복 확인하는 습관이 왜 방어의 핵심인지를 다룬다. 인증 시리즈 전체를 관통하는 가장 기본이 되는 구분이므로 첫 편에 놓는다. 이 구분이 몸에 배어 있지 않으면 뒤에 이어질 세션과 토큰, 권한 설계의 논의가 모두 흔들린다.

누구인가와 무엇을 해도 되는가

인증은 요청을 보낸 쪽이 누구인지를 확인하는 일이다. 제출한 자격 증명이 시스템이 아는 계정과 일치하는지, 정말 그 계정의 주인이 맞는지를 따지는 절차다. 로그인 화면에서 벌어지는 대부분의 처리가 여기에 속한다. 보안이라고 하면 흔히 이 인증 하나만 떠올리지만, 인증은 관문의 절반일 뿐이다.


인가는 신원이 확인된 요청자가 지금 이 동작을 해도 되는지를 확인하는 일이다. 같은 로그인 사용자라도 자기 글은 고칠 수 있지만 남의 글은 건드리면 안 되고, 일반 사용자는 관리 화면에 들어갈 수 없다. 이 경계를 지키는 것이 인가의 역할이다. 신원이 같아도 대상 자원과 동작에 따라 허락 여부가 갈리므로, 인가는 한 번 정해 두고 끝나는 값이 아니라 매번 따져 물어야 하는 판단에 가깝다.


두 단어를 한 문장으로 바꾸면 차이가 분명해진다. 인증은 너 누구냐는 질문이고, 인가는 너 이것을 해도 되느냐는 질문이다. 이렇게 나누어 두면 로그인 처리에서 답할 것과 권한 검사에서 답할 것이 저절로 갈라진다. 한 문장에 두 질문을 욱여넣으려 하면 그 지점에서 허점이 스며든다.


현실의 비유로 옮기면 이해가 쉽다. 건물에 들어갈 때 신분증으로 신원을 확인받는 것이 인증이라면, 그 신분으로 어느 층 어느 방까지 들어갈 수 있는지를 정하는 출입 권한이 인가다. 신분증이 진짜라고 해서 모든 방문이 열리는 것은 아니다. 안내 데스크에서 신분을 확인하는 절차와 보안 카드로 특정 층의 문이 열리는 절차는 서로 다른 장치이며, 로그인과 권한 검사도 그처럼 별개의 관문이다.

상태 코드가 나누는 두 관문

웹은 이 두 개념을 상태 코드로도 구분한다. 신원이 확인되지 않았을 때는 401(미인증)을 돌려주고, 신원은 확인됐으나 권한이 없을 때는 403(권한 없음)을 돌려준다. 상태 코드 자체가 이미 두 관문을 나누어 표현하고 있는 것이다. 이 둘을 정확히 쓰는 것만으로도 서버의 판단이 클라이언트에 그대로 전달된다.


401은 누구인지 모르겠으니 인증부터 하라는 신호다. 로그인이 만료됐거나 자격 증명이 없을 때 보내는 응답이다. 이 응답을 받은 클라이언트는 대개 로그인 화면으로 유도하거나 자격을 다시 갖추는 흐름으로 넘어간다. 다시 신원을 밝히면 통과할 수 있다는 여지를 담은 신호이므로, 이 응답을 받은 화면은 사용자를 내치기보다 다시 들어올 길을 안내하도록 설계된다.


403은 누구인지는 알겠으나 이 일은 허락할 수 없다는 신호다. 로그인은 되어 있으나 그 자원에 대한 권한이 없을 때 보낸다. 이 경우 다시 로그인시켜도 소용이 없다. 권한 자체가 없는 것이므로 화면 흐름도 로그인 유도가 아니라 접근 불가 안내로 가야 한다. 권한 없음 상황에 401을 쓰거나 그 반대로 쓰면, 클라이언트가 엉뚱한 대응을 하며 로그인 화면과 실패 사이를 무한히 오간다.


다만 상태 코드 선택에도 정보 노출을 고려하는 감각이 필요하다. 존재하지 않아야 할 자원에 대해 권한 없음을 그대로 알려 주면, 그 자원이 존재한다는 사실 자체가 새어 나갈 수 있다. 그래서 민감한 경우에는 권한 없음과 없음을 굳이 구분하지 않고 없음으로 통일해 응답하기도 한다. 방어의 관점에서는 상태 코드 하나에도 어떤 사실이 실려 나가는지를 따져야 한다.

인증이 먼저이고 인가가 나중이다

이 둘은 나란히 놓인 관문이 아니라 순서가 있는 관문이다. 반드시 인증을 먼저 통과한 다음에 인가를 따진다. 누구인지 모르는 상태에서는 무엇을 할 수 있는지 물어볼 근거가 없기 때문이다. 신원이 정해져야 그 신원에 붙은 권한을 조회할 수 있다.


요청을 처리하는 흐름도 이 순서를 따른다. 먼저 세션이나 토큰을 확인해 요청자가 누구인지 밝히고, 그다음에 그 사람이 지금 건드리려는 자원에 대한 권한이 있는지를 검사한다. 이 두 단계를 순서대로 통과해야 실제 작업이 실행된다. 미들웨어를 배치할 때 인증을 앞에, 인가 검사를 뒤에 두는 골격은 이 순서를 코드로 옮긴 것이다.


순서를 지키면 코드의 층이 자연스럽게 나뉜다. 인증 층은 요청자의 신원 정보를 확정해 뒤로 넘겨주는 역할만 하고, 인가 층은 그 신원 정보를 받아 이 요청을 허락할지 결정하는 역할만 한다. 각 층이 하나의 책임만 지므로 점검과 시험이 쉬워진다. 뭉뚱그려 두었을 때는 어느 쪽이 부실한지조차 가늠하기 어렵지만, 나누어 두면 로그인은 완벽한데 권한 검사가 비어 있는 경우와 그 반대의 경우를 각각 따로 발견할 수 있다.


인증에 성공했다고 해서 인가가 자동으로 따라오는 것은 아니다. 로그인한 사용자 전부에게 모든 문을 열어 주는 순간 인가는 사라진 것이나 마찬가지다. 인증은 무대에 오를 자격일 뿐이고, 무대 위에서 무엇을 해도 되는지는 인가가 따로 정한다. 반대로 인가만 촘촘하고 인증이 허술해 남의 신원을 쉽게 흉내 낼 수 있다면 권한 검사도 무의미하다. 두 관문은 서로를 전제로 삼기에, 어느 한쪽만 튼튼해서는 전체가 안전해지지 않는다.

뭉뚱그림이 여는 권한 상승

인증과 인가를 뭉뚱그려 처리할 때 나타나는 가장 무거운 결과가 권한 상승이다. 낮은 권한의 사용자가 높은 권한의 작업을 해내는 상황을 말한다. 일반 사용자가 남의 글을 고치는 작은 사례부터 관리 기능을 호출하는 큰 사례까지 폭이 넓지만, 뿌리는 모두 권한 검사를 생략했다는 하나의 원인에 닿는다.


흔한 형태는 화면에서 버튼만 숨기고 서버 검사를 빠뜨리는 것이다. 수정 버튼을 안 보이게 했으니 안전하다고 착각하기 쉽지만, 요청 자체는 주소만 알면 직접 보낼 수 있다. 화면을 가리는 것은 인가가 아니라 눈속임일 뿐이며, 실제 방어는 서버가 요청을 받아 처리하기 직전에 이루어져야 한다.


또 다른 형태는 자원의 소유권을 확인하지 않는 것이다. 로그인 여부만 보고 요청에 담긴 식별 번호를 그대로 신뢰하면, 다른 번호를 넣어 남의 자료에 접근하는 길이 열린다. 이런 직접 참조의 빈틈은 인가 검사를 자원 단위로 하지 않을 때 생긴다. 역할을 다루는 코드에서도 마찬가지여서, 관리자만 할 수 있는 작업에 역할 검사를 빠뜨리면 로그인만 한 일반 사용자가 그 기능에 닿는다.


권한 상승은 대개 조용히 존재하다가 한 번에 크게 드러난다. 정상 사용 흐름에서는 나타나지 않기 때문에, 기능이 잘 돌아가는 것만 보고 안전하다고 믿게 된다. 그래서 방어를 검증할 때는 정상 흐름만이 아니라 권한 없는 사용자가 남의 식별 번호를 넣거나 관리 전용 주소를 두드리는 상황을 일부러 시험에 포함해야 한다. 방어자가 스스로 이런 시도를 재현해 보면 정상 흐름에서는 결코 드러나지 않던 구멍이 모습을 드러낸다.


결국 방어의 핵심은 신뢰의 경계를 명확히 하는 데 있다. 클라이언트에서 온 신원 주장과 권한 주장은 모두 검증 대상일 뿐 믿을 근거가 아니다. 누구인지도, 무엇을 해도 되는지도 서버가 최종적으로 판단해야 권한 상승의 문이 닫힌다.

거절을 설계의 일부로 다루기

인증과 인가를 나누어 두면 거절의 처리도 두 갈래로 정돈된다. 신원이 확인되지 않은 요청과 권한이 없는 요청은 원인이 다르므로, 응답과 뒤이은 화면 흐름도 다르게 설계되어야 한다. 성공 경로만 다듬고 거절 경로를 방치하면, 정작 방어가 작동하는 순간에 사용자는 무엇이 막혔는지 알 수 없는 상태에 놓인다. 거절은 예외가 아니라 정상적으로 발생하는 결과이므로, 처음부터 설계 안에 자리를 잡아야 한다.


거절 응답을 만들 때는 원인을 얼마나 드러낼지 신중히 정한다. 방어의 관점에서는 정당한 사용자에게는 다음 행동을 안내할 만큼 알려 주되, 권한 없는 시도에는 시스템의 내부 사정을 최대한 감추는 균형이 필요하다. 어떤 자원이 존재하는지, 어떤 조건에서 통과되는지가 실패 메시지를 통해 새어 나가면, 그 정보 자체가 다음 시도의 발판이 된다. 실패 메시지는 짧고 일관되게 두는 편이 안전하다.


거절이 발생한 사실은 기록으로 남겨 두는 것이 좋다. 특정 계정이나 특정 주소에서 권한 없는 시도가 반복된다면, 그 자체가 이상 징후를 알리는 신호가 된다. 개별 요청 하나는 단순한 실수일 수 있으나, 같은 패턴이 쌓이면 방어자가 대응할 근거가 된다. 다만 기록에는 자격 증명이나 민감한 값이 그대로 담기지 않도록 걸러야 한다.


거절 처리를 한곳으로 모으면 일관성이 유지된다. 각 처리 흐름이 제각기 다른 방식으로 실패를 돌려주면, 어떤 곳은 지나치게 많은 정보를 흘리고 어떤 곳은 사용자를 막다른 길에 세운다. 인증 실패와 인가 실패를 공통의 처리로 모아 두면, 어디서 막히든 같은 규칙으로 응답하게 되어 빈틈이 줄어든다. 거절의 일관성은 그 자체가 하나의 방어선이다.

매 요청마다 다시 확인하기

가장 확실한 방어는 매 요청마다 인증과 인가를 다시 확인하는 것이다. 무상태 웹에서는 각 요청이 독립적이므로, 앞선 요청에서 통과했다는 사실이 지금 요청의 안전을 보장하지 않는다. 요청 하나하나를 새로 온 손님처럼 대하는 것이 원칙이다.


매번 확인한다고 하면 성능이 걱정될 수 있으나 실제 비용은 크지 않다. 세션이나 토큰 검증은 가볍고, 권한 조회도 대개 빠른 조회 한두 번으로 끝난다. 이 작은 비용을 아끼려다 권한 상승을 허용하는 것은 남는 장사가 아니다. 확인은 리소스 단위로 구체적이어야 한다. 그냥 로그인했는지가 아니라, 이 사용자가 바로 이 자원에 이 동작을 해도 되는지를 물어야 한다.


검사는 각 처리 흐름의 맨 앞에 두는 것이 안전하다. 실제 작업 코드보다 먼저 인증과 인가를 통과시키고, 통과하지 못하면 즉시 401이나 403으로 끊는다. 작업을 먼저 시작해 놓고 나중에 권한을 확인하면, 이미 일부가 처리된 상태에서 되돌려야 하는 번거로움이 생긴다. 관문을 먼저 통과시킨 뒤에야 실제 일을 시작하는 순서가 코드를 안전하게 만든다.


공통 로직은 미들웨어나 정책 함수로 모아 반복을 줄인다. 소유권 확인이나 역할 확인 같은 판단을 한곳에 모아 두면, 새 기능을 붙일 때 그 함수만 불러 쓰면 되고 권한 규칙이 바뀌어도 한곳만 고치면 전체에 반영된다. 검사를 여기저기 흩뿌리면 어느 하나를 빠뜨릴 위험이 커진다. 이 정책 함수들을 두 관문을 지키는 문지기로 삼고 새 기능이 반드시 그 문을 거치도록 통로를 좁혀 두는 것이 유지보수와 안전을 함께 얻는 길이다.


정리하면 인증은 누구인지를 묻고 인가는 무엇을 해도 되는지를 물으며, 인증을 먼저 통과한 뒤 인가를 따지는 순서가 지켜져야 한다. 이 둘을 뭉뚱그리면 권한 상승이 열리고, 매 요청마다 자원 단위로 다시 확인하는 습관이 그 문을 닫는다. 다음 편에서는 이 인증을 실제로 구현하는 가장 고전적인 방식인 세션 기반 인증을 들여다본다.