Django:django-allauth登录、注册、邮箱验证

发布于 2020-11-18  3,292 次阅读


django-allauth是最受欢迎的管理用户登录与注册的第三方Django安装包。它非常强大,能实现以下核心功能。今天我么就来看看如何使用它,简化我们用户注册,登录及账户管理。

  • 用户注册
  • 用户登录
  • 退出登录
  • 第三方auth登录(微信,微博等)
  • 邮箱验证
  • 登录后密码重置
  • 忘记密码,邮箱发送密码重置链接

我们的教程会一共分4部分。

  • 安装与基本使用(注册,登录, 邮箱验证,密码重置)
  • 用户资料扩展及修改
  • 实现第三方auth登录
  • 美化登录与注册表单

1.django-allauth的安装与设置

建议使用pip安装django-allauth。

pip install django-allauth

安装好后设置myproject/settings.py,将allauth相关APP加入到INSTALLED_APP里去。对于第三方的providers,你希望用谁就把它加进去。值得注意的是allauth对于站点设置django.contrib.sites有依赖,你必需也把它加入进去,同时设置SITE_ID。

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'django.contrib.sites',
    'allauth',
    'allauth.account',
    'allauth.socialaccount',
    'allauth.socialaccount.providers.github',

    'polls',
    'blog'
]

SITE_ID = 1

另外我们需设置BACKENDS并提供用户登录验证的方法和用户登录后跳转的链接。在本例中我们可以让用户通过email或者用户名登录。因为我们需要进行邮箱验证和通过电子邮箱发送密码重置链接,所以设置电子邮箱也是非常有必要的。如果你没有邮件服务器,你完全可以设置成自己的QQ或163邮箱,localhost环境下一样可以使用,非常简单。

# 基本设定

# 指定要使用的登录方法(用户名、电子邮件地址两者之一)
ACCOUNT_AUTHENTICATION_METHOD = 'username_email'
# 要求用户注册时必须填写email
ACCOUNT_EMAIL_REQUIRED = True

# 如果ACCOUNT_EMAIL_VERIFICATION = 'mandatory' ,用户必须通过邮箱验证后才能登陆 如果你不需要邮箱验证,只需要设置 ACCOUNT_EMAIL_VERIFICATION = 'none'
ACCOUNT_EMAIL_VERIFICATION = 'none'

# 设置登录和注册成功后重定向的页面,默认是 "/accounts/profile/"
LOGIN_REDIRECT_URL = '/accounts/profile/'

AUTHENTICATION_BACKENDS = (
    # django admin所使用的用户登录与django-allauth无关
    'django.contrib.auth.backends.ModelBackend',

    # allauth 身份验证
    'allauth.account.auth_backends.AuthenticationBackend',
)

# 邮箱设定
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 25
EMAIL_HOST_USER = 'chen_shiyangnihao@163.com' # 你的163账号和授权码
EMAIL_HOST_PASSWORD = '111qqq!'
EMAIL_USE_TLS = True  # 这里必须是 True,否则发送不成功
EMAIL_FROM = 'chen_shiyangnihao@163.com' # # 发送人 你的 163账号

# 默认显示的发送人,(邮箱地址必须与发送人一致),不设置的话django默认使用的webmaster@localhost
DEFAULT_FROM_EMAIL = 'chen_shiyangnihao@163.com'

配置路由:

urlpatterns = [
    path('admin/', admin.site.urls), # admin后台
    path('accounts/', include('allauth.urls')),
    path('polls/', include('polls.urls')),
    path('blog/', include('blog.urls')),
]

查看效果

配置好了settings和url后, 在terminal里面连续输入以下命令启动服务器.

python manage.py makemigrations
python manage.py migrate
python manage.py runserver

登录admin后,将 example.com 改为我们博客的域名,在开发环境下,我们用127.0.0.1:8000,再退出登录(一定要退出)。

现在你就可以访问以下链接查看allauth的效果了。由于我们已经设置好了邮箱,所以涉及邮箱验证和密码重置部分都可以正常进行的。

  • http://127.0.0.1:8000/accounts/signup/ #注册
  • http://127.0.0.1:8000/accounts/login/ #登录
  • http://127.0.0.1:8000/accounts/logout/ #退出
  • http://127.0.0.1:8000/accounts/password/reset/ #重置密码

用户注册与邮箱验证

