BACKEND ROOTS 02 · 2026.09.28

로그인하면 왜
내 결과만 보일까?

CSV 결과에 주인을 연결합니다.
Cursor에서 만드는 작은 앱으로 인증과 권한을 확인하세요.

지난주 Worker는 CSV를 처리하고 결과를 JSON 파일로 남겼습니다. 이번에는 브라우저에서 결과를 요청하고, 로그인한 사람의 결과만 보여주는 작은 앱을 만듭니다. 지난주 코드를 완성하지 않았어도 시작할 수 있습니다.

01 · 배운 것과 연결하기

구분 기존 학습 이번 주에 이어지는 질문
복습 · W3 API의 식당 비유 웨이터에게 결과를 요청하면 어떤 JSON이 돌아올까?
확장 · W8 프론트엔드와 백엔드 화면에서 숨긴 결과도 주소로 직접 요청하면 보일까?
확장 · W18 프론트엔드의 상태 화면에 이름이 보이는 것과 서버가 사용자를 확인하는 것은 같을까?
복습 · W19 Worker의 결과와 검증 결과 파일은 남았다. 이제 누가 볼 수 있어야 할까?
NEW 인증·인가·쿠키·세션 요청한 사람을 확인하고 결과의 소유자와 비교한다.

API 키를 넣어 서비스에 접근했던 경험도 인증과 연결됩니다. 다만 API 키는 프로그램·프로젝트를 식별할 수도 있어 개인의 로그인과 완전히 같은 것은 아닙니다.

02 · 파일에서 API로

19주차의 submit.py → worker.py → 결과 JSON은 로컬 파일 작업입니다. 웹 서버나 로그인 API를 만든 것은 아닙니다. 이번 샘플에는 W19 원본에 없던 소유자 owner를 추가합니다. 이번에는 이미 만들어진 가상 결과 JSON을 읽는 API를 만듭니다. Worker를 함께 실행하지 않습니다.

브라우저 → GET /api/results → 서버가 사용자와 소유자 확인 → 내 결과 JSON

Python은 언어, Flask는 Python으로 웹 요청을 받게 해주는 프레임워크입니다. API와 HTML 화면을 Flask 하나가 제공합니다. 별도 프론트엔드 서버나 Node.js는 필요하지 않습니다.

요청 하는 일
POST /api/login 아이디·비밀번호를 확인하고 로그인 쿠키 발급
GET /api/me 현재 로그인한 사용자 확인
GET /api/results 내 결과 목록만 조회
GET /api/results/작업ID 해당 결과의 소유자인지 확인 후 조회
POST /api/logout 서버의 세션과 브라우저 쿠키 제거

03 · 누구인가, 볼 수 있는가

인증(Authentication)은 “누구인가?”를 확인합니다. 인가(Authorization)는 “이 결과를 볼 수 있는가?”를 확인합니다. 병원에서 신분을 확인했다고 다른 환자의 차트를 볼 수 있는 것은 아닙니다.

상황 이번 앱의 응답 의미
로그인 없이 결과 요청 401 인증 필요
mina로 로그인하고 mina의 결과 요청 200 인증·인가 통과
mina로 로그인하고 joon의 결과 요청 403 인증했지만 권한 없음
로그인하고 존재하지 않는 작업 요청 404 결과 없음

버튼을 숨기는 것은 권한 검사가 아닙니다. 서버는 요청마다 세션의 사용자와 결과의 owner를 비교합니다. 브라우저가 보내는 사용자 이름을 소유자 판단의 근거로 믿지 않습니다. 이번 교육 앱은 403과 404를 구분하지만, 실제 서비스는 자원의 존재를 감추려고 모두 404로 응답하기도 합니다.

04 · 로그인 상태를 이어주는 번호표

