Django在做后台系统过程中,我们通常都会为view函数添加@login_required 装饰器,这个装饰器的主要作用就是在用户访问这个方法时,检查用户是否已经成功登陆,如果没有则重定向到登陆页面
登陆页面地址通过settings.LOGIN_URL来获取的,默认为/accounts/login/
from django.contrib.auth.decorators import login_required
@login_required(login_url='/login/')
def home(request):
return JsonResponse({data': 'ops'})
Middleware
通常对于一个后台系统来说,每一个页面都需要登陆才能访问,这样我们就需要给每一个view方法添加@login_required装饰器(咱之前写flask的时候就是这么搞的,现在觉得很蠢),那么有没有简单优雅一点的方式呢 可以通过Middleware中间件来实现中间件位于用户请求和程序响应之间,当用户访问一个url之后并不是直接交给view去处理,而是先经过中间件处理,然后再到view,路线是这样的
uer-->middleware-->view,所以针对全局所有view的操作就非常适合放在中间件里去处理
Django的中间件都定义在setttings的MIDDLEWARE的配置下
但是login_required这种是基于seesion的一种认证方式,咱不用这个,咱们用jwt
传统的登录鉴权和基于token的鉴权有什么区别
以Django的账号密码登录为例来说明传统的验证鉴权是怎么工作的,当我们登录页面输入账号密码提交表单后,会发送请求给服务器,服务器对发送过来的账号密码进行验证鉴权,验证鉴权通过后,把用户信息记录在服务端(django_session表中),同时返回给浏览器一个sessionid用来唯一标识这个用户,浏览器将sessionid保存在cookie中,之后浏览器的每次请求都一并将sessionid发送给服务器,服务器根据sessionid与记录的信息来做对比以验证身份
Token的鉴权方式就清晰很多了,客户端用自己的账号密码进行登录,服务端验证鉴权,验证鉴权通过生成Token返回给客户端,之后客户端每次请求都将Token放在header里一并发送,服务端收到请求时校验Token以确定访问者身份
session的主要目的是给无状态的http状态添加状态保持,通常在浏览器作为客户端的情况下比较通用,而Token的主要目的是为了鉴权,同时又不需要考虑CSRF防护以及跨域的问题,所以更多的用在专门给第三方提供的API的情况下,客户端请求无论是浏览器发起还是其他的程序发起都能很好的支持,所以目前基于Token的鉴权机制几乎已经成了前后端分离架构或者对外API访问的鉴权标准,得到广泛使用
关于JWT
网上关于jwt的介绍有很多,这里不细说,可以参考我往期的文章:浅谈JWT和实战_彭阳的技术博客_51CTO博客
只讲下django如何利用JWT实现自定用户认证,搜了几乎所有的文章都是说JWT如何结合DRF使用的,如果你的项目没有用到DRF框架,也不想仅仅为了鉴权API就引入庞大复杂的DRF框架,那么可以接着往下看
演示
pip install PyJWT==2.3.0
settings.py
INSTALLED_APPS = [
'account'
]
MIDDLEWARE = [
# "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.security.SecurityMiddleware',
'django.middleware.common.CommonMiddleware',
# 用户认证的拦截器
'common.middleware.AuthenticationMiddleware',
]
# 自定义用户模型
AUTH_USER_MODEL = 'account.User'
创建模型
account/models.py
from django.contrib.auth.hashers import make_password, check_password
from django.db import models
from django.utils.translation import gettext_lazy as _
from channels.db import database_sync_to_async
from itertools import chain
from typing import List, Dict, Any
class AsyncManage(models.Manager):
async def filter_async(self, *args, **kwargs):
return await database_sync_to_async(list)(super().filter(*args, **kwargs))
@database_sync_to_async
def get_async(self, *args, **kwargs):
return super().get(*args, **kwargs)
@database_sync_to_async
def create_async(self, **kwargs):
return super(AsyncManage, self).create(**kwargs)
@database_sync_to_async
def update_async(self, **kwargs):
return super(AsyncManage, self).update(**kwargs)
@database_sync_to_async
def first_async(self, *args, **kwargs):
return super().filter(*args, **kwargs).first()
class CommonModelMixin(models.Model):
objects = AsyncManage()
__slots__ = ()
class Meta:
abstract = True
class CustomMeta:
"""
自定义变量元类
Meta元类不允许添加新的变量,所以有新的变量可以再这里添加
"""
# to_json()方法中默认排除的字段名集合
default_excludes = ['pk']
def to_json(self, excludes: List[str] = None, includes: List[str] = None) -> Dict[str, Any]:
"""
Serialize the model instance to a Python dictionary.
:param excludes: list of field names to exclude from the serialized data.
:param includes: list of field names to include in the serialized data.
:return: a dictionary representation of the model instance.
"""
opts = self._meta
# 排除字段集合
excludes = set(excludes) if excludes else set()
# 包含字段集合
includes = set(includes) if includes else set()
# 添加默认排除的字段
excludes.update(self.CustomMeta.default_excludes)
fields = [
field.attname
for field in chain(opts.private_fields, opts.concrete_fields)
if field.attname not in excludes and (not includes or field.attname in includes)
]
fields.extend(
field
for field in opts._property_names
if (not excludes or field not in excludes) and (not includes or field in includes)
)
return {field: getattr(self, field) for field in fields}
class User(CommonModelMixin):
username = models.CharField(
max_length=100,
verbose_name=_('用户名'),
help_text=_('登录时所需的用户名')
)
nickname = models.CharField(
max_length=100,
verbose_name=_('昵称'),
help_text=_('用户使用的昵称')
)
password_hash = models.CharField(
max_length=100,
verbose_name=_('密码'),
help_text=_('非明文密码,经过哈希算法加密过的密码')
)
phone = models.CharField(
max_length=11,
null=True,
verbose_name=_('电话号码'),
help_text=_('用户有电话号码时,会需要填验证码')
)
type = models.CharField(
max_length=20,
default='default',
verbose_name=_('用户类型'),
help_text=_('1:正常用户 2:LDAP用户.LDAP认证方式未开放')
)
is_supper = models.BooleanField(
default=False,
verbose_name=_('管理员'),
help_text=_('账号是否为管理员')
)
is_active = models.BooleanField(
default=True,
verbose_name=_('是否激活'),
help_text=_('用户是否激活')
)
access_token = models.CharField(
max_length=32,
verbose_name=_('私人令牌'),
help_text=_('前端发送请求时使用该字段进行校验')
)
token_expired = models.IntegerField(
null=True,
verbose_name=_("令牌过期时间"),
help_text=_("未使用")
)
last_login = models.CharField(max_length=20, verbose_name="最近登录时间")
last_ip = models.CharField(max_length=50, verbose_name="最近登录ip")
ssh_config = models.TextField(null=True, default="{}")
game_permissions = models.TextField(null=True, default="[]")
USERNAME_FIELD = "username"
def __repr__(self):
return self.nickname
@staticmethod
def make_password(plain_password: str) -> str:
return make_password(plain_password, hasher='pbkdf2_sha256')
def verify_password(self, plain_password: str) -> bool:
return check_password(plain_password, self.password_hash)
class Meta:
db_table = 'users'
ordering = ('-id',)
用户认证中间件
common/middleware.py
import logging
import time
import jwt
from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.deprecation import MiddlewareMixin
from jwt import InvalidSignatureError, InvalidAlgorithmError, DecodeError, ExpiredSignatureError
from django.http import JsonResponse
logger = logging.getLogger(__name__)
class AuthenticationMiddleware(MiddlewareMixin):
"""
自定义登录验证类
"""
def process_request(self, request):
# 当path为不需要登录验证时跳过验证
if request.path in settings.AUTHENTICATION_EXCLUDES:
return None
# 获取头部的密钥
token = request.headers.get('token') or request.GET.get('token')
# 获取真实ip
x_real_ip = request.headers.get('x-real-ip', '')
if not token:
response = JsonResponse({"code": 401, "messages": "当前账号未登录", "data": None})
response.status_code = 401
return response
# 验证JWT密钥
try:
data = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256', ])
except (InvalidSignatureError, InvalidAlgorithmError, DecodeError):
response = JsonResponse({"code": 401, "messages": "当前账号未登录", "data": None})
response.status_code = 401
return response
except ExpiredSignatureError:
# sign过期
response = JsonResponse({"code": 401, "messages": "账号登录信息已过期,请重新登录!", "data": None})
response.status_code = 401
return response
# 验证密钥解密信息
now = int(time.time())
exp_time = data.get('exp', None)
username = data.get('username', None)
if not exp_time or exp_time < now:
response = JsonResponse({"code": 401, "messages": "账号登录信息已过期,请重新登录!", "data": None})
response.status_code = 401
return response
if not username:
response = JsonResponse({"code": 401, "messages": "当前账号未登录", "data": None})
response.status_code = 401
return response
user = get_user_model().objects.filter(username=username).first()
if not user:
response = JsonResponse({"code": 401, "messages": "当前账号未登录", "data": None})
response.status_code = 401
return response
if x_real_ip == user.last_ip and user.is_active:
request.user = user
return None
response = JsonResponse({"code": 401, "messages": "当前账号未登录", "data": None})
response.status_code = 401
return response
接下来就是迁移数据库
我们还需要一个生成用户Token的方法,通过给User model添加一个token的静态方法来处理
account/models.py
import jwt
import time
from django.conf import settings
from django.contrib.auth.hashers import make_password, check_password
from django.db import models
from django.utils.translation import gettext_lazy as _
from channels.db import database_sync_to_async
from itertools import chain
from typing import List, Dict, Any
class User(CommonModelMixin):
username = models.CharField(
max_length=100,
verbose_name=_('用户名'),
help_text=_('登录时所需的用户名')
)
nickname = models.CharField(
max_length=100,
verbose_name=_('昵称'),
help_text=_('用户使用的昵称')
)
password_hash = models.CharField(
max_length=100,
verbose_name=_('密码'),
help_text=_('非明文密码,经过哈希算法加密过的密码')
)
phone = models.CharField(
max_length=11,
null=True,
verbose_name=_('电话号码'),
help_text=_('用户有电话号码时,会需要填验证码')
)
type = models.CharField(
max_length=20,
default='default',
verbose_name=_('用户类型'),
help_text=_('1:正常用户 2:LDAP用户.LDAP认证方式未开放')
)
is_supper = models.BooleanField(
default=False,
verbose_name=_('管理员'),
help_text=_('账号是否为管理员')
)
is_active = models.BooleanField(
default=True,
verbose_name=_('是否激活'),
help_text=_('用户是否激活')
)
access_token = models.CharField(
max_length=32,
verbose_name=_('私人令牌'),
help_text=_('前端发送请求时使用该字段进行校验')
)
token_expired = models.IntegerField(
null=True,
verbose_name=_("令牌过期时间"),
help_text=_("未使用")
)
last_login = models.CharField(max_length=20, verbose_name="最近登录时间")
last_ip = models.CharField(max_length=50, verbose_name="最近登录ip")
ssh_config = models.TextField(null=True, default="{}")
game_permissions = models.TextField(null=True, default="[]")
USERNAME_FIELD = "username"
def __repr__(self):
return self.nickname
@staticmethod
def make_password(plain_password: str) -> str:
return make_password(plain_password, hasher='pbkdf2_sha256')
def verify_password(self, plain_password: str) -> bool:
return check_password(plain_password, self.password_hash)
@property
def token(self):
return self._generate_jwt_token()
def _generate_jwt_token(self):
payload = {'username': self.username, 'exp': int(time.time()) + 86400}
token = jwt.encode(payload, settings.SECRET_KEY, algorithm='HS256')
return token
class Meta:
db_table = 'users'
ordering = ('-id',)
添加自定义创建用户命令
account/management/commands/user.py
from django.core.cache import cache
from django.core.management.base import BaseCommand
from account.models import User
class Command(BaseCommand):
help = '账户管理'
def add_arguments(self, parser):
parser.add_argument('action', type=str, help='执行动作')
parser.add_argument('-u', required=False, help='账户名称')
parser.add_argument('-p', required=False, help='账户密码')
parser.add_argument('-n', required=False, help='账户昵称')
parser.add_argument('-s', default=False, action='store_true', help='是否是超级用户(默认否)')
def echo_success(self, msg):
self.stdout.write(self.style.SUCCESS(msg))
def echo_error(self, msg):
self.stderr.write(self.style.ERROR(msg))
def print_help(self, *args):
message = '''
账户管理命令用法:
user add 创建账户,例如:user add -u admin -p 123 -n 管理员 -s
user reset 重置账户密码,例如:user reset -u admin -p 123
user enable 启用被禁用的账户,例如:user enable -u admin
'''
self.stdout.write(message)
def handle(self, *args, **options):
action = options['action']
if action == 'add':
if not all((options['u'], options['p'], options['n'])):
self.echo_error('缺少参数')
self.print_help()
elif User.objects.filter(username=options['u']).exists():
self.echo_error(f'已存在登录名为【{options["u"]}】的用户')
else:
User.objects.create(
username=options['u'],
nickname=options['n'],
password_hash=User.make_password(options['p']),
is_supper=options['s'],
)
self.echo_success('创建用户成功')
elif action == 'enable':
if not options['u']:
self.echo_error('缺少参数')
self.print_help()
user = User.objects.filter(username=options['u']).first()
if not user:
return self.echo_error(f'未找到登录名为【{options["u"]}】的账户')
user.is_active = True
user.save()
cache.delete(user.username)
self.echo_success('账户已启用')
elif action == 'reset':
if not all((options['u'], options['p'])):
self.echo_error('缺少参数')
self.print_help()
user = User.objects.filter(username=options['u']).first()
if not user:
return self.echo_error(f'未找到登录名为【{options["u"]}】的账户')
user.password_hash = User.make_password(options['p'])
user.save()
self.echo_success('账户密码已重置')
else:
self.echo_error('未识别的操作')
self.print_help()
可以直接通过用户对象来生成Token
请求测试
拿到Token请求测试