LoginSignup
5

More than 3 years have passed since last update.

Django-allauthを使ってLINEログイン→push-messageをアカウントに送信するまで

Last updated at Posted at 2020-12-28

なんの記事?

  • 自作したDjangoで開発したweb appにソーシャルログインを追加するために、django-allauthモジュールを調べたので、本記事ではデモを動かしつつ、内部構造を理解していく記事です。

  • 今回は、LINEでソーシャルログインした後、取得したUIDを元にメッセージを送信する処理を実装しました。

  • Django-allauthのお話をしつつ、途中、五月雨にdjangoの復習を挟みます。

    • ログイン後、"accounts/profile"にredirectしているけど、どこで設定している?
    • Djangoのdefaultで設定されるsettings.LOGIN_REDIRECT_URL
    • Appconfig.ready()関数内でsignals.post_migrate()を使った、マイグレ時の初期データの読み込み
    • djangoがどのようにapplicationを読み込んでいるか?(公式doc
    • クラスベース汎用ビューの1つ:TemplateView
    • 汎用VIewクラス、TemplateVIewクラスの解説記事を参照にどんな実装になっているのか確認する。
  • 実装したコードはhttps://github.com/Jumpo-523/allauth-line-example に格納してあります。

環境

Python 3.7.4
Django==3.1.2
django-allauth==0.44.0

準備

  1. moduleのsource codeに格納されているexample.demo appを利用します。

  2. LINE側でLINEログインを作成する。

セットアップ

  • LINE DEVELOPERサイトで、LINE LOGINアプリの登録
    • 認証結果をweb applicationに返す際のcallback urlの設定をしておきます。
  • django adminで、LINEで作成したアプリのhook, callback関数の登録作業を実施する。
  • https://github.com/pennersr/django-allauth のexampleをforkする。
  • example/settings.pyをいじる
    • INSTALLED_APPS に 'allauth.socialaccount.providers.line',を追加
  • データ構造仕様をDBに反映させます。python manage.py migrate

上記まで出来たら、実際にstep by stepで開発していきたいと思います。

開発

LINEログインを利用して認証する

  • ユーザ情報・UIDをちゃんと保存するところまで解説します。

まずは動作確認

  • python manage.py runserverでサーバーを起動し実行させます。
  • http://localhost:8000/ にアクセスすると以下のページがレンダリングされます。

スクリーンショット 2020-12-28 11.12.48.png

次にLINE DEVELOPERサイト登録したLINE ログインを登録します。

Django admin siteを見てみると、

スクリーンショット 2020-12-28 11.13.12.png

すでにいろいろなDummy XXX appが存在しますね..

....なぜでしょう。example/demo/apps.pyを見てみると、

# example.demo > apps.py
class DemoConfig(AppConfig):
    name = 'example.demo'
    verbose_name = _('Demo')

    def ready(self):
        post_migrate.connect(setup_dummy_social_apps, sender=self)

どうやら、AppConfigのready()メソッド内で呼び出されるsetup_dummy_social_appsのおかげみたいです。

django.db.models.signals.post_migrateを利用することで、DBマイグレーション実行後にsetup_dummy_social_appsが呼び出されます。

setup_dummy_social_apps関数は、INSTALLED_APPSで設定したallauth.socialaccount.providersで登録される各ソーシャルアプリに対して、ダミーアプリを作成します。

※ちなみに、

  1. example.comというドメイン名はDjangoがデフォルトで用意するサイト名です。(公式doc)

django.contrib.sites registers a post_migrate signal handler which creates a default site named example.com with the domain example.com.

  1. DemoConfigはアプリケーション構成クラスといい、AppConfigクラスのサブクラスです。
    • djangoがどのようにアプリケーションをloadしているのかに関しては公式docに詳しく記載されています。
localhost:8000(及び、127.0.0.1:8000)を新規追加します。

スクリーンショット 2020-12-28 11.14.44.png

LINE LOGINのChannel ID/ Channel secretをClient id/ Secret keyに設定します。

スクリーンショット 2020-12-28 11.12.14.png

Lineでログインしてみる

http://localhost:8000/ にアクセスして、Sign inリンクを押下→Lineリンクを押下して、ログインを試してみます。

スクリーンショット 2020-12-28 11.10.29.png

あれ、失敗した。。なんでや

SocialApp matching query does not exist

  • どうやらweb app内部で持っている SocialAppとSitesの紐付けができていないことが原因らしいです。。
  • Site_id=1はexample.comが設定されているが、SocialAppに紐付けたSItesに含まれていないことで怒られてしまいました。
  • そもそも、Siteってなんのためにあるのかよくわかってないですが、ここは後日勉強します。

スクリーンショット 2020-12-28 11.09.28.png

Siteを設定したし、もう大丈夫だろうと、Lineをクリックし、LINEログインを実行してみたところ...

スクリーンショット 2020-12-28 11.07.47.png

Social Netowork Login Failure...うーむなんでや???

返されるcallback urlを見てみると、

"GET /accounts/line/login/callback/?error_description=%27scope%27+is+not+specified.&state=lka8qKd4yOVF&error=invalid_scope HTTP/1.1" 200 731

とあり、「scopeが特定されていない」とある。

原因:setttings.pySOCIALACCOUNT_PROVIDERS変数を追加してない

django-allauthはSOCIALACCOUNT_PROVIDERS変数で、各PROVIDER(SOCIALLOGINで利用したいアプリ)ごとに認可の範囲を指定できます。

指定しない場合、default値として空dictが渡されてしまい、providerは認証することができなくなってしまいます。

例えば今回のLINEのケースであれば「LINE経由で登録してくれたユーザに個別メッセージを送りたい」ので、プロフィール情報と共にIDトークンを取得できるように、['profile','openid']を指定します。

SOCIALACCOUNT_PROVIDERS = {
    'line': {
        'SCOPE': ['profile','openid'],
    }
}

その他、細かい仕様に関しては、公式ドキュメントを参照してください。

ちなみに、

  • SOCIALACCOUNT_PROVIDERSという変数でsettings.pyで設定された変数はどうやって使われているか?

    • app_settings.pySOCIALACCOUNT_というprefixが付いているもののみ取り出され、app_settings moduleとしてDjango-allauthに使われています。(app_settings.py fileで未設定の変数のdefault値も確認できます。)
  • SOCIALACCOUNT_PROVIDERSで設定した情報は、Providerクラスのget_settings()で呼び出されるように設計されています。

SCOPEをprofileopenidに設定して、再度チャレンジしましょう。

スクリーンショット 2020-12-28 11.07.05.png

無事、自分のLINEアカウントを利用して、ログインすることができました。

スクリーンショット 2020-12-28 11.06.40.png

ですが、LINEのユーザ名がうまく、djangoに渡せておらず、Successfully signed in as user.と表示されてしまいます。

LINEのユーザ名をdjangoに渡したい。

なんでuserという名前でdjangoに渡されてしまうのか。

  • 結論から言うと、_process_signupでclean_username処理で引っかかってしまい、ユーザ登録処理の途中で、空文字に置き換えられてしまうからです。

    • 空文字に置き換えられた場合、allauth.account.adapter.DefaultAccountAdapterクラスのpopulate_usernameメソッドによって、[first_name, last_name, email, username, "user"]のいずれかの値がusernameとして保存されます。
    • ['profile','openid']のみをSCOPEに設定したLineログインの場合、first_name, last_name, emailの情報は抽出されない為空文字になり、結果usernameとして有効な文字列は消去法で"user"となってしまうのです。
  • breakpointを貼って処理を追ってみるとわかるのですが、LINEからdisplayNameとして、ユーザ名("Jumpei Takubo")は渡されています。しかし、苗字と名前の間にスペースが入っており、これがUsernameValidatorに引っかかってしまうのです。

    • ただ、extra_data fieldにJSON形式でdisplayNameがキーに含まれてはいます。

どうやって対処する?

  • 対処方法は思いつく限りで2通りあるかと思います。
  1. SignUp時にユーザに修正してもらう。

  2. validatorの処理をカスタマイズして、素通りさせる(あまり良くない?)

    1. SignUp時にユーザに修正してもらう。
  • ソーシャルログイン機能を利用しつつ、必要な情報はユーザに手入力させるようにform画面を作るのが一つ逃げ道かなと思います。
  1. validatorの処理をカスタマイズして、素通りさせる(あまり良くない?)
  • 一番手取り早いのはvalidatorの処理をカスタマイズして、許容するパターンを予め用意しておくのが良いかなと思います。
  • 本記事では、validation checkを全て無効化します。下記、その実装になります。

settings.py

ACCOUNT_USERNAME_VALIDATORS = 'example.demo.validators.custom_username_validators'

demo/validators.py

import re
from django.core import validators
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _

@deconstructible
class CustomUsernameValidator(validators.RegexValidator):
    # regex = r'^[\w.@+-]+\Z' # defaultで用意される異常パターン
    regex = r'' # 異常パターンを全部許容する
    message = _(
        'Enter a valid username. This value may contain only English letters, '
        'numbers, and @/./+/-/_ characters.'
    )
    flags = re.ASCII
custom_username_validators = [CustomUsernameValidator()]

adminページにて、登録したuserを削除して、再度LINEログインを実施してみると...

スクリーンショット 2020-12-28 11.05.05.png
スクリーンショット 2020-12-28 11.06.20.png

見事、LINEのdisplayNameをusernameとして保存することができました!

無事Successfully signed in as Jumpei Takubo.と表示されています。

最後に、登録したユーザに個別メッセージを送信してみたいと思います。

取得したUIDを元にメッセージを送信するまで

  • まず、ログインしたユーザのuidが保存されていることを確認します。

スクリーンショット 2020-12-27 17.40.27.png

  • 次に、ユーザ情報をもとにLINE message APIを利用してメッセージを送信する
  1. Line Developpersで、Messaging APIを作成

  2. push messageの仕様を確認(公式ドキュメント

  3. views.pyに関数を作成

     def notify_by_message_api(request):
         user = get_social_account_user(request)
         if user is None:
             return JsonResponse({"error": "You don't have social account."}, status=400)
         # TODO: develop push notifications using user_id.
         # https://developers.line.biz/ja/reference/messaging-api/#send-push-message
         from linebot import LineBotApi
         from linebot.models import TextSendMessage
         from linebot.exceptions import LineBotApiError
         # import pdb; pdb.set_trace()
         try:
             line_bot_api = LineBotApi(os.environ["LINE_CHANNEL_ACCESS_TOKEN"])
             line_bot_api.push_message(f'{user.uid}', TextSendMessage(text='初めまして、webサイトへの登録ありがとうございます。'))
         except (LineBotApiError, KeyError) as e:
             # error handle
             logger.error(f'Something went wrong! {e}')
             return JsonResponse({"success": False, "error": str(e)}, status=400)
         # social_account.uid
         # messageだけclientに返したい。
         # ajaxの使い方を復習する。
         return JsonResponse({"success": True, 'message':"I correctly sent messages"})

4.profile.htmlにnotify ボタンと、ボタン押下時のajax処理のコードを追加する。

  •  {% extends "base.html" %}
    
     {% block content %}
     <script src="https://code.jquery.com/jquery-3.5.0.js" 
     integrity="sha256-r/AaFHrszJtwpe+tHyNi/XCfMxYpbsRg2Uqn0x3s2zc=" 
     crossorigin="anonymous"></script>
     <script type="text/javascript">
         var hoge = function(){
             $.ajax(
                 {
                     type:"GET",
                     url: "/notify/",
                     data:{},
                     'dataType':'json',
                     'success': function(response){
                         alert("Okay, I sent a message to your LINE account.")
                     },
                     'error':function(response){
                         alert("you failed to send a message to your LINE account. ")
                     },
                 })
             };
     </script>
     <h1>Profile</h1>
    
     <p>Your profile</p>
    
     {% if social_account %}
     <h1> Hello {{ social_account.extra_data.displayName }}</h1>
    
     <input type="button" class="notify" value="Notify to {{ social_account.extra_data.displayName }}"
      onclick="hoge()"/>
     {% endif %}
     <!-- button to push a notification to uid . -->
     {% endblock %}
    
  1. demo/urls.pyで押下時http://localhost:8000/notify/にアクセスされた際に上記notify_by_message_apiにroutingできるように設定
   urlpatterns = [
     ...
       path("notify/", views.notify_by_message_api),
     ...
   ]

これで実装は完了です!

あとは、web上でボタンを押して、自分のアカウントを確認して、作成したmessage API botから、下記のようにコメントが届いているか、確認してみてください!

IMG_0165.jpg

追記:jinja2に渡したsocial_accountと言う変数に付いて

  • {% if social_account %}は追加実装しているけど、これは何?どうやって渡している?
    • social_accountは、著者がTemplateViewを継承したクラスにcontextとして渡したソーシャルアカウントのobjectです。
    • login後のredirect urlである accounts/profileで呼び出されるviewとしてTemplateViewを継承したProfileTemplateViewget_context_dataメソッドで追加しています。
    • get_context_dataメソッドをoverrideすることで、templateに渡す変数を追加することができます。
class ProfileTemplateView(TemplateView):
    form_class = None
    social_account = None
    template_name = "profile.html"

    # ...

        def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        context["social_account"] = self.social_account
        return context

profile = ProfileTemplateView.as_view() 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5