Django + Vue. Реализуем вход через Google

Никто не любит при регистрации на сайте вводить каждый раз одно и то же: имя пользователя, электронную почту и т.д. Либо постоянно создавать и запоминать новые пароли. По этой причине, вход через сторонние приложения вроде Google, Facebook или VK очень популярен.

Такие сторонние приложения используют протокол OAuth2. В статье я не буду объяснять, что это за протокол и как его реализовать. Вместо этого реализуем вход на сайт через Google использую уже готовые библиотеки. Бэкэнд напишем на Django и Django Rest Framework, а фронтэнд на Vue.js

Конечный результат:

Исходный код урока: https://github.com/apirobot/django-vue-google-auth

Создаем Google приложение

Прежде чем приступить к написанию кода, нужно создать приложение в консоли для разработчика. Через это приложение будет происходить аутентификация на нашем сайте.

  • Вводим название проекта и нажимаем на кнопку “Создать”.
  • После создания проекта нажимаем на кнопку “Включить API и сервисы”.
  • Через поиск находим Google+ API и нажимаем на кнопку “Включить”.
  • Создаем учетную запись.
  • Настраиваем учетные данные.
  • Добавляем http://localhost:8080 в разрешенные источники. Под этим адресом будет работать Vue.js приложение.
  • Открываем созданный клиент.
  • Копируем идентификатор клиента и секрет клиента. В дальнейшем это понадобится.

Backend

Что будем использовать:

  1. django;
  2. django-rest-framework для создания REST API;
  3. django-allauth и django-rest-auth для аутентификации в системе. django-rest-auth предоставляет уже рабочие REST API endpoints (конечные точки). Не нужно париться по поводу реализации своей системы входа и регистрации;
  4. django-rest-framework-jwt для аутентификации через JWT (JSON Web Token). Этот токен создается при успешной аутентификации пользователя в системе, после чего токен передается на фронтэнд пользователю. Теперь, при каждом запросе к бэкэнду с фронтэнда, токен прикрепляется к заголовку запроса. Это такой способ идентификации. Подробнее про JSON Web Token здесь;
  5. django-cors-header для поддержки кроссдоменных запросов. Без нее мы не сможем делать запросы с фронтэнда (http://localhost:8080) на бэкэнд (http://localhost:8000).

Создаем проект и устанавливаем зависимости

Создаем корневую папку проекта:

$ mkdir django-vue-auth
$ cd django-vue-auth

Создаем папку для бэкэнда:

$ mkdir backend
$ cd backend

Устанавливаем зависимости:

$ pipenv install django djangorestframework djangorestframework-jwt django-allauth django-rest-auth django-cors-headers

Активируем виртуальное окружение:

$ pipenv shell

Создаем проект:

$ django-admin startproject thisisproject .

Настраиваем settings.py

Добавляем настройки в файл:

# thisisproject/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites',  # don't forget

    'corsheaders',
    'rest_framework',
    'rest_framework.authtoken',
    'rest_auth',
    'rest_auth.registration',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.google',
]

SITE_ID = 1

MIDDLEWARE = [
    ...
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    ...
]

CORS_ORIGIN_ALLOW_ALL = True

AUTHENTICATION_BACKENDS = [
    'django.contrib.auth.backends.ModelBackend',
    'allauth.account.auth_backends.AuthenticationBackend',
]

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
        'rest_framework.authentication.BasicAuthentication',
    ),
}

# По умолчанию, `django-rest-auth` использует аутентификацию через
# обычные токены. Нам нужна аутентификация через JWT токены.
REST_USE_JWT = True

Делаем миграции:

$ python manage.py migrate

Реализуем вход через Google

Создаем Google приложение на сайте. Для этого понадобится идентификатор клиента и секрет клиента. Зайдите в панель администратора и создайте приложение в разделе Social Applications как на картинке:

Создаем отдельное приложение для аутентификации:

$ django-admin startapp authentication

Добавляем представление, через которое будет происходить аутентификация в Google:

# authentication/views.py

from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from rest_auth.registration.views import SocialLoginView


class GoogleLogin(SocialLoginView):
    adapter_class = GoogleOAuth2Adapter

Добавляем это представление в urls приложения:

# authentication/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('google/', views.GoogleLogin.as_view(), name='google_login')
]

Обновляем urls проекта:

# thisisproject/urls.py

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('auth/', include('authentication.urls')),
]