注册时如果报CSRF token missing or incorrect错误,可将CsrfViewMiddleware注释掉或者在模板表单中添加{% csrf_token %}

django-allauth 常见设置选项
# 要求用户注册时必须填写email
ACCOUNT_EMAIL_REQUIRED = True 
# 注册中邮件验证方法: "强制(mandatory)"、 "可选(optional)" 或 "否(none)" 之一
(注册成功后,会发送一封验证邮件,用户必须验证邮箱后,才能登陆)
ACCOUNT_EMAIL_VERIFICATION (="optional") 
# 作用于第三方账号的注册
SOCIALACCOUNT_EMAIL_VERIFICATION = 'optional' / 'mandatory' / 'none'
# 邮件发送后的冷却时间(以秒为单位)
ACCOUNT_EMAIL_CONFIRMATION_COOLDOWN (=180) 
# 邮箱确认邮件的截止日期(天数)
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS (=3) 

# 指定要使用的登录方法(用户名、电子邮件地址或两者之一)
ACCOUNT_AUTHENTICATION_METHOD (="username" | "email" | "username_email") 
# 登录尝试失败的次数
ACCOUNT_LOGIN_ATTEMPTS_LIMIT (=5) 
# 从上次失败的登录尝试,用户被禁止尝试登录的持续时间
ACCOUNT_LOGIN_ATTEMPTS_TIMEOUT (=300) 
# 更改为True,用户一旦确认他们的电子邮件地址,就会自动登录
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION (=False) 

# 更改或设置密码后是否自动退出
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE (=False) 
# 更改为True,用户将在重置密码后自动登录
ACCOUNT_LOGIN_ON_PASSWORD_RESET (=False) 
# 控制会话的生命周期,可选项还有: "False" 和 "True"
ACCOUNT_SESSION_REMEMBER (=None) 

# 用户注册时是否需要输入邮箱两遍
ACCOUNT_SIGNUP_EMAIL_ENTER_TWICE (=False) 
# 用户注册时是否需要用户输入两遍密码
ACCOUNT_SIGNUP_PASSWORD_ENTER_TWICE (=True) 
# 用户不能使用的用户名列表
ACCOUNT_USERNAME_BLACKLIST (=[]) 
# 加强电子邮件地址的唯一性
ACCOUNT_UNIQUE_EMAIL (=True) 
# 用户名允许的最小长度的整数
ACCOUNT_USERNAME_MIN_LENGTH (=1) 
# 使用从社交账号提供者检索的字段(如用户名、邮件)来绕过注册表单
SOCIALACCOUNT_AUTO_SIGNUP (=True) 

# 设置登录后跳转链接
LOGIN_REDIRECT_URL (="/") 
# 设置退出登录后跳转链接
ACCOUNT_LOGOUT_REDIRECT_URL (="/")
# 用户登出是否需要确认确认(True表示直接退出,不用确认;False表示需要确认)
ACCOUNT_LOGOUT_ON_GET (=True)

django-allauth的URLs及视图

下面是django_allauth所有内置的URLs,均可以访问的。

  • /accounts/login/(URL名account_login): 登录
  • /accounts/signup/ (URL名account_signup): 注册
  • /accounts/password/reset/(URL名: account_reset_password) :重置密码
  • /accounts/logout/ (URL名account_logout): 退出登录
  • /accounts/password/set/ (URL名:account_set_password): 设置密码 
  • /accounts/password/change/ (URL名: account_change_password): 改变密码(需登录)
  • /accounts/email/(URL名: account_email) 用户可以添加和移除email,并验证
  • /accounts/social/connections/(URL名:socialaccount_connections): 管理第三方账户

2.用户资料扩展及修改

django-allauth 并没有提供展示和修改用户资料的功能,也没有对用户资料进行扩展,所以我们需要自定义用户模型来进行扩展

1.创建 app 及配置

由于 django-allauth 已经占用了 account 这个 app,所以我们可以创建一个叫 Myaccount 的 app,并将其加入 settings.py 配置文件的 INSTALL_APPS 中,同时把 url 也加入到项目 settings.py 中。

python manage.py startapp Myaccount

配置路由

因为我们希望用户在登录或注册成功后,自动跳转到个人信息页 "/accounts/profile/",所以在前面的配置中加入了如下代码

# Myblog/settings.py

LOGIN_REDIRECT_URL = "/accounts/profile/"

2.创建用户模型及表单