HTTP 요청 자체가 이전 로그인을 자동으로 기억하는 것은 아닙니다. 앱이 요청에 식별 정보를 연결합니다.

  1. 로그인 성공: 서버가 예측하기 어려운 번호표인 세션 ID를 만듭니다.
  2. 서버 장부: 세션 ID → 사용자, 만료 시각을 서버 메모리에 기록합니다.
  3. 쿠키 발급: 브라우저는 서버가 보낸 ID를 쿠키로 보관합니다.
  4. 다음 요청: 브라우저가 같은 서버로 쿠키를 보내면 서버가 장부를 확인합니다.
  5. 로그아웃: 서버 장부의 항목과 브라우저 쿠키를 모두 지웁니다.

쿠키는 브라우저의 보관·전달 수단이고, 세션은 여러 요청에 걸쳐 로그인 상태를 관리하는 방식입니다. 이번 앱은 ID만 쿠키에 넣는 서버 세션입니다. HttpOnly는 JavaScript가 쿠키를 직접 읽지 못하게 합니다. SameSite=Lax와 변경 요청의 Origin 확인도 적용합니다.

새로고침하면 쿠키가 다시 전송되므로 로그인이 유지됩니다. 서버 재시작 시에는 메모리 장부가 없어져 다시 로그인해야 합니다. 결과 JSON 파일은 삭제되지 않습니다. 세션은 발급 후 1800초가 지나면 만료됩니다.

비밀번호는 솔트를 사용하는 비밀번호 전용 해시 함수로 검증합니다. 해싱은 유출 시 추측 공격을 어렵게 만들지만, 약한 비밀번호까지 안전하게 바꾸지는 않습니다. 실습의 공개 비밀번호는 가상 계정 전용입니다.

JWT는 어디에 해당할까?

JWT는 JSON Web Token의 약자입니다. W3·W19에서 배운 JSON으로 “누구에 대한 정보인지, 언제까지 유효한지” 등을 담아, 프로그램 사이에서 주고받을 수 있는 문자열로 표현하는 표준 형식입니다. 여기서 토큰(token)은 요청할 때 제시하는 증표를 뜻합니다. AI가 글을 잘라 세는 토큰과는 다른 맥락입니다.

로그인에 쓰는 서명된 JWT는 “사용자 정보와 유효기간이 적힌, 위조 여부를 확인할 수 있는 출입증”으로 생각하면 됩니다. JWT 자체가 로그인 기능을 만들어 주는 것은 아닙니다. 앱이 아이디·비밀번호 등을 확인한 뒤 이 형식으로 증표를 발급할 수 있는 것입니다.

로그인에서 쓰는 순서

  1. mina가 아이디·비밀번호로 로그인합니다.
  2. 서버가 확인을 마치고 “사용자 mina, 정해진 시각까지 유효”라는 내용을 담은 JWT에 서명해 발급합니다.
  3. 브라우저는 결과 조회 요청에 JWT를 함께 보냅니다. 예를 들어 요청 헤더에 Authorization: Bearer <JWT>를 넣습니다. Bearer는 이 토큰을 제시해 인증받는다는 뜻입니다.
  4. 서버는 신뢰하는 발급자의 서명, 만료 시각, 이 서비스용 토큰인지 등을 검증합니다. 내용을 읽는 것만으로 인증을 통과시키지 않습니다.
  5. 검증 후에도 결과의 소유자가 mina인지 확인합니다. 유효한 JWT가 있어도 남의 결과를 볼 권한까지 생기지는 않습니다.
ByteByteGo JWT 설명도. 위쪽은 헤더·페이로드·서명 구조, 왼쪽 아래는 로그인·토큰 발급·요청·검증 흐름, 오른쪽 아래는 대칭키와 비대칭키 서명 방식입니다.
출처: ByteByteGo · JWT 101 (새 탭). 이미지를 누르면 원본 크기로 확대할 수 있습니다.

왼쪽 아래 흐름부터 보세요. ① 로그인 → ② 계정 확인 → ③ JWT 발급 → ④ 브라우저 보관 → ⑤ 요청에 JWT 첨부 → ⑥ 서버 검증 → ⑦ 데이터 응답 순서입니다. 위쪽은 토큰의 구조이고, 오른쪽 아래 서명 방식은 심화 내용입니다.

