【django】LINE認証を行い、メールアドレスで新規登録、ログインをする方法(django-allauthなしでOK)

【django】LINE認証を行い、メールアドレスで新規登録、ログインをする方法(django-allauthなしでOK)

前提

models.py
メールアドレスとパスワードでログインする方式。

from django.db import models
from django.conf import settings

from django.contrib.auth.models import (
    BaseUserManager, AbstractBaseUser, PermissionsMixin
)
from django.utils.translation import gettext_lazy, gettext as _


LANGUAGES = settings.LANGUAGES
NUMBER_REGEX = settings.NUMBER_REGEX

class UserManager(BaseUserManager): # ☆3

    def create_user(self, username, email, password=None):
        if not email:
            raise ValueError('Enter Email!')
        user = self.model(
            username=username,
            email=email,
        )
        user.set_password(password)
        user.is_active = True
        user.save(using=self._db)
        return user

    def create_superuser(self, username, email, password=None):
        user = self.model(
            username=username,
            email=email,
        )
        user.set_password(password)
        user.is_staff = True
        user.is_active = True
        user.is_superuser = True
        user.save(using=self._db)
        return user


class User(AbstractBaseUser, PermissionsMixin): # ☆1

    # 使いたいFieldを追加

    username = models.CharField(max_length=20, verbose_name="username")
    email = models.EmailField(max_length=255, unique=True, verbose_name="email")
    phone = models.CharField(validators=[NUMBER_REGEX], max_length=15, verbose_name="phone", blank=True, null=True)
    lang = models.CharField(max_length=20,verbose_name="language", choices=LANGUAGES, blank=False, null=False)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)

    objects = UserManager()

    USERNAME_FIELD = 'email' # ☆2 このテーブルのレコードを一意に識別
    REQUIRED_FIELDS = ['username'] # スーパーユーザ作成時に入力する

    class Meta:
        db_table = 'users'

    def __str__(self):
        return self.email


チャネルIDとチャンネルシークレットを取得しておく。これは他のサイトでも詳しく紹介されているため、割愛します。

.envに以下のように記述。

SOCIAL_AUTH_LINE_KEY=チャンネルID
SOCIAL_AUTH_LINE_SECRET=チャンネルシークレット

settings.pyで.envから取得。

#################################
#---------LINE LOGIN----------
#################################

SOCIAL_AUTH_LINE_KEY = os.environ.get('SOCIAL_AUTH_LINE_KEY')
SOCIAL_AUTH_LINE_SECRET = os.environ.get('SOCIAL_AUTH_LINE_SECRET')


LINE認証からログイン、新規登録までの流れ

LINEdevelopersのこれ見てもいまいちピンとこないので、今回やりたいことを非常に簡単にまとめると、

①ユーザーが「LINEでログインする」のボタンを押す
②そのURLがLINEの情報アクセス許可のためのURLでユーザーはそこでアクセス許可を出す。
③自分のサイトにリダイレクト
④リダイレクトのURLにパラメータがついているので、それでトークンを取得する。
⑤取得したトークンでユーザーの情報を取得する。

⑥ユーザーの情報をもとにログイン、新規登録を行う。
⑦ログイン完了画面に遷移

①と⑦はclassベースビューで行い、その他は関数ベースビューを使う(今回は)。

中ではこんなに処理があるけど、実際にユーザーがやるのはログインボタンを押して承認したら、ログイン完了画面が出てくるだけ。

ぶっちゃけこれでもイメージがしにくいけど、実際にやってみると「あーこんなもんか」となる。あとは、なんでこんなことするの?となる。


具体的な手順

1.ユーザー認証を済ませてリダイレクトURLからcodeを取得する

LINEが用意してくれているユーザー認証画面に遷移するurlと認証後にリダイレクトされるurlの2つを追加する。

urls.py

from django.urls import path,include

from . import views

