Python 초보자를 위한 FastAPI(5) 1
지난 파트에서 기본적인 HTTP 권한 부여를 구현했다. 모든 것이 작동하는 것 같지만 여전히 뭔가 부족한 느낌이다.
인증 시스템은 작동하지만 앱은 실제 사람이 사용해야 한다. 기본 인증은 이 경우에 적합하지 않은 것 같다.
문제는 모든 요청에 실제 사용자에게는 실용적이지 않은 Authorization: Basic USERNAME:PASSWORD
헤더가 필요하다는 것이다. 사용자가 매번 username
과 password
를 입력하는 것을 원하지 않기 때문이다. 따라서 브라우저에 정보를 보관하기 위해 무언가를 해야 한다. 하지만 사용자의 비밀번호를 클라이언트 측에 저장하는 것은 결코 좋은 생각이 아니다.
이것이 바로 API 키가 있는 이유이다. 이는 민감한 정보를 포함하지 않는 짧은 수명의 일련의 횡설수설이며, 필요한 경우 언제든지 취소할 수 있습니다. API 키는 브라우저의 로컬 저장소에 저장하거나 쿠키 형태로 저장할 수 있으므로 사용자의 인증 정보가 유출될 염려가 없다.
지난번처럼 트렌드를 파악하고 최신 기술을 사용하는 대신. 먼저 옛날 방식 세션 쿠키를 사용하도록 한다.
지금까지 해온 거의 모든 작업에서 FastAPI의 문서는 훌륭했습니다. 하지만 문서에는 세션 쿠키를 사용하는 방법에 대한 설명이 거의 없다...
하지만 FastAPI가 Starlette 위에 구축되었다는 것은 알고 있습니다. 그럼 거기서 뭔가 찾을 수 있을 수도 있을 것이다.
그 전에 사용자 "john"이 로그인하고 로그아웃할 수 있는 두 개의 엔드포인트가 필요하다. 이것은 어렵지 않을 것이다.
app
├── __init__.py
├── main.py
├── dependencies.py
├── routers
├── __init__.py
├── tracks.py
├── users.py
├── login.py
├── logout.py
dockerfile
docker-compose.yml
requirements.txt
이제 다음과 같은 파일을 만들었겼다. login.py
와 logout.py
라는 두 경로를 추가하고 dependencies.py
라는 새 파일을 생성하여 한 모듈에 국한되지 않는 내용을 저장한다.
그런 다음 main.py
에 연결한다.
app.include_router(tracks.router, prefix="/tracks", tags=["Tracks"], responses=fallback)
app.include_router(users.router, prefix="/users", tags=["Users"], responses=fallback)
app.include_router(login.router, prefix="/login", tags=["Login"], responses=fallback)
app.include_router(logout.router, prefix="/logout", tags=["Logout"], responses=fallback)
지금. 세션... FastAPI 문서에 언급된 적이 없는 무엇일가?
그래서 Starlette 문서로 이동하여 SessionMiddleware
는 것을 찾았다.
Note
서명된 쿠키 기반 HTTP 세션을 추가한다. 세션 정보는 읽을 수 있지만 수정할 수는 없다.request.session
딕셔너리 인터페이스를 사용하여 세션 데이터에 액세스하거나 수정한다.
우리가 필요한 것과 같다.
먼저 미들웨어를 사용하는 방법을 알아야 한다. FastAPI 인스턴스인 app
에 add_middleware
라는 메서드가 있다.
app.add_middleware(
SessionMiddleware,
same_site="strict",
session_cookie=MY_SESSION_ID,
secret_key="mysecret",
)
그리고 add_middleware
는 여러 인수를 받는다. 첫 번째는 실제 사용하려는 미들웨어이고 나머지는 미들웨어에 대한 옵션이다.
이 접근 방식을 사용하려면 보안을 위해 https_only
를 설정하고 secret_key
를 다른 곳에 저장하고 mysecret
과 같은 어리석은 값 대신 무작위로 생성 된 값을 사용하는 것이 좋다.
Starlette 문서에 따르면 이제 request.session
을 액세스할 수 있어야 한다.
그러나 미들웨어에는 또 하나의 종속성 itsdangerous
이 필요하다. 사용하지 말아야 할 것 같은데... 아무튼 어떤 기능을 하는지 살펴보자.
Note
일부 데이터를 신뢰할 수 없는 환경으로 전송했다가 나중에 다시 가져오고 싶을 때가 있다. 이 작업을 안전하게 수행하려면 변경 사항을 감지할 수 있도록 데이터에 서명해야 한다. 본인만 아는 키가 주어지면 데이터를 암호화하여 서명하고 다른 사람에게 넘길 수 있다. 데이터를 돌려받으면 아무도 데이터를 변조하지 않았음을 확인할 수 있다. 수신자는 데이터를 볼 수 있지만 키를 가지고 있지 않으면 데이터를 수정할 수 없다. 따라서 키를 비밀스럽고 복잡하게 유지하면 괜찮을 것이다.
그렇다면 SessionMiddleware
가 쿠키 내부의 값에 서명하는 데 사용하는 것 같다? 하지만 좋은 기능인 것 같고 꼭 필요한 기능인 것 같다. 요구사항에 넣고 이미지를 다시 빌드해 보겠다.
fastapi==0.103.1
pydantic==2.3.0
uvicorn==0.23.2
itsdangerous==2.1.2
종속성이 포함되면 docker compose up -d --build
를 실행하여 이미지를 다시 빌드하고 컨테이너를 다시 시작한다.
이제 /login
라우트에 대한 작업을 시작할 수 있다. /login
라우트에서는 사용자가 username
과 password
가 포함된 양식을 제출해야 한다. 양식 데이터를 읽으려면 매개변수를 Form()
으로 Annotated
해야 한다.
from fastapi import ..., Form
하지만 작동하려면 여전히 추가 종속성이 필요하다. python-multipart
이다. requirement.txt
파일에 넣겠다.
fastapi==0.103.1
pydantic==2.3.0
uvicorn==0.23.2
python-multipart==0.0.6
itsdangerous==2.1.2
이제 다시 빌드해야 한다. 더 좋은 방법을 생각해내야 할 필요가 있다. 아마 다음 기회에 ...
컨테이너가 재시작되었으니 이제 /login
작업을 시작해 보겠다.
import secrets
from typing import Annotated
from fastapi import APIRouter, HTTPException, status, Form
from fastapi.security import APIKeyCookie
from fastapi.requests import Request
from dependencies import user, MY_SESSION_ID
router = APIRouter()
security = APIKeyCookie(name=MY_SESSION_ID)
@router.post("")
async def login(
request: Request,
username: Annotated[str, Form()],
password: Annotated[str, Form()],
):
is_correct_user = secrets.compare_digest(username, user["username"])
is_correct_password = secrets.compare_digest(password, user["password"])
if not is_correct_user or not is_correct_password:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
)
request.session.update({"username": username})
return {"success": True}
이 라우트에는 세 매개변수가 필요하다. request
, username
및 password
이다. username
과 password
는 매우 자명하며 사용자가 로그인하는 데 필요하고 양식 데이터의 일부이다. request
는 request.session
을 액세스하기 위해 필요하기 때문이다. 그리하여 그 값을 수정할 수 있다. 이 경우 username
을 사용자의 세션 사전에 넣는다. 그리고 마지막으로 요청이 성공적으로 완료되었음을 알 수 있는 성공 메시지를 프론트엔드에 반환한다.
이제 사용자가 로그인이 가능하니 /users/me
라우트를 다시 작성할 차례이다.
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import APIKeyCookie
from dependencies import MY_SESSION_ID
from fastapi.requests import Request
router = APIRouter()
security = APIKeyCookie(name=MY_SESSION_ID)
@router.get("/me", dependencies=[Depends(security)])
async def get_current_user(
request: Request,
):
if not request.session:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
)
return request.session
훨씬 깔끔해졌다!
그래서 가장 먼저 해야 할 일이 있다. 보안 전략을 HTTPBasic
에서 APIKeyCookie
로 변경하고 SessionMiddleware
에서 설정한 것과 동일한 이름을 지정해야 한다.
그리고 get_current_user
함수 내에서 세션 쿠키가 존재하는지 여부만 확인하면 된다. 요청에 세션이 없으면 401
오류를 발생시킨다. 요청에 유효한 세션 쿠키가 포함되어 있으면 username
을 기억하지 못하는 것처럼 username
이 무엇인지 알려주면 된다.
정말 많은 일을 해온 것 같다. 이 흐름이 예상한 대로 작동하는지 확인해 보자.
내 /login
라우트에는 "Request body"에 명시된 대로 양식이 필요하며, username
과 password
필드는 문자열과 필수로 포시된다.
"Execute" 버튼을 클릭하면 다음과 같은 응답 헤더와 {"success": true}를 반환한다.
HTTP/1.1 200 OK
date: Fri, 13 Oct 2023 11:50:02 GMT
server: uvicorn
content-length: 16
content-type: application/json
set-cookie: my_session_id=eyJ1c2VybmFtZSI6ICJqb2huIn0=.ZSku6g.G50HfhgGbz-_Wi9qgGrXEbVlXeg; path=/; Max-Age=1209600; httponly; samesite=strict
쿠키도 적절히 설정되었다.
users/me
라우트로 이동하여 "john"을 되찾을 수 있는지 시도해 보자.
오른쪽 상단에 작은 자물쇠 아이콘이 보인다. 이는 이 경로가 보호된 경로임을 나타내는 것이다. 그리고 실행하여 정확히 응답을 받는다.
마지막 단계는 "john"이 우리 앱에서 로그아웃할 수 있는 방법이 필요하다. 개발 도구에서 세션 쿠키를 제거하라고만 할 수는 없기 떄문이다.
사용자가 로그아웃하는 방법은 매우 간단하다.
from fastapi import APIRouter
from fastapi.security import APIKeyCookie
from fastapi.requests import Request
router = APIRouter()
@router.post("")
async def logout(
request: Request,
):
request.session.clear()
return {"success": True}
/login
과 마찬가지로 프론트엔드 친화적인 백엔드 개발자라는 이유만으로 성공 메시지를 반환한다.
한번 시도하여 보자!
HTTP/1.1 200 OK
date: Fri, 13 Oct 2023 11:59:57 GMT
server: uvicorn
content-length: 16
content-type: application/json
set-cookie: my_session_id=null; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; httponly; samesite=strict
그리고 쿠키가 성공적으로 취소되었다. users/me
로 이동하여 사용자 데이터를 다시 요청하면 이제 {"detail": "Not authenticated"}
를 반환한다.
개념적으로 동일한 한 가지 작업을 수행하는 방법에는 여러 가지가 있을 수 있다. 먼저 HTTPBasic
접근 방식을 시도했다. 잘 작동하지만 "사용자 친화적"이지 않아서 별로 마음에 들지 않았다. 개인적으로는 이 세션 쿠키 접근 방식이 실제 사용자에게 더 좋다고 생각한다. 그리고 현재 우리가 사용하고 있는 많은 웹사이트에서 여전히 널리 사용되고 있다.
하지만 인증 세계에는 더 많은 것이 있다! 실제 사용자와 컴퓨터 모두에 적합한 일반 API 토큰이 있다. 그리고 상태 비저장, 다자간 서비스 등에서 흔히 사용되는 JSON 웹 토큰(JWT)도 있다.
다음에는 요즘 많이 사용되는 인증과 권한 부여 방식인 OAuth2를 구현해 볼 예정이다. 트렌드를 따라잡기 위해서이다.
이 포스트는 여기까지이다. 세션 쿠키를 수행하는 방법을 FastAPI 문서에서 찾을 수 없다면 이 포스팅이 도움이 될 것이다.
모든 소스 코드는 저자의 깃허브에서 찾을 수 있다.
1: 이 페이지는 FastAPI by A Python Beginner (5)을 편역한 것임.