Теперь, чтобы проверить, работает ли аутентификация через Google, нужно отправить POST запрос по ссылке http://localhost:8000/auth/google/ и в теле этого запроса передать access token. Access token мы будем получать позже через Vue.js. Но для проверки работоспособности бэкэнда, получим access token через Google OAuth2 Playground:

После получения токена доступа, делаем запрос к серверу:

В ответ сервер вернул информацию о пользователе, а также создал новый JWT токен.

Создаем приложение users

Библиотека django-allauth, после успешной аутентификации через Google, создает нового пользователя и новый социальный аккаунт:

Однако, эта библиотека по умолчанию сохраняет только email, username, first_name и last_name пользователя. Но что если нужно сохранить больше полей? Например фотографию.

Этим сейчас и займемся. Создадим свою модель User, добавим туда поле photo и после успешной аутентификации через Google, сохраним в это поле ссылку на фотографию пользователя.

Создаем приложение:

$ django-admin startapp users

Создаем модель User:

# users/models.py

from django.contrib.auth.models import AbstractUser
from django.db import models


class User(AbstractUser):
    photo = models.URLField(blank=True)

    def __str__(self):
        return self.username

После успешной аутентификации, библиотека django-allauth создает социальный аккаунт и прикрепляет к нему пользователя. Перехватим сохранения социального аккаунта через сигналы и обновим ссылку на фотографию у пользователя:

# users/signals.py

from django.db.models.signals import post_save
from django.dispatch import receiver

from allauth.socialaccount.models import SocialAccount


@receiver(post_save, sender=SocialAccount)
def add_extra_data_to_the_user(sender, instance, created, *args, **kwargs):
    instance.user.photo = instance.extra_data['picture']
    instance.user.save()

Не забудьте импортировать сигналы:

# users/apps.py

from django.apps import AppConfig


class UsersConfig(AppConfig):
    name = 'users'

    def ready(self):
        import users.signals

Так как UserSerializer, который предоставляет библиотека django-rest-auth не содержит поле photo, то создадим в таком случае свой:

# users/serializers.py

from rest_framework import serializers

from .models import User


class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('username', 'email', 'first_name', 'last_name', 'photo')
        read_only_fields = ('email', )

Обновляем настройки проекта:

# thisisproject/settings.py

INSTALLED_APPS = [
    ...
    'users.apps.UsersConfig',
]

AUTH_USER_MODEL = 'users.User'

REST_AUTH_SERIALIZERS = {
    'USER_DETAILS_SERIALIZER': 'users.serializers.UserSerializer',
}

Frontend

Что будем использовать:

  1. vue.js;
  2. vue-cli для генерации vue.js проекта;
  3. axios для отправления запросов к бэкэнду;
  4. vue-google-signin-button для входа через Google и получения токена доступа.

Создаем проект и устанавливаем зависимости

Переходим в корневую папку проекта:

$ cd django-vue-auth

Создаем проект:

$ vue init webpack thisisproject

Переименовываем папку:

$ mv thisisproject frontend
$ cd frontend

Устанавливаем зависимости:

$ npm install vue-google-signin-button axios --save

Регистрируем vue-google-signin-button:

// src/main.js

import Vue from 'vue'
import GSignInButton from 'vue-google-signin-button'
import App from './App'

Vue.config.productionTip = false

Vue.use(GSignInButton)

/* eslint-disable no-new */
new Vue({
  el: '#app',
  components: { App },
  template: '<App/>'
})

Добавляем скрипт в index.html, без которого библиотека vue-google-signin-button работать не будет:

<!-- index.html -->

<!DOCTYPE html>
<html>
  <head>
    ...
  </head>
  <body>
    ...
    <script src="https://apis.google.com/js/api:client.js"></script>
</html>

Свои стили писать не будем, поэтому добавим уже готовые от spectre.css:

<!-- index.html -->

<!DOCTYPE html>
<html>
  <head>
    ...
    <link rel="stylesheet" href="https://unpkg.com/spectre.css/dist/spectre.min.css">
  </head>
  <body>
    ...
</html>

Реализуем вход

Обновляем App.vue:

<!-- src/App.vue -->

<template>
  <div id="app">
    <div class="container">
      <div class="columns" style="margin-top: 100px;">
        <div class="column col-2 centered">
          <!-- Если user - пустой объект, то отображаем кнопку
          входа через Google -->
          <g-signin-button
            v-if="isEmpty(user)"
            :params="googleSignInParams"
            @success="onGoogleSignInSuccess"
            @error="onGoogleSignInError"
          >
            <button class="btn btn-block btn-success">
              Google Signin
            </button>
          </g-signin-button>
          <user-panel v-else :user="user"></user-panel>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios'

