1.用户登录接口设计
接收用户通过POST方法提交的登录信息,提交的数据是JSON格式数据。
从user表中email找出匹配的一条记录,验证密码是否正确。验证通过说明是合法用户登录,显示欢迎页面。 验证失败返回错误状态码,例如4xx整个过程都采用A JAX异步过程,用户提交JSON数据,服务端获取数据后处理,返回JSON。
路由配置:
from django.conf.urls import url
from .views import reg, login
urlpatterns = [
url(r'^$', reg),
url(r'^login$', login),
]
登录代码:
@require_POST # 只允许post请求的方式登录
def login(request: HttpRequest):
try:
payload = simplejson.loads(request.body)
print(payload, '+++++++++++')
email = payload['email']
password = payload['password'].encode()
print(password, '%%%%%%%%%%', type(password))
user = User.objects.get(email=email) # 只有一条
print(user.password, '~~~~~~~~~', type(password))
if bcrypt.checkpw(password, user.password.encode()):
# # 验证成功
# token = gen_token(user.id)
# res = JsonResponse({
# # 'use_id': user.id,
# # 'email': email,
# # 'name': user.name,
# 'user': json_ify(user, exclude=('password', )),
# 'token': token
# })
# # res.set_cookie('jwt', token)
# return res
session: SessionStore = request.session
print(type(session), session)
print(session.keys())
session.set_expiry(300) # 会话过期,单位秒
session['user_id'] = user.id
# 对于频繁需要使用的数据,使用字符串拼出来,省得还要从数据库中查询
session['user_info'] = "{} {} {}".format(user.id, user.name, user.email)
res = JsonResponse({
'user': json_ify(user, exclude=['password']),
'user_info': session['user_info']
})
return res
else:
return JsonResponse({'error': "用户名或密码错误"}, status=400)
except Exception as e:
logging.error(e)
# 失败返回错误信息和400,所有其他错误一律用户名密码错误;有时候错误信息不宜太详细
return JsonResponse({'error': "用户名或密码错误"}, status=400)
认证接口:
如何获取浏览器提交的token信息?
- 使用Header中的Authorization,通过这个header增加token信息。 通过header发送数据,方法可以是Post、Get
- 自定义header:在Http请求头中使用X-JWT字段来发送token
认证流程:
基本上所有的业务都有需要认证用户的信息。在这里比较时间戳,如果过期,就直接抛未认证成功401,客户端收到后就该直接跳转到登录页。 如果没有提交user id,就直接重新登录。如果用户查到了,填充user对象。
request -> 时间戳比较 -> user id 比较 -> 向后执行
Django的认证:
django.contrib.auth中提供了许多方法,这里主要介绍其中的三个:
- authenticate(**credentials)提供了用户认证,即验证用户名以及密码是否正确 user = authentica(username='someone',password='somepassword')
- login(HttpRequest, user, backend=None),该函数接受一个HttpRequest对象,以及一个认证了的User对象 。此函数使用django的session框架给某个已认证的用户附加上session id等信息。
- logout(request) :注销用户 ,该函数接受一个HttpRequest对象,无返回值。 当调用该函数时,当前请求的session信息会全部清除 该用户即使没有登录,使用该函数也不会报错 还提供了一个装饰器来判断是否登录django.contrib.auth.decorators.login_required 。本项目使用了无session机制,且用户信息自己建表管理,所以,认证需要自己实现。(最后又补充了session机制如何实现)
中间件技术Middleware:
官方定义,在Django的request和response处理过程中,由框架提供的hook钩子中间件技术在1.10后发生了改变,我们当前使用1.11版本,可以使用新的方式定义。
from django.http import HttpRequest, HttpResponse
class SimpleMiddleware1:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
print(1, '-' * 30)
print(isinstance(request, HttpRequest))
print(request.GET)
print(request.POST)
print(request.body)
# 之前相当于老版本的process_request
# return HttpResponse(b'', status=404)
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
print(101, '-' * 30)
return response
def process_view(self, request, view_func, view_args, view_kwargs):
print(request)
print(2, '-' * 30)
print(view_func.__name__, view_args, view_kwargs)
# 观察view_func名字,说明在process_request之后,process_view之前已经做好了路径映射
return None # 继续执行其它的process_view或view
# return HttpResponse('111', status=201)
class SimpleMiddleware2:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
print(3, '-' * 30)
# return HttpResponse(b'', status=404)
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called.
print(102, '-' * 30)
return response
def process_view(self, request, view_func, view_args, view_kwargs):
print(request)
print(4, '-' * 30)
print(view_func.__name__, view_args, view_kwargs)
# return None # 继续执行其它的process_view或view
return HttpResponse('2222', status=201)
结论:
Django中间件使用的洋葱式,但有特殊的地方
- 新版中间件先在 __call__ 中get_response(request)之前代码(相当于老版本中的process_request)
- settings中的顺序先后执行所有中间件的get_response(request)之前代码
- 全部执行完解析路径映射得到view_func
- settings中的顺序先后执行process_view部分:return None 继续向后执行;return HttpResponse() 就不在执行其它函数的preview函数了,此函数返回值作为浏览器端的响应
- 执行view函数,前提是前面的所有中间件process_view都返回None
- 逆序执行所有中间件的get_response(request)之后代码
- 特别注意,如果get_response(request)之前代码中return HttpResponse(),将从当前中间件立即返回给浏览器端,从洋葱中依次反弹。
自定义中间件:
class BlogAuthMiddleware(object):
"""自定义认证中间件"""
def __init__(self, get_response):
self.get_response = get_response
# 初始化执行一次
def __call__(self, request):
# 视图函数之前执行
# 认证
print(type(request), '+++++++++++++++++')
print(request.GET)
print(request.POST)
print(request.body) # json数据
print('-'*30)
response = self.get_response(request)
# 视图函数之后执行
return response
# 要在setting的MIDDLEWARE中注册
中间件拦截所有视图函数,但只有一部分请求需要提供认证,所以,考虑其他方法。如果绝大多数都需要拦截,个别例外,采用中间件较为合适。 中间件有很多用途,适合拦截所有请求和响应。例如浏览器端的IP是否禁用、UserAgent分析、异常响应的统一处理。
装饰器
在需要认证的view函数上增强认证功能,写一个装饰器函数。谁需要认证,就在这个view函数上应用这个装饰器。定义常量 可以定义在当前模块中,也可以定义在settings.py中。
def authenticate(view_func):
"""此装饰器的作用是用户认证和验证过期"""
def wrapper(*args):
*s, request = args # args为元组,如果元组只有一个元素,request会先拿;如果元组有多个元素,request会拿最后一个
session: SessionStore = request.session
print(session.items(), '~~~~~~~~~~~')
payload = session # 这么做只是不想修改下面的代码了
# print(s, '&&&&&&&&&&&')
# print(request, '$$$$$$$$$$$$$$$$$$$')
# <WSGIRequest: GET '/users/test'> $$$$$$$$$$$$$$$$$$$
# 认证,越早越好
# print('-----------')
# jwt_header = request.META.get(AUTH_HEADER, '')
# print(jwt_header, '*********') # 生产环境中,所有测试代码用logging.debug,所有输出使用logging.info
#
# if not jwt_header:
# print('00000000000000000000')
# return HttpResponse(status=401)
# print(jwt_header, '*********')
#
# # 解码
# try:
# payload = jwt.decode(jwt_header, settings.SECRET_KEY, algorithms=['HS256'],
# options={'verify_signature': True})
# print(payload)
# except Exception as e: # 解码同时验证过期,有任何异常,都不能通过认证
# logging.error(e)
# print('1111111111111111')
# return HttpResponse(status=401)
print('-' * 30)
# 查询数据库,虽然前面的验证通过,但是此用户可能已经被禁用,一定要查
user_id = payload.get('user_id', 0)
if user_id == 0:
print('222222222222222222222')
return HttpResponse(status=401)
try:
user = User.objects.get(pk=user_id)
request.user = user # 动态增加属性
except Exception as e:
logging.error(e)
return HttpResponse(status=401)
res = view_func(*args) # 注意这里的*args是参数解构
return res
return wrapper
JWT过期问题
- 在decode的时候,默认开启过期验证,如果过期,则抛出异常
- 需要在payload中增加claim exp,也就是exp的键值对,记录过期的时间点
- exp要求是一个整数int的时间戳,或时间
- exp键值对存在,才会进行过期校验
import jwt
import datetime
import threading
event = threading.Event()
SECRET_KEY = 'k*)_*v2%04niq0#5xc6fkl@p0pqjn2=hrm^yw3vdxloom2v7+2'
payload = {
'user': 'sun',
'school': 'mag',
'exp': datetime.datetime.now().timestamp() + 3 # 3秒过期
}
enc = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
print(enc)
try:
while not event.wait(1):
x = jwt.decode(enc, SECRET_KEY, algorithms=['HS256'])
print(x)
except Exception as e:
print(type(e), e, '~~~~~~~~~~')
# <class 'jwt.exceptions.ExpiredSignatureError'> Signature has expired ~~~~~~~~~~
登录接口的代码:
@require_POST # 只允许post请求的方式登录
def login(request: HttpRequest):
try:
payload = simplejson.loads(request.body)
print(payload, '+++++++++++')
email = payload['email']
password = payload['password'].encode()
print(password, '%%%%%%%%%%', type(password))
user = User.objects.get(email=email) # 只有一条
print(user.password, '~~~~~~~~~', type(password))
if bcrypt.checkpw(password, user.password.encode()):
# # 验证成功
# token = gen_token(user.id)
# res = JsonResponse({
# # 'use_id': user.id,
# # 'email': email,
# # 'name': user.name,
# 'user': json_ify(user, exclude=('password', )),
# 'token': token
# })
# # res.set_cookie('jwt', token)
# return res
session: SessionStore = request.session
print(type(session), session)
print(session.keys())
session.set_expiry(300) # 会话过期,单位秒
session['user_id'] = user.id
# 对于频繁需要使用的数据,使用字符串拼出来,省得还要从数据库中查询
session['user_info'] = "{} {} {}".format(user.id, user.name, user.email)
res = JsonResponse({
'user': json_ify(user, exclude=['password']),
'user_info': session['user_info']
})
return res
else:
return JsonResponse({'error': "用户名或密码错误"}, status=400)
except Exception as e:
logging.error(e)
# 失败返回错误信息和400,所有其他错误一律用户名密码错误;有时候错误信息不宜太详细
return JsonResponse({'error': "用户名或密码错误"}, status=400)