LabHub
学习 学习路径 课程

FastAPI — 타입이 곧 계약이다 · 토큰이 있어도 남의 문서는 읽을 수 없다 · 讲解

토큰이 있어도 남의 문서는 읽을 수 없다의 설계 원리

在 LabHub 中继续学习

한 줄 요약

인증·권한·소유권을 분리하고 같은 404로 리소스 존재를 숨깁니다.

概念图: 한 줄 요약 · 왜 이게 필요했나 · 어떻게 동작하나 · 계약을 읽고 실패를 예측하는 워크시트

왜 이게 필요했나

로그인한 사용자가 주소의 문서 번호만 바꾸어 다른 팀의 문서를 읽었다. 토큰이 유효하다는 사실과 특정 문서를 읽을 권리는 별개다. 이 실습에서는 고정 토큰 사전을 사용해 인증 서버를 대신한다. 토큰을 발급하거나 JWT 서명을 구현하는 실습이 아니라, 인증 결과 이후의 권한 경계를 구현하는 실습이다.

어떻게 동작하나

요청은 Bearer 헤더 형식 검사, 토큰 조회, scope 확인, 소유자 확인 순서로 진행한다. 인증 정보가 없거나 틀리면 401이고 WWW-Authenticate 헤더를 보낸다. 신원은 확인됐으나 read 권한이 없으면 403이다. 읽을 권한이 있는 사용자가 없는 문서나 남의 문서를 요청하면 둘 다 404다. 공개 응답에는 id와 title만 남겨 내부 소유자와 비용을 유출하지 않는다.

헤더 → 인증 401 → scope 403 → 소유권/존재 404 → 공개 필드 200

계약을 읽고 실패를 예측하는 워크시트

다음은 구현을 통째로 외우는 답안이 아니라 단계별 코드 리뷰입니다. 각 변경 조각은 의도적으로 계약을 깨뜨립니다. 변경 후에도 정상 사례가 통과할 수 있다는 점에 주의하세요. 실행 전에 어느 입력·예외·상태를 관측하면 차이가 드러날지 예상하고, 구현 후에는 그 예상과 결과를 비교합니다.

1. Bearer 헤더를 분리한다

bearer(header)는 정확히 'Bearer '로 시작하고 뒤에 공백 없는 토큰 하나가 있을 때 토큰을 반환합니다. None·빈 토큰·다른 scheme·추가 공백은 ValueError입니다.

판단의 근거: 헤더를 임의로 여러 조각으로 나누면 공백 오류를 정상 토큰으로 받아들일 수 있습니다.

리뷰할 잘못된 변경 조각:

header[6:]

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

2. 신원을 복사해 반환한다

principal(token, users)는 토큰 사전의 사용자 {id, scopes}를 반환하되 scopes 리스트까지 복사합니다. 모르는 토큰은 ValueError입니다.

판단의 근거: 반환된 scopes를 수정해 원래 사용자 권한까지 바뀌면 요청 간 권한이 섞입니다.

리뷰할 잘못된 변경 조각:

user["scopes"]

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

3. 권한은 정확히 비교한다

require_scope(user, scope)는 scopes에 scope 문자열이 정확히 있을 때 None, 없으면 PermissionError를 냅니다. read-all은 read가 아닙니다.

판단의 근거: 부분 문자열 비교는 더 긴 권한 이름을 다른 권한으로 오해합니다.

리뷰할 잘못된 변경 조각:

if False:

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

4. 소유권을 별도로 확인한다

visible(user, document)는 document가 None이 아니고 owner가 user의 id와 정확히 같을 때만 True입니다.

판단의 근거: 리소스 없음과 타인 소유를 같은 판정으로 묶습니다.

리뷰할 잘못된 변경 조각:

True

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

5. 응답 필드를 허용 목록으로 고른다

public_document(document)는 id와 title만 가진 새 딕셔너리입니다. owner나 internal_cost는 포함하지 않습니다.

판단의 근거: 원본에서 필드를 지우지 말고 새 응답을 조립합니다.

리뷰할 잘못된 변경 조각:

("id", "title", "owner")

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

6. 오류를 HTTP 계약에 맞춘다

authenticate(header, users)는 bearer와 principal을 연결합니다. ValueError는 HTTPException(401)이며 headers의 WWW-Authenticate 값은 Bearer입니다.

판단의 근거: 인증 실패와 애플리케이션 오류를 500 하나로 뭉치지 않습니다.

리뷰할 잘못된 변경 조각:

HTTPException(403,

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

7. 거절 순서를 고정한다

read_document(user, documents, document_id)는 read scope 없으면 HTTPException(403), 없거나 남의 문서면 HTTPException(404), 아니면 public_document 결과입니다.

판단의 근거: 인증 이후에도 scope와 소유권은 각각 확인해야 합니다.

리뷰할 잘못된 변경 조각:

HTTPException(403, "not found")

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

8. 실제 요청에서 경계를 닫는다

create_app(users, documents)는 GET /documents/{document_id}에서 Authorization 헤더를 받아 authenticate와 read_document를 호출하는 FastAPI 앱을 반환합니다. 200·401·403·404와 비공개 필드 제거를 실제 요청으로 검증하세요.

판단의 근거: 함수가 따로 맞아도 경로에서 호출을 빠뜨리면 접근 제어는 적용되지 않습니다.

리뷰할 잘못된 변경 조각:

authorization: str | None = None

이 조각이 들어간 함수의 공개 계약과 비교해 보세요. 성공 사례 하나로는 구분되지 않는다면 거절되어야 할 입력이나 실패 이후의 상태를 관측 대상으로 선택합니다.

현장에서 만나는 모습

고정 토큰 사전은 교육용 입력이다. 운영 인증에는 만료·서명·폐기·안전한 보관이 추가로 필요하다. 404를 같게 만드는 것만으로 응답 시간이나 접근 로그를 통한 모든 추론이 사라지는 것도 아니다. 거절된 요청이 원본 데이터까지 바꾸지 않았는지 같이 검사한다.

다음 실습에서 할 것

여덟 단계가 하나의 실행 가능한 결과물로 이어집니다. Bearer 헤더를 분리한다 → 신원을 복사해 반환한다 → 권한은 정확히 비교한다 → 소유권을 별도로 확인한다 → 응답 필드를 허용 목록으로 고른다 → 오류를 HTTP 계약에 맞춘다 → 거절 순서를 고정한다 → 실제 요청에서 경계를 닫는다.

각 단계는 함수나 파일이 존재한다는 사실이 아니라 실제 반환값·예외·상태 변화를 검사합니다. 정답을 본 뒤에는 일부러 경계 비교나 정리 코드를 바꾸어 어떤 시험이 실패하는지 확인하세요. 앞선 시험이 다음 단계에서도 유지되는 이유를 설명하고, 이 실습이 보장하지 않는 운영 조건을 한 가지 적어 보세요.