geekdoc-python-zh/docs/overiq/193.md

23 KiB
Raw Permalink Blame History

Django 用户登录和注销

原文:https://overiq.com/django-1-11/django-logging-users-in-and-out/

最后更新于 2020 年 7 月 27 日


Django 为登录和注销用户提供了内置的 URL 模式和视图功能。但是在我们将它们添加到我们的项目之前,我们将使用 Django 身份验证框架提供的一些实用功能自行创建登录和注销系统。

身份验证()和登录()功能

Django 认证框架(django.contrib.auth)提供authenticate()login()功能,其工作分别是认证和登录用户。

authenticate()函数接受两个关键字参数usernamepassword,如果usernamepassword有效,则返回一个类型为User的对象。否则返回None

>>>
>>> from django.contrib import auth
>>>
>>> user = auth.authenticate(username='admin', password='passwordd')
>>>
>>> user
<User: admin>
>>>
>>> if user is not None:
...     print("Credentials are valid")
... else:
...     print("Invalid Credentials")
...
Credentials are valid
>>>
>>>

authenticate()功能只验证提供的凭证是否有效。它不会登录用户。

要登录用户,我们使用login()功能。它需要两个参数,request对象(HttpRequest)和一个User对象。它的工作原理是使用Django 会话框架在会话中保存用户标识。

用户一旦登录,应该可以注销,这是logout()功能的职责。

注销()功能

要注销用户,我们使用logout()功能。它接受请求(HttpRequest)对象并返回None。调用logout()功能会完全删除与登录用户相关的会话数据和 cookie。

需要注意的是,如果用户还没有登录,调用logout()函数不会抛出任何错误。

现在我们有足够的知识来推出我们自己的登录系统。

创建登录系统

在 djangobin app 的views.py文件中,在文件末尾添加loginlogoutuser_details视图,如下所示:

djangobin/django_project/djangobin/views.py

#...
from django.core.mail import mail_admins
from django.contrib.auth.models import User
from django.contrib import auth
import datetime
from .forms import SnippetForm, ContactForm
from .models import Language, Snippet, Tag
from .utils import paginate_result

#...

def profile(request):
    #...

def login(request):
    if request.user.is_authenticated():
        return redirect('djangobin:admin')

    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        user = auth.authenticate(username=username, password=password)

        if user is not None:
            # correct username and password login the user
            auth.login(request, user)
            return redirect('djangobin:user_details')

        else:
            messages.error(request, 'Error wrong username/password')

    return render(request, 'djangobin/login.html')

def logout(request):
    auth.logout(request)
    return render(request,'djangobin/logout.html')

def user_details(request):    
    user = get_object_or_404(User, id=request.user.id)    
    return render(request, 'djangobin/user_details.html', {'user': user})

然后用下面的代码创建三个模板login.htmllogout.htmluser_details.html:

决哥/决哥 _ 项目/决哥/样板/决哥/登录. html

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

{% block title %}
    Login - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="row">
        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Login</h4>
            <hr>

            {% if messages %}
                {% for message in messages %}
                    <p class="alert alert-info">{{ message }}</p>
                {% endfor %}
            {% endif %}

                <form method="post">

                    {% csrf_token %}

                    <table class="table">
                        <tr>
                            <th><label for="id_username">Username:</label></th>
                            <td><input type="text" name="username" id="id_username" required /></td>
                        </tr>
                        <tr>
                            <th><label for="id_password">Password:</label></th>
                            <td><input type="password" name="password" id="id_password" required /></td>
                        </tr>
                        <tr>
                            <td><input type="hidden" name="next" value=""></td>
                            <td><button type="submit" class="btn btn-primary">Submit</button></td>
                        </tr>
                    </table>

                </form>            
        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="/password-reset/">Forgot Password?</a> <br>
                <a href="/register/">Create new account.</a> <br>
                <a href="/contact/">Feedback</a>
            </p>
        </div>

    </div>

{% endblock %}

