로그인을 하면 백엔드에서 쿠키를 돌려주고 세션을 만든다. 로그인할 때마다 쿠키와 세션을 비교하며 확인한다.
로그인을 하면 백엔드에서 사용자의 정보가 담긴 토큰을 돌려보내준다. 이를 쿠키 대신에 로컬스토리지라는 곳에 저장한다.(주민등록증같은 느낌)
토큰&세션 관련 자료 : https://www.geeksforgeeks.org/session-vs-token-based-authentication/
simple jwt 관련 자료 : https://django-rest-framework-simplejwt.readthedocs.io/en/latest/getting_started.html
jwt 저장 관련 자료 : https://velog.io/@0307kwon/JWT%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-localStorage-vs-cookie
필요한 모듈 설치
pip install djangorestframework-simplejwt
pip install djangorestframework
pip install django
settings.py에 추가
INSTALLED_APPS = [
'rest_framework',
'user',
'rest_framework_simplejwt',
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
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'),
]
이렇게 하고 포스트맨에서 로그인 해보자!!
views.py에서 에러처리를 따로 안해도 TokenObtainPairView가 view에서 자체적으로 만들어준다.
제대로 로그인을 해보면 아래와 같은 결과가 나온다.
jwt 공식 사이트 : https://jwt.io/
jwt 공식 사이트에 토큰을 넣어보자!
결과값이 3가지로 나뉘어서 나온다.
-> 처음 토큰만 입력했을 때는 인증이 안된 상태라 invalid라고 뜨는데 이것을 인증을 하려면 settings.py에 있는 secret key를 입력해야한다.
그리고 이 secret key는 .env파일로 관리를 해야한다.
결과값은 토큰 타입만 다르고 나머지는 같다!
만료가 된다. 만료가 되면 자동으로 로그아웃
누군가가 탈취를 하면 서버쪽에서 삭제를 시킬 수 없다.
만료일은 내가 변경할 수 있다.
access가 만료가 되면 refresh를 이용해 재발급 받을 수 있다.
유저 커스터마이징 관련 자료 : https://docs.djangoproject.com/en/4.1/topics/auth/customizing/
from django.db import models
from django.contrib.auth.models import (
BaseUserManager, AbstractBaseUser
)
class UserManager(BaseUserManager):
def create_user(self, email, password=None):
"""
Creates and saves a User with the given email, date of
birth and password.
"""
if not email:
raise ValueError('Users must have an email address')
user = self.model(
email=self.normalize_email(email),
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, email, password=None):
"""
Creates and saves a superuser with the given email, date of
birth and password.
"""
user = self.create_user(
email,
password=password,
)
user.is_admin = True
user.save(using=self._db)
return user
class User(AbstractBaseUser):
email = models.EmailField(
verbose_name='email address',
max_length=255,
unique=True,
)
is_active = models.BooleanField(default=True)
is_admin = models.BooleanField(default=False)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
def __str__(self):
return self.email
def has_perm(self, perm, obj=None):
"Does the user have a specific permission?"
# Simplest possible answer: Yes, always
return True
def has_module_perms(self, app_label):
"Does the user have permissions to view the app `app_label`?"
# Simplest possible answer: Yes, always
return True
@property
def is_staff(self):
"Is the user a member of staff?"
# Simplest possible answer: All admins are staff
return self.is_admin
핼퍼 클래스로 필수적으로 있어야 한다!
핼퍼 클래스에는 추가적으로 메소드들이 정의되어 있고, db와 송신하는 것들이 저장되어 있다.
핼퍼 클래스가 없으면 오류가 발생한다.
올려놓은 코드는 장고 공식 사이트에서 커스텀유저용으로 올라온것을 가져온 것인데 모든 것들이 필수 요소들이니 빼먹지 말아야 한다!!!
유저 커스터마이징 관련 자료 : https://docs.djangoproject.com/en/4.1/topics/auth/customizing/
from django import forms
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.exceptions import ValidationError
from user.models import User
class UserCreationForm(forms.ModelForm):
password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
class Meta:
model = User
fields = ('email',)
def clean_password2(self):
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise ValidationError("Passwords don't match")
return password2
def save(self, commit=True):
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
class UserChangeForm(forms.ModelForm):
password = ReadOnlyPasswordHashField()
class Meta:
model = User
fields = ('email', 'password', 'is_active', 'is_admin')
class UserAdmin(BaseUserAdmin):
form = UserChangeForm
add_form = UserCreationForm
list_display = ('email', 'is_admin')
list_filter = ('is_admin',)
fieldsets = (
(None, {'fields': ('email', 'password')}),
('Permissions', {'fields': ('is_admin',)}),
)
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('email', 'password1', 'password2'),
}),
)
search_fields = ('email',)
ordering = ('email',)
filter_horizontal = ()
admin.site.register (User, UserAdmin)
admin.site.unregister(Group)
수정할 때 trailing 콤마 주의하기!! 콤마 안붙이면 오류난당~~
이렇게 작성하면 비밀번호 해싱도 되고, 이메일로 유저 검색도 가능하다!!
from django.urls import path, include
from user import views
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('signup/', views.UserView.as_view(), name='user_view'),
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from user.serializers import UserSerializer
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)
from rest_framework import serializers
from user.models import User
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = "__all__"
기본만큼만 하면 회원가입이 되긴 되지만 비밀번호 해싱이 안된채로 회원가입이 되어서 로그인이 안된다!
이를 해결하기 위해서는 뭔가 더 추가해주어야 한다!!
from rest_framework import serializers
from user.models import User
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
이렇게 작성하면 비밀번호 해싱이 되어서 로그인이 잘 된다!!!
PayLoad 부분에 email이 나오게 커스터마이징을 해보자!!
페이로드 커스터마이징 공식문서 : https://django-rest-framework-simplejwt.readthedocs.io/en/latest/customizing_token_claims.html
from django.urls import path, include
from user import views
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('signup/', views.UserView.as_view(), name='user_view'),
path('api/token/', views.CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
2번째 path views.CustomTokenObtainPairView.as_view()로 수정
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from user.serializers import CustomTokenObtainPairSerializer, UserSerializer
from rest_framework_simplejwt.views import (
TokenObtainPairView,
)
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)
class CustomTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
class CustomTokenObtainPairView 추가
from rest_framework import serializers
from user.models import User
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
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
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
token['email'] = user.email
return token
class CustomTokenObtainPairSerializer 추가
이렇게 하면 PayLoad에 email이 추가된다!!
유저 settings.py 관련 자료 : https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html
settings.py에 아래 내용 추가
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=720),}
로그인 시간이 720분으로 변경되었다!!
from django.urls import path, include
from user import views
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
path('signup/', views.UserView.as_view(), name='user_view'),
path('mock/', views.mockView.as_view(), name='mock_view'),
path('api/token/', views.CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
]
mock 관련 url 추가!
from rest_framework.views import APIView
from rest_framework import status
from rest_framework.response import Response
from rest_framework import permissions
from user.serializers import CustomTokenObtainPairSerializer, UserSerializer
from rest_framework_simplejwt.views import (
TokenObtainPairView,
)
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)
class CustomTokenObtainPairView(TokenObtainPairView):
serializer_class = CustomTokenObtainPairSerializer
class mockView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
return Response("get 요청")
class mockView 추가&관련 import 추가
1. header로 들어가기
2. key값은 Authorization, value값은 Bearer +access token 작성
Bearer작성하고 한칸 띄고 access toekn 작성해야 한다!
3. send
4. 잘 로그인 되었다는 "get 요청"이 뜬다!!
1. 주소를 http://127.0.0.1:8000/user/api/token/refresh/ 로 변경
2. login으로 가서 로그인 다시하고 refresh token 복사해오기
3. 사진처럼 붙여넣고 send
4. 결과값으로 access token이 나온다!
js에서 버튼이 눌려지고 있는지 항상 꼭꼭 확인하기!!!
한줄 한줄 잘 실행되고 있는지 확인해가면서 개발하자!!
안그러면 나중에 오류가 날 때 어디서 안되는건지 알 수가 없다..
자바스크립트는 친절하게 어디서 오류가 나는지 알려주지 않기 때문에...
화살표 함수 관련 자료 : https://poiemaweb.com/es6-arrow-function
getElementById 관련 자료 : https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementById
자바스크립트 변수인 let, const, var의 차이 관련 자료: https://www.howdy-mj.me/javascript/var-let-const
<!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>
<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="handleSignin()">제출</button>
</form>
</body>
</html>
window.onload = ()=>{
console.log("로딩완료")
}
function handleSignin(){
const email = document.getElementById("email").value
const password = document.getElementById("password").value
console.log(email, password)
}
fetch 관련 자료 : https://developer.mozilla.org/ko/docs/Web/API/Fetch_API/Using_Fetch
window.onload = ()=>{
console.log("로딩완료")
}
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/user/signup/', {
headers:{
'content-type':'application/json',
},
method:'POST',
body:JSON.stringify({
"email":email,
"password":password
})
})
console.log(response)
}
같은 도메인에서 요청을 보낼때는 허용이 되어있는데 다른 도메인에서 요청을 보낼 때는 보안상의 문제로 별도의 허용이 필요하다. 이 때 CORS로 허용해주면 된다.
CORS 관련 자료 : https://github.com/adamchainz/django-cors-headers
<!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>
window.onload = ()=>{
console.log("로딩완료")
}
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/user/api/token/', {
headers:{
'content-type':'application/json',
},
method:'POST',
body:JSON.stringify({
"email":email,
"password":password
})
})
const response_json = await response.json()
console.log(response_json)
}
여기까지 하면 로그인이 되는것까지 확인할 수 있다.
하지만 아직 access token이 로컬스토리지에 저장되지 않은상태이다!
토큰을 로컬스토리지에 저장해보자!!
window.onload = ()=>{
console.log("로딩완료")
}
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/user/api/token/', {
headers:{
'content-type':'application/json',
},
method:'POST',
body:JSON.stringify({
"email":email,
"password":password
})
})
const response_json = await response.json()
console.log(response_json)
localStorage.setItem("access", response_json.access);
localStorage.setItem("refresh", response_json.refresh);
}
아래의 코드를 기존 코드에 추가하면 된다!!
localStorage.setItem("access", response_json.access);
localStorage.setItem("refresh", response_json.refresh);
window.onload = ()=>{
console.log("로딩완료")
}
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/user/signup/', {
headers:{
'content-type':'application/json',
},
method:'POST',
body:JSON.stringify({
"email":email,
"password":password
})
})
console.log(response)
}
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/user/api/token/', {
headers:{
'content-type':'application/json',
},
method:'POST',
body:JSON.stringify({
"email":email,
"password":password
})
})
const response_json = await response.json()
console.log(response_json)
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);
}
제일 아래쪽에 const base64Url부터 끝까지의 내용을 추가!
<!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>홈페이지</title>
<script src="index.js"></script>
</head>
<body>
<h1>홈페이지</h1>
<div id="intro"></div>
</body>
</html>
console.log("로딩되었습니다!")
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
}
<!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>
<button type="button" onclick="handleMock()">모크 api</button>
</body>
</html>
window.onload = ()=>{
console.log("로딩완료")
}
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/user/signup/', {
headers:{
'content-type':'application/json',
},
method:'POST',
body:JSON.stringify({
"email":email,
"password":password
})
})
console.log(response)
}
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/user/api/token/', {
headers:{
'content-type':'application/json',
},
method:'POST',
body:JSON.stringify({
"email":email,
"password":password
})
})
const response_json = await response.json()
console.log(response_json)
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);
}
async function handleMock(){
const response = await fetch('http://127.0.0.1:8000/user/mock/', {
headers:{
"Authorization":"Bearer " + localStorage.getItem("access")
},
method:'GET',
})
console.log(response)
}
<!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>
<button type="button" onclick="handleMock()">모크 api</button>
<button type="button" onclick="handleLogout()">로그아웃</button>
</body>
</html>
window.onload = ()=>{
console.log("로딩완료")
}
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/user/signup/', {
headers:{
'content-type':'application/json',
},
method:'POST',
body:JSON.stringify({
"email":email,
"password":password
})
})
console.log(response)
}
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/user/api/token/', {
headers:{
'content-type':'application/json',
},
method:'POST',
body:JSON.stringify({
"email":email,
"password":password
})
})
const response_json = await response.json()
console.log(response_json)
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);
}
async function handleMock(){
const response = await fetch('http://127.0.0.1:8000/user/mock/', {
headers:{
"Authorization":"Bearer " + localStorage.getItem("access")
},
method:'GET',
})
console.log(response)
}
function handleLogout(){
localStorage.removeItem("access")
localStorage.removeItem("refresh")
localStorage.removeItem("payload")
}