FrameWork/FastAPI
로그인, 로그아웃, OAuth2PasswordRequestForm
mansoorrr
2024. 6. 21. 12:03
1. 로그인 컴포넌트 만들기
- 회원가입 컴포넌트와 비슷하다.
- 로그인 할때는 사용자이름과 비밀번호만 입력하면 된다
[컴포넌트만들어 등록]
// Login.svelte
<script>
import {link} from 'svelte-spa-router';
import {push} from 'svelte-spa-router';
import fastapi from '../lib/api';
import Error from '../components/Error.svelte';
// 화면에서 전송될 데이터들을 위한 변수 초기화
let login_username = '';
let login_password = '';
let error = {detail:[]};
function login(event) {
event.preventDefault()
let url = "/api/user/login"
let params = {
'username': login_username,
'password': login_password,
}
fastapi('post', url, params,
(json) => {
error = {detail:[]} //초기화
push("/")
},
(json_error) => {
error = json_error
}
)
}
</script>
<div class="container">
<h3 class="border-bottom mt-3 py-3">로그인</h3>
<Error error={error}/>
<form action="post">
<div class="mb-3">
<label for="username">이름</label>
<input type="text" class="form-control" bind:value={login_username}>
</div>
<div class="mb-3">
<label for="password">비밀번호</label>
<input type="text" class="form-control" bind:value={login_password}>
</div>
</form>
<button class="btn btn-dark" on:click={login}>로그인</button>
<a use:link href="/" class="btn btn-secondary">취소</a>
</div>
// App.svelte
const routes = {
"/": Home,
"/detail/:question_id": Detail,
"/question-create": QuestionCreate,
"/user-create": UserCreate,
"/login": Login, // 등록
}
</script>
2. 라우팅
- 로그인도 화면에서 들어오는 데이터(post)를 폼을 통해 backend로 전달해야함
- 하지만 회원가입할때처럼 schema를 통해 관리하지 않고 OAuth2 인증을 사용함
- 이유는 인증 및 권한 부여를 위해 사용하는 인증방식
- 인증 및 권한 부여는 토큰을 통해 이루어 짐
- 따라서 로그인을 하면 해당 사용자의 정보(사용자별 토큰, 이름 등)가 클라이언트로 전송(라우팅의 리턴 값)되어야함
- 이를 위해 OAuth2PasswordRequestForm과 jwt를 이용함 아래 피키지 설치 필요
- pip install python-multipart
- pip install "python-jose[cryptography]"
- 토큰을 만들기 위해서 '유효기간', '사용자명', 'secret_key', 알고리즘이 필요
- secret_key: python shell에서 import secrets > secrets.token_hex(32)를 통해 받을 수 있음
- 토큰을 만들어서 리턴으로 추출
[라우팅]
#-------------------- user_schema.py
class Token(BaseModel):
access_token: str
token_type: str
username: str
#-------------------- user_router.py
from domain.user import user_schema
from fastapi.security import OAuth2PasswordRequestForm
from jose import jwt
#----- 비밀번호 해시화
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
#----- 토큰만들기 위해 필요한 것
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # 토큰 유효기간
SECRET_KEY = <랜덤키>
ALGORITHM = "HS256" #토큰만들때 사용할 알고리즘
@router.post('/login', response_model=user_schema.Token) #응답할 형식 지정
def login(
form_data:OAuth2PasswordRequestForm=Depends(), #폼 생성
db:Session=Depends(get_db),
):
#----- 화면에서 입력된 데이터가 db에 있는지 확인
user = db.query(User).filter(User.username==form_data.username).first() #유저검색
# db에 없는 유저이거나 비밀번호가 다르게 입력되었을 경우(db에 hash로 저장되어 있었으니 그걸 다시 변경해서 비교하기 위해 verify사용)
if not user or pwd_context.verify(form_data.password, uswer.password):
raise HttpException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect user or password",
headers={"WWW-Authenticate": "Bearer"}
)
#----- db에 있는 유저이면서 비밀번호도 일치하면
#토큰 부여
data = {
"sub": user.username #사용자명
"exp": datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) #토큰 사용기간
}
access_token = jwt.encode(data, SECRET_KEY, ALGORITHM) #토큰생성
#----- 클라이언트로 보낼 값 도출
return {
"access_token": access_token,
"token_type": "bearer",
"username": user.username,
}
3. fastapi함수, login함수 수정
- OAuth2인증을 사용할때는 Content-Type: application/x-www-form-urlencoded을 사용해야 함
- 별도 조건으로 걸어줌
// ------------- api.js파일에 아래 내용 추가
import qs from "qs" // npm install qs
let method = operation;
let CONTENT_TYPE = 'application/json';
let body = JSON.stringify(params);
// LOGIN할때 OAuth2를 사용하게 되면 아래의 타입을 갖는다 > 이부분 추가
if(operation === 'login') {
method = 'post';
CONTENT_TYPE = 'application/x-www-form-urlencoded';
body = qs.stringify(params);
}
let _url = import.meta.env.VITE_SERVER_URL + url;
let options = {
method: method,
headers: {
'Content-Type': CONTENT_TYPE,
}
}
// ---------- Login.svelte 파일 수정
<script>
fastapi('login', url, params, (json) => {....} ) // 이렇게 수정
</script>
4. 엑세스 토큰과 로그인 정보 저장
- routing을 통해 로그인 정보를 도출함
- 그럼 이제 인증과 권한 부여를 하기 위해 localStorage(클라이언트 저장소)에 이 내용을 저장할 수 있게 해야함
- 저장하여 다른 페이지에서도 이것을 사용할 수 있도록 store로 관리
[store.js에 전역 설정]
- access_token, username, is_login을 store로 관리
- 수시로 변경될 수 있도록 적용: subscribe()
// ---------- store.js
import {writable} from 'svelte/store';
const persist_storage = (key, initValue) => {
const storedValueStr = localStorage.getItem('key'); //localStorage에 현재 값이 있는지 확인
const stored = writable(storedValueStr != null ? JSON.parse(storedValueStr): initValue); //localStorage에 있을때와 없을때
// 변경될때 수행할 녀석
stored.subscribe((val) => {
localStorage.setItem(key, JSON.stringify(val));
})
return stored;
}
// 전역에서 사용할 수 있게 지정
export const access_token = persist_storage('access_token', '');
export const username = persist_storage('username', '');
export const is_login = persist_storage('is_login', false);
[전역변수 사용]
- 로그인하면 localStorage에 정보 저장하여 인증 및 권한 부여
- 로그인한 사람만 사용할 수 있는 기능: 로그아웃, 질문등록, 답변등록
// Login.svelte
import {access_token, username, is_login} from '../lib/store';
// 이부분 수정
fastapi('login', url, params,
(json) => {
$access_token = json.access_token;
$username = json.username
$is_login = true
push("/")
},
(json_error) => {
error = json_error
}
)
- 로그인하면 로그아웃이 나타나도록 Navigation Bar 수정
<script>
import {access_token, username, is_login} from '../lib/store'
function logout() {
$access_token = "";
$username = "";
$is_login = false;
}
<script>
// 이부분 수정
{#if $is_login}
<span class="nav-button">
<a use:link href="/login" on:click={logout}>로그아웃({$username})</a>
</span>
{:else}
<span class="nav-button">
<a use:link href="/user-create">회원가입</a>
</span>
<span class="nav-button">
<a use:link href="/login">로그인</a>
</span>
{/if}
- 질문등록버튼과, 답변등록에도 수정
// ---------- Home.svelte 수정
<script>
import {is_login} from '../lib/store';
<script>
<a use:link href="/question-create" class="btn btn-dark {$is_login ? '' : 'disabled'}" >질문등록</a>
// ---------- Detail.svelte 수정
<script>
import {is_login} from '../lib/store';
</script>
<form>
<textarea rows="15" class="form-control my-3" disabled={$is_login ? '' : 'disabled'} bind:value={content}></textarea>
</form>
<input type="submit" class="btn btn-dark {$is_login ? '': 'disabled'}" on:click={saveAnswer}>