djangobin/django _ project/djangobin/templates/djangobin/logout . html

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

{% block title %}
    Logout - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="container">

        <p>You are logged out. <a href="{% url 'djangobin:login' %}">Click here</a> to login again.</p>

    </div>

{% endblock %}

djangobin/django _ project/djangobin/templates/djangobin/user _ details . html

{% extends 'djangobin/base.html' %}

{% block title %}
    User Details - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="row">

        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Account Details</h4>

            <hr>

            <dl class="dl-horizontal">
                <dt>Username</dt>
                <dd>{{ request.user.username }}</dd>

                <dt>Email</dt>
                <dd>{{ request.user.email }}</dd>

                <dt>Date Joined</dt>
                <dd>{{ request.user.date_joined }}</dd>

                <dt>Last Login</dt>
                <dd>{{ request.user.last_login }}</dd>

                <dt>Snippet created</dt>
                <dd>{{ request.user.profile.get_snippet_count }}</dd>
            </dl>

        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="">My Pastes</a> <br>
                <a href="">Settings</a> <br>
                <a href="">Change Password.</a> <br>
                <a href="{% url 'djangobin:logout' %}">Logout.</a> <br>
            </p>
        </div>

    </div>

{% endblock %}

这里没有什么特别的,我们只是使用我们在Django 认证框架基础知识一章中学到的一些属性来获取一些关于登录用户的信息。

base.html文件中添加登录和注销页面的链接,如下所示:

决哥/决哥 _ project/决哥/样板/决哥/base.html