그림의 Secret은 JWT에 담아 보내는 값이 아니라 서명을 만드는 데 사용하는 비밀키입니다. 그림의 Base64 표기는 정확히는 Base64url이며, 요청 헤더 이름은 Authorization입니다. 서버의 토큰 발급 응답과 브라우저가 이후 요청에 넣는 헤더를 구분해서 보세요. 서버는 서명 외에 만료 시각 등 유효 조건도 확인합니다.

이번 실습은 JWT 대신 서버 세션을 사용합니다. 그림은 비교 설명용입니다. JWT의 Stateless는 토큰 검증을 위해 사용자별 세션 장부를 조회하지 않을 수 있다는 뜻이지, 사용자 데이터나 접근 권한 검사까지 필요 없다는 뜻은 아닙니다.

문자열 안에는 무엇이 들어갈까?

여기서 설명하는 일반적인 서명 JWT는 점으로 구분된 세 부분입니다. 아래는 모양을 설명하는 예시이며 실제 토큰은 아닙니다.

헤더.페이로드.서명
부분 쉬운 뜻 담는 내용
헤더(Header) 증표의 형식 안내 토큰 종류, 서명 알고리즘
페이로드(Payload) 증표에 적힌 정보 사용자 식별자, 만료 시각 등
서명(Signature) 위조 여부를 검사하는 값 발급 후 내용이 바뀌었는지 검증할 때 사용

페이로드를 풀어 읽으면 다음과 같은 JSON이 나올 수 있습니다. 예시 숫자는 만료 시각을 나타내며, 실행에 사용할 토큰이 아닙니다.

{"sub": "mina", "exp": 2000000000}

sub는 이 토큰이 누구에 대한 것인지, exp는 언제 만료되는지를 뜻합니다. exp는 1970-01-01 00:00:00 UTC부터 센 초 단위 시각입니다. 이런 정보 항목을 클레임(claim)이라고 부릅니다.

헤더와 페이로드의 Base64url은 데이터를 문자열로 표현하는 인코딩이지, 내용을 숨기는 암호화가 아닙니다. 일반적인 서명 JWT는 내용을 읽을 수 있으므로 비밀번호나 민감정보를 넣지 않습니다. mina를 admin으로 고쳐도 서명이 맞지 않아 검증에서 거부됩니다. JWT에는 암호화 형태도 있지만 여기서는 서명 형태만 비교합니다. JWT 표준 설명

이번 실습의 세션·쿠키와 비교하면

개념 비유 사용자 확인 방법
이번 실습의 세션 ID 번호만 적힌 번호표 서버 장부에서 번호에 연결된 사용자 조회
로그인용 서명 JWT 정보와 위조 방지 표시가 담긴 출입증 서명과 유효 조건 검증 후 사용자 정보 확인
쿠키 브라우저가 증표를 보관하고 전달하는 수단 그 자체가 사용자 확인 방식은 아님

쿠키에는 세션 ID도, JWT도 담을 수 있습니다. 따라서 쿠키와 JWT 중 하나를 고르는 관계가 아닙니다. JWT가 모든 서버 상태를 없애거나 로그아웃·폐기 문제까지 자동 해결하지는 않습니다. 출입증처럼 토큰을 가진 사람이 사용할 수 있으므로 토큰 값도 공유하지 않습니다.

이번 실습은 JWT를 만들지 않습니다. ax_lab_session 쿠키에는 임의의 세션 ID가 들어 있고, 서버 장부에서 사용자를 찾습니다. JWT는 같은 로그인 문제를 풀 때 만날 수 있는 또 다른 증표 형식으로 이해하면 됩니다.

05 · Cursor에서 새 앱 만들기

