博文之用户登录接口设计

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发送数据,方法可以是PostGet
  • 自定义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对象 。此函数使用djangosession框架给某个已认证的用户附加上session id等信息。
  • logout(request) :注销用户 ,该函数接受一个HttpRequest对象,无返回值。 当调用该函数时,当前请求的session信息会全部清除 该用户即使没有登录,使用该函数也不会报错 还提供了一个装饰器来判断是否登录django.contrib.auth.decorators.login_required 。本项目使用了无session机制,且用户信息自己建表管理,所以,认证需要自己实现。(最后又补充了session机制如何实现)

中间件技术Middleware:

官方定义,在Djangorequestresponse处理过程中,由框架提供的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)

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值