首先自定义的 User 模型继承了 AbstractUser ,AbstractUser 是 django 自带用户类,可扩展用户个人信息,AbstractUser 模块下有:password、username、first_name、last_name、email、last_loginl、is_superuserl、is_staffl、is_activel、date_joined 字段,自定义用户User扩展了 nickname、link 及头像 avatar 字段,此处重写了User的 save() 方法以便上传的头像以用户名为文件夹分类。

from allauth.account.models import EmailAddress
from django.db import models
from django.contrib.auth.models import AbstractUser

#用pillow、django-imagekit模块设置图片,可以处理图片,生成指定大小的缩略图,前端显示src="{{ user.avatar.url }}
from imagekit.models import ProcessedImageField
from imagekit.processors import ResizeToFill

#扩展Django自带的User模型字
class User(AbstractUser):
    nickname = models.CharField(max_length=30, blank=True, default='', verbose_name='昵称')
    # 扩展用户个人网站字段
    link = models.URLField('个人网址', blank=True, help_text='提示:网址必须填写以http开头的完整形式', default='')
    # 扩展用户头像字段,upload_to后必须是相对路径,上传路径已设置为media,因此upto不需要media/avatar,数据库中avatar/...,前端用avatar.url为media/avatar/...
    avatar = ProcessedImageField(upload_to='avatar',default='avatar/default.png',verbose_name='头像',
                                processors=[ResizeToFill(100, 100)], # 处理后的图像大小
                                format='JPEG', # 处理后的图片格式
                                options={'quality': 95} # 处理后的图片质量
                                )

    #重写User的save()方法保存上传的头像目录
    def save(self, *args, **kwargs):
        # 当用户更改头像的时候,avatar.name = '文件名',其他情况下avatar.name = 'upload_to/文件名'
        if len(self.avatar.name.split('/')) == 1:
          self.avatar.name = self.username + '/' + self.avatar.name
        #调用父类的save()方法后,avatar.name就变成了'upload_to/用户名/文件名'
        super(User, self).save()

    def email_verified(self):
        # 验证邮箱
        if self.is_authenticated:
            result = EmailAddress.objects.filter(email=self.email)
            if len(result):
                return result[0].verified
        else:
            return False

    # 定义网站管理后台表名
    class Meta:
      verbose_name = '用户信息'
      verbose_name_plural = verbose_name #指定模型的复数形式是什么,如果不指定Django会自动在模型名称后加一个’s’
      ordering = ['-id']
      #admin后台显示名字关联到此表的字段的后天显示名字

    def __str__(self):
      return self.username

为了让 Django 能够识别自定义的用户模型,需要在 settings.py 中需配置AUTH_USER_MODEL='Myaccount.User',注册的用户就会基于自定义User模型创建,并一同创建account中与自定义User关联的模型。

from django import forms
from .models import User

class ProfileForm(forms.Form):
 class Meta:
  # 关联的数据库模型,这里是用户模型
  model = User
  # 前端显示、可以修改的字段(admin中)
  fields = ['nickname', 'link', 'avatar']

3.创建视图并配置URLs

from django.conf.urls import re_path
from . import views

app_name = "Myaccount"

urlpatterns = [
    re_path(r'^profile/$', views.profile, name='profile'),
    re_path(r'^profile/update/$', views.profile_update, name='profile_update'),
]

展示资料视图函数根据需求自定义,本博客前端用的是Ajax请求修改网站link与头像avatar供参考

#Myaccount/views.py

from django.contrib import messages
from django.shortcuts import render, redirect
from django.http import HttpResponse

from Myaccount import models
# Create your views here.

#auth中用户权限有关的类。auth可以设置每个用户的权限。
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt

# 使用login_required装饰器,用户只有登录了才能访问其用户资料
from Myaccount.forms import ProfileForm


@login_required
#个人信息
def profile(request):
    # AUTH_USER_MODEL 类型的对象,表示当前登录的用户。
    user = request.user
    return render(request, 'Myaccount/profile.html', {'user': user})

import json
import base64
@login_required # 使用login_required装饰器,用户只有登录了才能访问其用户资料
@csrf_exempt #取消当前函数防跨站请求伪造功能,即便settings中设置了全局中间件。
def profile_update(request):
    '''更新个人资料'''
    if request.method == 'POST':
        # instance参数表示用model实例来初始化表单,这样就可以达到通过表单来更新数据
        form = ProfileForm(request.POST, instance=request.user)
        if form.is_valid():
            form.save()
            # 添加一条信息,表单验证成功就重定向到个人信息页面
            messages.add_message(request, messages.SUCCESS, '个人信息更新成功!')
            return redirect('Myaccount:profile')
    else:
        # 不是POST请求就返回空表单
        form = ProfileForm(instance=request.user)

    return render(request, 'Myaccount/change_profile.html', context={'form': form})