준비

  1. auth-lab이라는 새 폴더를 만들고 Cursor의 File → Open Folder로 엽니다.
  2. Terminal → New Terminal에서 py --version으로 Python 3.10 이상인지 확인합니다. py가 없다면 python --version을 확인하고 아래 첫 명령의 py를 python으로 바꿉니다. 둘 다 없으면 Python 설치 후 Cursor를 다시 엽니다.
  3. Cursor Agent에 아래 프롬프트를 붙여 넣습니다. 생성된 파일을 확인한 뒤 실행합니다.

이 폴더에 Python 3.10+와 Flask로 로그인 교육용 미니 앱을 만들어줘.
목표는 로그인 후 내 CSV 작업 결과만 보고 타인 결과는 거부되는 것을 확인하는 거야.

파일: app.py, templates/index.html, sample-results.json, requirements.txt, README.md, .gitignore.
Flask==3.1.3을 사용하고 Node.js, DB, 외부 서비스, Worker 실행, 회원가입은 넣지 마.
Flask 한 프로세스가 화면과 API를 제공하고 127.0.0.1:5000에서 debug=False로 실행해.

가상 계정 mina와 joon, 공통 실습 비밀번호 study1234를 제공해.
Werkzeug generate_password_hash/check_password_hash로 비밀번호를 검증해.
sample-results.json에 각 계정의 결과 하나씩을 저장해:
mina-001: owner=mina, source_file=sample.csv, row_count=4,
columns=[project,owner,status], missing_values={project:0,owner:2,status:1}.
joon-001: owner=joon, source_file=survey.csv, row_count=3,
columns=[project,owner,status], missing_values={project:0,owner:0,status:1}.
두 결과의 status는 completed. 파일 경로는 app.py 위치 기준으로 처리해.

POST /api/login: JSON username/password 검사 후 임의 세션 ID 발급.
Flask 기본 session 쿠키 대신 서버 메모리 딕셔너리에 사용자와 만료 시각 저장.
ID는 secrets.token_urlsafe(32), 수명은 1800초. 재로그인 시 이전 ID 폐기.
쿠키에는 ID만 저장하고 HttpOnly, SameSite=Lax, Path=/ 설정.
GET /api/me: 현재 사용자 반환, 비로그인은 401.
GET /api/results: 서버가 owner를 확인해 내 결과만 반환.
GET /api/results/작업ID: 내 결과는 200, 타인은 403, 없는 ID는 404.
모든 결과 API는 인증을 먼저 검사해. 사용자 이름을 쿼리로 받아 신뢰하지 마.
POST /api/logout: 서버 세션 삭제와 쿠키 삭제. 만료·변조 세션은 401.
변경 요청은 Origin이 http://127.0.0.1:5000인지 확인하고 아니면 거부해.
Host도 127.0.0.1:5000만 허용하고 응답은 Cache-Control: no-store로 설정해.
입력 타입 오류는 400으로 처리하고 비밀번호·세션 ID를 로그에 남기지 마.

화면: 실습 계정 안내, 로그인 폼, 현재 사용자, 내 결과 조회,
다른 계정 결과 접근 확인 버튼, 로그아웃. HTTP 상태와 한국어 이유를 보여줘.
타인 결과 버튼은 실제 API를 호출해야 해. 결과는 textContent로 표시해.
localStorage에 인증 정보를 저장하지 마. 새로고침 시 /api/me로 상태 확인해.
실패·로그아웃 시 이전 사용자의 결과를 지우고 서버 연결 오류도 안내해.

README에 Windows Cursor 터미널 실행 명령 3개와 접속 주소를 적어줘:
py -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe app.py

검증: 비로그인 401, 틀린 비밀번호 401, 본인 결과 200, 타인 결과 403,
없는 결과 404, 새로고침 유지, 로그아웃·만료·서버 재시작 후 401.
서버를 재시작해도 결과 파일은 남아야 해.
.gitignore에는 .venv/와 __pycache__/를 넣어줘.
로컬 교육용이며 인터넷 배포는 이번 범위가 아니라는 설명을 넣어줘.

터미널에서 실행

