고성능, 안정적, 역방향 프록시와 같은 다양한 기능을 가진 경량화된 웹 서버
HTTP response(in access token) → Nginx/login → FastAPI/auth_check → Nginx/login → FastAPI/auth/success
Nginx
Dockerfile
FROM ubuntu:20.04
RUN apt update
RUN apt install -y curl \
vim \
wget \
build-essential \
libpcre3 \
libpcre3-dev \
zlib1g \
zlib1g-dev \
libssl-dev
WORKDIR /app
RUN wget https://nginx.org/download/nginx-1.24.0.tar.gz && \
tar -zxvf nginx-1.24.0.tar.gz
WORKDIR /app/nginx-1.24.0
RUN ./configure \
--sbin-path=/usr/bin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--pid-path=/var/run/nginx.pid \
--with-http_ssl_module \
--with-http_auth_request_module
RUN make
RUN make install
COPY nginx.conf /app
RUN mv /app/nginx.conf /etc/nginx/nginx.conf
CMD ["nginx", "-g", "daemon off;"]
nginx.conf
pid /var/run/nginx.pid;
events {}
http {
upstream api-server {
server fastapi:8000;
}
include /etc/nginx/mime.types;
server {
listen 80;
location /login {
auth_request /auth;
error_page 401 = @auth_failed;
rewrite / /auth/success break;
proxy_pass http://api-server;
}
location /auth {
internal;
rewrite / /users/me break;
proxy_pass http://api-server;
}
location /proxy_test {
rewrite / /check break;
proxy_pass http://api-server;
}
location @auth_failed {
internal;
rewrite / /users/me break;
proxy_pass http://api-server;
}
}
}
FastAPI
Dockerfile
FROM python:3.10.13-bullseye
WORKDIR /app
RUN pip install "fastapi[all]" "passlib[bcrypt]" "python-jose[cryptography]"
COPY main.py ./
CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--log-level", "debug"]
from datetime import datetime, timedelta, timezone
from typing import Annotated
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from passlib.context import CryptContext
from pydantic import BaseModel
# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
fake_users_db = {
"johndoe": {
"username": "johndoe",
"full_name": "John Doe",
"email": "johndoe@example.com",
"hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",
"disabled": False,
}
}
class Token(BaseModel):
access_token: str
token_type: str
class TokenData(BaseModel):
username: str | None = None
class User(BaseModel):
username: str
email: str | None = None
full_name: str | None = None
disabled: bool | None = None
class UserInDB(User):
hashed_password: str
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
app.router.redirect_slashes = False
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
def get_user(db, username: str):
if username in db:
user_dict = db[username]
return UserInDB(**user_dict)
def authenticate_user(fake_db, username: str, password: str):
user = get_user(fake_db, username)
if not user:
return False
if not verify_password(password, user.hashed_password):
return False
return user
def create_access_token(data: dict, expires_delta: timedelta | None = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise credentials_exception
token_data = TokenData(username=username)
except JWTError:
raise credentials_exception
user = get_user(fake_users_db, username=token_data.username)
if user is None:
raise credentials_exception
return user
async def get_current_active_user(
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.disabled:
raise HTTPException(status_code=400, detail="Inactive user")
return current_user
@app.post("/token")
async def login_for_access_token(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
) -> Token:
user = authenticate_user(fake_users_db, form_data.username, form_data.password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.username}, expires_delta=access_token_expires
)
return Token(access_token=access_token, token_type="bearer")
@app.get("/users/me", response_model=User)
async def read_users_me(
current_user: Annotated[User, Depends(get_current_active_user)]
):
return current_user
@app.get("/users/me/items")
async def read_own_items(
current_user: Annotated[User, Depends(get_current_active_user)]
):
return [{"item_id": "Foo", "owner": current_user.username}]
@app.get("/check")
async def access_check():
return {"message": "Access OK"}
@app.get("/auth/success")
async def success_auth():
return {"message": "Successful authentication!"}
@app.get("/security")
async def security_page():
return {"message": "Welcom security page."}
Docker Compose.yaml
version: "3.7"
services:
nginx:
image: vp_nginx:latest
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf
networks:
- my_network
fastapi:
image: fastapi_token:latest
ports:
- "8000:8000"
volumes:
- ./fastapi_token/main.py:/app/main.py
networks:
- my_network
networks:
my_network:
Fastapi의 /token 에서 ID/PW를 입력 후 Bearer Token 을 복사한다.
Postman 또는 curl 에서 Authorization 헤더에 Bearer Token 값을 추가 후 nginx/login 으로 보낸다.
성공 시
{
"message": "Successful authentication!"
}
실패 시
{
"detail": "Could not validate credentials"
}
위 처럼 인증 실패 시에도 Fastapi 서버의 message를 가져오는 것을 확인할 수 있다.