[Vue] Vue with DRF 2

Jingi·2024년 5월 14일

Web

목록 보기
40/40
post-thumbnail

Authentication with DRF


개요


  • 인증 로직 진행을 위해 User모델 관련 코드 활성화
  • user ForiegnKey 주석 해제
    # articles/models.py
    from django.db import models
    from django.conf import settings
class Article(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE
    )
    title = models.CharField(max_length=100)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
```
  • serializers의 read_only_fileds 주석 해제

    # articles/serializers.py
    class ArticleSerializer(serializers.ModelSerializer):
        class Meta:
            model = Article
            fields = '__all__'
            read_only_fields = ('user',)
  • article_list view 함수에서 게시글 생성 시 user 정보도 저장될 수 있도록 주석 해제

    @api_view(['GET', 'POST'])
    @permission_classes([IsAuthenticated])
    def article_list(request):
        if request.method == 'GET':
            articles = get_list_or_404(Article)
            serializer = ArticleListSerializer(articles, many=True)
            return Response(serializer.data)
    
        elif request.method == 'POST':
            serializer = ArticleSerializer(data=request.data)
            if serializer.is_valid(raise_exception=True):
                # serializer.save()
                serializer.save(user=request.user)
                return Response(serializer.data, status=status.HTTP_201_CREATED)
    
  • DB 초기화

    • db.sqlite3 삭제
    • migrations 파일 삭제
  • Migration 과정 재 진행

인증


Authentication

수신된 요청을 해당 요청의 사용자 또는 자격증명과 연결하는 메커니즘

→ 누구인지를 확인하는 과정

permisions

요청에 대한 접근 허용 또는 거부 여부를 결정

인증과 권한

  • 순서상 인증이 먼저 진행, 수신 요청을 해당 요청의 사용자 또는 해당 요청이 서명된 토큰과 같은 자격증명자료와 연결
  • 그런 다음 권한 및 제한 정책은 인증이 완료된 해당 자격 증명을 사용하여 요청을 허용해야 하는 지를 결정

DRF 에서의 인증

  • 인증은 항상 view 함수 시작 시, 권한 및 제한 확인이 발생하기 전, 다른 코드의 진행이 허용되기 전 실행 됨
  • 인증 자체로는 들어오는 요청을 허용하거나 거부할 수 없으며, 단순히 요청에 사용된 자격증명만 시별한다는 점에 유의

승인되지 않은 응답 및 금지된 응답

  • 인증되지 않은 요청이 권한을 거부하는 경우 해당되는 두가지 오류 코드를 응답
  1. HTTP 401 Unathorized
    • 요청된 리소스에 대한 유효한 인증 자격이 없이 깨문에 클라이언트 요청이 완료되지 않았음
  2. HTTP 403 Forbidden
    • 서버에 요청이 전달되었지만 권한때문에 거절됨
    • 401과 다른점은 서버는 클라이언트가 누군지 알고 있음

인증 체계 설정


  1. 전역설정
  2. View함수별 설정

전역 설정

  • Default_Authentication_classes를 사용

  • 사용 예시

    REST_FRAMEWORK = {
        # Authentication
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework.authentication.BasicAuthentication',
            'rest_framework.authentication.TokenAuthentication',
    
        ]
    }

View 함수 별 설정

  • Authenticaiton_classes 데코레이터를 사용
  • 사용예시
    @api_view(['GET', 'POST'])
    @permission_classes([IsAuthenticated])
    def article_list(request):
        pass

DRF가 제공하는 인증 체계

  1. BasicAuthentication
  2. TokenAuthentication
  3. SessionAuthentication
  4. RemoteUserAuthentication

TokenAuthentication

  • token 기반 HTTP 인증 체계
  • 기본 데스크톱 및 모바일 클라이언트와 같은 클라이언트-서버 설정에 적합

→ 서버가 인증된 사용자에게 토큰을 발급, 사용자는 매 요청마다 발급받은 토큰을 요청과 함께 보내 인증 과정을 거침

Token 인증 설정


TokenAuthentication 적용 과정

  1. 인증 클래스 설정
  2. Installed_APPS 추가
  3. Migrate 진행
  4. 토큰 생성 코드 작성

1. 인증 클래스 설정

  • Token Authentication 활성화 코드 주석 해제

  • 기본적으로 모든 View함수가 토큰 기반 인증이 진행될 수 있도록 설정

    # settings.py
    REST_FRAMEWORK = {
        # Authentication
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework.authentication.TokenAuthentication',
    
        ]
    }

2. INSTALLED_APPS 추가

  • rest_framework.authtoken주석 해제
    INSTALLED_APPS = [
        'articles',
        'accounts',
        'rest_framework',
        'rest_framework.authtoken',
        'dj_rest_auth',
        'corsheaders',
        'django.contrib.sites',
        'allauth',
        'allauth.account',
        'allauth.socialaccount',
        'dj_rest_auth.registration',
        'django.contrib.admin',
        'django.contrib.auth',
        'django.contrib.contenttypes',
        'django.contrib.sessions',
        'django.contrib.messages',
        'django.contrib.staticfiles',
    ]

Migrate 진행

  • Migrate
    • python manage.py migrate

토큰 생성 코드 작성

  • 인증된 사용자에게 자동으로 토큰을 생성해주는 역할
    from django.db.models.signals import post_save
    from django.dispatch import receiver
    from rest_framework.authtoken.models import Token
    from django.conf import settings
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        Token.objects.create(user=instance)

```

