Install
pip install python-jose[cryptography]
pip install passlib[bcrypt]
from fastapi import FastAPI, Depends, HTTPException, Form, Request, Response, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from jose import jwt, JWTError
from datetime import datetime, timedelta
import uvicorn
app = FastAPI()
# Jinja2 템플릿 설정
templates = Jinja2Templates(directory="./")
# JWT 설정
SECRET_KEY = "your-secret-key" # 실제 사용 시 더 복잡한 값으로 변경해야 합니다.
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# 사용자 정보 (실제로는 DB나 외부 저장소에서 가져와야 합니다.)
fake_users_db = {
"test": {
"username": "test",
"password": "1234"
},
"test2": {
"username": "test2",
"password": "12345"
}
}
# OAuth2PasswordBearer 객체를 사용하여 토큰을 가져올 수 있습니다.
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + 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(request: Request, token: str = Depends(oauth2_scheme)):
try:
token = request.cookies.get("access_token")
if token is None:
return None
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
return None
return {"username": username}
except JWTError:
print('JWTError')
@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = fake_users_db.get(form_data.username)
if user is None or user["password"] != form_data.password:
raise HTTPException(status_code=401, detail="Invalid credentials")
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={"sub": user["username"]}, expires_delta=access_token_expires)
response = RedirectResponse(url="/secure-data/", status_code=status.HTTP_303_SEE_OTHER)
response.set_cookie(
key='access_token',
value=access_token,
httponly=True
)
return response
@app.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
return templates.TemplateResponse("login.html", {"request": request})
@app.get("/secure-data/")
async def get_secure_data(request: Request):
user = await get_current_user(request)
if user is None:
return {'message': 'login failed', 'user':user}
return templates.TemplateResponse("secure.html", {"request": request, "user": user['username']})
@app.get("/logout")
async def logout(request: Request):
response = templates.TemplateResponse("login.html", {"request":request})
response.delete_cookie(key="access_token")
return response
if __name__ == '__main__':
uvicorn.run("login_test:app", host="127.0.0.1", port=8000, reload=True)
로그인 된 상태로 다른 페이지에 넘어갔을 때, 로그인 정보를 잘 저장한 채 넘어가지는지
로그인하지 않고 secure-data에 접근했을 때, login fail을 반환하는지 체크
로그인 후, secure-data에 username 정보가 잘 나타나는 것 확인
하지만 그 후 코드 실행 종료하고, 로그인하지 않고 secure-data에 접근하자 username 정보가 또 뜨는 것을 확인
혹시 쿠키가 브라우저에 계속 남아있는 것이 아닌가 하여 다른 브라우저에서 로그인하지 않고 secure-data 접근하니 login fail 반환
쿠키란?

