Python - Django认证系统 Auth模块及Jwt
后端实现登陆认证
Django默认已经提供了认证系统 Auth模块。认证系统包含:
- 用户管理
- 权限[RBAC]
- 用户组[角色组]
- 密码哈希系统
- 用户登录或内容显示的表单和视图
- 一个可插拔的后台系统
Django默认用户的认证机制依赖Session机制,我们在项目中将引入JWT认证机制,将用户的身份凭据存放在Token字符串中,然后对接Django的认证系统,帮助我们来实现:
- 用户的数据模型
- 用户密码的加密与验证
- 用户的权限系统
Django用户模型类
Django认证系统中提供了用户模型类User保存用户的数据,默认的User包含以下常见的基本字段:
字段名 | 字段描述 |
---|---|
username |
必选。150个字符以内。 用户名可能包含字母数字,_ ,@ ,+ . 和- 个字符。 |
first_name |
可选(blank=True )。 少于等于30个字符。 |
last_name |
可选(blank=True )。 少于等于30个字符。 |
email |
可选(blank=True )。 邮箱地址。 |
password |
必选。 密码的哈希加密串。 (Django 不保存原始密码)。 原始密码可以无限长而且可以包含任意字符。 |
groups |
与Group 之间的多对多关系。 |
user_permissions |
与Permission 之间的多对多关系。 |
is_staff |
布尔值。 设置用户是否可以访问Admin 站点。 |
is_active |
布尔值。 指示用户的账号是否激活。 它不是用来控制用户是否能够登录,而是描述一种帐号的使用状态。 |
is_superuser |
是否是超级用户。超级用户具有所有权限。 |
last_login |
用户最后一次登录的时间。 |
date_joined |
账户创建的时间。 当账号创建时,默认设置为当前的date/time。 |
常用方法:
-
set_password
(raw_password)设置用户的密码为给定的原始字符串,并负责密码的。 不会保存
User
对象。当None
为raw_password
时,密码将设置为一个不可用的密码。 -
check_password
(raw_password)如果给定的raw_password是用户的真实密码,则返回True,可以在校验用户密码时使用。
管理器方法:
管理器方法即可以通过User.objects.
进行调用的方法。
-
create_user
(username, email=None, password=None, ***extra_fields*)创建、保存并返回一个
User
对象。 -
create_superuser
(username, email, password, ***extra_fields*)与
create_user()
相同,但是设置is_staff
和is_superuser
为True
。
创建用户模块的子应用
cd luffyapi/apps/
python ../../manage.py startapp users
在settings.py文件中注册子应用。
INSTALLED_APPS = [
...
'users',
]
创建自定义的用户模型类
Django认证系统中提供的用户模型类及方法很方便,我们可以使用这个模型类,但是字段有些无法满足项目需求,如本项目中需要保存用户的手机号,需要给模型类添加额外的字段。
Django提供了django.contrib.auth.models.AbstractUser
用户抽象模型类允许我们继承,扩展字段来使用Django认证系统的用户模型类。
我们可以在apps中创建Django应用users,并在配置文件中注册users应用。
在创建好的应用models.py中定义用户的用户模型类。
from django.db import models
# Create your models here.
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
mobile = models.CharField(max_length=15, unique=True, verbose_name="手机号码")
avatar = models.ImageField(upload_to="avatar", null=True, blank=True, verbose_name="头像")
class Meta:
db_table = 'ly_users'
verbose_name = '用户'
verbose_name_plural = verbose_name
我们自定义的用户模型类还不能直接被Django的认证系统所识别,需要在配置文件中告知Django认证系统使用我们自定义的模型类。
在配置文件中进行设置
AUTH_USER_MODEL = 'users.User'
AUTH_USER_MODEL
参数的设置以点.
来分隔,表示应用名.模型类名
。
注意:Django建议我们对于AUTH_USER_MODEL参数的设置一定要在第一次数据库迁移之前就设置好,否则后续使用可能出现未知错误。
执行数据库迁移
python manage.py makemigrations
python manage.py migrate
执行python manage.py migrate
命令时:系统报错类似如下:
django.db.migrations.exceptions.InconsistentMigrationHistory: Migration reversion.0001_squashed_0004_auto_20160611_1202 is applied before its dependency users.0001_initial on database 'default'.
这是表示有一个叫reversion的子应用使用了原来的废弃的users模型,但是目前数据库已经设置了默认的子应用为users
的模型了,所以产生了冲突。那么这种冲突,我们需要清除原来的迁移文件和数据库中的所有信息就可以解决了。
备份数据库信息
数据库备份 哪一张数据库那一张表(luffy) 库下的表直接点(luffy.)
mysqldump -uroot -p123456 luffy > luffy_2019_12_19.sql
解决步骤:
1. 备份数据库,删除关于用户原来的数据表信息和表结构[如果刚开始开发,则直接清除库中所有数据表即可。
2. 删除apps下面的所有子应用中migrations目录下除了__init__.py以外的所有迁移文件
3. 删除在django.contrib.admin和django.contrib.auth模块里面的migrations迁移文件,除了__init__.py
4. 删除在xadmin和reversion模块中的migrations的迁移文件,除了__init__.py。
5. 执行数据迁移,把备份数据,除了用户以外的全部恢复执行即可。
6. 使用manage.py createsuperuser创建管理员即可
Django REST framework JWT
在用户注册或登录后,我们想记录用户的登录状态,或者为用户创建身份认证的凭证。我们不再使用Session认证机制,而使用Json Web Token认证机制。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT的构成
JWT就一段字符串,由三段信息构成的,将这三段信息文本用.
链接一起就构成了Jwt字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).
header
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
payload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准声明
- 公共声明
- 私有声明
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"history": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"
}
然后将其进行base64加密,得到JWT的第二部分。
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature
JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用.
连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
jwt的优点:
1. 可以很方便的完成分布式站点的单点登录
2. jwt主要保存在客户端,服务器只需要保存秘钥,所有降低服务器的存储眼里
3. jwt的载荷可以存储一些数据发送给客户端,客户端可以查看对应的载荷中的数据,所以可以用户进行双方数据传输
jwt的缺点:
1. jwt实现比传统的session要复杂
2. jwt的信息保存在客户端,所以一经发放,无法回收。在此过程中,不好控制。保存在客户端部的数据容易被用户操作。
关于签发和核验JWT,我们可以使用Django REST framework JWT扩展来完成。
安装配置JWT
安装
pip install djangorestframework-jwt
配置
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
import datetime
JWT_AUTH = {
# 设置客户端的jwt的exp的有效期为1天
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1),
}
- JWT_EXPIRATION_DELTA 指明token的有效期
生成jwt
Django REST framework JWT 扩展的说明文档中提供了手动签发JWT的方法
注意,这段代码将会在注册的时候,我们会使用到。
from rest_framework_jwt.settings import api_settings
jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
在用户注册或登录成功后,在序列化器中返回用户信息以后同时返回token即可。
后端实现登陆认证接口
Django REST framework JWT提供了登录获取token的视图,可以直接使用
在子应用路由urls.py中
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
path(r'login/', obtain_jwt_token),
]
在主路由中,引入当前子应用的路由文件
urlpatterns = [
...
path('users/', include("users.urls")),
# include 的值必须是 模块名.urls 格式,字符串中间只能出现一个圆点
]
接下来,我们可以通过postman来测试下功能
2.3.7 前端保存jwt
我们可以将JWT保存在cookie中,也可以保存在浏览器的本地存储里,我们保存在浏览器本地存储中
浏览器的本地存储提供了sessionStorage 和 localStorage 两种:
- sessionStorage 浏览器关闭即失效
- localStorage 长期有效
使用方法
sessionStorage.变量名 = 变量值 // 保存数据
sessionStorage.setItem("变量名","变量值"); // 保存数据
sessionStorage.变量名 // 读取数据
sessionStorage.getItem("变量名") // 读取数据
sessionStorage.removeItem("变量名") // 删除一个数据
sessionStorage.clear() // 清除所有sessionStorage保存的数据
localStorage.变量名 = 变量值 // 保存数据
localStorage.setItem("变量名","变量值"); // 保存数据
localStorage.变量名 // 读取数据
localStorage.getItem("变量名") // 读取数据
localStorage.removeItem("变量名") // 删除一个数据
localStorage.clear() // 清除所有sessionStorage保存的数据
2.3.9 多条件登录
JWT扩展的登录视图,在收到用户名与密码时,也是调用Django的认证系统中提供的authenticate()来检查用户名与密码是否正确。
我们可以通过修改Django认证系统的认证后端(主要是authenticate方法)来支持登录账号既可以是用户名也可以是手机号。
修改Django认证系统的认证后端需要继承django.contrib.auth.backends.ModelBackend,并重写authenticate方法。
authenticate(self, request, username=None, password=None, **kwargs)
方法的参数说明:
- request 本次认证的请求对象
- username 本次认证提供的用户账号
- password 本次认证提供的密码
我们想要让用户既可以以用户名登录,也可以以手机号登录,那么对于authenticate方法而言,username参数即表示用户名或者手机号。
重写authenticate方法的思路:
- 根据username参数查找用户User对象,username参数可能是用户名,也可能是手机号
- 若查找到User对象,调用User对象的check_password方法检查密码是否正确
在users/utils.py中编写:
# jwt自定义返回函数
def jwt_response_payload_handler(token, user=None, request=None):
"""
自定义jwt认证成功返回数据
:parameter token 本次响应给客户端的jwt字符串
:parameter user 本次查询出来的用户模型对象
:parameter request 本次客户端的请求对象
"""
return {
'token': token,
'id': user.id,
'username': user.username
}
"""自定义用户多条件认证"""
# pycharm自动导包 快捷键: 光标移动到对应的类名中,使用 Alt + Enter
from django.contrib.auth.backends import ModelBackend
from django.db.models import Q
def get_account_by_user(username):
"""根据username获取用户信息"""
from .models import User
try:
user = User.objects.get(Q(username=username) | Q(mobile=username))
except User.DoesNotExist:
user = None
return user
class UsernameMobileAuthBackend(ModelBackend):
"""实现用户多条件登录"""
def authenticate(self, request, username=None, password=None, **kwargs):
user = get_account_by_user(username)
if user is not None and user.check_password(password) and user.is_active:
return user
在配置文件settings/dev.py中告知Django使用我们自定义的认证后端
AUTHENTICATION_BACKENDS = [
'users.utils.UsernameMobileAuthBackend',
]