import UserPanel from '@/components/UserPanel'

export default {
  name: 'App',
  components: {
    UserPanel
  },
  data () {
    return {
      user: {},
      googleSignInParams: {
        client_id: 'your_client_id_here'
      }
    }
  },
  methods: {
    onGoogleSignInSuccess (resp) {
      const token = resp.Zi.access_token
      // После успешного входа через Google,
      // отправляем токен доступа на бэкэнд и получаем взамен
      // пользователя и JWT токен
      // P.S. JWT токен в нашем примере не нужен, поэтому его не сохраняем
      axios.post('http://localhost:8000/auth/google/', {
        access_token: token
      })
        .then(resp => {
          this.user = resp.data.user
        })
        .catch(err => {
          console.log(err.response)
        })
    },
    onGoogleSignInError (error) {
      console.log('OH NOES', error)
    },
    isEmpty (obj) {
      return Object.keys(obj).length === 0
    }
  }
}
</script>

Добавляем панель для пользователя, которая отображается после успешного входа:

<!-- src/components/UserPanel.vue -->

<template>
  <div class="panel">
    <div class="panel-header text-center">
      <figure class="avatar avatar-lg">
        <img :src="user.photo" alt="Avatar">
      </figure>
      <div class="panel-title h5 mt-10">
        {{ user.first_name }} {{ user.last_name }}
      </div>
      <div class="panel-subtitle">{{ user.username }}</div>
    </div>
    <nav class="panel-nav">
      <ul class="tab tab-block">
        <li class="tab-item active">
          <a>Profile</a>
        </li>
      </ul>
    </nav>
    <div class="panel-body">
      <div class="tile tile-centered">
        <div class="tile-content">
          <div class="tile-title">E-mail</div>
          <div class="tile-subtitle">{{ user.email }}</div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'UserPanel',
  props: ['user']
}
</script>

8 Comments

  1. Anton February 1, 2019 at 2:53 pm

    Какие версии библиотек `django-allauth` и `django-rest-auth` в этом туториале?

    Reply
  2. Антон Александрович Безкровный February 17, 2019 at 2:29 pm

    Крутое руководство, все получилось, спасибо!

    но не могу понять один момент:

    я захожу, авторизуюсь через Google, все ок – мои данные показываются на страничке.
    Но как сделать, чтобы если я жму обновить страницу все авторизация сохранялась?
    Все сбрасывается(

    Reply
    1. apirobot February 19, 2019 at 6:42 am

      Тебе нужно сохранить JWT токен в localstorage после запроса к `http://localhost:8000/auth/google/`. Затем при каждом запросе отправлять этот токен на сервер через headers.

      Reply
  3. meduzik August 6, 2019 at 9:23 am

    Не мог бы попробовать получать knox token через социалки?
    Нашел такую либу, но не могу понять, как получать knox token:
    https://github.com/st4lk/django-rest-social-auth

    Reply
  4. Grigoriy August 14, 2019 at 8:56 pm

    Спасибо за мануал. Все круто. Добавил получил токен от гугла и затем получил JWT токен. Но когда пытаюсь получить данные с сервера, получаю такой вот ответ:
    {
    “code”: “token_not_valid”,
    “detail”: “Given token not valid for any token type”,
    “messages”: [
    {
    “message”: “Token is invalid or expired”,
    “token_class”: “AccessToken”,
    “token_type”: “access”
    }
    ]
    }

    Обрыл весь гугл, но ни слова. Не понятно, почему свой же JWT токен не принимается?

    Reply
  5. DL October 29, 2021 at 6:42 pm

    В вашем приложении, как я поняла, при авторизации пользователя сначала происходит получение Authorization Code, который затем вместе с секретом клиента отправляется от приложения на порту 8080 к Google для получения access_token. Но у меня возник вопрос: где в приложении, находящемся на порту 8080, указан секрет клиента? (Я понимаю, что он не должен быть в клиентской части, поступающей в браузер, т.к. это не безопасно, т.о. я думаю он должен быть в серверной части приложения, находящегося на порту 8080, но я нигде его не нашла)

    Reply
    1. DL October 30, 2021 at 9:30 am

      Я поняла в чем ошибалась: я думала, что под капотом у вас отрабатывает Authorization Code Flow, а у вас работает оказывается Implicit Flow.

      Reply

Leave a Reply to DL Cancel reply

Your email address will not be published. Required fields are marked *