PythonのWebアプリフレームワーク「Django」を使ってTwitter認証してみた

# 概要

以前、Pythonスクリプトで__直接__TwitterAPIを叩いてみる記事を投稿しました。
[PythonでTwitter API を利用していろいろ遊んでみる](https://qiita.com/bakira/items/00743d10ec42993f85eb)

しかし、これだと

- Consumer Key
- Consumer Secret

に加え、

- __Access Token__
- __Access Token Secret__

が必要なので__特定のユーザー__でしかAPI操作を出来ませんでした。

なので、今回はPythonのWebアプリフレームワークの「__Django__」を使って、__ブラウザ上__でTwitter認証をして__各アカウント単位の認証処理__を行うところまでを作ってみました。

# Djangoとは

__ジャンゴ__と読みます。

思想としては、__model-view-templateデザインパターン__に沿って開発を行います。
__model-view-controller__と似ていますが少し違うようです。

(@udoooom さんご指摘ありがとうございます！)

また、非常に__開発をしやすい__フレームワークとなっており、__インストール__〜__Twitter認証成功__までを__数時間程度__で動くものを作ることが出来ました。

# 手順

## 開発環境

- __Python　== 3.5.1__
- __Django　==　2.0.1__
- __social-auth-app-django == 2.1.0__

また、TwitterAPIのConsumer情報は
[PythonでTwitter API を利用していろいろ遊んでみる](https://qiita.com/bakira/items/00743d10ec42993f85eb)
で取得したものを__そのまま流用__しますので、まだ作成されていない方はまずは[コチラ](https://qiita.com/bakira/items/00743d10ec42993f85eb)の記事を参考に__Twitterアプリを作成__するところまで進めてください。

## モジュールのインストール

とりあえず__大元__のDjangoフレームワークをインストールします。

```shell:command
pip install django
```

## プロジェクトの作成

まずは__プロジェクト__を作成します。

```
django-admin startproject project
```

__プロジェクト作成直後__は以下のような__ディレクトリ構成__になっています。

```shell:tree
.
└── project
    ├── manage.py
    └── project
        ├── __init__.py
        ├── settings.py
        ├── urls.py
        └── wsgi.py
```

## DBのmigrate

Djangoには__DB__も含まれています。
デフォルトでは__sqlite3__が使われるようです。

まずは__migrate__を行いましょう。

```shell:command
cd myApp
python manage.py migrate
```

__正常__に完了します。

<img width="506" alt="screencapture- 2018-01-10 14.55.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/7b5171be-b0e0-3ff4-0cac-a9d097b5698f.png">

## 管理ユーザーの作成

次に__管理ユーザー__を作成します。

```shell:command
python manage.py createsuperuser
```

__ユーザー名/メールアドレス/パスワード/パスワード(確認)__の入力を求められるので良しなに。

<img width="383" alt="screencapture- 2018-01-10 14.56.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/7897ec79-63d2-e7d4-b104-aca29c4d6692.png">

## サーバー起動

一旦、この状態で__サーバー__を起動してみましょう。

```shell:command
python manage.py runserver
```

<img width="451" alt="screencapture- 2018-01-10 20.23.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/a6488e9c-caaa-a13c-5d90-559ea9ae3955.png">

ブラウザで、__http://localhost:8000__ にアクセスすると以下のような__アプリ画面__が表示されると思います。

![screencapture- 2018-01-10 15.10.17.png](https://qiita-image-store.s3.amazonaws.com/0/113654/9e44e017-4abb-0c8b-2e4a-2213c854f8cd.png)

## 管理画面へのログイン

DJangoは__標準で管理画面__を用意してくれています。

__http://localhost:8000/admin__ にアクセスします。

先ほど作成した__ユーザー名/パスワード__を入力してください。

![screencapture- 2018-01-10 15.11.33.png](https://qiita-image-store.s3.amazonaws.com/0/113654/25a9d840-c141-70a1-49e9-1b7b31f85402.png)

__ログイン成功！！！__

## アプリケーションの作成

次は__アプリケーション__を作成します。

Djangoでは、プロジェクトの中に__複数のアプリケーション__を作成することが出来るらしいです。

```shell:command
python manage.py startapp twitterManager
```

すると、__こんなディレクトリ構成__になります。

```command:tree
.
└── project
    ├── db.sqlite3
    ├── manage.py
    ├── project  # 共通
    │   ├── __init__.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    └── twitterManager  # アプリ①
        ├── __init__.py
        ├── admin.py
        ├── apps.py
        ├── migrations
        │   └── __init__.py
        ├── models.py
        ├── tests.py
        └── views.py
```

そして、作成したアプリを__プロジェクト管理対象__に含めるために、以下ファイルを編集します。

```diff:project/settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
+   'twitterManager.apps.TwittermanagerConfig',
]
```

そして、以下のコマンドでmigration対象に入れておきます。

```shell:command
python manage.py makemigrations twitterManager
python manage.py migrate twitterManager
```

__※何も変更してないので差分は発生しませんが、念のためです。__

## Twitter認証に使うモジュールのインストール

今回は[__social-auth-app-django__](https://github.com/python-social-auth/social-app-django)を使います。

```shell:command
pip install social-auth-app-django
```

## 認証情報の作成

以下のファイルを作成し、__TwitterアプリケーションのConsumer情報__を記載してください。

```py:project/configs/twitter.py
SOCIAL_AUTH_TWITTER_KEY = 'XXXXXXXXXXXXXXX'
SOCIAL_AUTH_TWITTER_SECRET = 'YYYYYYYYYYYYYY'
```

__<font color="red">※gitリポジトリの場合、認証情報はgit管理外にしたいので、.gitignoreに追加しておきましょう。</font>__

```ini:.gitignore
**twitter.py
```

## settings.pyの修正

こんな感じに変えます。

```diff:project/settings.py
"""
Django settings for project project.

Generated by 'django-admin startproject' using Django 2.0.1.

For more information on this file, see
https://docs.djangoproject.com/en/2.0/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.0/ref/settings/
"""

import os
+ from project.configs import twitter

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = []


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'twitterManager.apps.TwittermanagerConfig',
+   'social_django',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'project.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
+               'social_django.context_processors.backends',
+               'social_django.context_processors.login_redirect',
            ],
        },
    },
]

WSGI_APPLICATION = 'project.wsgi.application'


# Database
# https://docs.djangoproject.com/en/2.0/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}


# Password validation
# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/2.0/topics/i18n/

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.0/howto/static-files/

STATIC_URL = '/static/'

+ AUTHENTICATION_BACKENDS = [
+ 	    'social_core.backends.twitter.TwitterOAuth',
+ 	    'django.contrib.auth.backends.ModelBackend',
+ 	]

+ SOCIAL_AUTH_TWITTER_KEY = twitter.SOCIAL_AUTH_TWITTER_KEY
+ SOCIAL_AUTH_TWITTER_SECRET = twitter.SOCIAL_AUTH_TWITTER_SECRET
```

## ログイン/ログアウト画面の追加

次に__ログイン画面__と__ログアウト画面__を表示するための準備をします。

以下のファイルを作成します。

```py:twitterManager/urls.py
from django.contrib import admin
from django.urls import path
from django.conf.urls import include
import django.contrib.auth.views

urlpatterns = [
    path('', include('social_django.urls', namespace = 'social')),
    path('login/',
	        django.contrib.auth.views.login,
	        {
	            'template_name': 'login/index.html',
	        },
	        name='login'),
    path('logout/',
        django.contrib.auth.views.logout,
        {
            'template_name': 'logout/index.html',
        },
        name='logout'),

]
```

__ログイン画面/ログアウト画面__それぞれのHTMLを作成します。
取り急ぎの__表示確認レベル__なので適当で。

```html:twitterManager/templates/login/index.html
<html>
<head>
 <title>ログイン</title>
</head>
<body>
  <div>
    <button type="button" onclick="location.href='{% url 'social:begin' 'twitter' %}'">ログイン</button>
  </div>
</body>
</html>
```

```html:twitterManager/templates/logout/index.html
<html>
<head>
 <title>ログアウト</title>
</head>
<body>
  <div>
    <p>
      ログアウトしました。
    </p>
    <p>
      <a href="/twitterManager/login"><button type="button" >ログインページへ</button></a>
    </p>
  </div>
</body>
</html>
```

最後に__既存__のproject/urls.pyを編集します。

```diff:project/urls.py
from django.contrib import admin
from django.urls import path
+ from django.conf.urls import include

urlpatterns = [
    path('admin/', admin.site.urls),
+   path('twitterManager/', include('twitterManager.urls')),
]
```

## migrateの実行

__social-auth-app-django__を新たに追加しているので、__migrate__をする必要があります。
migrateしないまま__サーバー起動__をすると、

![screencapture- 2018-01-10 17.42.31.png](https://qiita-image-store.s3.amazonaws.com/0/113654/8ad91e14-b6bc-1f52-9480-2ba401fddc62.png)

が発生してしまいます。

migrateが必要なタイミングはサーバ起動時のログに出るので気にしておきましょう。

<img width="1033" alt="screencapture- 2018-01-10 21.11.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/cd3de7ee-d681-567f-82b4-78f0ed44b729.png">


```shell:command
python manage.py migrate
```

<img width="600" alt="screencapture- 2018-01-10 20.27.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/6bee34b2-bd35-618b-b931-e59c38dbf996.png">


## 確認

この状態で__サーバー起動__します。

```shell:command
python manage.py runserver
```

[ログイン画面](http://localhost:8000/twitterManager/login)

![screencapture- 2018-01-10 16.23.34.png](https://qiita-image-store.s3.amazonaws.com/0/113654/85d621ec-8ea0-44a4-bb9e-be67df233de4.png)

[ログアウト画面](http://localhost:8000/twitterManager/logout)

![screencapture- 2018-01-10 16.23.45.png](https://qiita-image-store.s3.amazonaws.com/0/113654/a6a48157-8fe9-8e16-34d1-c8113deb4dcf.png)

__とてもシンプル__なページが表示されると思います。笑

## しかしここで問題が・・・

__意気揚々__とログインボタンを押したところ、

<img width="767" alt="screencapture- 2018-01-10 21.09.31.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/60a488ba-07d1-8b05-5e03-a73a4aad0344.png">

__<font color="red">401エラー</font>が発生しました。__

### 原因はCallbackURLの未入力

__Twitterアプリケーション作成時__に__Callback URL__を__<font color="red">未入力</font>__にすると、__Webシステム__として使う場合は__<font color="red">401エラー</font>__が起きてしまうようです。

__特に利用する項目ではない__ので、とりあえず__ダミーURL__でも入れておきましょう。

![screencapture- 2018-01-10 17.38.58.png](https://qiita-image-store.s3.amazonaws.com/0/113654/815ef94c-9959-36df-680c-d7d8c9d6211b.png)

これで__連携確認画面__が表示されました！！！

![screencapture- 2018-01-10 17.39.21.png](https://qiita-image-store.s3.amazonaws.com/0/113654/0983e67d-4fe2-e74b-b5d0-461823011a73.png)

## 次こそ・・・！

__連携アプリを認証ボタン__をクリックすると、

![screencapture- 2018-01-10 17.44.50.png](https://qiita-image-store.s3.amazonaws.com/0/113654/1720cd23-2e8b-426c-0873-98f679f6d638.png)

__おぉ・・・！！(wktk)__

<img width="769" alt="screencapture- 2018-01-10 21.17.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/954e0a51-d7d3-5f11-1776-726260b48853.png">

![image.png](https://qiita-image-store.s3.amazonaws.com/0/113654/ec95fd91-d77b-fa44-8a81-9fd7f5bdd049.png)

### 原因はソーシャル認証成功後のリダイレクト指定

__認証成功時__に遷移させる__リダイレクトURL__を指定する必要があります。
デフォルトでは `/accounts/profile/` に飛ばされてしまうようですね。

取り急ぎ、__会員トップ画面__を用意して__リダイレクト__させてみましょう。

```diff:twitterManager/urls.py
+ from . import views

urlpatterns = [
    path('', include('social_django.urls', namespace = 'social')),
+   path('top/', views.top_page, name="top_page"),
    path('login/',
	        django.contrib.auth.views.login,
	        {
	            'template_name': 'login/index.html',
	        },
	        name='login'),
    path('logout/',
        django.contrib.auth.views.logout,
        {
            'template_name': 'logout/index.html',
        },
        name='logout'),

]
```

```py:twitterManager/views.py
from django.shortcuts import render

# Create your views here.

from django.shortcuts import render
from django.http.response import HttpResponse
from django.contrib.auth.decorators import login_required
from social_django.models import UserSocialAuth

@login_required
def top_page(request):
    user = UserSocialAuth.objects.get(user_id=request.user.id)
    pageDic = {
        'hoge': 'fuga',
        'user': user
    }
    return render(request, 'top/index.html', pageDic)
```

```html:twitterManager/templates/top/index.html
<html>
<head>
 <title>会員トップ</title>
</head>
<body>
  <div>
    <h1>会員トップ</h1>
    <h2>{{ user }}</h2>
    <div>
      <p>UserId:{{ user.access_token.user_id }}</p>
      <p>OAuthTokenSecret:{{ user.access_token.oauth_token_secret }}</p>
      <p>OAuthToken:{{ user.access_token.oauth_token }}</p>
      <p>ScreenName:{{ user.access_token.screen_name }}</p>
    </div>
    <h3>hoge:{{ hoge }}</h3>
    <p>
      <a href="/twitterManager/logout"><button type="button" >ログアウト</button></a>
    </p>
  </div>
</body>
</html>
```

```diff:project/settings/py
+ SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/twitterManager/top'
```

これでどうだ・・・！

<img width="438" alt="screencapture- 2018-01-10 20.10.52.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/820398f3-1c9d-932e-1682-6dfcfd812070.png">

取れた！！！

## 他のアカウントでも確認

__いや待てよ？__
Twitterアプリケーションの所有者と認証アカウントが__Bakira_Tech__で同じだから正常に動いているだけであって、__他のアカウント__ではうまくいかないのでは？という__不安__が頭を過ぎりました。。。

<img width="425" alt="screencapture- 2018-01-10 20.14.24.png" src="https://qiita-image-store.s3.amazonaws.com/0/113654/e8c2979e-2055-7194-b529-a0c4306fb14a.png">


__余　計　な　お　世　話　で　し　た　！　！　！　__

これで__Twitter認証__が完成したので、あとはOAuthTokenを使って__ロジック__なり__デザイン__を組み込んでいけばそれなりのものが作れそうですね♪

## 最終的な構成

以下のようになりました！
__(ベストプラクティスではないので悪しからず・・・orz)__

```shell:ディレクトリ構成
.
└── project
    ├── db.sqlite3
    ├── manage.py
    ├── project
    │   ├── __init__.py
    │   ├── configs
    │   │   └── twitter.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    └── twitterManager
        ├── __init__.py
        ├── admin.py
        ├── apps.py
        ├── migrations
        │   └── __init__.py
        ├── models.py
        ├── templates
        │   ├── login
        │   │   └── index.html
        │   ├── logout
        │   │   └── index.html
        │   └── top
        │       └── index.html
        ├── tests.py
        ├── urls.py
        └── views.py
```

# 終わりに

__知識０__からの作業だったので、調べながら試行錯誤しましたが、それでも__会員トップ画面が表示される__まで__5時間くらい__の学習コストで動くものが作れました。

Djangoフレームワーク、__楽しいです♪__

## 参考にさせて頂いた記事

__非常に助かりました！！！__
- [DjangoでTwitter OAuth](https://qiita.com/y-tsutsu/items/4c9edec8fbefb3116bf6)
- [DjangoでOauth認証をしたあとにTwitter APIを叩く](https://qiita.com/ichiren1/items/41281a9a48101ab09a69#_reference-43bd3ef6f36a0f9af9f5)
- [社会性フィルター実装したtwitterクライアントを作った](https://qiita.com/xKxAxKx/items/9522ddfe8b4431b9a772)
- [OAuth::Unauthorized 401 Unauthorizedなんてエラーがでたら](https://qiita.com/hirokishirai/items/5a43977a38ecd922bfb9)