본문 바로가기

회고록(TIL&WIL)

WIL 2022.07.01 CORS, rest_framework_simplejwt, E2E 회원가입/로그인

CORS (Cross-origin resource sharing 교차 출처 리소스 공유)

FE에서 BE로 request를 줄 때 CORS 설정을 해야한다 그렇지 않으면 통신이 되지않는다.

1. CORS 패키지 설치

pip install django-cors-headers

2. settings.py 코드 작성

INSTALLED_APPS = [
    ...
    'rest_framework',
    'corsheaders',
    ...
]

MIDDLEWARE = [
    ...
    "corsheaders.middleware.CorsMiddleware",  # 아래의 코드보다 무조건 위에 적어야함
    "django.middleware.common.CommonMiddleware",
    ...
]

# FE의 주소값을 입력해주어야한다.
CORS_ALLOWED_ORIGINS = [
    "http://localhost:5500",
    "http://127.0.0.1:5500",
]

json데이터로 보낼 때는 headers 를 만들어 json으로 보내 주는 것을 명시 해야한다. 그렇지 않으면 415 오류가 발생한다.

회원가입 javascript

async function join() {

    // 입력받은 데이터 가져오기
    const joinData = {
        email: document.getElementById("input-id-join").value,
        fullname: document.getElementById("input-fullname-join").value,
        password: document.getElementById("input-password-join").value,
        password_confirm: document.getElementById("input-password-confirm").value,
    }

    // 입력받은 데이터를 BE서버에 회원가입 url로 request 요청
    const response = await fetch(`${backend_base_url}/user/`, {
        // headers를 통해 json 데이터임을 알려줘야 415 오류가 발생하지않는다.
        headers: {
            Accept: "application/json",
            'Content-type': "application/json"
        },
        method: "POST",
        body: JSON.stringify(joinData)
    })

    // response 받은 내용을 json 화
    respose_json = await response.json()

    // 정상적인 통신이 되었을 경우 = 회원가입 완료 > 로그인페이지로
    if (response.status == 201) {
        alert("회원가입 완료!")
        window.location.replace(`${frontend_base_url}/templates/user/login.html`);
    } else {
        alert(response.status)
    }

}

 

DRF - simple JWT

1. pip install

pip install djangorestframework-simplejwt

2. settings.py - 다른건 다몰라도 꼭 settings.py에 코드를 추가해야지만 정상적으로 BE views.py에서 request.user 를 통해 로그인유저에 대해서 데이터를 가져 올 수 있게된다.

INSTALLED_APP = [
    ...
    'rest_framework_simplejwt',
    ...
]

...
REST_FRAMEWORK = {
	...
    'DEFAULT_AUTHENTICATION_CLASSES': [ # session 혹은 token을 인증 할 클래스 설정
        'rest_framework_simplejwt.authentication.JWTAuthentication',
        ...
    ],
    ...
}

# JWT 관련 세부 설정 (생략 가능)
from datetime import timedelta
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': False,
    'BLACKLIST_AFTER_ROTATION': False,
    'UPDATE_LAST_LOGIN': False,

    'ALGORITHM': 'HS256',
    'SIGNING_KEY': SECRET_KEY,
    'VERIFYING_KEY': None,
    'AUDIENCE': None,
    'ISSUER': None,
    'JWK_URL': None,
    'LEEWAY': 0,

    'AUTH_HEADER_TYPES': ('Bearer',),
    'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',
    'USER_AUTHENTICATION_RULE': 'rest_framework_simplejwt.authentication.default_user_authentication_rule',

    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
    'TOKEN_TYPE_CLAIM': 'token_type',
    'TOKEN_USER_CLASS': 'rest_framework_simplejwt.models.TokenUser',

    'JTI_CLAIM': 'jti',

    'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
    'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
    'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}

3. urls.py - 이미 생성 되어있는 클래스들을 import 해와서 url에 지정해줘서 사용함

# user/urls.py
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView

# 토큰 유효성 검사용
from user.views import OnlyAuthenticatedUserView

# user/
urlpatterns = [
    path('', views.UserAPIView.as_view()),
    path('api/token/', TokenObtainPairView.as_view(), name="token_obtain_pair"),
    path('api/token/refresh/', TokenRefreshView.as_view(), name="token_refresh"),
    # 토큰 유효성 검사용
    path('api/authonly/', OnlyAuthenticatedUserView.as_view()),
]