创建模板文件

展示个人资料模板文件

# Myaccount/templates/Myaccount/profile.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>个人资料</title>
</head>
<body>

<!--消息块-->
{% if messages %}
<div class="container">
    {% for message in messages %}
    <div class="alert {% if message.tags %}alert-{{ message.tags }}{% else %}alert-secondary{% endif %} alert-dismissible rounded-0 fade show" role="alert">
        {{ message }}
        {% if not user.email_verified %}
        <a href="{% url 'account_email' %}" style="text-decoration:none">验证邮箱.</a>
        {% endif %}
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
        </button>
    </div>
    {% endfor %}
</div>
{% endif %}

{% if user.is_authenticated %}
<a href="{% url 'Myaccount:profile_update' %}">修改资料</a>
<a href="{% url 'account_logout' %}">注销</a>
{% endif %}

<p>Welcome, {{ user.username }}</p>

<ul>
    <li>nick_name: {{ user.nickname }}</li>
    <li>link: {{ user.link }}</li>
    <li>avatar: {{ user.avatar }}</li>
</ul>

</body>
</html>

由于修改个人资料需要处理表单, 我们可以安装 django-crispy-forms 插件来处理(美化)表单

# 安装 
pip install django-crispy-forms
 
# 加入 INSTALLED_APPS
INSTALLED_APPS = [
    ...,
    'allauth.socialaccount.providers.weibo',
    'allauth.socialaccount.providers.github',
 
    'crispy_forms',  # bootstrap 表单样式
]
 
# 配置表单插件使用的样式
CRISPY_TEMPLATE_PACK = 'bootstrap4'

更新个人资料模板文件

# Myaccount/templates/Myaccount/change_profile.html
{% load crispy_forms_tags %}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>修改资料</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css"
          integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
</head>
<body>

<!--消息块-->
{% if messages %}
<div class="container">
    {% for message in messages %}
    <div class="alert {% if message.tags %}alert-{{ message.tags }}{% else %}alert-secondary{% endif %} alert-dismissible rounded-0 fade show" role="alert">
        {{ message }}
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
        </button>
    </div>
    {% endfor %}
</div>
{% endif %}

{% if user.is_authenticated %}
<a href="{% url 'Myaccount:profile_update' %}">修改资料</a>
<a href="{% url 'account_logout' %}">注销</a>
{% endif %}

<div class="container">
    <form method="post" enctype="multipart/form-data" action="{% url 'Myaccount:profile_update'%}">>
        {% csrf_token %}
        {{ form|crispy }}
        <button class="btn btn-info btn-sm rounded-0" type="submit">更新资料</button>
    </form>
</div>

</body>
</html>

4.设计前端模板

需要注意前后端的交互信息,通过View中传来的当前用户模型User,可通过Django的模板语言显示用户名:{{ user.username }},用户邮箱:{{ user.link }}、用户头像链接:{{ user.avatar.url }}、以及用户邮箱:{{ user.emailaddress_set .0}}(django-allauth在注册用户时创建的ACCOUNTS表,关联自定义模型User,包含用户的邮箱信息及各种方法)、Django-allauth判断邮箱是否验证:{% if user.emailaddress_set .0.verified %}、Django判断用户是否登录:{% if user.is_authenticated %}等等。

3.美化页面模板

django-allauth自带的页面很简陋,我们需要进行美化。如果你是通过pip安装的django-allauth,模板位置一般在python安装位置或者虚拟环境下的blog_env\Lib\site-packages中,找到\allauth\templates下的整个account文件夹,或者从github上( allauth项目地址 )将allauth的/templates/accounts/文件夹整个拷贝到你的本地项目中的templates目录下,因为django-allauth总是会在templates/accounts/文件夹中寻找模板。

查看allauth路径命令:python3 -c "import allauth;print(allauth.path)"

美化base.html