Dj-Rest-Auth 라이브러리


  • 회원가입, 인증, 비밀번호 재설정, 사용자 세버 정보 검색, 회원 정보 수정 등 다양한 인증 관련 기능을 제공하는 라이브러리

Dj-Rest-Auth 설치 및 적용

  • 설치
    • pip install dj-rest-auth
  • 추가 App 주석 해제
    # settings.py
    INSTALLED_APPS = [
        'articles',
        'accounts',
        'rest_framework',
        'rest_framework.authtoken',
        'dj_rest_auth',
        ...,
    ]
  • 추가 URL 주석 해제
    urlpatterns = [
        path('admin/', admin.site.urls),
        path('api/v1/', include('articles.urls')),
        path('accounts/', include('dj_rest_auth.urls')),
    ]

Dj-Rest-Auth의 Registration 기능 추가 설정

  1. 패키지 추가 설치
  2. 추가 App등록
  3. 추가 URL 등록
  4. Migrate

Registration 기능 추가

  1. 패키지 추가 설치

    • pip install 'dj-rest-auth[with_social]'
  2. 추가 App 주석 해제

    INSTALLED_APPS = [
        ...,
        'django.contrib.sites',
        'allauth',
        'allauth.account',
        'allauth.socialaccount',
        'dj_rest_auth.registration',
        'django.contrib.admin',
        ...,
    ]
    
    SITE_ID = 1
  3. 관련설정 코드

    ACCOUNT_EMAIL_VERIFICATION = 'none'
    
    MIDDLEWARE = [
        ...,
        'allauth.account.middleware.AccountMiddleware',
    ]
  4. 추가 URL 주석 해제

    from django.contrib import admin
    from django.urls import path, include
urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include('articles.urls')),
    path('accounts/', include('dj_rest_auth.urls')),
    path('accounts/signup/', include('dj_rest_auth.registration.urls')),
]
```
  1. Migrate 진행

Token 발급 및 활용


Token 발급

Token 활용

  • 게시글 작성 과정을 통해 Token 사용 방법 익히기
  • Postman을 활용해 게시글 작성 요청
  • Body에 게시글 제목과 내용 입력
  • Headers에 발급받은 Token 작성 후 요청 성공 확인
    • key : Authorization
    • value : Token 토큰 값

클라이언트가 Token으로 인증 받는 방법

  1. Authorization HTTP Header에 포함
  2. 키 앞에는 문자열 Token이 와야하며 공백으로 두 문자열을 구분해야함

발급 받은 TOKEN을 인증이 필요한 요청마다 함께 보내야 함

권한 정책 설정


  1. 전역 설정
  2. View 함수 별 설정

1. 전역 설정

  • DEFAULT_PERMISSION_CLASSES를 사용

  • 사용 예시

    # settings.py
    REST_FRAMEWORK = {
        # Authentication
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework.authentication.TokenAuthentication',
    
        ]
    }
  • 지정하지 않을 경우 기본적으로 이 설정은 무제한 액세스를 허용

    # settings.py
    REST_FRAMEWORK = {
        # Authentication
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework.permissions.AllowAny',
        ]
    }

2. View함수 별 설정

  • permission_classes 데코레이터를 사용

  • 사용 예시

    # articles/views.py
    
    from rest_framework.decorators import permission_classes
    from rest_framework.permissions import IsAuthenticated
    
    @api_view(['GET', 'POST'])
    @permission_classes([IsAuthenticated])
    def article_list(request):
        pass

DRF가 제공하는 권한 정책

  1. IsAUthenticated
  2. IsAdminUser
  3. IsAuthenticatedOrReadOnly
  4. ..

IsAuthenticated 권한

  • 인증되지 않은 사용자에 대한 권한을 거부, 그렇지 않은 경우 권한을 허용
  • 등록된 사용자만 API에 엑세스 할 수 있도록 하려는 경우에 적합

IsAuthenticated 권한 설정


IsAuthenticated 권한 설정

  • DEFAULT_PERMISSION_CLASSES 주석 해제

  • 기본적으로 모든 VIew 함수에 대한 접근 허용

    # settings.py
    REST_FRAMEWORK = {
        # Authentication
        'DEFAULT_AUTHENTICATION_CLASSES': [
            'rest_framework.authentication.TokenAuthentication',
        ],
        # permission
        'DEFAULT_PERMISSION_CLASSES': [
            'rest_framework.permissions.AllowAny',
        ],
    }
  • permissio_classes관련 코드 주석 해제

    • 전체 게시글 조회 및 생성시에만 인증된 사용자만 진행할 수 있도록 권한 설정

      # articles/views.py
      
      from rest_framework.decorators import permission_classes
      from rest_framework.permissions import IsAuthenticated
      
      @api_view(['GET', 'POST'])
      @permission_classes([IsAuthenticated])
      def article_list(request):
          pass

권한 활용

  • 만약 관리자만 전체 게시글 조회가 가능한 권한이 설정 되었을 때, 인증된 일반 사용자가 조회 요청을 할 경우 어떻게 되는지 응답 확인하기

  • 테스트를 위해 임시로 관리자 관련 권한 클래스 IsAdminUser로 변경

    from rest_framework.permissions import IsAdminUser
    
    @api_view(['GET', 'POST'])
    @permission_classes([IsAdminUser])
    def article_list(request):
        pass
  • 전체 게시글 조회 요청

  • 403 Forbidden 응답 확인

  • IsAdminUser삭제 후 IsAuthenticated 권한으로 복구

    from rest_framework.permissions import IsAuthenticated
    
    @api_view(['GET', 'POST'])
    @permission_classes([IsAuthenticated])
    def article_list(request):
        pass

Authentication with Vue


시작하기 전에


  • 정상 작동하던 게시글 전체 조회가 작동하지 않음
    • 401 status code확인
  • 게시글 조회 요청시 인증에 필요한 수단을 보내지 않고 있으므로 게시글 조회가 불가능 해진 것

회원 가입


회원 가입 로직 구현

  • SignupView route관련 코드 주석 해제

    import SignUpView from '@/views/SignUpView.vue'
    
    const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        {
        path: '/signup',
        name: 'SignUpView',
        component: SignUpView
        }
    ]
    })
    
  • app컴포넌트에 SignupView 컴포넌트로 이동하는 RouterLink 작성

    <template>
    <header>
        <nav>
        <RouterLink :to="{ name: 'ArticleView' }">Articles</RouterLink> |
        <RouterLink :to="{ name: 'SignUpView' }">회원가입</RouterLink> |
        </nav>
    </header>
    <RouterView />
    </template>
  • 회원가입 form작성

    <template>
    <div>
        <h1>회원가입</h1>
        <form @submit.prevent="signUp">
        <div>
            <label for="username">username : </label>
            <input type="text" v-model.trim="username" id="username">
        </div>
        <div>
            <label for="password1">password : </label>
            <input type="password" v-model.trim="password1" id="password1">
        </div>
        <div>
            <label for="password2">password confirmation : </label>
            <input type="password" v-model.trim="password2" id="password2">
        </div>
        <input type="submit">
        </form>
    </div>
    </template>
  • 사용자 입력 데이터와 비인딩될 반응형 변수 작성

    import { ref } from 'vue'
    import { useCounterStore } from '@/stores/counter'
    
    const username = ref(null) 
    const password1 = ref(null)
    const password2 = ref(null)
  • SignUpView 컴포넌트 출력 확인

  • 회원 가입 요청을 보내기 위한 signUp함수가 해야 할 일

    1. 사용자 입력 데이터를 받아

    2. 서버로 회원가입 요청을 보냄

      
      export const useCounterStore = defineStore('counter', () => {
      
      const signUp = function () {
      ...
      }
      
      return { articles, API_URL, getArticles, signUp, logIn, token, isLogin }
      }, { persist: true })
```
  • 컴포넌트에 사용자 입력 데이터 저장 후 store의 signUp함수를 호출하는 함수 작성

    const signUp = function () {
    const payload = {
        username: username.value,
        password1: password1.value,
        password2: password2.value
    }
    store.signUp(payload)
    }
  • 실제 회원 가입 요청을 보내는 store의 signUp함수 작성

    const signUp = function (payload) {
        // 1. 사용자 입력 데이터를 받아
        const username = payload.username
        const password1 = payload.password1
        const password2 = payload.password2
        // const { username, password1, password2 } = payload
    
        // 2. axios로 django에 요청을 보냄
        axios({
        method: 'post',
        url: `${API_URL}/accounts/signup/`,
        data: {
            username, password1, password2
        }
        })
        .then((response) => {
        console.log('회원가입 성공!')
        const password = password1
        logIn({ username, password })
        })
        .catch((error) => {
        console.log(error)
        })
    }