{# ... #}
                    {% if request.user.is_authenticated %}
                        <ul class="dropdown-menu">
                            <li><a href="">My Pastes</a></li>
                            <li><a href="">Account Details</a></li>
                            <li><a href="">Settings</a></li>
                            <li role="separator" class="divider"></li>
                            <li><a href="{% url 'djangobin:logout' %}">Logout</a></li>
                        </ul>
                    {% else %}
                        <ul class="dropdown-menu">
                            <li><a href="">Sign Up</a></li>
                            <li><a href="{% url 'djangobin:login' %}">Login</a></li>
                        </ul>
                    {% endif %}
                </li>
            </ul>
        </div><!-- /.navbar-collapse -->
    </div><!-- /.container-fluid -->
</nav>

<div class="container">

    <div class="row">

        <div class="col-lg-9 col-md-9">

            {% if not request.user.is_authenticated and not request.path == '/login/'  %}
                <p class="alert alert-info">
                    <a href="{% url 'djangobin:login' %}" class="alert-link">Login</a> to access other cool features.
                </p>
            {% endif %}

            {% block main %}
                {#  override this block in the child template  #}
            {% endblock %}

        </div>
{# ... #}

最后,在 djangobin 应用的urls.py文件中添加以下三种 URL 模式:

决哥/决哥 _ 项目/决哥/URL . py】

#...
urlpatterns = [
    #...
    url('^contact/$', views.contact, name='contact'),
    url(r'^login/$', views.login, name='login'),
    url(r'^logout/$', views.logout, name='logout'),
    url(r'^userdetails/$', views.user_details, name='user_details'),
]

启动开发服务器,访问http://127.0.0.1:8000/login/。你应该得到这样一页:

输入虚假的用户名和密码,您会得到如下错误:

现在输入正确的用户名和密码,您将被重定向到用户详细信息页面:

要注销,请单击页面右侧的注销链接。您应该会看到这样的注销页面:

使用内置的登录()和注销()视图

Django 提供了两个内置视图django.contrib.auth.login()django.contrib.auth.logout(),分别用于登录和注销用户。

要使用这些视图,请从django.contrib.auth包导入它们,并更新urls.py文件中的loginlogout网址模式,如下所示:

决哥/决哥 _ 项目/决哥/URL . py】

#...
from django.contrib.auth import views as auth_views
from . import views

# app_name = 'djangobin'

urlpatterns = [
    #...
    url(r'^login/$', auth_views.login, name='login'),
    url(r'^logout/$', auth_views.logout, name='logout'),
    url(r'^userdetails/$', views.user_details, name='user_details'),
]

保存urls.py文件,访问http://127.0.0.1:8000/login/。您将获得如下TemplateDoesNotExist例外:

问题在于,默认情况下,django.contrib.auth.login()视图会寻找一个名为registration/login.html的模板。然而Django 没有提供这个模板,这就是为什么会引发TemplateDoesNotExist异常。

另外,请注意模板加载器死后部分。它告诉你 Django 试图找到模板的确切顺序。

我们可以使用template_name关键字参数将不同的模板传递给django.contrib.auth.login()视图,如下所示:

url(r'^login/$',
    auth_views.login, 
    {'template_name': 'djangobin/login.html'}, 
    name='login'
)

同样,默认情况下,django.contrib.auth.logout()视图使用管理应用(django.contrib.admin)中的registration/logged_out.html模板。如果你从 Django 管理网站注销,你会看到同样的模板。

参观http://127.0.0.1:8000/logout/自己看看。

就像django.contrib.auth.login()视图一样,我们可以通过将template_name关键字参数传递给django.contrib.auth.logout()视图来使用不同的模板,如下所示:

url(r'^logout/$', 
    auth_views.logout, 
    {'template_name': 'djangobin/logout.html'}, 
    name='logout'
)

修改登录和注销网址模式以使用自定义模板,如下所示:

决哥/决哥 _ 项目/决哥/URL . py】

#...

urlpatterns = [
    #...
    url(r'^login/$', auth_views.login, {'template_name': 'djangobin/login.html'}, name='login'),
    url(r'^logout/$', auth_views.logout, {'template_name': 'djangobin/logout.html'}, name='logout'),
    url(r'^userdetails/$', views.user_details, name='user_details'),    
]

接下来,更新login.html模板,使用django.contrib.auth.login()提供的form模板变量,视图如下:

决哥/决哥 _ 项目/决哥/样板/决哥/登录. html

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

{% block title %}
    Login - {{ block.super }}
{% endblock %}

{% block main %}

    <div class="row">
        <div class="col-lg-6 col-md-6 col-sm-6">

            <h4>Login</h4>
            <hr>

            {% if messages %}
                {% for message in messages %}
                    <p class="alert alert-info">{{ message }}</p>
                {% endfor %}
            {% endif %}

            <form method="post">

                {% csrf_token %}

                <table class="table">
                    {{ form.as_table }}
                    <tr>
                        <td>&nbsp;</td>
                        <td><button type="submit" class="btn btn-primary">Submit</button></td>
                    </tr>
                </table>

            </form>
        </div>

        <div class="col-lg-6 col-md-6 col-sm-6">
            <h4>Related Links</h4>
            <p>
                <a href="/password-reset/">Forgot Password?</a> <br>
                <a href="/register/">Create new account.</a> <br>
                <a href="#">Feedback</a>
            </p>
        </div>

    </div>

{% endblock %}

我们的登录视图几乎准备好了。访问http://127.0.0.1:8000/login/并尝试使用错误的用户名和密码登录。你会遇到这样的错误:

请尝试使用正确的用户名和密码再次登录。成功后,您将重定向至/accounts/profile/网址。这是django.contrib.auth.login()视图的另一个默认行为。

我们在 djangobin 的urls.py中没有任何 URL 模式来匹配/accounts/profile/的 URL 路径,这就是服务器返回 HTTP 404 错误的原因。

我们可以使用LOGIN_REDIRECT_URL设置轻松覆盖这种行为。打开settings.py文件,在文件末尾添加LOGIN_REDIRECT_URL,如下所示:

djangobin/django _ project/django _ project/settings . py

#...
MANAGERS = (
    ('OverIQ', 'manager@overiq.com'),
)

LOGIN_REDIRECT_URL = 'djangobin:index'

这将把重定向网址从/accounts/profile/改为/

我们也可以直接传递网址路径,而不是传递网址模式的名称。

从现在开始,成功登录后,django.contrib.auth.login()视图会将用户重定向到/ URL 路径,而不是/accounts/profile/

但这仍然有一些局限性。例如,假设您正在浏览趋势片段,然后决定登录。登录后,再次重定向到趋势页面而不是/ URL 更有意义。

要做到这一点,我们可以嵌入一个名为next的隐藏字段,其中包含登录后要重定向到的 URL。

django.contrib.auth.login()视图接收到next作为开机自检数据时,它会重定向到隐藏的next字段中指定的网址。

django.contrib.auth.login()视图还提供了一个名为next的上下文变量,其中包含用户登录后将被重定向的网址。next变量的值可以是/accounts/profile/LOGIN_REDIRECT_URL变量中指定的网址。

我们使用如下查询字符串指定next字段的值:

http://127.0.0.1:8000/login/?next=/trending/

打开login.html并添加名为next的隐藏字段,如下所示:

决哥/决哥 _ 项目/决哥/样板/决哥/登录. html

{# ... #}
            <form method="post">

                {% csrf_token %}

                <table class="table">
                    {{ form.as_table }}
                    <tr>
                        <td><input type="hidden" name="next" value="{{ next }}"></td>
                        <td><button type="submit" class="btn btn-primary">Submit</button></td>
                    </tr>
                </table>

            </form>
{# ... #}

以上代码是这样工作的:

如果我们使用http://localhost:8000/login/网址访问登录页面,那么登录django.contrib.auth.login()后会将用户重定向到/网址。另一方面,如果我们访问登录页面,使用http://127.0.0.1:8000/login/?next=/trending/网址,那么django.contrib.auth.login()视图会将用户重定向到/trending/网址。

接下来,修改base.htmlnext查询参数提供一个值,如下所示:

决哥/决哥 _ project/决哥/样板/决哥/base.html

{# ... #}
<div class="container">

    <div class="row">

        <div class="col-lg-9 col-md-9">

            {% if not request.user.is_authenticated and not request.path == '/login/'  %}
                <p class="alert alert-info">
                    <a href="{% url 'djangobin:login' %}?next={{ request.path }}" class="alert-link">Login</a> to access other cool features.
                </p>
            {% endif %}
{# ... #}

让我们测试一下是否一切正常。

如果您已经登录,请先通过直接访问http://localhost:8000/logout/网址或点击页面右上角的注销链接注销。

然后,导航至登录页面(http://localhost:8000/login/),输入正确的用户名和密码。成功后,您将被重定向到 djangobin 的索引页面:

再次注销,并通过单击趋势分析片段页面中的“登录”链接再次导航到登录页面。这次登录后,您将被重定向到/trending/而不是/网址。

使用电子邮件和密码登录

如您所见默认情况下Django 要求您输入用户名和密码才能登录应用。如果你故意想要这种行为,没关系。然而,为了向您展示如何选择替代路线,我们的 djangobin 应用将使用电子邮件和密码来验证用户。为了完成这项任务,我们将创建一个自定义表单和视图函数。

打开forms.py并添加LoginForm类,如下所示:

djangobin/django _ project/djangobin/forms . py

#...
class ContactForm(forms.Form):
    #...

class LoginForm(forms.Form):
    email = forms.EmailField()
    password = forms.CharField(widget=forms.PasswordInput)

接下来,修改login()视图功能,使用LoginForm如下:

djangobin/django_project/djangobin/views.py

#...
from .forms import SnippetForm, ContactForm, LoginForm
#...

def login(request):
    if request.method == 'POST':

        f = LoginForm(request.POST)
        if f.is_valid():

            user = User.objects.filter(email=f.cleaned_data['email'])

            if user:
                user = auth.authenticate(
                    username=user[0].username,
                    password=f.cleaned_data['password'],
                )

                if user:
                    auth.login(request, user)
                    return redirect( request.GET.get('next') or 'djangobin:index' )

            messages.add_message(request, messages.INFO, 'Invalid email/password.')
            return redirect('djangobin:login')

    else:
        f = LoginForm()

    return render(request, 'djangobin/login.html', {'form': f})

在第 12 行,我们正在检查与提交的电子邮件相关联的任何用户是否存在。

如果用户存在,在第 15 行,我们使用authenticate()功能对其进行认证。请注意,传递给authenticate()函数的参数仍然是用户名和密码。

如果认证成功,我们使用login()功能登录用户并重定向。

更新urls.py文件中的loginlogout网址模式,使用views.py文件的login()logout功能,如下所示:

决哥/决哥 _ 项目/决哥/URL . py】

#...
urlpatterns = [
    #...
    url(r'^login/$', views.login, name='login'),
    url(r'^logout/$', views.logout, name='logout'),
    url(r'^userdetails/$', views.user_details, name='user_details'),
]

访问登录页面,输入不正确的电子邮件和密码。您将得到如下错误:

现在,输入正确的电子邮件和密码,您将被重定向到索引页面。

我们的登录和注销系统工作正常,但是从可用性的角度来看,还是有一个问题。

问题是登录表单对登录用户仍然可见。

向登录用户显示登录表单是毫无意义的。要解决该问题,只需在login()查看功能开始时检查用户是否登录,如下所示:

djangobin/django_project/djangobin/views.py

#...

def login(request):

    if request.user.is_authenticated:
        return redirect('djangobin:profile', username=request.user.username)

    if request.method == 'POST':

        f = LoginForm(request.POST)
        if f.is_valid():

如果您现在访问http://localhost:8000/login/,登录后,您将被重定向到用户配置文件页面。

目前,用户配置文件页面只显示用户的姓名。我们将对其进行更新,以显示即将到来的课程中的片段列表。

限制访问

实现登录系统的关键是防止对管理页面的未授权访问。

限制访问页面的一个简单方法是首先使用is_authenticated()方法检查用户是否通过身份验证,然后相应地重定向用户。例如:

def our_view(request):
    if not request.user.is_authenticated():
        return redirect("login")

    return render(request, 'app/view.html')

我们可以在每个管理视图功能开始时复制并粘贴这个条件。这是可行的,但 Django 提供了一个更好的方法。

限制页面访问的首选方式是使用login_required装饰器。要使用login_required装饰器,您必须从django.contrib.auth.decorators模块导入它。

让我们更新user_detailslogout视图以使用login_required装饰器,如下所示:

djangobin/django_project/djangobin/views.py

#...
from django.contrib import auth
from django.contrib.auth.decorators import login_required
import datetime
from .forms import SnippetForm, ContactForm, LoginForm
#...

#...

@login_required
def logout(request):
    auth.logout(request)
    return render(request,'djangobin/logout.html')

@login_required
def user_details(request):
    user = get_object_or_404(User, id=request.user.id)
    return render(request, 'djangobin/user_details.html', {'user': user})

以下是login_required装饰器的工作原理:

如果用户没有登录,那么它会将用户重定向到/accounts/login/(默认登录网址),将当前绝对网址作为一个值传递给next查询参数。另一方面,如果用户登录了,那么login_required将什么也不做。

要更改默认登录网址,我们使用LOGIN_URL设置。LOGIN_URL接受网址路径或网址模式的名称。打开settings.py文件,在文件末尾添加以下变量。

djangobin/django _ project/django _ project/settings . py

#...

LOGIN_REDIRECT_URL = 'djangobin:index'

LOGIN_URL = 'djangobin:login'

这将默认登录从/accounts/login/更改为/login/。如果您尝试访问应用了login_required装饰器的视图,您将被重定向到/login/网址,而不是/accounts/login/

要验证更改,请访问http://localhost:8000/userdetails/网址,您将被重定向到http://localhost:8000/login/?next=/userdetails/