py -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe app.py

명령을 위에서부터 순서대로 실행하고 터미널을 켜 둔 상태로 브라우저에서 http://127.0.0.1:5000을 엽니다. 종료는 터미널에서 Ctrl+C입니다. 가상환경 활성화나 두 번째 터미널은 필요하지 않습니다. 처음 패키지를 설치할 때는 인터넷 연결이 필요합니다.

막히면 완성 예제 ZIP을 내려받아 압축을 풀고, app.py가 보이는 auth-lab 폴더를 Cursor에서 열어 같은 명령을 실행하세요. 참고: 실행 안내 · 서버 코드 · 샘플 결과.

06 · 성공과 거부를 직접 확인하기

순서 직접 할 일 정상 결과
1 로그인 전 내 결과 조회 401 · 인증 필요
2 mina / 틀린 비밀번호 입력 401 · 로그인 실패
3 mina / study1234 로그인 후 조회 200 · mina-001만 표시
4 다른 계정 결과 접근 확인 클릭 403 · 서버가 거부
5 브라우저에서 /api/results/joon-001 직접 열기 403 · 버튼을 우회해도 거부
6 새로고침 후 내 결과 조회 로그인 유지, 본인 결과 표시
7 로그아웃하고 다시 조회 401, 이전 결과 지워짐
8 joon / study1234로 로그인 joon-001만 표시, mina 결과는 403
9 로그인 상태에서 /api/results/not-found 열기 404
10 서버를 Ctrl+C로 종료 후 마지막 명령 재실행 다시 로그인 필요, 결과 파일은 그대로

개발자도구의 Network에서 /api/results 응답을 확인하세요. 화면에 타인 결과를 숨기는 것이 아니라 응답 JSON 자체에 타인 결과가 없는지 봅니다. Application의 Cookies에서는 ax_lab_session과 HttpOnly 설정을 확인합니다. 쿠키 값을 공유하거나 외부 사이트에 붙여 넣지 않습니다.

선택 확인: 개발자도구에서 쿠키 값을 임의 문자열로 바꾸면 401입니다. 참고 코드의 SESSION_TTL을 5로 바꿔 서버를 재시작하고 다시 로그인한 뒤 5초가 지나면 조회가 401인지 확인합니다. 이후 1800으로 되돌립니다.

막혔을 때

증상 확인할 것
app.py 또는 requirements.txt를 찾지 못함 Cursor에서 연 폴더와 터미널 위치가 일치하는지 확인
No module named flask 설치·실행에 모두 .venv의 python.exe를 사용했는지 확인
접속되지 않음 서버 터미널이 실행 중인지 확인. HTML 파일을 더블클릭하지 않기
주소 안내 또는 Origin 거부 localhost 대신 정확히 http://127.0.0.1:5000 사용
포트가 이미 사용 중 전에 실행한 실습 서버를 해당 터미널에서 Ctrl+C로 종료
새로고침 후 401 세션 만료·서버 재시작·쿠키 삭제 여부 확인. localStorage로 우회하지 않기

07 · 파일이 남는데 DB는 왜 필요할까?

파일도 데이터를 보관할 수 있습니다. DB를 배우는 이유는 저장 여부 하나가 아닙니다. 사용자가 늘면 “mina의 지난달 작업”, “실패한 작업만”, “소유자가 바뀐 결과”를 정확하게 찾고 관계를 관리해야 합니다. 여러 사람이 동시에 수정할 때의 일관성도 필요합니다.

21주차는 검색·관계·동시 수정이라는 질문에서 출발합니다. 트랜잭션은 그때 다룹니다. 이번 결과 조회 앱은 가상 데이터를 사용하는 로컬 교육용이며, 회원가입·배포·실제 Worker 연결은 포함하지 않습니다. 인터넷 서비스에는 HTTPS, Secure 쿠키, 영속 세션 저장소와 별도의 운영 설계가 필요합니다.

공식 자료

← 19주차 Worker · 전체 학습 노트