Header
토큰의 유형(type)
Hashing algorithm 으로 구성
Payload
토큰에 넣을 정보
claim은 정보의 한 조각을 의미하며 payload 에는 여러 개의 claim을 넣을 수 있음
claim의 종류
Registered claims
pulic claims
private claims
Signature
REST framework JWT Auth 설치
pip install djangorestframework-jwt
settings.py > REST_FRAMEWORK
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
urls.py
from rest_framework_jwt.views import obtain_jwt_token
#...
urlpatterns = [
'',
# ...
path('api-token-auth/', obtain_jwt_token),
]
settings.py > JWT_AUTH
import datetime
JWT_AUTH = {
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}
user 커스텀으로 만들기
from django.db import models
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
pass
accounts > serializers.py
from rest_framework import serializers
from django.contrib.auth import get_user_model
User = get_user_model()
class UserSerializer(serializers.ModelSerializer):
# write_only는 시리얼라이징은 하지만 응답에는 포함시키지 않는다는 의미
# 비밀번호를 응답에 표현한다면 보안상의 유출이 되는 것이기 떄문
password = serializers.CharField(write_only=True)
class Meta :
model = User
fields = ('username', 'password')
accounts > views.py
회원가입 부분 기본모양 만들기
물론 이거 전에 urls.py 에 주소와 views.py 를 연결하는 부분 만들어주기
client 에서 온 데이터를 받음
패스워드와 패스워드확인이 일치하는지 일단 확인
UserSerializer를 통해 데이터 직렬화
validation 을 진행하고 password도 같이 직렬화 실행
(read_only값이기 떄문에 따로 해줘야함)
user 데이터를 저장하고, 저장값과 함께 201 상태코드를 보내줌
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .serializers import UserSerializer
@api_view(['POST'])
def signup(request):
#1-1. Client에서 온 데이터를 받아서
password = request.data.get('password')
password_confirmation = request.data.get('passwordConfirmation')
#1-2. 패스워드 일치 여부 체크
if password != password_confirmation:
return Response({'error': '비밀번호가 일치하지 않습니다.'}, status=status.HTTP_400_BAD_REQUEST)
#2. UserSerializer를 통해 데이터 직렬화
serializer = UserSerializer(data=request.data)
#3. validation 작업 진행 -> password도 같이 직렬화 진행
if serializer.is_valid(raise_exception=True):
user = serializer.save()
#4. 비밀번호 해싱 후
user.set_password(request.data.get('password'))
user.save()
# password는 직렬화 과정에는 포함 되지만 → 표현(response)할 때는 나타나지 않는다.
return Response(serializer.data, status=status.HTTP_201_CREATED)
this.$router.push({name : 위치 })
를 통해 페이지 이동 시켜주기<template>
<div>
<h1>Signup</h1>
<div>
<label for="username">사용자 이름: </label>
<input type="text" id="username" v-model="credentials.username">
</div>
<div>
<label for="password">비밀번호: </label>
<input type="password" id="password" v-model="credentials.password">
</div>
<div>
<label for="passwordConfirmation">비밀번호 확인: </label>
<input type="password" id="passwordConfirmation" v-model="credentials.passwordConfirmation">
</div>
<button @click="signup">회원가입</button>
</div>
</template>
<script>
import axios from 'axios'
// const SERVER_URL = process.env.VUE_APP_SERVER_URL
export default {
name: 'Signup',
data: function () {
return {
credentials : {
username : null,
password : null,
passwordConfirmation : null,
}
}
},
methods: {
signup: function () {
axios ({
method : 'post',
url : 'http://127.0.0.1:8000/accounts/signup/',
data: this.credentials,
})
.then(res => {
console.log(res)
// 회원가입에 성공하면 로그인 페이지로 보내기
this.$router.push({ name : 'Login'})
})
.catch(err => {
console.log(err)
})
}
}
}
</script>
http://127.0.0.1:8000/accounts/api-token-auth/ 에 postman을 통해 정보 보내기
.
을 기준으로 3등분 되어있음을 확인할 수 있다settings.py > SECRET_KEY
의 값을 넣으면 됨<template>
<div>
<h1>Login</h1>
<div>
<label for="username">사용자 이름: </label>
<input type="text" id="username" v-model="credentials.username">
</div>
<div>
<label for="password">비밀번호: </label>
<input type="password" id="password" v-model="credentials.password">
</div>
<button @click="login">회원가입</button>
</div>
</template>
<script>
import axios from 'axios'
// const SERVER_URL = process.env.VUE_APP_SERVER_URL
export default {
name: 'Login',
data: function () {
return {
credentials : {
username : null,
password : null,
}
}
},
methods: {
login: function () {
axios({
method : 'post',
url : 'http://127.0.0.1:8000/accounts/api-token-auth/',
data : this.credentials,
})
.then(res => {
// 로컬스토리지에 토큰 저장
localStorage.setItem('jwt', res.data.token)
// TodoList로 이동
this.$router.push({ name : 'TodoList'})
})
.catch(err => {
console.log(err)
})
}
}
}
</script>
App.vue 에서 로그인 체크
로컬스토리지에 jwt가 존재하는지를 판단(created)해서 isLogin에 값 넣기
isLogin의 값에 따라 보이는 메뉴 달라짐
로그인 페이지에서 로그인을 하게 된다면 login 이벤트를 받아서 isLogin 수정
router-link를 a태그로써 랜더링 되지만 a태그 고유의 이벤트를 없애고, vue가 사용하는 특수한 형태로 만들어진 것
그래서 @click 으로 했을 떄 이벤트가 발생하지 않음
@click.native 로 해줘야 클릭했을 때 이벤트가 발생되게 됨
<template>
<div id="app">
<div id="nav">
<span v-if="isLogin">
<router-link :to="{ name: 'TodoList' }">Todo List</router-link> |
<router-link :to="{ name: 'CreateTodo' }">Create Todo</router-link> |
</span>
<span v-else>
<router-link :to="{ name: 'Signup' }">Signup</router-link> |
<router-link :to="{ name: 'Login' }">Login</router-link>
</span>
</div>
<router-view @login="isLogin = true"/>
</div>
</template>
<script>
export default {
name: 'App',
data: function () {
return {
isLogin : false,
}
},
methods: {
},
created : function(){
// 로컬스토리지에 jwt 이 존재하는지에 따라 로그인 여부 판단하기
const token = localStorage.getItem('jwt')
if(token){
this.isLogin = true
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>
사용할 모델에 user 를 foreign key 로 가지도록 만듬
from django.db import models
from django.conf import settings # user는 직접참조가 아님
# Create your models here.
class Todo(models.Model):
user = models.foreignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="todos")
# user를 foreign key로 가질 떄 이렇게
# todo.user는 접근이 되지만, user에서는 user.todo_set으로 접근해야되기 떄문에 좀더 편하게 user.todos로 related_name을 이용하여 바꾸기
title = models.CharField(max_length=50)
completed = models.BooleanField(default=False)
def __str__(self):
return self.title
todos > views.py
내용 수정from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.response import Response
from rest_framework.decorators import api_view
from rest_framework.decorators import authentication_classes, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from .serializers import TodoSerializer
from .models import Todo
@api_view(['GET', 'POST'])
def todo_list_create(request):
if request.method == 'GET':
todos = Todo.objects.all()
serializer = TodoSerializer(todos, many=True)
return Response(serializer.data)
elif request.method == 'POST':
serializer = TodoSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
@api_view(['PUT', 'DELETE'])
def todo_update_delete(request, todo_pk):
todo = get_object_or_404(Todo, pk=todo_pk)
if request.method == 'PUT':
serializer = TodoSerializer(todo, data=request.data)
if serializer.is_valid(raise_exception=True):
serializer.save()
return Response(serializer.data)
elif request.method == 'DELETE':
todo.delete()
return Response({ 'id': todo_pk })
추가가 잘 되는지 확인하기
<template>
<div>
<ul>
<li v-for="(todo, idx) in todos" :key="idx">
<span @click="updateTodoStatus(todo)" :class="{ completed: todo.completed }">{{ todo.title }}</span>
<button @click="deleteTodo(todo)" class="todo-btn">X</button>
</li>
</ul>
<button @click="getTodos">Get Todos</button>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'TodoList',
data: function () {
return {
todos: [],
}
},
methods: {
setToken : function () { // header 내용에 토큰 붙여주기
const token = localStorage.getItem('jwt')
const config = {
Authorization : `JWT ${token}`
}
return config
},
getTodos: function () {
axios({
method: 'get',
url: 'http://127.0.0.1:8000/todos/',
headers : this.setToken()
})
.then((res) => {
console.log(res)
this.todos = res.data
})
.catch((err) => {
console.log(err)
})
},
deleteTodo: function (todo) {
axios({
method: 'delete',
url: `http://127.0.0.1:8000/todos/${todo.id}/`,
headers : this.setToken()
})
.then((res) => {
console.log(res)
this.getTodos()
})
.catch((err) => {
console.log(err)
})
},
updateTodoStatus: function (todo) {
const todoItem = {
...todo,
completed: !todo.completed
}
axios({
method: 'put',
url: `http://127.0.0.1:8000/todos/${todo.id}/`,
data: todoItem,
headers : this.setToken(),
})
.then((res) => {
console.log(res)
todo.completed = !todo.completed
})
},
},
created: function () {
if (localStorage.getItem('jwt')){ // 로그인이 되어있다면 todo 보이고
this.getTodos()
} else { // 로그인이 안되어 있다면 Login페이지로 이동
this.$router.push({name : 'Login'})
}
}
}
</script>
<style scoped>
.todo-btn {
margin-left: 10px;
}
.completed {
text-decoration: line-through;
color: rgb(112, 112, 112);
}
</style>