FastAPI에서 제공하는 로그인 패키지를 사용한다. (fastapi-login)
1. 패키지 설치
pip install fastapi-login
가상환경에 진입 후 터미널에 입력해 주세요.
2. (FastAPI를 시작하기 위한) 기본 코드 작성
from fastapi import FastAPI
SECRET = 'your-secret-key' #인증에 사용할 비밀 키 정의
app = FastAPI() #FastAPI 클래스의 인스턴스 생성
3. LoginManager 추가
from fastapi_login import LoginManager
manager = LoginManager(SECRET, token_url='/auth/token') #비밀 키, 토큰 url 사용 여부 지정
로그인 매니저는 사용자 인증과 토큰을 관리해 주는 역할을 합니다.
4. html 템플릿 추가
from fastapi.templating import Jinja2Templates
templates = Jinja2Templates(directory="templates")
html 템플릿이 저장된 디렉토리를 사용하여 Jinja2Templates의 인스턴스를 생성합니다.
5. 빈 딕셔너리 생성 (사용자 DB로 사용, 임시)
fake_users_db = {}
-
위 코드들을 다 정리하면 이렇게 됩니다 (나중에 쓸 모듈 미리 추가)
from fastapi import FastAPI, Form, HTTPException
from fastapi.responses import JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi_login import LoginManager, login_required
from fastapi.security import OAuth2PasswordRequestForm
app = FastAPI()
SECRET = "your-secret-key"
manager = LoginManager(SECRET, token_url="/token", use_cookie=True)
templates = Jinja2Templates(directory="templates")
fake_users_db = {}
내가 몰라서 정리하는 모듈 설명 정리...
| form | 사용자가 입력한 폼 데이터를 처리하기 위한 class |
| HTTPException | http 예외 처리를 위한 class |
| JSONResponse | JSON 형태의 응답을 생성하기 위한 class |
| Jinja2Templates | html template을 사용하기 위한 class |
| LoginManager | 사용자 인증과 토큰 관리 및 처리를 위한 class |
| login_required | 로그인이 필요한 엔드포인트(/)에서 사용하는 데코레이터(@) |
| OAuth2PasswordRequestForm | OAuth2 표준에 따라 비밀번호 그랜트 타입을 사용하여 사용자 로그인을 처리하기 위한 class |
비밀번호 그랜트 타입이란?
OAuth 2.0 프로토콜의 한 유형. 클라이언트가 사용자의 아이디와 비밀번호를 직접 수신하여 토큰을 얻는 방법이다.
6. User class 정의
class User:
def __init__(self, username: str, password: str):
self.username = username
self.password = password
사용자의 정보(user name, password)를 저장하는 class를 만듭니다.
7.user_loader 작성
@manager.user_loader
def load_user(username: str):
user = fake_users_db.get(username, None)
if user:
return {"username": user.username, "password": user.password}
return None
사용자를 이름으로 불러오는 역할을 합니다.
8. /register
@app.post("/register")
async def register(username: str = Form(...), password: str = Form(...)):
if username in fake_users_db:
raise HTTPException(status_code=400, detail="이미 등록된 아이디입니다.")
user = User(username, password)
fake_users_db[username] = user
return {"message": "회원가입이 완료되었습니다! /login으로 접속해 주세요."}
post 메소드를 사용합니다.
Form class를 사용하여 폼 데이터를 받고, 이미 등록되어 있다면 400과 함께 에러를 반환합니다.
새로운 사용자라면 fake_users_db에 정보를 추가하고 return을 반환합니다.
9. /login
app.get("/login", response_class=HTMLResponse)
def read_login(request: Request):
return templates.TemplateResponse("login.html", {"request": request})get 메소드를 사용합니다.
html 파일을 리턴하여 사용자에게 login 페이지를 반환합니다.
10. /token
@app.post("/token")
async def login(data: OAuth2PasswordRequestForm = Depends()):
try:
user_data = load_user(data.username)
if user_data and data.password == user_data["password"]:
token = manager.create_access_token(data=dict(sub=user_data["username"]))
response = RedirectResponse(url="/users/me")
return JSONResponse(content={"access_token": token["access_token"]})
else:
raise HTTPException(status_code=401, detail="아이디 또는 비밀번호가 올바르지 않습니다.")
except Exception as e:
error_message = f"An error occurred: {str(e)}"
return JSONResponse(content={"detail": error_message}, status_code=500)제일 애먹은 부분이자 아직 안 풀린 부분...
일단 post 메소드를 사용합니다.
사용자 로그인을 위한 루트 함수를 정의하고, OAuth2PasswordRequestForm을 사용하여 로그인 폼 데이터를 받습니다.
로그인 버튼을 눌렀을 때, 사용자 정보가 맞다면 그 아이디의 토큰을 생성하여 /users/me로 리다이렉트하고, 올바르지 않다면 401 에러를 반환합니다.
여기에서 제가 아직 해결 못한 부분은 올바르지 않은 정보를 치면 401 에러와 함께 아이디 또는 비밀번호가 잘못되었다는 메시지가 나오는데, 올바른 정보를 치면 메시지는 나오지 않지만 여전히 401 에러는 반환됩니다.
print(error_message)를 추가하여 콘솔에서 확인도 해 봤는데 그냥 인터넷 서버 오류라는 말만 나오고 명확한 답이 나오지 않아서 아직 해결을 못했습니다 ㅠㅠ
불행인지 다행인지는 모르겠으나 이건 회원가입과 로그인, 로그아웃의 구조를 이해하는 데에 가볍게 쓰이는 자료라고 생각하고 공부한 부분이라... 멋사 수업 자료를 만들 때에는 호환이 되어야 해서 DB 바탕으로 하는 법으로 할 예정입니다.
11. /logout
from fastapi.responses import RedirectResponse
@app.post("/logout")
async def logout(request: Request):
response = RedirectResponse("/")
response.delete_cookie("Authorization")
return response
post 메소드를 사용합니다.
로그아웃 버튼을 누르면 /(초기 화면)으로 리다이렉트하고 쿠키를 삭제하여 로그아웃을 수행합니다.
전체 코드
main.py
from fastapi import FastAPI, Depends, HTTPException, Request, Form
from fastapi.security import OAuth2PasswordRequestForm
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from fastapi_login import LoginManager
app = FastAPI()
SECRET = "your-secret-key"
manager = LoginManager(SECRET, token_url="/token", use_cookie=True)
templates = Jinja2Templates(directory="templates")
fake_users_db = {}
class User:
def __init__(self, username: str, password: str):
self.username = username
self.password = password
@manager.user_loader
def load_user(username: str):
user = fake_users_db.get(username, None)
if user:
return {"username": user.username, "password": user.password}
return None
@app.post("/register")
async def register(username: str = Form(...), password: str = Form(...)):
if username in fake_users_db:
raise HTTPException(status_code=400, detail="이미 등록된 아이디입니다.")
user = User(username, password)
fake_users_db[username] = user
return {"message": "회원가입이 완료되었습니다! /login으로 접속해 주세요."}
@app.post("/token")
async def login(data: OAuth2PasswordRequestForm = Depends()):
try:
user_data = load_user(data.username)
if user_data and data.password == user_data["password"]:
token = manager.create_access_token(data=dict(sub=user_data["username"]))
response = RedirectResponse(url="/users/me")
return JSONResponse(content={"access_token": token["access_token"]})
else:
raise HTTPException(status_code=401, detail="아이디 또는 비밀번호가 올바르지 않습니다.")
except Exception as e:
error_message = f"An error occurred: {str(e)}"
print(error_message)
return JSONResponse(content={"detail": error_message}, status_code=500)
@app.get("/login", response_class=HTMLResponse)
def read_login(request: Request):
return templates.TemplateResponse("login.html", {"request": request})
@app.get("/register", response_class=HTMLResponse)
def read_register(request: Request):
return templates.TemplateResponse("register.html", {"request": request})
@app.get("/users/me", response_class=HTMLResponse)
async def read_users_me(request: Request, token: dict = Depends(manager)):
user_info = fake_users_db.get(token.get("sub"), None)
if user_info:
return templates.TemplateResponse("user_info.html", {"request": request, "user": user_info})
else:
raise HTTPException(status_code=401, detail="토큰이 유효하지 않거나 사용자를 찾을 수 없습니다.")
@app.post("/logout")
async def logout(request: Request):
response = RedirectResponse("/")
response.delete_cookie("Authorization")
return response
@app.get("/", response_class=HTMLResponse)
def read_main(request: Request):
return templates.TemplateResponse("main.html", {"request": request})
main.html
<!-- templates/main.html -->
<!DOCTYPE html>
<html>
<head>
<title>Main</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
padding: 20px;
}
</style>
</head>
<body>
회원가입을 하려면 /register <br/>
로그인을 하려면 /login을 입력하세요.
</body>
</html>
login.html
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
padding: 20px;
}
</style>
</head>
<body>
<form action="/token" method="post">
<input type="text" name="username" placeholder="아이디" required />
<input type="password" name="password" placeholder="비밀번호" required />
<input type="submit" value="로그인">
</form>
</body>
</html>
register.html
<!DOCTYPE html>
<html>
<head>
<title>Register</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
padding: 20px;
}
</style>
</head>
<body>
<form action="/register" method="post">
<input type="text" name="username" placeholder="아이디" required />
<input type="password" name="password" placeholder="비밀번호" required />
<input type="submit" value="회원가입">
</form>
</body>
</html>
user_info.html
<!DOCTYPE html>
<html>
<head>
<title>User Info</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
padding: 20px;
}
</style>
</head>
<body>
<h1>Welcome, {{ user.username }}!</h1>
<p>Here is your user information:</p>
<ul>
<li>Username: {{ user.username }}</li>
<li>Password: {{ user.password }}</li>
</ul>
<form action="/logout" method="post">
<input type="submit" value="Logout">
</form>
</body>
</html>'FastAPI' 카테고리의 다른 글
| [FastAPI] 내가 보려고 정리하는 FastAPI (conda) (0) | 2024.02.14 |
|---|