見出し画像

【django】たった3STEPでLINEログインを導入(django-allauthなしでOK)

djangoでLINE認証やログインをするとき、正直情報も少なく、何をどうすれば良いの?という感じだったので、誰かの役に立つかと思いできる限り詳しく書きました。

ネットやchatgptなどで調べるとdjango-allauthとか状況をさらに複雑にしてくる奴が出てくるのですが、2024年7月の段階で、これらは利用せず、単純にdjangoとLINEのapiのみでログイン機能を実装できました。

これだけでも結構なログインができると思うのですが、簡単なので、ひとまずこれができてからセキュリティ等追加していっても良いんじゃないかなと思います!

人気だったら有料化しようかなとも思うのですが、お役立ち情報のためとりあえずは無料で公開しておきます。

成果物はこちら


前提設定

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はセキュリティの観点から必要らしい。

https://developers.line.biz/ja/docs/line-login/integrate-line-login/#making-an-authorization-request

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

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

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

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


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

https://developers.line.biz/ja/docs/line-login/integrate-line-login/#get-access-token

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


まさしくこの内容

https://ja.stackoverflow.com/questions/61565/web%E3%81%AEline%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E3%81%A7redirect-uri-does-not-match

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が入っているので、それを利用してログインを行う。ここではセキュリティ、検証的なものは考慮していないので注意。

https://developers.line.biz/ja/docs/line-login/verify-id-token/#get-profile-info-from-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']

        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'


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


この記事が気に入ったらサポートをしてみませんか?