app_name = "accounts"
urlpatterns = [
    path('i18n/', include('django.conf.urls.i18n')),
    path("signup/", views.signup, name="signup"),
    path("signout/", views.signout, name="signout"),
    path("signin/", views.signin, name="signin"),
    path("changeinfo/", views.changeinfo, name="changeinfo"),
    path("profile/", views.profile, name="profile"),
    #追加(認証前)
    path("line_login/", views.line_login, name="line_login"),
    #追加(認証後リダイレクト)
    path("line_token/", views.line_token, name="line_token"),
]

認証後のurlをsettings.pyに記述し、これをLINEdevelopersのコールバックURLにも追加する。自分は本番環境と開発環境それぞれにリダイレクトurlを設定した。LINEに追加したコールバックURLとdjangoに追加したリダイレクトurlが一致しないとうまくいかないので注意。

settings.py

#本番環境なら
LINE_REDIRECT_URL = 'https://hogehoge.com/accounts/line_token'
#local環境なら
LINE_REDIRECT_URL = 'http://127.0.0.1:8000/accounts/line_token'


templatesのどこかにこのviewsを呼び出すためのurlを乗せておく

<p>LINEで <a href="{% url 'accounts:line_login' %}">ログイン</a></p>

views.py

from django.middleware.csrf import get_token
from django.conf import settings

SOCIAL_AUTH_LINE_KEY = settings.SOCIAL_AUTH_LINE_KEY
SOCIAL_AUTH_LINE_SECRET = settings.SOCIAL_AUTH_LINE_SECRET
LINE_REDIRECT_URL = settings.LINE_REDIRECT_URL

def line_login(request):
    try:
        token = get_token(request)
        scope = "profile%20openid%20email"
        line_login_url =  f'https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id={SOCIAL_AUTH_LINE_KEY}&redirect_uri={LINE_REDIRECT_URL}&state={token}&scope={scope}'
        return redirect(line_login_url)
    except:
        return redirect("accounts:signin")

scopeではユーザー情報で取得したい項目を承認してもらうために設定する。必要に合わせて変更できる。
tokenはセキュリティの観点から必要らしい。

これでline_login_urlにリダイレクトすると、URLにcodeとstateをつけて返してくれる。

具体的にはhttps://hogehoge.com/accounts/line_token/?state=123456abc&code=098765zyxのようになっている。

このcodeが次に利用する許可コードになっており、これとシークレットキー(チャンネルシークレット)を利用すると今度はid_tokenを取得でき、そのid_tokenでユーザーの情報を取得できる。

セキュリティの観点から必要なことだろうけど、二度手間だなあとは思う。。。

2.id_token(アクセストークン)の取得

正直ここが一番難しい。リクエストヘッダをつけたり、必須項目をパラメータにつけるのはまだ良いけど、response = requests.post(url, headers=headers, data=data)で受け取るとuriが一致しないだの、postが変だの情報の少ないエラーが続く。


まさしくこの内容

redirect_uriはこの後のリダイレクト先じゃなくて、さっきリダイレクトしたやつとここで設定するuriが一致するかどうかの検証らしい。この辺の説明がドキュメントも薄いし、先人も少ないのでなかなかハマった。

ちなみにreponseはjson形式で返ってきて、その中にid_tokenが入っている感じ。

やり方さえ知ってたらこんな感じで10何行かでできるのに、ここまで手こずるのは正直LINE側の使用の問題ではないかと思う。この辺の説明はもうちょっと書いていて欲しかった。

views.py

def line_token(request):
    try:
        code = request.GET.get('code')
        url = "https://api.line.me/oauth2/v2.1/token"

        # リクエストヘッダ
        headers = {
            "Content-Type": "application/x-www-form-urlencoded"
        }

        # リクエストボディ
        data = {
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": LINE_REDIRECT_URL,
            "client_id": SOCIAL_AUTH_LINE_KEY,
            "client_secret": SOCIAL_AUTH_LINE_SECRET
        }

        # POSTリクエストを送信
        response = requests.post(url, headers=headers, data=data)
        data = response.json()
        access_token = data['id_token']

        #続きは後述

