1. 인증 및 인가
웹 인증방법과 도구는 다양하다
- 유저아이디/비밀번호: 전통적인 HTTP 기본 및 다이제스트 인증
- API 키: 비밀정보가 담긴 임의의 긴 문자열
- OAuth2: 인증 및 인가를 위한 일련의 표준
- JSON 웹 토큰(JWT): 암호로 서명된 유저 정보 인코딩 방식
# HTTP 기본 인증을 사용해 유저 정보 가져오기: auth.py
import uvicorn
from fastapu import Depends, FastAPI
from fastapi.security import HTTPBasic, HTTPBasicCredentials
app = FastAPI()
basic = HTTPBasic()
@app.get("/who")
def get_user(
creds: HTTPBasicCredentials = Depends(basic)):
return {"username": creds.username, "password": creds.password}
if __name__ == __main__":
uvicorn.run("auth:app", reload=True)
1.1 OAuth2
개방형 인증의 약자인 OAuth2 2.0은 웹사이트나 애플리케이션이 유저를 대신해 웹 앱에서 호스팅하는 리소스에 접근할 수 있도록 설계된 표준이다. 다양한 상황에 맞는 flow를 제공하며 여기서는 인증 코드 flow를 사용할 것이다.
- JWT 다루기
pip install python-jose[cryptography]
- 비밀번호 다루기
pip install bcrypt
-양식 다루기
pip install python-multipart
- Pydantic의 상속을 사용한 유저모델 정의
from pydantic import BaseModel
# 유저의 기본 모델. name: 유저의 이름. 외부로 노출되는 유일한 속성
class PublicUser(BaseModel):
name: str
# 유저가 처음 가입 시 사용할 요청 본문의 Pydantic 모델. PublicUser 상속
class SignInUser(PubliUser):
password: str
# PrivateUser: FastAPI 서버 내부에서 서비스-데이터 계층 통신 시 사용
class PrivateUser(PublicUser):
hash: str
- 유저 서비스 계층 : OAuth2와 JWT 함수가 추가되었다는 점에서 다른 서비스 계층 모듈과 다르다. 여러 함수가 웹 계층보다 유저 서비스 계층에 있는 것이 더 깔끔하다고 생각하지만, OAuth2 웹 계층 함수 몇개는 web/user.py에 작성하는 것이 낫다. CRUD 함수는 거의 통과함수지만 차후 메트릭을 만드는 데 유용하다. 이 디자인은 유저 데이터에 접근하기 위해 데이터 계층을 런타임에 사용할 수 있도록 지원한다.
# service/user.py : 책의 저작권 때문에 코드의 일부만 기입했다. 주석은 코드에 필요한 순서이다.
from datatime import timedelta, datatime
import os
from jose import jwt
import bcrypt
from model.user import PublicUser, PrivateUser, SignInUser
if os.getenv("CRYPTID_UNIT_TEST"):
from fake import user as data
else:
from data import user as data
# 시크릿키 변경필요
SECRET_KEY = "HELLOWORLD!@#$"
ALGORITHM = "HS256"
def verify_password(plain: str, hash: str) => bool:
"""plain을 해시 값과, 데이터 베이스의 hash 값과 비교한다."""
password_bytes = plain.encode('utf-8')
hash_bytes = hash.encode('utf-8')
is_valid = bcrypt.checkpw(password_bytes, hash_bytes)
return is_valid
def get_hash(plain: str) -> str:
"""plain의 해시값을 반환한다."""
password_bytes = plain.encode('utf-8')
salt = bcrypt.gensalt()
hashed_password = bcrypt.hashpw(password.bytes, salt)
return hashed_password.decode('utf-8')
# JWT 접근 토큰으로부터 username를 반환한다.
# OAuth 토큰을 풀어서 PublicUser를 반환한다.
# 데이터베이스에서 username에 매칭되는 User를 반환한다.
# name과 plain 암호로 유저를 인증한다.
# JWT 접근 토큰을 반환한다.
1.2 JWT
JWT는 인증방법이 아닌 인코딩 체계이며, 저수준의 세부사항은 RFC7519에 정의됐다. OAuth2 및 기타방법에 대한 인증정보를 전달하는 데 사용한다. JWT는 세가지 섹션이 있는 읽기가능한 문자열이다.
- header: 사용된 암호화 알고리츰 및 토큰유형
- payload
- signature
각 섹션은 Base64URL 형식으로 인코딩된 JSON 문자열로 구성된다.
1.3 서드파티 인증: OIDC
많은 웹사이트에서 구글, 페이스북 등 OAuth2를 기반으로 구축된 OIDC라는 표준을 사용하는 경우가 많다. 외부 OIDC 사용 사이트에 연결하면 이 장의 예시에서와 같이 OAuth2 접근토큰과 ID 토큰이 반환된다. FastAPI에서 사용하는 서드파티 패키지는 다음과 같다.
- FastAPI OIDC
- fastapi-서드파티 인증
- FastAPI 리소스 서버
- oauthlib
- OIC
- OIDC 클라이언트
- OpenID Connect
향후 공식문서와 튜토리얼에 FastAPI OIDC 예시가 포함될것이라고 한다.
1.4 인가
인증은 '누가'를, 인가는 '무엇'(어떤 리소스/웹 엔드포인트에 어떤 방식으로 접근할 지)를 처리한다. 인증사레의 진행과정을 보자.
1. 관리자 방문자만 추적하고 나머지는 익명으로 유지
- 인증된 유저 아이디의 Admin 테이블을 사용. 이름을 조회하고 일치하면 유저테이블에서 해시된 비밀번호와 대초한다.
2. 모든 방문자를 인증하지만 일부 엔드포인트는 관리자권한을 부여해야 하는 경우
- 앞과 같이 유저 테이블에서 모든 유저를 인증한 다음, Admin 테이블을 확인해 이 유저가 관리자인지 확인한다.
3. 두가지 이상의 권한 유형에 대해
- Permission 정의 테이블을 사용한다.
- UserPermission 테이블을 사용한다. 유저와 권한을 쌍으로 묶는 이 테이블을 접근 제어 목록이라고 한다.
4. 권한 조합이 복잡한 경우 레벨을 추가하고 역할(독립된 권한 집합)을 정의한다.
- Role 테이블을 만든다.
- User 및 Role 항목을 페어링하는 UserRole 테이블을 만든다. 이를 역할기반 접근제어(RBAC)라고도 한다.
1.5 미들웨어
FastAPI를 사용하면 웹 계층에서 다음 작업을 수행하는 코드를 삽입하는 것이 가능하다.
- 요청을 가로챈다.
- 요청에 대한 작업을 수행한다.
- 요청을 경로함수에 전달한다.
- 패치함수가 반환한 응답을 가로챈다.
- 응답으로 무언가를 수행한다.
- 호출자에게 응답을 반환한다.
위의 작업은 파이썬 데코레이터가 래핑하는 함수에 수행하는 작업과 유사하다. 미들웨어나 Depends()를 사용한 의존성 주입 기능을 사용할 수 있다. 미들웨어는 CORS같은 글로벌 보안문제인 경우 더 편리하다.
1.6 CORS
교차 출처 리소스 공유(Cross-origin resource sharing)는 웹 사이트가 신뢰하는 다른 서버와 통신하는 데 사용한다. 자바스크립트 프론트가 FastAPI와 같은 프레임워크 기반의 백엔드와 통신하면 Origin이 동일하지 않다.
- 프로토콜: HTTP 또는 HTTPs
- 도메인: google.com 또는 localhost 같은 인터넷 도메인
- 포트: 80, 443 또는 8000 같은 해당 도메인의 숫자 TCP/IP 포트
백엔드가 신뢰할 프론트를 지정하는 것이 CORS다. 대표적으로 설정하는 값은 Origin, HTTP 메서드/헤더, CORS 캐시 제한시간 등이 있다.
# CORS 미들웨어 활성화
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origin=["https://ui.cryptids.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/test_cors")
def test_cors(request: Request):
print(request)
1.7 서드파티 패키지
FastAPI 생태계는 빠르게 성장하고 있으며 많은 패키지가 작업을 대신한다. 아래의 패키지는 향후 사용도가 높을 것으로 보이는 유용한 패키지이다.
- FastAPI User
- FastAPI JWT
- FastAPI-Login
- fastapi-auth0
- AuthX
- FastAPI-User-Auth
- fastapi-authz
- fastapi-opa
- FastAPI-key-auth
- FastAPI Auth Middleware
- fastapi-jwt
- fastapi_auth2
2. 테스트
2.1 테스트의 종류
- 단위테스트 : 어떤 계층에서 개별기능을 테스트한다.
- 통합테스트 : 여러 계층에 걸쳐 연결성을 테스트한다.
- 전체테스트 : 전체 API와 그 아래 있는 스택을 테스트한다.
2.2 테스트 대상
주어진 입력대로 출력되는지, 입력 누락, 중복입력, 입력 타입/순서 오류, 입력값 오류, 대규모 입출력 등을 확인할 수 있다.
- 웹계층 : Pydantic이 모델과의 불일치를 감지하고 422 HTTP 상태코드를 반환한다.
- 데이터계층 : 데이터베이스가 누락 데이터, 중복 데이터, SQL 쿼리 구문 오류에 대해 예외를 발생시킨다. 대용량 데이터 결과를 제너레이터나 페이징을 통해 처리하지 않고 한번에 전달하면 시간초과나 메모리 부족이 발생할 수 있다.
- 모든계층 : 모든 버그가 발생할 수 있다.
2.3 pytest
파이썬은 Unittest라는 표준 패키지가 있었고 그것을 개선한 Nose라는 서드파티 패키지도 있다. 하지만 현재 대부분의 개발자는 기능이 더 많고 사용도 쉬운 pytest를 사용한다. 아래는 설치명령어와 기능이다.
pip install pytest
# 자동 mocker 픽스처 사용을 위해 설치
pip install pytest-mock
- 테스트 탐색: 파일명이 test로 시작하거나 끝나는 파이썬 파일을 찾아 자동으로 실행한다.
- 단언 실패 상세: 실패한 단언문은 예상 결과와 실제 결과를 보여준다.
- 픽스처: 픽스처 함수는 전체 테스트 스크립트에 대해 한번만 실행될 수도 있고 모든 테스트에 대해 실행될 수도 있다. 픽스처는 표준 테스트 데이터나 데이터베이스 초기화 데이터를 테스트 함수에 매개변수로 제공하는 역할을 한다. 픽스터는 일종의 의존성 주입이며, FastAPI가 웹 경로 함수에 의존성을 제공하는 것과 같이 일반 테스트 함수에 특정 테이터를 전달한다.
- 매개변수화 테스트 : 이것을 이용하면 테스트 함수에 여러 테스트 데이터를 제공할 수 있다.
2.3.1 모의
단위테스트는 코드베이스에 있는 각 함수에 대해 테스트용 인수를 전달하고 예상 값을 반환하는 지 확인하면 된다. 이 방법은 순수함수에 적합한데, 대부분의 함수는 다른 함수를 호출하기에 각 외부함수 호출을 모의(mocking)함으로써 해결할 수 있다. 파이썬에서 함수는 일급객체이므로 한 함수를 다른 함수로 대체할 수 있다.
# mod1.py
def preamble() -> str:
return "The sum is "
# mod2.py
import mod1
def summer(x :int, y: int) -> str:
return mod1.preamble() + f"{x+y}"
summer() 함수는 인수로 전달된 숫자를 합한 후, 값을 이어 문자열로 반환한다.
# pytest 스크립트: test_summer1.py
import mod2
def test_summer():
assert "The sum is 11"
만약 덧셈 작업의 성공여부만 테스트하고 싶다면 어떻게 해야할까?
from unittest import mock
import mod1
import mod2
# with 구문으로 컨텍스트 관리자로 다룸
def test_caller_a():
with mock.patch("mod1.preamble", return_value=""):
assert "11" == mod2.summer(5, 6)
# as mock_preamble로 선언한 모의 객체 사용
def test_caller_b():
with mock.patch("mod1.preamble") as mock_preamble:
mock_preamble.return_value = ""
assert "11" == mod2.summer(5, 6)
# 파이썬 데코레이터로 모의객체 정의, 인수로 전달
@mock.patch("mod1.preamble", return_value="")
def test_caller_c(mock_preamble):
assert "11" == mod2.summer(5, 6)
# mock_preamble 호출에 대한 return_value 따로 설정
@mock.patch("mod1.preamble")
def test_caller_d(mock_preamble):
mock_preamble.return_value = ""
assert "11" == mod2.summer(5, 6)
2.3.2 테스트 더블과 fake
모의 테스트를 수행하려면 summer () 함수가 mod1 모듈에서 preamble () 함수를 가져온다는 사실을 알아야했다. 이는 특정 변수와 모듈 이름에 대한 지식이 필요한 구조적 테스트였다. 이에 반해 함수의 지식이 필요없는 행위테스트를 실행하려면 테스트 더블을 사용하면 된다.
테스트 더블을 이용하면 테스트 대상 코드에서 우리가 원하는 부분을 분리할 수 있다. 예를 들어 preamble() 함수가 빈 문자열을 반환하도록 할 수 있다. 또 다른 방법은 import 구문을 이용하는 것이다.
# 단위테스트에서 사용하는 테스트더블
import os
if os.getenv("UNIT_TEST"):
import fake_mod1 as mod1
else:
import mod1
def summer(x:int, y:int) -> str:
return mod1.preamble() + f"(x+y)"
# 더블 모듈: fake_mod1.py
def preamble() => str:
return ""
# 테스트 스크립트: test_summer_fake.py
import os
os.environ["UNIT_TEST"] = "true"
import mod2
def test_summer_fake():
assert "11" == mod2.summer(5, 6)
import 전환 방법은 환경변수에 대한 검사를 추가해야한다. 하지만 함수호출에 대한 특정 모의 객체를 작성하지 않아도 된다.
2.3.3 통합테스트 자동화
통합테스트는 서로 다른 계층에 있는 코드가 상호작용을 얼마나 잘 하는지 검증한다. 이 책에는 DB로 SQLite를 사용하는데, 표준적인 SQL 구문을 사용한다면 다른 데이터베이스도 인메모리 SQLite를 이용해 적절하게 모의할 수 있다 비표준 SQL 구문을 사용한다면 다음 모듈을 활용해 특정 데이터베이스를 모의할 수 있다.
- PostgresSQL: pgmock
- MongoDB: Mongomock
- 그외 : pytest가 가장 널리 사용된다.
리포지터리 패턴은 중간에 있는 간단한 인메모리 데이터 저장소다. 실제 데이터베이스 작업에는 다단계 쿼리와 일종의 세션 처리가 필요하다. 리포지터리 패턴은 의존성 주입과 잘 어울리는 패턴이다.
2.3.4 전체 테스트 자동화
전체 테스트는 가능한 한 프로덕션 환경처럼 모든 계층을 함께 실행한다. API의 각 엔드포인트에 대한 전체 테스트는 두가지 방법으로 수행할 수 있다.
- HTTP/HTTPS: 서버에 접속하는 개별 파이썬 테스트 클라이언트를 작성한다. HTTPie와 같은 독립형 클라이언트나 Requests 스크립트를 이용해 이 작업을 수행한다.
- TestClient 모듈: FastAPI/Starlette에 내장된 객체를 활용해 명시적인 TCP 연결 없이 서버에 직접 접속한다.
'내용 복습 > python' 카테고리의 다른 글
FastAPI 공부내용 1일차 (0) | 2025.04.25 |
---|---|
데이터 공부 with 파이썬 2일차 (1) | 2025.02.08 |
데이터분석 with 파이썬 1 (0) | 2025.02.06 |