로그인


76page

로그인 로직 구현

  • LogInView route

    import LogInView from '@/views/LogInView.vue'
    
    const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        {
        path: '/login',
        name: 'LogInView',
        component: LogInView
        }
    ]
    })
  • App 컴포넌트에 LogInView컴포넌트로 이동하는 RotuerLink 작성

    Untitled

  • 로그인 form작성

    <template>
    <header>
        <nav>
        <RouterLink :to="{ name: 'LogInView' }">로그인</RouterLink>
        </nav>
    </header>
    <RouterView />
    </template>
  • 사용자 입력 데이터와 바인딩 될 반응형 변수 작성

    <script setup>
    import { ref } from 'vue'
    import { useCounterStore } from '@/stores/counter'
    
    const username = ref(null)
    const password = ref(null)
    </script>
  • 로그인 요청을 보내기 위한 logIn함수가 해야 할 일

    1. 사용자 입력 데이터를 받아

    2. 서버로 로그인 요청 및 응답 받은 토큰 저장

      import { ref, computed } from 'vue'
      import { defineStore } from 'pinia'
      import axios from 'axios'
      import { useRouter } from 'vue-router'
      
      export const useCounterStore = defineStore('counter', () => {
      const logIn = function (payload) {
          ...
      }
      return { articles, API_URL, getArticles, signUp, logIn, token, isLogin }
      }, { persist: true })
      
  • 컴포넌트에 사용자 입력 데이터를 저장 후 store의 logIn함수를 호출하는 함수 작성

  • 실제 로그인 요청을 보내는 store의 logIn함수 작성

    const logIn = function (payload) {
        // 1. 사용자 입력 데이터를 받아
        const { username, password } = payload
        // 2. axios로 django에 요청을 보냄
        axios({
        method: 'post',
        url: `${API_URL}/accounts/login/`,
        data: {
            username, password
        }
        })
        .then((response) => {
            // console.log('로그인 성공!')
            // console.log(response)
            // console.log(response.data.key)
            // 3. 로그인 성공 후 응답 받은 토큰을 저장
            token.value = response.data.key
            router.push({ name : 'ArticleView' })
        })
        .catch((error) => {
            console.log(error)
        })
    }
  • 로그인 테스트

  • 응답 객체 안에 Django가 발급한 Token이 함께 온 것을 확인

