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,
    }

docs에서 로그인하면 나오는 화면. status=200나오면서 Token에 맞게 잘 도출되고 있다

 

 

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}>

좌, 우: store변수 적용한 것