<학습 목표>
- 쿠키 세션 방식 로그인과 토큰 방식 로그인의 차이를 이해한다
- 쿠키와 로컬스토리지의 차이를 이해한다
- JWT의 구조를 이해한다
- 장고에서 JWT를 이용해서 회원가입과 로그인을 구현할 수 있다
- 브라우저의 로컬스토리지에 백엔드에서 받은 토큰을 저장할 수 있다
- 프론트에서 로컬스토리지의 토큰을 헤더에 실어서 백엔드로 보낼 수 있다
- 포스트맨에서 헤더에 토큰을 실어서 백엔드로 보낼 수 있다
- 토큰의 만료기간을 설정할 수 있다
- 토큰이 만료되면 refresh token을 다시 받아올 수 있다
<학습 내용>
- 토큰방식 로그인 vs 세션방식 로그인
- 세션방식은 모든 유저의 정보와 세션 정보를 서버에서 관리함
- 클라이언트에서 사용자의 인증 정보를 서버에 전달함(로그인 할 때 아이디, 비밀번호를 입력하면 그 값이 서버로 전달된다는 말)
- 서버는 인증을 처리한 뒤 해당 사용자에 대해 세션을 생성함
- 세션 정보는 서버에 저장되고 클라이언트는 세션 id를 받아 브라우저(쿠키)에 저장
- 클라이언트는 이후 요청에 대해 세션 id를 서버에 넘김
- 서버는 전달 받은 세션 id를 매치되는 저장 중인 세션 정보로 인증을 처리함
- 만약 세션 id가 만료되었을 경우 1번 과정부터 다시 이루어짐
- 토큰방식은 토큰 정보를 서버에 저장하지 않음. 대표적인 방식으로 JWT(JSON Web Token)가 있음
- 클라이언트에서 사용자의 인증 정보를 서버에 전달(세션 방식과 동일)
- 서버는 인증 정보로 인증을 처리하고 (세션 대신) JWT를 생성하여 클라이언트에 전달
- 클라이언트는 JWT를 브라우저(localStorage)에 저장
- 클라이언트는 이후 이루어지는 요청에 JWT를 이용
- 서버는 JWT를 검증하여 인증을 처리
- JWT가 만료되면 토큰을 refresh 함
- 세션방식의 단점은 매번 인증을 위해 데이터베이스에 질의를 거쳐야 한다는 것.
- 슈퍼에서 주류를 구매할 때 상황으로 비유를 하자면 세션 방식은 주류 구매자(클라이언트)가 주민등록번호(사용자의 인증 정보)를 말하면 주류 판매자(서버)는 주민센터(데이터베이스)에 전화해서 이 사람의 주민번호가 이게 맞는지 확인하는 방식이고, 토큰 방식은 주류 구매자(클라이언트)가 술을 사려고 할 때(로그인, 회원가입 등을 하려고 할 때) 주류 판매자(서버)에게 주민등록증(토큰)을 제시하면 바로 구매할 수 있는 방식이라고 생각하면 됨
- 로컬스토리지 vs 쿠키
- 쿠키는 매번 모든 요청에 실려서 보내짐. 회원정보 같은 매번 보내줘야 하는 것은 쿠키에 쓰는 것이 유리함.
- 쿠키의 최대 크기 4,096 bytes, 로컬스토리지는 5MB. 많은 양을 저장할 때는 로컬 스토리지가 유리함.
- 쿠키는 만료가 있지만, 로컬스토리지는 만료가 없음.
- 쿠키를 설정하는 건 약간 어려움. 로컬스토리지가 사용하기에 훨씬 편리함.
- JWT를 어디에 저장해야 할지는 의견이 분분함. 이 강의에서는 JWT를 로컬스토리지에 저장할 예정.
<실습>
프로젝트 폴더명 : drt_project
- DRF에서 JWT 사용하기(simplejwt)
- DRF 공식 문서 : https://www.django-rest-framework.org/#installation
- simpleJWT 공식 문서 : https://django-rest-framework-simplejwt.readthedocs.io/en/latest/getting_started.html
- 일단 필요한 패키지인 장고, DRF, JWT를 설치해준다
pip install django djangorestframework djangorestframework-simplejwt
- settings.py에 필요한 것을 추가해준다
# drf_project/settings.py
...
INSTALLED_APPS = [
...
'rest_framework',
'rest_framework_simplejwt',
...
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
- users 앱을 생성한 후 drf_project/urls.py 에 users를 연결해준다
# drf_project/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('users/', include('users.urls')),
]
- 그 다음 유저앱의 urls.py에 가서 simpleJWT 공식 문서에 나와 있는대로 써준다
# users/urls.py
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
- 계정을 만들어야 테스트가 가능하므로 관리자 계정을 생성한다 명령어는 아래와 같다
python manage.py createsuperuser
- 아이디 입력하고 메일 형식에 맞게 입력하고 비밀번호 2번 써주면 끝(참고로 비밀번호는 화면에 보이지 않는다 안 써진다고 당황할 거 없음 써지고 있는 거니까)
- makemigrations, migrate 안 되어 있으면 해주고 서버 실행한 다음 포스트맨에서 테스트 해본다
- 다 맞게 설정했는지 확인하고, 위에서 만든 슈퍼유저 아이디와 비밀번호를 아래쪽 큰 네모가 있는 곳에 입력하고 send를 누른다
- 위와 같은 화면이 나오면 성공!
- JWT 정보 확인해보기
- 정보 확인하는 사이트 : https://jwt.io/
- 포스트맨에서 확인한 긴문장이 뭘 의미하는지 알고 싶으면 일단 복사한다
- jwt.io 페이지 가서 스크롤을 조금 내리면 아래와 같은 창이 뜬다
- 왼쪽 Encoded에 복사한 문구를 붙여넣으면 오른쪽 Decoded에서 해당 토큰이 뭘 의미하는지 알 수 있다
- JWT의 토큰 구조는 헤더.페이로드.서명 으로 되어 있다
- 빨간색 부분이 헤더를 의미하고, 보라색은 페이로드, 하늘색은 서명을 의미한다
- 택배로 비유하자면, 헤더는 택배 내용물의 정보가 들어있는 송장이고, 페이로드는 택배 속 내용물이고, 서명은 ..서명...이다
- 헤더는 알고리즘, 타입 등 인증 정보의 간략 정보가 담기고, 페이로드에 실질적인 인증 정보(data)가 담기고, 서명에는 검증을 위한 것들이 담김
- exp는 토큰 만료기간을 의미하는데 우리가 알아볼 수 없는 숫자로 되어 있다 그 위에 마우스를 올리면 시간 형식으로 돼있는 것을 확인할 수 있다
- custom user 만들고 등록하기
- 공식문서: https://docs.djangoproject.com/en/4.2/topics/auth/customizing/
- 공식문서 링크 타고 들어가서 제일 밑으로 내리면 A full example이 있다 여길 참고하면 된다
- users앱 models.py에 A full example 담긴 걸 쭉 복사하고 원한다면 클래스명을 바꾸고 싶은 이름으로 바꿔도 된다
- 커스텀 유저를 만드는 경우 settings.py에서 auth 유저모델을 지정해줘야 한다
AUTH_USER_MODEL = "앱이름.유저클래스명"
- 나는 users앱 models.py에 유저 클래스명을 User로 했기 때문에 아래와 같이 설정해줬다
AUTH_USER_MODEL = "users.User"
- custom user 어드민 만들기
- 유저 모델을 커스텀 할 때 유저매니저 모델도 같이 작성해줘야 한다(장고 문서에서 예시로 제공한 건 다 필수 사항임)
- 위 공식문서에서 models.py 복사한 부분 아래에 보면 admin.py에 작성하라고 하는 부분이 있다 그 부분을 다 복사한다
- 다 복사해보면 먼저 import 부분에 오류가 있다고 뜰 거다
from customauth.models import MyUser
- customauth는 내가 만는 유저앱의 이름이 들어갈 자리다. 나는 유저앱 이름을 users로 지었으니 customauth를 users로 변경해준다
- 그리고 MyUser는 models.py에 있는 유저모델의 클래스명이다. 난 User로 이름을 변경해줬으므로 MyUser를 User로 변경한다
- 다만 MyUser가 아래에 여러군데에서 쓰인 것을 확인할 수 있을 거다
- vscode의 기능을 이용해 한 번에 변경해주면 빼먹은 곳 없이 잘 변경할 수 있다
- MyUser에 왼쪽 마우스 한 번 클릭한 후 오른쪽 마우스를 클릭한다 '모든 항목 변경'이 있다 영어버전에는 뭐라고 써있는지는 모르겠는데 오른쪽에 단축키가 ctrl+F2가 써있는 부분이 해당되는 부분일 것이다
- 그걸 클릭하면 MyUser부분에 모든 커서가 잡힌다
- 원래 써 있는 걸 지우고 User로 바꿔준다
- 앞에 My만 지우고 싶은 경우 커서를 방향키로 이동하면 단어 기준으로 이동하기 때문에 M 앞으로 커서가 이동한다 그 상태에서 delete 키를 2번 눌러 My를 지워준다
- MyUser가 모두 User로 바뀐 것을 확인할 수 있다
- 코드를 찬찬히 보면서 models.py에서 커스텀한 부분이 있다면 변경한대로 수정해주면 된다
- 회원가입과 로그인을 위한 urls.py와 views.py 코드 짜기
- 먼저 users앱에 serializers.py 파일 생성 후 아래와 같이 코드를 입력한다
from rest_framework import serializers
from users.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
- 그 다음 views.py를 작성해준다
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from users.serializers import UserSerializer
# Create your views here.
class UserView(APIView):
def post(self, request):
serializer = UserSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response({'message':'가입완료!'}, status=status.HTTP_201_CREATED)
else:
return Response({'message':f'${serializer.errors}'}, status=status.HTTP_400_BAD_REQUEST)
- 필요한 것들을 import해주고, 클래스형 뷰로 작성해준다
- post 요청일 때 함수, 2주차에서 배운대로 serializer에 데이터를 담아서 유효성검사를 해준 후 저장하고 Response를 띄운다
- 포스트맨으로 회원가입 테스트를 해보면
- 가입이 잘 된 것을 확인할 수 있다
- 근데 데이터베이스에 가보면
- 패스워드가 암호화 되지 않은 채로 저장된 것을 확인할 수 있다
- 시리얼라이저에서 암호화 작업을 해줘야 한다
- 아까 적어둔 UserSerializer 클래스에 create 함수를 만든다
- def crea 정도까지 쓰면 아래 create가 나오는 걸 확인할 수 있는데 tab 눌러서 자동완성해보면
def create(self, validated_data):
return super().create(validated_data)
- 이렇게 작성된다 원래 있는 create 함수에 오버라이딩을 하는 거임 이제 여기서 수정을 해나가면 되는데
# users/serializers.py
...
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
def create(self, validated_data):
user = super().create(validated_data)
password = user.password
user.set_password(password)
return user
- user 변수에 데이터를 담는다 validated_data에는 회원가입 시 입력하는 email과 password가 담겨있다
- 이때 password에는 암호화 되어 있지 않은 상태로 입력한 그대로 나온다
- user 변수에 저장된 password를 password 변수에 담고 set_password 메서드를 이용해 암호화한다
- 이후 포스트맨으로 실행해 보면 회원가입은 잘 되지만
- DB에는 여전히 1234 그대로 저장되는 것을 확인할 수 있다
- DB에 암호화된 패스워드를 저장하기 위해서는 아래와 같이 save() 메서드를 꼭 써줘야 한다
# users/serializers.py
...
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
def create(self, validated_data):
user = super().create(validated_data)
password = user.password
user.set_password(password)
user.save() # 꼭 추가!!
return user
- 이렇게 하고 다시 포스트맨으로 회원가입을 하고 DB에 가보면 암호화된 패스워드가 저장된 것을 확인할 수 있다
- 나중에 회원정보를 수정할 때를 대비해서 users/serializers.py 파일에 update함수도 똑같이 작성해준다
# users/serializers.py
...
class UserSerializer(serializers.ModelSerializer):
class Meta:
...
def create(self, validated_data):
...
def update(self, validated_data):
user = super().create(validated_data)
password = user.password
user.set_password(password)
user.save()
return user
- JWT 페이로드 커스터마이징 해보기
- 공식 문서 : https://django-rest-framework-simplejwt.readthedocs.io/en/latest/customizing_token_claims.html
- 이번엔 위 문서를 참조한다
- 근데 강의와 좀 다르다
- 강의에서 참고하는 문서랑 같은 것 같은데 문서가 변경된건지...여튼 다르긴 한데 강의와 똑같이 수정하려고 한다
- 일단 users/serializers.py에 공식문서에 있는 예시를 담는다
# users/serializers.py
...
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
...
class UserSerializer(serializers.ModelSerializer):
...
# 이 부분 추가함
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
# Add custom claims
token['email'] = user.email
return token
- 그대로 복붙 해오는데 예시에는 token['name'] = user.name이라고 되어 있다 근데 내가 커스텀한 유저모델에는 이메일을 받게끔 되어 있으므로 name을 email로 바꿔줬다
- 예시에 있는 import는 맨 위에 붙여줬다
- 다음은 users/views.py로 이동한다
# users/views.py
...
from rest_framework_simplejwt.views import (
TokenObtainPairView,
)
from users.serializers import CustomTokenObtainPairSerializer
class UserView(APIView):
...
class CustomTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
- 지금 공식문서에는 settings.py를 수정하라고 나와 있는 것 같은데 강의에서 참조하고 있는 공식문서 버전에는 views.py에 저 클래스를 추가하라고 나와있다
- 원래는 MyTokenObtainPairView인데 이름을 CustomTokenObtainPairView로 바꿨다 클래스명은 원하는대로 바꿔도 된다
- 시리얼라이저에서 정의한 것들을 받아와서 serializer_class 변수에 저장한다
- 다음은 users/urls.py도 수정해줘야 한다
# users/urls.py
...
urlpatterns = [
...
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
...
]
- 원래는 이렇게 되어 있었는데 views.py에서 새로 정의한 클래스명으로 바꿔줘야 한다
path('api/token/', views.CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
- 이렇게 변경하면 끝!
- 그 다음엔 포스트맨에서 로그인을 실행한 후 access 토큰을 복사한다
- 여기 value값의 큰따옴표 안쪽 내용물만 복사한다
- jwt.io 페이지에 가서 붙여넣는다
- 페이로드에 이메일이 추가된 것을 확인할 수 있다
- 토큰 주기 설정과 DRF에서 권한설정 하기
토큰 주기 설정
- 공식 문서 : https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html
- 위에서 참고한 simpleJWT에서 settings로 가면 여러가지 설정을 할 수 있는 예시가 나온다
# Django project settings.py
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": settings.SECRET_KEY,
"VERIFYING_KEY": "",
"AUDIENCE": None,
"ISSUER": None,
"JSON_ENCODER": 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),
"TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
"TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
"TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
}
- 이 중 맨 위에 두 줄이 엑세스 토큰과 리프레시 토큰의 주기를 설정하는 것이다
- 개발 단계에서는 엑세스 토큰 주기를 넉넉하게 늘려서 테스트할 때 다시 로그인 하는 수고로움을 덜 수 있다
- 다른 건 바꾸지 않으면 그냥 기본값으로 설정되기 때문에 두 줄만 가져와서 수정한다
# drf_project/settings.py
from datetime import timedelta
...
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=720),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
}
DRF에서 권한설정하기
- 토큰을 이용해서 회원 정보를 수정할 수 있다
- 일단 테스트를 위해 users/view.py에 mockView 클래스를 생성한다
# users/views.py
...
from rest_framework import permissions
...
class UserView(APIView):
...
class CustomTokenObtainPairView(TokenObtainPairView):
...
class mockView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
user = request.user
user.is_admin = True
user.save()
return Response("get 요청")
- permissions는 import를 해줘야 한다
- 로그인 된 사용자만 mockView 함수에 접근할 수 있다
- is_admin이 1로 변경되게끔 코드를 짰다
- urls.py에 mockView 함수를 연결해준다
# users/urls.py
...
urlpatterns = [
...
path('mock/', views.mockView.as_view(), name='mock_view'),
...
]
- 이제 포스트맨에서 로그인을 한 다음 access 토큰을 복사한다
- 이 권한 설정을 하기 위해서는 Headers에 Key는 Authorization Value는 Bearer <access 토큰 값> 과 같은 형식으로 작성해줘야 한다
- bearer로 하면 안 되고 꼭 대문자 B를 써서 Bearer로 해야 한다
- 변경 됐는지 데이터베이스에서 확인할 수 있다
- 7번 유저정보의 is_admin이 1로 바뀐 것을 볼 수 있다
- 근데 이게 권한설정하는 건가....? 그냥 회원정보 수정하는 거 아닌가?
- 왜 권한설정이라고 하는 건지 이유를 모르겠다..
- refreshtoken으로 access token 받기
- 포스트맨으로 테스트 해보겠다
- refresh API를 새로 만들고 url 설정 후 그냥 send를 누르면 어떤 걸 보내야 하는지 예시가 나온다
- 로그인할 때 나오는 refresh 토큰을 복사해서 붙여넣어준다
- 위 네모 박스의 큰 따옴표 안에 내용물을 복사한다
- raw에 복사한 것을 넣고 send를 누르면 access 토큰이 재발급 된다
- 토큰이 만료되었을 때 로그인을 다시 하지 않고 refresh 토큰으로 access 토큰을 재발급 받아서 로그인 한 효과를 누릴 수 있다
<예제>
- 프론트엔드 예시
회원가입
- 먼저 front 전용 폴더를 따로 만들고 새창을 열어준다
- index.html 파일을 만들고 ! + tab을 눌러 html 기본 환경을 설정해준다
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Signup</title>
</head>
<body>
<h1>회원가입 페이지</h1>
<form>
<input type="email" name="email" id="email" placeholder="email" />
<input type="password" name="password" id="password" placeholder="password" />
<button type="button" onclick="handleSignin()">제출</button>
</form>
</body>
</html>
- 회원가입 페이지라는 글자가 h1 크기로 뜨고 그 아래에 이메일과 비밀번호를 적는 input태그를 만들어줬다
- 그리고 button을 만들어주는데, 원래 form태그를 사용할 때는 버튼의 타입이 submit으로 되어 있어서 폼형태로 백엔드로 넘어가게끔 해줬었는데 지금은 js를 사용할 것이기 때문에 타입은 그냥 button으로 한다
- onclick은 제출 버튼을 눌렀을 때 실행되는데 js에서 작성할 함수를 적어놓았다
- 제출 버튼을 누르면 handleSignin()함수가 실행된다
- 이제 api.js 파일을 생성하고 index.html 파일의 head 태그 안에 js파일을 연결할 문장을 넣는다
<script src="api.js"></script>
- api.js 파일로 가서 연결이 잘 됐는지 확인하기 위해 로딩되었을 때 나올 문구를 찍어준다
// api.js
window.onload = () => {
console.log("로딩됨")
}
- js문법을 따로 공부해야겠지만 일단 이건 창이 로딩되었을 때 콘솔이 찍히도록 된 것이다
- 이렇게 html과 js파일이 잘 연결된 것을 확인할 수 있다
- 이제 api.js로 가서 코드를 작성 해준다
// api.js
function handleSignin() {
const email = document.getElementById("email").value
const password = document.getElementById("password").value
console.log(email, password)
- 지난번에 했던 대로 email이나 password 값이 바뀌는 것을 막기 위해 const로 선언해주고 html에서 id가 email과 password인 요소를 가져온다
- 뒤에 .value가 붙지 않으면 인풋태그 생긴 것 그대로 찍힌다 .value를 해줘야 아래처럼 내가 입력한 그 값이 찍힌다
- 그 다음 이어서 작성해주는데
- 함수 앞에 async를 붙인다
// api.js
async function handleSignin() {
const email = document.getElementById("email").value
const password = document.getElementById("password").value
console.log(email, password)
const response = await fetch('http://127.0.0.1:8000/users/signup/', {
headers: {
'content-type': 'application/json'
},
method: 'POST',
body: JSON.stringify({
"email": email,
"password": password
})
})
console.log(response)
}
- fetch를 쓸 때 async와 await이 필요한 것 같은데 이건 따로 공부를 해야 한다..일단 이렇게 적으라니까 적었다
- response라는 상수값에 백엔드에서 지정한 회원가입 페이지가 나오는 url을 입력하고, 포스트맨에서 보내는 것을 수동으로 적은 것이라 생각하고 headers, method, body를 작성해준다
- 이때 프론트 주소와 백엔드 주소가 달라 cors 문제가 생길 것이다 저번에 해결한 것처럼 백엔드 작업환경에서 cors 패키지를 다운 받고 settings.py를 수정한다음 하면 된다
- body 뒤에 있는 JSON.stringify는 입력한 문장을 JSON형태로 변환시켜주는 메서드라 생각하면 된다
- 저걸 안 쓰면 사용자가 입력을 잘못했을 때 나오는 400 Bad Request가 뜬다
- JSON.stringify까지 추가해주면 회원가입이 문제없이 완료되는 걸 확인할 수 있다
- 데이터베이스에도 gu8@gu.gu가 유저에 들어가 있을 걸 볼 수 있다
로그인
- 다음은 로그인이다
- html은 회원가입이랑 똑같이 구성해준다
<!-- login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login</title>
<script src="api.js"></script>
</head>
<body>
<h1>로그인 페이지</h1>
<form>
<input type="email" name="email" id="email" placeholder="email" />
<input type="password" name="password" id="password" placeholder="password" />
<button type="button" onclick="handleLogin()">제출</button>
</form>
</body>
</html>
- 다만 타이틀은 Login, 바디태그에 h1 태그에 로그인 페이지로 하고, 제출 버튼 눌렀을 때 실행될 함수는 handleLogin()이다
- 다시 api.js 파일에 가서 로그인 함수를 만들어준다
// api.js
...
async function handleLogin() {
const email = document.getElementById("email").value
const password = document.getElementById("password").value
const response = await fetch('http://127.0.0.1:8000/users/api/token/', {
headers: {
'content-type': 'application/json'
},
method: 'POST',
body: JSON.stringify({
"email": email,
"password": password
})
})
}
- 여기까지는 회원가입 함수와 똑같고 url만 로그인 페이지로 바꿔준 상태다
- 이 아래에 할 것은 로그인 했을 때 가져오는 access 토큰과 refresh토큰, 그리고 access 토큰을 decode한 걸 localstorage에 저장할 것이다
// api.js
...
async function handleLogin() {
...
const response_json = await response.json()
localStorage.setItem("access", response_json.access); // access 토큰 localStorage에 저장
localStorage.setItem("refresh", response_json.refresh); // refresh 토큰 localStorage에 저장
const base64Url = response_json.access.split('.')[1];
const base64 = base64Url.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) // access 토큰을 decode해서 localStorage에 저장
}
페이지에 로그인한 이메일 띄우기
- 띄우고 싶은 html 바디 태그 안에 div를 작성해준다
<div id="intro"></div>
- 해당 html과 연결된 js 파일로 이동한다
// index.js
window.onload = () => {
const payload = localStorage.getItem("payload");
const payload_parse = JSON.parse(payload)
console.log(payload_parse.email)
const intro = document.getElementById("intro")
intro.innerText = payload_parse.email
}
- 로컬스토리지에서 payload에 담긴 값을 payload라는 변수에 넣는다
- 이때 스트링 형태로 들어가게 되는데 값을 가져오고 싶으면 오브젝트 형태로 변환해야 한다
- 그것이 JSON.parse()이고, payload를 오브젝트화한 값을 payload_parse에 담는다
- payload_parse.email 하면 로그인한 email에 접근 완료
- 이 이메일을 html에 띄우려면 document.getElementById("아이디값")을 변수에 담고 변수.innerText 라는 메서드를 이용하면 html에 내용을 띄울 수 있다
fetch로 access 토큰을 헤더에 실어서 보내는 법
- 먼저 login.html의 폼 태그 아래에 access 토큰을 확인할 버튼을 만든다
<button type="button" onclick="handleMock()">모크 api</button>
- api.js 가서 handleMock() 함수를 작성한다
// api.js
async function handleMock() {
const response = await fetch('http://127.0.0.1:8000/users/mock/', {
headers: {
"Authorization": "Bearer " + localStorage.getItem("access")
},
method: 'GET',
})
console.log(response)
}
- url을 mock로 변경해주고 포스트맨에서 썼던 것처럼 headers에 key값은 Authorization, value값은 먼저 Bearer 를 띄어쓰기까지 잘 써주고 로컬스토리지에서 getItem 메서드를 사용해서 access 토큰 값을 GET방식으로 가져온다
- 페이지 가서 모크 api 버튼을 눌러보면
- 뭔가가 잘 찍히는 것을 확인할 수 있다
- 백엔드 작업환경에서도 실행이 잘 됐다는 것을 알 수 있다
- 모크함수가 실행되면 해당 유저의 is_admin이 1로 바뀌게끔 설정을 했었는데 데이터베이스에 가보면 제대로 실행됐다는 것을 알 수 있다
로그아웃
- 구글에 jwt localstorage delete라고 검색했더니 removeItem을 쓰면 된다고 한다
- 일단 로그인 페이지에 로그아웃 버튼을 만들어줬다
<button type="button" onclick="handleLogout()">로그아웃</button>
- api.js 파일에서 handleLogout() 함수를 작성한다
// api.js
function handleLogout() {
localStorage.removeItem("access")
localStorage.removeItem("refresh")
localStorage.removeItem("payload")
}
- 끝
- 현재 로그인 된 상태라 로컬스토리지에 payload, access, refresh가 있는 것을 확인할 수 있다
- 이 상태에서 로그아웃 버튼을 누르면
- 로컬스토리지가 비워지는 것을 확인할 수 있다