요청과 토큰


Token을 sotre에 저장하여 인증이 필요한 요청마다 함께 보낸다.

토큰 저장 로직 구현

  • 반응형 변수 token 선언 및 토큰 저장

    export const useCounterStore = defineStore('counter', () => {
    const articles = ref([])
    const API_URL = 'http://127.0.0.1:8000'
    const token = ref(null)
    const logIn = function (payload) {
        ...
            token.value = response.data.key
            router.push({ name : 'ArticleView' })
        .catch((error) => {
            console.log(error)
        })
    }
    
    return { articles, API_URL, getArticles, signUp, logIn, token, isLogin }
    }, { persist: true })
    
  • 다시 로그인 요청 후 sotre에 저장된 토큰 확인

토큰이 필요한 요청

  1. 게시글 전체 목록 조회 시
  2. 게시글 작성 시

게시글 전체 목록 조회 with token

  • 게시글 전체 목록 조회 요청 함수 getArticles에 token추가

    export const useCounterStore = defineStore('counter', () => {
    
    const getArticles = function () {
        axios({
        method: 'get',
        url: `${API_URL}/api/v1/articles/`,
        )
    }
return { articles, API_URL, getArticles, signUp, logIn, token, isLogin }
}, { persist: true })

```
  • 401상태 코드가 사라지고 게시글이 정상적으로 출력되는 것을 확인

  • 게시글 전체 목록 조회 요청 함수 getArticles에 token 추가

    import { ref, computed } from 'vue'
    import { defineStore } from 'pinia'
    import axios from 'axios'
    import { useRouter } from 'vue-router'
    
    export const useCounterStore = defineStore('counter', () => {
    
    const getArticles = function () {
        axios({
        method: 'get',
        url: `${API_URL}/api/v1/articles/`,
        headers: {
            Authorization: `Token ${token.value}`
        }
        })
        .then(response => {
            articles.value = response.data
        })
        .catch(error => {
            console.log(error)
        })
    }
return { articles, API_URL, getArticles, signUp, logIn, token, isLogin }
}, { persist: true })

```
  • 게시글 작성 확인

인증 여부 확인


사용자의 인증 여부에 따른 추가 기능 구현

  1. 인증되지 않은 사용자
    • 메인 페이지 접근 제한
  2. 인증된 사용자
    • 회원 가입 및 로그인 페이지에 접근 제한

인증 상태 여부를 나타낼 속성 값 지정

  • token 소유 여부에 따라 로그인 상태를 나타낼 isLogin 변수 작성
  • 그리고 computed를 활용해 token값이 변할 때만 상태를 계산하도록 함
  const isLogin = computed(()=>{
    if (token.value == null){
      return false
    } else {
      return true
    }
  })

1. 인증되지 않은 사용자는 메인 페이지 접근 제한

  • 전역 네비게이션 가드 beforeEach를 활용해 다른 주소에서 메인 페이지로 이동시 인증되지 않은 사용자라면 로그인 페이지로 이동시키기
    import { useCounterStore } from '@/stores/counter'
router.beforeEach((to, from) => {
const store = useCounterStore()
// 인증된 사용자는 회원가입과 로그인 페이지에 접근 할 수 없음
if ((to.name === 'SignUpView' || to.name === 'LogInView') && (store.isLogin === true)) {
    window.alert('이미 로그인 했습니다.')
    return { name: 'ArticleView' }
}
})
```
  • 브라우저 local storage에서 token을 삭제 후 메인 페이지 접속 시도

2. 인증 된 사용자는 회원 가입과 로그인 페이지에 접근 제한

  • 다른 주소에서 회원 가입 또는 로그인 페이지로 이동시 이미 인증된 사용자라면 메인페이지로 이동 시키기
    import { useCounterStore } from '@/stores/counter'
router.beforeEach((to, from) => {
const store = useCounterStore()
// 인증되지 않은 사용자는 메인 페이지에 접근 할 수 없음
if (to.name === 'ArticleView' && store.isLogin === false) {
    window.alert('로그인이 필요해요!!')
    return { name: 'LogInView' }
}

// 인증된 사용자는 회원가입과 로그인 페이지에 접근 할 수 없음
if ((to.name === 'SignUpView' || to.name === 'LogInView') && (store.isLogin === true)) {
    window.alert('이미 로그인 했습니다.')
    return { name: 'ArticleView' }
}
})
```
  • 로그인 후 회원가입, 로그인 페이지 접속 시도

기타 기능 구현


자연스러운 애플리케이션을 위한 기타 기능 구현

  1. 로그인 성공 후 자동으로 메인 페이지로 이동하기
  2. 회원가입 성공 후 자동으로 로그인까지 진행하기

1. 로그인 성공 후 자동으로 메인 페이지로 이동하기

export const useCounterStore = defineStore('counter', () => {
  const articles = ref([])
  const API_URL = 'http://127.0.0.1:8000'
  const token = ref(null)

  const logIn = function (payload) {
    // 1. 사용자 입력 데이터를 받아
    const { username, password } = payload
    // 2. axios로 django에 요청을 보냄
    axios({
      method: 'post',
      url: `${API_URL}/accounts/login/`,
      data: {
        username, password
      }
    })
      .then((response) => {
        token.value = response.data.key
        router.push({ name : 'ArticleView' })
      })
      .catch((error) => {
        console.log(error)
      })
  }

  return { articles, API_URL, getArticles, signUp, logIn, token, isLogin }
}, { persist: true })

2. 회원가입 성공 후 자동으로 로그인까지 진행하기

  const signUp = function (payload) {
    ...
     .then((response) => {
       console.log('회원가입 성공!')
       const password = password1
       logIn({ username, password })
     })
     .catch((error) => {
       console.log(error)
     })
  }
  • 토큰은 장고가 알아서 지워주기때문에 로그아웃은 장고에게 토큰지워달라고 요청보내고
  • vue쪽 logout은 그 뒤에 초기화시켜주면 된다.
  • 회원 탈퇴의 경우 django에서 회원 삭제 후 세션데이터 삭제를 진행하면 된다.

참고


Django Signals


  • 이벤트 알림 시스템
  • 애플리케이션 내에서 특정 이벤트가 발생할 때, 다른 부분에게 신호를 보내어 이벤트가 발생했음을 알릴 수 있음
  • 주로 모델의 데이터 변경 또는 저장, 삭제와 같은 작업에 반응하여 추가적인 로직을 실행하고자 할 때 사용
    • 예를 들어 사용자가 새로운 게시글을 작성할 때마다 특정 작업을 수행하려는 경우

환경 변수


애플리케이션의 설정이나 동작을 제어하기 위해 사용되는 변수

환경 변수의 목적

  • 개발 테스트 및 프로덕션 환경에서 다르게 설정되어야 하는 설정 값이나 민감한 정보(ex. API key)를 포함
  • 환경 변수를 사용하여 애플리켕션의 설정을 관리하면, 다양한 환경에서 일관된 동작을 유지하면서 필요에 따라 변수를 쉽게 변경할 수 있음
  • 보안적인 이슈를 피하고, 애플리케이션을 다양한 환경에 대응하기 쉽게 만들어 줌

Vite에서 환경 변수를 사용하는 법

  • VITE 로 시작해야한다.

Vue프로젝트 진행 시 유용한 자료

요약


  1. 로그인 관련 User모델 및 시리얼라이저 생성

  2. RestFramework Authentcation 설정

    Authentication - Django REST framework

  3. Dj-Rest-Auth 라이브러리

  4. 권한 정책 설정

profile
데이터 분석에서 백엔드까지...

0개의 댓글