4. views.py - 로그인 정보 확인용

#user/views.py
from rest_framework_simplejwt.authentication import JWTAuthentication
# user/
class UserAPIView(APIView):
    # JWT 인증방식 클래스 지정하기
    authentication_classes = [JWTAuthentication]
    
    # 로그인 한 유저 정보 출력
    def get(self, request):
        user = UserModel.objects.get(id=request.user.id)        
        return Response(UserSerializer(user).data, status=status.HTTP_200_OK)

5.0 javascript 작성 전 중복 코드 방지하기 위해 _base.js 작성해서 html에 우선 include 해오기

// _base.js
// For All
const backend_base_url = "http://127.0.0.1:8000"
const frontend_base_url = "http://127.0.0.1:5500"

5. jwt 로그인 구현 (FE - javascript)

// user.js
async function login() {
    const loginData = {
        email: document.getElementById("input-id-init").value,
        password: document.getElementById("input-password-init").value,
    }

    const response = await fetch(`${backend_base_url}/user/api/token/`, {
        headers: {
            Accept: "application/json",
            'Content-type': "application/json"
        },
        method: "POST",
        body: JSON.stringify(loginData)
    })

    response_json = await response.json()

    if (response.status == 200) {
        // 로컬스토리지에 jwt access 토큰과 refresh 토큰 저장
        localStorage.setItem("access", response_json.access)
        localStorage.setItem("refresh", response_json.refresh)

        // 파싱하는 부분 복사해서 사용하기! 
        const base64Url = response_json.access.split('.')[1];
        const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
        const jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
            return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
        }).join(''));

        localStorage.setItem("payload", jsonPayload);
        // window.location.replace(`${frontend_base_url}/`);
        alert("환영합니다!")
        window.location.replace(`${frontend_base_url}/templates/art/main.html`);
    } else {
        alert(response.status)
    }

}

6. auth.js - 발급 받은 토큰 자동 refresh, 토큰 없을 경우 로그인창으로 돌려보내기

// auth.js
window.onload = ()=>{
	// 로컬스토리지에 저장되어있는 payload 가져오기
    const payload = JSON.parse(localStorage.getItem("payload"));
	
    // payload 정보 없을 경우 = 로그인 하지않음
    if (payload == null){
        window.location.replace(`${frontend_base_url}/templates/user/login.html`);
    }
    // 아직 access 토큰의 인가 유효시간이 남은 경우
    if (payload.exp > (Date.now() / 1000)){
        
    } else {
        // 인증 시간이 지났기 때문에 다시 refreshToken으로 다시 요청을 해야 한다.
        // refresh 토큰으로 access 토큰 얻어오는 코드 작성
        const requestRefreshToken = async (url) => {
              const response = await fetch(url, {
                  headers: {
                      'Content-Type': 'application/json',
                  },
                  method: "POST",
                  body: JSON.stringify({
                      "refresh": localStorage.getItem("refresh")
                  })}
              );
              return response.json();
        };

        // 위애서 작성한 함수에 url넣어 작동시켜 refresh 받은 accessToken을 localStorage에 저장
        requestRefreshToken(`${backend_base_url}/user/api/token/refresh/`).then((data)=>{
            // 새롭게 발급 받은 accessToken을 localStorage에 저장
            const accessToken = data.access;
            localStorage.setItem("access", accessToken);
        });
    }

};

7. 로그인되어있는지 안되어있는지 검증은 위에서 만든 js 들을 순서대로 임포트해오기만 하면 끝

<!-- JS import -->
<script src="/static/js/_base.js"></script>
<script src="/static/js/auth.js"></script>

7.1 로그인된 유저의 정보를 가져와 사용하려면 fetch api 요청 시 headers에 Authorization 을 통해 access토큰을 보내 줘야만 한다. "Bearer " 를 꼭 붙여줘야한다.(세팅 때 변경 할 수 있으나 관용적으로 쓰인다.)

// 비동기 통신 async 내가 가진 상품 리스트 출력
async function getMyGalleryList() {

    const response = await fetch(`${backend_base_url}/mygallery/`, {
        headers: {
            Accept: "application/json",
            'content-type': "application/json",
            "Authorization": "Bearer " + localStorage.getItem("access")
        },
        method: 'GET',
        // body: JSON.stringify(Data)
    })

    response_json = await response.json()