不管是注册,登录还是重置密码页面,html模板主要内容都是表单。我们将使用bootstrap迅速美化模板和表单。最快速的方式就是修改base.html(如果没有该文件,你需要创建一个),加入boostrap样式和js。


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="description" content="用户账号管理,使用django-allauth社交用户系统,支持Baidu、Github等社交账号登录。">
    <meta name="keywords" content="django-allauth,社交用户系统,OAuth 2.0">

    <title>{% block head_title %}{% endblock %}</title>
    {% load static %}
    <link href="https://cdn.bootcss.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-PsH8R72JQ3SOdhVi3uxftmaW6Vc51MKb0q5P2rRUpPvrszuE4W1povHYgTpBfshb" crossorigin="anonymous">
    <link href="https://netdna.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css" rel="stylesheet">
    <link href="{% static 'Myaccount/1.css' %}" rel="stylesheet" type="text/css">
</head>
<body>

{% block message %}
    {% if messages %}
    <div class="container">
        {% for message in messages %}
        <div class="alert {% if message.tags %}alert-{{ message.tags }}{% else %}alert-secondary{% endif %} alert-dismissible rounded-0 fade show" role="alert">
            {{ message }}
            <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
            </button>
        </div>
        {% endfor %}
    </div>
    {% endif %}
{% endblock %}

{% block base_content %}
<div class="container">
    <div class="row">
        <div class="col-12 col-sm-8 col-md-6 offset-sm-2 offset-md-3 px-xl-5">
            <div class="card rounded-0 px-3 px-lg-4">
                <div class="card-header text-center bg-white py-2">
                    <h3 class="my-1 text-info">{% block user_title %}账号管理{% endblock %}</h3>
                </div>
                <div class="card-body card-login">{% block content %}{% endblock %}</div>
                <div class="text-center mb-5" id="social-login">
                    <div class="login-title">
                        <span>快速登录</span>
                    </div>
                    <div class="login-link">
                        <a class="mx-4" href="/accounts/weibo/login/?next={{ next_url }}" title="社交账号登录有点慢,请耐心等候!"><i class="fa fa-weibo fa-2x"></i></a>
                        <a class="mx-4" href="/accounts/github/login/?next={{ next_url }}" title="社交账号登录有点慢,请耐心等候!"><i class="fa fa-github fa-2x"></i></a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block extra_body %}
{% endblock %}

</body>
</html>

未美化注册页面

美化注册表单

{% extends "account/base.html" %}

{% load i18n %}
{% load widget_tweaks %}

{% block head_title %}{% trans "Signup" %}{% endblock %}

{% block content %}
<h1>{% trans "Sign Up" %}</h1>

<p>{% blocktrans %}Already have an account? Then please <a href="{{ login_url }}">sign in</a>.{% endblocktrans %}</p>

<form class="signup" id="signup_form" method="post" action="{% url 'account_signup' %}">
  {% csrf_token %}
    {% include 'account/snippets/bs4_form.html' with form=form %}
  {% if redirect_field_value %}
  <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
  {% endif %}

    <div class="form-group">
  <button type="submit" class="btn btn-success">{% trans "Sign Up" %}</button>
    </div>
</form>

{% endblock %}

创建css文件用于修改样式

# Myaccount/static/Myaccount/1.css
.secondaryAction {
    color: #868e96;
}
.secondaryAction:hover {
    text-decoration: none;
    color: #007bff;
}
.asteriskField {
    margin-left: .25rem;
    color: #dc3545;
}
#social-login .login-title {
    position: relative;
    display: block;
    margin-bottom:10px;
}
#social-login span {
    color:#999;
}
#social-login span:before,
#social-login span:after {
    position: absolute;
    top: 50%;
    background: #eee;
    width: 38%;
    height: 1px;
    content: '';
}
#social-login span:before {
    left:0;
}
#social-login span:after {
    right:0;
}
.fa-weibo {
    color: #e12f11;
    opacity: .8;
}
.fa-github {
    color: #333;
    opacity: .8;
}
.fa-weibo:hover,
.fa-github:hover {
    opacity: 1;
}
.btn-sm {
    padding:.2rem .7rem;
}
.change_profile .form-control,
.card-login .form-control {
    border-radius: 0;
}
.change_profile .alert,
.card-login .alert {
    border-radius: 0;
}
.change_profile .alert li,
.card-login .alert li {
    margin-bottom: .5rem;
}
.change_profile .alert ul,
.card-login .alert ul {
    padding-left:.5rem;
    margin-bottom: 0;
}
#profile-avatar .avatar {
    width:80px;
    padding: .25rem;
    background-color: #fff;
    border: 1px solid #dee2e6;
    border-radius: .25rem;
}

