Django丨Authentication 用户认证系统

Django丨Authentication 用户认证系统

1. 介绍🍀

Django自带一个用户认证系统,用于处理用户账户、群组、许可和基于cookie的用户会话。

Django这个认证系统 , 它提供了认证 (authentiaction) 和 授权功能 (authorization) , 这两种功能在某些地方时耦合的。

默认情况下,当项目创建了后,认证的相关模块已经自动添加到settings文件内了:

INSTALLED_APPS配置中:

  • django.contrib.auth:包含认证框架的核心以及默认模型;
  • django.contrib.contenttypes:内容类型系统,用于给模型关联许可;

MIDDLEWARE配置中:

  • django.middleware.security.SecurityMiddleware:通过请求管理会话;
  • django.contrib.auth.middleware.AuthenticationMiddleware:将会话和用户关联;

2. User对象 🍀

User对象是认证系统的核心,它们通常表示与你的站点进行交互的用户 , 并用于启用限制访问 , 注册用户信息和给创建者关联内容等

用户模型主要有下面几个字段:usernamepasswordemailfirst_namelast_name

2.1 创建用户🍀

创建用户的几个方法:

  • 1、create:

    创建一个普通用户,密码是明文的。

  • 2、create_user:

    创建一个普通用户,密码是密文的。(可以不指定email)

    1
    2
    3
    4
    5
    6
    7
    from django.contrib.auth.models import User    
    user = User.objects.create_user(username='',password='',email=''
    user.save()

    # 这时候,user是一个User类的实例,已经保存在数据内,你可以随时修改它的属性,例:
    user.last_name = 'jeremy'
    user.save() # 当修改属性的时候,需要 save()
  • 3、create_superuser:

    创建一个超级用户,密码是密文的,必须要指定 email 参数
    (跟上面的方法一样)

2.2 修改密码🍀

django默认会对密码进行加密,因此不能直接对密码字段进行直接操作。

修改密码有两个方法:

  • 使用命令行:python manage.py changepassword username

    • 提供了一种从命令行更改用户密码的方法 , 它提示你修改一个给定的user密码 , 你必须输入两次 , 如果两次输入匹配 , 密码就会立即被修改 , (如果你没有提供user , 命令行将尝试修改与当前系统用户匹配的用户名的密码)
  • 使用set_password()方法;

    1
    2
    3
    4
    from django.contrib.auth.models import User
    u = User.objects.get(username='john')
    u.set_password('new password')
    u.save()
  • 如果你安装了Django admin , 可以在身份验证系统的管理页面上更改用户的密码

  • Django还提供视图和表单允许用户修改他们自己的密码

  • 注意 : 更改用户密码将会注销所有会话 , 详见 : Session invalidation on password change

2.3 check_password(passwd)🍀

用户需要修改密码的时候 首先要让他输入原来的密码 ,如果给定的字符串通过了密码检查,返回 True
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@login_required
def set_password(request):
user = request.user
state = None
if request.method == 'POST':
old_password = request.POST.get('old_password', '')
new_password = request.POST.get('new_password', '')
repeat_password = request.POST.get('repeat_password', '')
if user.check_password(old_password):
if not new_password:
state = 'empty'
elif new_password != repeat_password:
state = 'repeat_error'
else:
user.set_password(new_password)
user.save()
return redirect("/log_in/")
else:
state = 'password_error'
content = {
'user': user,
'state': state,
}
return render(request, 'set_password.html', content)

2.4 用户认证🍀

使用authenticate()来验证一组凭证,它接收关键字参数credentials,默认为usernamepassword

1
2
3
4
5
authenticate(request=None, **credentials):
"""
request: HttpRequest对象
credentials: 默认username和password
"""

根据每一个认证的后端去验证,

如果某个后端凭证通过则返回一个User对象,如果凭证对后端都无效,则主动触发PermissionDenied,并返回None

1
2
3
4
5
6
7
8
from django.contrib.auth import authenticate

user = authenticate(username='john', password='secret')

if user is not None:
# A backend authenticated the credentials
else:
# No backend authenticated the credentials

3. 权限和授权🍀

django本身提供一个简单的权限系统,它提供了一种为特定用户和用户组分配权限的方法。

Django中的admin站点也使用了该权限系统 , 使用的权限如下 :

  • 查看add表单和添加对象仅限于具有add权限的用户类型对象
  • 查看change表单和更改对象仅限于具有change权限的用户类型对象
  • 删除一个对象仅限于具有delete权限的用户类型对象

权限不但可以根据每个对象的类型 , 而且可以根据特定的对象实例设置 , 通过ModelAdmin提供的has_add_permission(), has_change_permission()has_delete_permission()方法 , 可以针对相同类型的不同对象实例自定义权限

User对象具有两个多对多字段 : groupsuser_permissions

User对象可以使用和其他Django模型一样的方式访问他们相关联的对象 , 如下:

1
2
3
4
5
6
7
8
myuser.groups.set([group_list])
myuser.groups.add(group, group, ...)
myuser.groups.remove(group, group, ...)
myuser.groups.clear()
myuser.user_permissions.set([permission_list])
myuser.user_permissions.add(permission, permission, ...)
myuser.user_permissions.remove(permission, permission, ...)
myuser.user_permissions.clear()

3.1 默认权限🍀

django.contrib.auth在你的INSTALLED_APPS配置中列出时 , 它将确保为你安装的应用中的每个Django模型创建3个默认的权限 , 即add,changedelete

假设你现在有个app叫做foo,有个model叫做bar,使用下面的方式可以测试默认权限:

1
2
3
add: user.has_perm('foo.add_bar')
change: user.has_perm('foo.change_bar')
delete: user.has_perm('foo.delete_bar')

当你运行manage.py migrate时, 将创建这些权限 ; 在django.contrib.auth添加INSTALLED_APPS之后 , 首次运行migrate时 , 将为所有先前安装的模型创建默认权限 , 以及当时安装的任何新模型之后 , 每次运行manage.py migrate, 它将为新的模型创建默认权限(创建权限的函数与post_migrate信号连接)

3.2 用户组🍀

django.contrib.auth.models.Group模型是一种对用户进行分类的通用方式 , 通过这种方式你可以引用权限或其他标签都这些用户 ;

一个用户可以属于任意多个组, 组中每个用户自动具有该组的权限, 例如, 如果Site editors组具有can_edit_home_page权限 , 那么该组中的任何用户都具有该权限

除了权限之外, 组还是给分类用户分配标签,添加功能的便捷方法; 例如, 你可以创建一个组Special users, 只有在该组中的用户才能够访问会员的页面

3.3 编程方式创建权限🍀

虽然我们可以在模型的Meta类中自定义权限, 但是你也可以直接创建权限, 例如, 你可以在myapp中为BlogPost模型创建can_publish权限:

1
2
3
4
5
6
7
8
9
10
11
from myapp.models import BlogPost
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType

content_type = ContentType.objects.get_for_model(BlogPost)

permission = Permission.objects.create(
codename='can_publish',
name='Can Publish Posts',
content_type=content_type,
)

然后该权限可以通过user_permissions属性或者通过Grouppermissions属性分配给用户

3.4 权限缓存🍀

ModelBackend在第一次需要访问User对象的权限时会对权限进行缓存, 由于对新添加的权限并不会立即检查, 所以这种做法对request-response循环是非常有利的 (例如在admin中), 如果你想要在添加新的权限后马上在测试或视图检查他们, 最简单的解决办法是从数据库中重新获取User, 如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from django.contrib.auth.models import Permission, User
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404

from myapp.models import BlogPost

def user_gains_perms(request, user_id):
user = get_object_or_404(User, pk=user_id)
# any permission check will cache the current set of permissions
user.has_perm('myapp.change_blogpost')

content_type = ContentType.objects.get_for_model(BlogPost)
permission = Permission.objects.get(
codename='change_blogpost',
content_type=content_type,
)
user.user_permissions.add(permission)

# Checking the cached permission set
user.has_perm('myapp.change_blogpost') # False

# Request new instance of User
# Be aware that user.refresh_from_db() won't clear the cache.
user = get_object_or_404(User, pk=user_id)

# Permission cache is repopulated from the database
user.has_perm('myapp.change_blogpost') # True

...

4. Web请求中的认证 🍀

Django使用SessionsMiddleware来拦截request objects到认证系统中

认证系统为每个请求提供一个request.user属性来代表当前的用户,
如果当前的 用户未登录 或者 用户注销 后 , 该属性将会被赋值给AonnymousUser(匿名用户) 实例 , 否则该属性将会是一个User实例

is_authenticated属性可以判断用户是否登录,这样可以做到对未登录的用户进行限制访问的页面。

我们可以使用is_authenticated属性来进行区分:

1
2
3
4
5
6
if request.user.is_authenticated:
# Do something for authenticated users.
...
else:
# Do something for anonymous users.
...

4.1 登录用户 🍀

如果你有一个经过身份验证的用户 , 你想把它附带到当前的会话中 , 可以通过login()函数完成

1
2
3
4
5
6
login(request, user, backend=None):    
"""
request:HttpRequest对象
user:User对象
backend:后端
"""

login()使用DjangoSession框架来将用户的ID保存在session

注意 , 匿名会话期间的任何数据集在用户登录后都会保留在会话中

1
2
3
4
5
6
7
8
9
10
11
12
from django.contrib.auth import authenticate, login
def my_view(request):
username = request.POST['username']
password = request.POST['password']
user = authenticate(request, username=username, password=password)
if user is not None:
login(request, user)
# Redirect to a success page.
...
else:
# Return an 'invalid login' error message.
...

4.2 选择验证后端 🍀

用户登录时, 用户的ID和用于身份验证的后端保存在用户的会话中, 这允许相同的身份验证后端在将来的请求中获取用户的详细信息. 保存会话中的认证后端选择如下:

  1. 使用可选的backend参数的值 (如果提供)
  2. 使用user.backend属性的值 (如果存在)
  3. 如果只有一个, 则使用AUTHENTICATION_BACKENDS中的后端
  4. 否则, 触发异常

4.3 登出用户-logout 🍀

要登出一个已经通过django.contrib.auth.login()登入的用户 , 可以在视图中使用django.contrib.auth.logout()

1
2
3
4
5
logout(request):    
"""
request:HttpRequest对象
没有返回值
"""

实例:

1
2
3
4
5
from django.contrib.auth import logout

def logout_view(request):
logout(request)
# Redirect to a success page.

调用logout()时, 当前请求的会话数据将被彻底清理, 这是为了防止另外一个人使用相同的Web浏览器登入并访问前一个用户的会话数据, 如果你想在用户登出之后可以立即访问放入会话中的数据, 则需要在调用django.conruib.auth.logout()之后放入

4.4 限制访问页面 🍀

很多时候,我们要区分 已登录用户未登录用户,只对登录的用户开放一些页面,限制未登录用户的行为。办法有很多,下面是主要的几种:

  1. 原始方式 🍀

限制访问页面的简单原始方法时检查request.user.is_authenticated, 并重定向到登录页面:

1
2
3
4
5
6
7
from django.conf import settings
from django.shortcuts import redirect

def my_view(request):
if not request.user.is_authenticated:
return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
# ...

或者显示一个错误信息:

1
2
3
4
5
6
from django.shortcuts import render

def my_view(request):
if not request.user.is_authenticated:
return render(request, 'myapp/login_error.html')
# ...
  1. 使用装饰器 🍀
    原型:login_required(redirect_field_name='next', login_url=None)[source]

被该装饰器装饰的视图,强制要求用户必须登录后才可以访问。

1
2
3
4
5
from django.contrib.auth.decorators import login_required

@login_required
def my_view(request):
...

出错请注意:
下面。
该装饰器工作机制:

  • 如果用户未登陆,重定向到settings.LOGIN_URL,传递当前绝对路径作为url字符串的参数,例如:/accounts/login/?next=/polls/3/
  • 如果用户已经登录,执行正常的视图

此时,默认的url中使用的参数是next,如果你想使用自定义的参数,请修改login_required()redirect_field_name参数,如下所示:

1
2
3
4
5
from django.contrib.auth.decorators import login_required

@login_required(redirect_field_name='my_redirect_field')
def my_view(request):
...

如果你这么做了,你还需要重新定制登录模板,因为它引用了redirect_field_name变量。

login_required()装饰器还有一个可选的longin_url参数。例如:

1
2
3
4
5
from django.contrib.auth.decorators import login_required

@login_required(login_url='/accounts/login/')
def my_view(request):
...

注意:如果不指定login_url参数,请确保你的settings.LOGIN_URL和登陆视图保持正确的关联。例如:

1
2
from django.contrib.auth import views as auth_views
url(r'^accounts/login/$', auth_views.login),