3.アクセストークンを使ってユーザー情報を取得しログインする

ここまでこれたらあとは大丈夫。先ほどのアクセストークンをつけて同じようにjsonを受け取ると、中にemailやusernameが入っているので、それを利用してログインを行う。ここではセキュリティ、検証的なものは考慮していないので注意。

        #続き        
        url = f'https://api.line.me/oauth2/v2.1/verify?id_token={access_token}&client_id={SOCIAL_AUTH_LINE_KEY}'
        response = requests.post(url)
        data = response.json()

        username = data['name']
        email = data['email']
        password = data['sub']

        try:
            user = User.objects.get(email=email)
            login(request, user)
        except User.DoesNotExist:
            user = User.objects.create_user(username=username, email=email, password=password)
            login(request, user)
        return redirect('accounts:profile')
    except:
        return redirect("accounts:signin")

全体像

views.pyの全体像を載せておきます。モジュールのimportなどは適宜行なってください。

SOCIAL_AUTH_LINE_KEY = settings.SOCIAL_AUTH_LINE_KEY
SOCIAL_AUTH_LINE_SECRET = settings.SOCIAL_AUTH_LINE_SECRET
LINE_REDIRECT_URL = settings.LINE_REDIRECT_URL

#ユーザー認証とアクセス権の許可
def line_login(request):
    try:
        token = get_token(request)
        scope = "profile%20openid%20email"
        line_login_url =  f'https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id={SOCIAL_AUTH_LINE_KEY}&redirect_uri={LINE_REDIRECT_URL}&state={token}&scope={scope}'
        return redirect(line_login_url)
    except:
        return redirect("accounts:signin")

#id_tokenを取得し、そこからユーザー情報を取得 → ログイン
def line_token(request):
    try:
        code = request.GET.get('code')
        url = "https://api.line.me/oauth2/v2.1/token"

        # リクエストヘッダ
        headers = {
            "Content-Type": "application/x-www-form-urlencoded"
        }

        # リクエストボディ
        data = {
            "grant_type": "authorization_code",
            "code": code,
            "redirect_uri": LINE_REDIRECT_URL,
            "client_id": SOCIAL_AUTH_LINE_KEY,
            "client_secret": SOCIAL_AUTH_LINE_SECRET
        }

        # POSTリクエストを送信
        response = requests.post(url, headers=headers, data=data)
        data = response.json()
        access_token = data['id_token']
        url = f'https://api.line.me/oauth2/v2.1/verify?id_token={access_token}&client_id={SOCIAL_AUTH_LINE_KEY}'
        response = requests.post(url)
        data = response.json()

        username = data['name']
        email = data['email']
        password = data['sub']

        #ログインor新規登録
        try:
            user = User.objects.get(email=email)
            login(request, user)
        except User.DoesNotExist:
            user = User.objects.create_user(username=username, email=email, password=password)
            login(request, user)
        return redirect('accounts:profile')
    except:
        return redirect("accounts:signin")
    
    

urls.py

from django.urls import path,include

from . import views

app_name = "accounts"
urlpatterns = [
    path('i18n/', include('django.conf.urls.i18n')),
    path("signup/", views.signup, name="signup"),
    path("signout/", views.signout, name="signout"),
    path("signin/", views.signin, name="signin"),
    path("changeinfo/", views.changeinfo, name="changeinfo"),
    path("profile/", views.profile, name="profile"),
    #追加
    path("line_login/", views.line_login, name="line_login"),
    path("line_token/", views.line_token, name="line_token"),
]

settings.py

#################################
#---------LINE LOGIN----------
#################################

SOCIAL_AUTH_LINE_KEY = os.environ.get('SOCIAL_AUTH_LINE_KEY')
SOCIAL_AUTH_LINE_SECRET = os.environ.get('SOCIAL_AUTH_LINE_SECRET')
LINE_REDIRECT_URL = 'http://127.0.0.1:8000/accounts/line_token'

以上です。お疲れ様でした。

Comments

No comments yet. Why don’t you start the discussion?

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です