美化 login 登录页面

# templates/account/login.html
{% extends "account/base.html" %}
 
{% load i18n %}
{% load account socialaccount %}
{% load crispy_forms_tags %}
 
{% block head_title %}{% trans "Sign In" %}{% endblock %}
 
{% block content %}
<form class="login" method="POST" action="{% url 'account_login' %}">
    {% csrf_token %}
    {{ form|crispy }}
    {% if redirect_field_value %}
    <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
    {% endif %}
    <a class="secondaryAction" href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
    <button class="pull-right btn btn-info btn-sm rounded-0" type="submit">{% trans "Sign In" %}</button>
</form>
{% endblock %}

4.第三方登录(飞书)

1.首先去飞书开发平台创建一个应用获取到App ID和App Secret

2.在安全设置里面添加安全的重定向地址,不添加的话飞书会认为是非法请求

3.编写飞书登录代码,找到allauth/socialaccount/providers目录新建feishu目录,allauth所有的第三方登录都在providers目录下,allauth安装目录可以通过如下命令查看到:

python3 -c 'import allauth;print(allauth.path)'

新建__init__.py文件此文件为空

新建client.py

# -*- coding: utf-8 -*-
# @Time    : 2020-11-18 11:13
# @Author  : chenshiyang
# @Email   : chenshiyang@blued.com
# @File    : client.py
# @Software: PyCharm

import requests
import json
from collections import OrderedDict

from django.utils.http import urlencode

from allauth.socialaccount.providers.oauth2.client import (
    OAuth2Client,
    OAuth2Error,
)


class FeishuOAuth2Client(OAuth2Client):

    app_access_token_url = 'https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal/'

    def get_redirect_url(self, authorization_url, extra_params):
        params = {
            "app_id": self.consumer_key,
            "redirect_uri": self.callback_url,
            "scope": self.scope,
            "response_type": "code",
        }
        if self.state:
            params["state"] = self.state
        params.update(extra_params)
        sorted_params = OrderedDict()
        for param in sorted(params):
            sorted_params[param] = params[param]
        return "%s?%s" % (authorization_url, urlencode(sorted_params))

    def app_access_token(self):
        """获取 app_access_token(企业自建应用)

        Returns:
            {
                "code":0,
                "msg":"ok",
                "app_access_token":"xxxxx",
                "expire":7200,  // 过期时间,单位为秒(两小时失效)
                "tenant_access_token":"xxxxx"
            }
        """

        data = {
            "app_id": self.consumer_key,
            "app_secret": self.consumer_secret,
        }

        self._strip_empty_keys(data)
        url = self.app_access_token_url

        # TODO: Proper exception handling
        resp = requests.request('POST', url, data=data)
        access_token = None
        if resp.status_code == 200:
            access_token = resp.json()
        if not access_token or "app_access_token" not in access_token:
            raise OAuth2Error(
                "Error retrieving app access token: %s" % resp.content)
        return access_token['app_access_token']

    def get_access_token(self, code):
        data = {
            "grant_type": "authorization_code",
            "code": code,
            "app_access_token": self.app_access_token()
        }
        params = None
        self._strip_empty_keys(data)
        url = self.access_token_url
        if self.access_token_method == "GET":
            params = data
            data = None
        # TODO: Proper exception handling
        resp = requests.request(self.access_token_method, url, params=params, data=json.dumps(data), headers={'Content-Type': 'application/json'})
        access_token = None
        if resp.status_code == 200:
            access_token = resp.json()
        if not access_token or "data" not in access_token or "access_token" not in access_token['data']:
            raise OAuth2Error("Error retrieving access token: %s" % resp.content)
        return access_token['data']

新建provider.py

# -*- coding: utf-8 -*-
# @Time    : 2020-11-18 11:16
# @Author  : chenshiyang
# @Email   : chenshiyang@blued.com
# @File    : provider.py
# @Software: PyCharm

from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider


class FeishuAccount(ProviderAccount):
    def get_avatar_url(self):
        return self.account.extra_data.get("avatar_big")

    def to_str(self):
        return self.account.extra_data.get(
            "name", super(FeishuAccount, self).to_str()
        )