chatgpt 피셜
맨 마지막 문단 내용처럼 로그인 성공하면 매번 재로그인할 필요 없이 상태 유지 가능
@app.post("/token")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") 에서 (tokenUrl = “token”) 부분이 ‘/token’ Url 접근을 의미
/token 엔드포인트에선 사용자 인증, 액세스 토큰 발급 및 반환과 같은 작업을 처리한다.
get_current_user()
secure-data처럼 인증 보호가 된 엔드포인트에 접근할 때, 현재 토큰의 유효성을 검사하는 함수
jwt.decode() 함수와 SECRET_KEY, ALGORITHM 정보를 참조해 검사토큰이 유효하지 않거나 만료된 경우 Error raise
로그인 정보를 유지한 채 다른 페이지 이동?
user = await get_current_user(request)
if user is None:
return {'message': 'login failed'}
get_current_user() 함수를 await하며 user 정보를 받고, 만약 None을 받는다면(user 정보가 없거나 만료된 토큰) 예외 처리문제
현재 코드는 로그인 성공하면 html의 form action을 통해 /token 엔드포인트로 이동
이전 코드
@app.post("/token")
async def login_for_access_token(response: Response, form_data: OAuth2PasswordRequestForm = Depends()):
user = fake_users_db.get(form_data.username)
if user is None or user["password"] != form_data.password:
raise HTTPException(status_code=401, detail="Invalid credentials")
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={"sub": user["username"]}, expires_delta=access_token_expires)
response.set_cookie(
key='access_token',
value=access_token,
httponly=True
)
return RedirectResponse(url="/secure-data/", status_code=status.HTTP_303_SEE_OTHER)
이전엔 함수의 파라미터로 response를 받도록 사용
@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
user = fake_users_db.get(form_data.username)
if user is None or user["password"] != form_data.password:
raise HTTPException(status_code=401, detail="Invalid credentials")
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(data={"sub": user["username"]}, expires_delta=access_token_expires)
response = RedirectResponse(url="/secure-data/", status_code=status.HTTP_303_SEE_OTHER)
response.set_cookie(
key='access_token',
value=access_token,
httponly=True
)
return responseresponse를 함수에서 받는 것이 아닌 새로 만들어주고, 쿠키 세팅한 채로 return
Response는 뭘까?
HTTP 응답을 나타내는 클래스
FastAPI에서는 Response 객체를 반환하는 것으로 해당 응답을 클라이언트에게 전송
JSON, HTML, 파일 등 다양한 응답 유형을 자동으로 처리
자동으로 생성할 땐 직접 Response 객체를 만들 필요가 없음
특별한 응답을 생성하거나 상태 코드, 헤더 등을 변경해야 할 때는 Response 객체를 사용
# 자동 생성 된 response를 받는 경우
@app.get("/custom-response")
async def custom_response():
# Response 객체를 생성하여 커스텀 응답 생성
content = "This is a custom response."
headers = {"Custom-Header": "Value"}
response = Response(content, status_code=200, headers=headers)
return response
# 자동 생성 response를 받지 않고 내가 직접 생성
@app.get("/custom-response")
async def custom_response():
# Response 객체를 생성하여 커스텀 응답 생성
content = "This is a custom response."
headers = {"Custom-Header": "Value"}
response = Response(content, status_code=200, headers=headers)
return response
Python
@app.get("/logout")
async def logout(request: Request):
response = templates.TemplateResponse("login.html", {"request":request})
response.delete_cookie(key="access_token")
return response
HTML
<!DOCTYPE html>
<html>
<head>
<title>Secure Data</title>
</head>
<body>
<h1>Secure Data</h1>
<p>{{user}}</p>
<button id="logout-btn">Logout</button>
<script>
document.getElementById("logout-btn").addEventListener("click", () => {
// 로컬 스토리지에서 토큰 삭제
localStorage.removeItem("access_token");
// 로그아웃 후 로그인 페이지로 이동
window.location.href = "/logout";
});
</script>
</body>
</html>
로그아웃 버튼을 클릭하면, 로그인 페이지로 다시 넘어감
login 엔드포인트와 동일하게 맞추고 싶었지만 힘들어 보임
login에서 sqlite 연동
db = SessionLocal()
user = get_user(db, form_data.username)
get_user 은 crud.py에서 유저의 (user_id, password) 를 가져옴signup 등록
@app.get("/signup")
def get_signup_form(request: Request):
return templates.TemplateResponse("login.html", context={"request": request})
@app.post("/signup")
def login(username: str = Form(...), password: str = Form(...)):
db = SessionLocal()
user_info = schemas.User(user_id=username, password=password)
new_user = create_user(db, user_info)
return {"user_id": username, "user_list": get_users(db)}
secure-data ⇒ home.html
모든 함수에 login authentication 적용
user = await get_current_user(request)
if user is None:
return {'message': 'login failed', 'user':user}
user_id = user['username']
모든 함수에 위 코드 및 함수 파라미터테 request: Request 추가
현재 프론트가 안 되어 있어 swagger에서 실험 가능
로그인한 상태로 execute 하면 return 잘 반환
로그아웃 후 execute 하면 failed 잘 반환