class FeishuProvider(OAuth2Provider):
    id = "feishu"
    name = "feishu"
    account_class = FeishuAccount

    def extract_uid(self, data):
        return data["open_id"]


    def extract_common_fields(self, data):
        return dict(username=data.get("name"), name=data.get("name"))


provider_classes = [FeishuProvider]

新建tests.py 如果不编写测试用例此文件可为空

新建urls.py

# -*- coding: utf-8 -*-
# @Time    : 2020-11-18 11:19
# @Author  : chenshiyang
# @Email   : chenshiyang@blued.com
# @File    : urls.py
# @Software: PyCharm

from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns

from .provider import FeishuProvider


urlpatterns = default_urlpatterns(FeishuProvider)

新建views.py

# -*- coding: utf-8 -*-
# @Time    : 2020-11-18 11:21
# @Author  : chenshiyang
# @Email   : chenshiyang@blued.com
# @File    : views.py
# @Software: PyCharm

import requests

from django.urls import reverse

from allauth.account import app_settings
from allauth.socialaccount.providers.oauth2.views import (
    OAuth2Adapter,
    OAuth2CallbackView,
    OAuth2LoginView,
)
from allauth.socialaccount.providers.oauth2.client import (
    OAuth2Error,
)
from allauth.utils import build_absolute_uri

from .client import FeishuOAuth2Client
from .provider import FeishuProvider


class FeishuOAuth2Adapter(OAuth2Adapter):
    provider_id = FeishuProvider.id

    authorization_url = 'https://open.feishu.cn/open-apis/authen/v1/index'
    access_token_url = 'https://open.feishu.cn/open-apis/authen/v1/access_token'
    app_access_token_url = 'https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal/'
    user_info_url = 'https://open.feishu.cn/open-apis/authen/v1/user_info'

    @property
    def authorize_url(self):
        settings = self.get_provider().get_settings()
        url = settings.get(
            "AUTHORIZE_URL", self.authorization_url
        )
        return url

    def complete_login(self, request, app, token, **kwargs):
        openid = kwargs.get("response", {}).get("openid")
        resp = requests.get(
            self.user_info_url,
            headers ={'Content-Type': 'application/json',
                      'Authorization': f'Bearer {token}',},
        )
        extra_data = resp.json()
        if extra_data['code'] != 0:
            raise OAuth2Error("Error retrieving code: %s" % resp.content)
        extra_data = extra_data['data']

        return self.get_provider().sociallogin_from_response(request, extra_data)


class FeishuOAuth2ClientMixin(object):
    def get_client(self, request, app):
        callback_url = reverse(self.adapter.provider_id + "_callback")
        protocol = (
            self.adapter.redirect_uri_protocol or app_settings.DEFAULT_HTTP_PROTOCOL
        )
        callback_url = build_absolute_uri(request, callback_url, protocol=protocol)
        provider = self.adapter.get_provider()
        scope = provider.get_scope(request)
        client = FeishuOAuth2Client(
            request,
            app.client_id,
            app.secret,
            self.adapter.access_token_method,
            self.adapter.access_token_url,
            callback_url,
            scope,
        )
        return client


class FeishuOAuth2LoginView(FeishuOAuth2ClientMixin, OAuth2LoginView):
    pass


class FeishuOAuth2CallbackView(FeishuOAuth2ClientMixin, OAuth2CallbackView):
    pass


oauth2_login = FeishuOAuth2LoginView.adapter_view(FeishuOAuth2Adapter)
oauth2_callback = FeishuOAuth2CallbackView.adapter_view(FeishuOAuth2Adapter)

最终目录文件如下:

4.INSTALLED_APPS注册allauth 飞书登录

5.admin后台设置social applications

6.base.html添加飞书登录链接

<div class="login-link">
                        <a class="mx-4" href="/accounts/weibo/login/?next={{ next_url }}" title="社交账号登录有点慢,请耐心等候!"><i class="fa fa-weibo fa-2x"></i></a>
                        <a class="mx-4" href="/accounts/feishu/login/?next={{ next_url }}" title="社交账号登录有点慢,请耐心等候!"><img class="fa fa-feishu" src="{% static 'Myaccount/images/feishu.ico' %}"></a>
                    </div>

目前我编写的django-allauth飞书登录代码已经合并master分支可以直接使用

参考:大江狗 | django-allauth教程

参考: https://blog.csdn.net/bbwangj/article/details/89093616


一名测试工作者,专注接口测试、自动化测试、性能测试、Python技术。