9- vue django restful framework 打造生鲜超市 -用户登录和手机注册(上)

Vue+Django REST framework实战

搭建一个前后端分离的生鲜超市网站 Django rtf 完成 手机注册和用户登录(上)

drf的token登录和原理

用户的下单,个人中心等功能都是需要用户登录之后才能进行的。

用户的登录在前后端分离的开发中和我们之前基于模板template进行开发的是有一定区别的。

用户登录的session和cookie等在模板中是有用的。

我们drf的页面中右上角的log in为什么可以实现登录,是因为我们配置了

    # 调试登录
    path('api-auth/', include('rest_framework.urls'))

它里面有login 和 loginout。里面的login调用了loginview,这个view是django自带的view

    @method_decorator(csrf_protect)

会验证我们的csrf。我们打开登录的界面f12可以看到一个隐藏的csrf

mark

使用csrf进行表单的验证。防止跨站攻击。

前后端分离的系统,不需要做crsf的验证。app和网站服务端本来就是跨站了。

api guide 中的auth

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    )
}

所有drf中的全局变量设置都是在上图中的位置设置。

SessionAuthentication实际上是使用了django里面的sessionMiddleware

    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',

每当一个request进来的时候,这两个meddleware就会将我们的cookie里的session_id转换成request.user

rest_framework/authentication.py源码解读:

class SessionAuthentication(BaseAuthentication):
    """
    Use Django's session framework for authentication.
    """

    def authenticate(self, request):
        """
        Returns a `User` if the request session currently has a logged in user.
        Otherwise returns `None`.
        """

        # Get the session-based user from the underlying HttpRequest object
        user = getattr(request._request, 'user', None)

        # Unauthenticated, CSRF validation not required
        if not user or not user.is_active:
            return None

        self.enforce_csrf(request)

        # CSRF passed with authenticated user
        return (user, None)

mark

其中的authenticate就是从我们的request中取出user。实际上还依赖的是我们django自带的session机制。

即使我们不填写,也会默认为这两个默认的。

mark

drf为我们提供了三种不同的auth

sessionauth在浏览器中比较常见,它会自动设置cookie,并将session等带到我们的服务器。

前后端分离的系统中这种sessionauth比较少见。

TokenAuth

INSTALLED_APPS = (
    ...
    'rest_framework.authtoken'
)

tokenauth会为我们建一张表的,凡是会有表产生的。都必须放到install app中 否则会造成makemigrate报错。

makemigrations
migrate

mark

mark

这里的user_id实际是一个外键指向user。

步骤2: 为你的用户创建一个token

一个用户在创建的时候就应该拥有一个token,但是现在我们还并没有拥有token。

测试:

createsuperuser

并没有为我们创建token

官方代码:

from rest_framework.authtoken.models import Token

token = Token.objects.create(user=...)
print token.key

实际在用户注册的时候我们就可以调用上面的代码。让它为我们生成一个token

我们必须为我们的token生成配置相关的url

官方文档代码:

from rest_framework.authtoken import views
urlpatterns += [
    url(r'^api-token-auth/', views.obtain_auth_token)
]

django2.0代码

from rest_framework.authtoken import views
urlpatterns += [
    path('api-token-auth/', views.obtain_auth_token)
]

我们获取token只需要向这个接口post数据。

使用postman

mark

mark

mark

可以看到我们的数据库中已经有了一个token.外键指向的用户是1

  • 如何使用我们拿到的token

对于客户端进行身份验证,令牌密钥应该包含在AuthorizationHTTP头中。关键字应以字符串文字“Token”为前缀,用空格分隔两个字符串。例如:

Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b

key 与 value

注意:如果您想在标题中使用不同的关键字,比如Bearer简单地创建子类TokenAuthentication并设置keyword类变量。

如果成功通过身份验证,则TokenAuthentication提供以下凭据。

request.user将是一个Django User实例。 request.auth将是一个rest_framework.authtoken.models.Token实例。

mark

拿着数据库里刚刚发给我们的auth token, 请求goods接口。

在goodsviewset继承的ListModelMixin中打上断点。

我的postman不支持与django调试的配合。因此我采用print大法

mark

可以看到此时确实无法取到我们想要的用户。

默认我们的auth class中只有基础认证和session认证,这时我们想要使用token认证就要添加token认证

        'rest_framework.authentication.TokenAuthentication'

这些认证类和middlewire一样会优先调用类里面的auth方法

这种方法会将user放入我们的request当中去。

这三种会逐一认证,只要通过就会放入request.user中

mark

可以看到成功的打印出了当前用户。

request.data中存放着用户从前端post过来的数据。request只会将post和field中的东西放入我们的data中来。

head中设置的token会被放进request.auth中

想要知道用户传递过来的auth时只需要request.auth

django源码中的sessionMiddleware中有一个process_request方法 和一个process_response方法。

django/contrib/sessions/middleware.py:

 def process_request(self, request):
        session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
        request.session = self.SessionStore(session_key)

    def process_response(self, request, response):

setting中注册的middleware会将用户request的数据经过这些middlware中有process_request方法和process_response方法注册进入。

当用户的request进入view之前会将这些process_request通通调用一遍

如果用户post过来的是session_id那么我们的session middleware就会起作用。

会执行上面代码从request.cookies中获取到setting中设置的SESSION_COOKIE_NAME 这里仅仅是完成了把session放入request。

这里的middleware和scrapy中的middlware是一样的、

http://www.projectsedu.com/2016/10/17/django%E4%BB%8E%E8%AF%B7%E6%B1%82%E5%88%B0%E8%BF%94%E5%9B%9E%E9%83%BD%E7%BB%8F%E5%8E%86%E4%BA%86%E4%BB%80%E4%B9%88/

mark

  1. 浏览器发起一个http 的请求。http请求会通过python的handler序列化出一个httprequest对象
  2. 经过request的middleware(这些middlware就是我们在django的setting中配置的middlware)
  3. 只有重载了process_request才叫做,request middlware

这里面我们不一定只加工传过来的request。也可以直接返回。这种情况会直接返回,不会进入view中。

全局拦截器,拦截用户的浏览器是否是Chrome浏览器

process_request中直接return response

只要重载了process_response的middlware都会被调用,调用的顺序。

mark

request的时候顺序是从上往下的,而response的时候是从下至上。

  • 如果没有被提前返回。则会去调用url conf里面的配置
  • 这个urlconf找到对应的之后,就会被我们的view middlware进行执行。
  • viewmiddleware如果没有提前返回。就会调用我们自己view

AuthenticationMiddleware的process_request会判断我们的request中是否有session

就会设置我们的user

assert hasattr(request, 'session'),
request.user = SimpleLazyObject(lambda: get_user(request))

调用get_user方法,而get_user方法又会调用我们的auth.get_user

def get_user(request):
    if not hasattr(request, '_cached_user'):
        request._cached_user = auth.get_user(request)
    return request._cached_user

auth.get_user就会通过session获取到user

        user_id = _get_user_session_key(request)
        backend_path = request.session[BACKEND_SESSION_KEY]

session里实际上有很多东西。通过session获取到我们的backend

user = backend.get_user(user_id)

通过backend的get_user获取到我们的user对象。

        if backend_path in settings.AUTHENTICATION_BACKENDS:

这里是对于我们的授权backend的判断,说明我们可以在setting中设置我们自己的

drf在setting中设置的auth和我们前面django自带的middlware是不一样的。 django自带的是会对每一个request做一些处理。而drf的auth是来验证用户登录的。

rest_framework/authentication.py中的TokenAuthentication

def get_authorization_header(request):
    """
    Return request's 'Authorization:' header, as a bytestring.

    Hide some test client ickyness where the header can be unicode.
    """
    auth = request.META.get('HTTP_AUTHORIZATION', b'')
    if isinstance(auth, text_type):
        # Work around django test client oddness
        auth = auth.encode(HTTP_HEADER_ENCODING)
    return auth

会调用get_authorization_header获取到request中的HTTP_AUTHORIZATION取到的值就是后面的value

mark

如果你后面的auth长度等于1或大于2都是不合法的,会抛出异常401 这是drf为我们处理的。

如果等于2

也就是此时的值为Token 76c4705854086725ddfa1e83cbf5c2f0413b9458

他就会通过

auth = get_authorization_header(request).split()

通过空格进行分割。形成列表。

token = auth[1].decode()

取到后面的值。也就是我们的token值

前面的Token的校验是通过转化为小写。和小写的keyword进行校验的。

 if not auth or auth[0].lower() != self.keyword.lower().encode():

mark

可以经验证我们使用小写的也不会影响。而且这个关键字可以通过比如Bearer简单地创建子类TokenAuthentication并设置keyword类变量。覆盖该变量进行实现。

取到了token之后重点就在:

return self.authenticate_credentials(token)
    def authenticate_credentials(self, key):
        model = self.get_model()
        try:
            token = model.objects.select_related('user').get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed(_('Invalid token.'))

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

        return (token.user, token)

这个方法里会调用我们的model,

    def authenticate_credentials(self, key):
        model = self.get_model()

get_model方法会获取到我们的Token 这个model

    def get_model(self):
        if self.model is not None:
            return self.model
        from rest_framework.authtoken.models import Token
        return Token

Token这个model就是我们对应的数据库中的那张表

class Token(models.Model):
    """
    The default authorization token model.
    """
    key = models.CharField(_("Key"), max_length=40, primary_key=True)
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL, related_name='auth_token',
        on_delete=models.CASCADE, verbose_name=_("User")
    )
    created = models.DateTimeField(_("Created"), auto_now_add=True)

这里我们就可以重写这个model及Token auth为我们定制出token的过期时间等。

    def authenticate_credentials(self, key):
        model = self.get_model()
        try:
            token = model.objects.select_related('user').get(key=key)
        except model.DoesNotExist:
            raise exceptions.AuthenticationFailed(_('Invalid token.'))

        if not token.user.is_active:
            raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))

        return (token.user, token)

这里是在通过我们的key(token值),取Token model中的关联user中获取token.user 获取token对象。

这里有两种异常情况:

不存在这个对应的token,或者这个token找到了但是对应的用户未激活。

那么问题来了,表里的token值是哪里来的呢?

    # token授权登录,获取token需要向该地址post数据
    path('api-token-auth/', views.obtain_auth_token)
obtain_auth_token = ObtainAuthToken.as_view()

ObtainAuthTokenview的post方法中的get_or_create会get。没有就create

    def post(self, request, *args, **kwargs):
        serializer = self.serializer_class(data=request.data,
                                           context={'request': request})
        serializer.is_valid(raise_exception=True)
        user = serializer.validated_data['user']
        token, created = Token.objects.get_or_create(user=user)
        return Response({'token': token.key})

刚才我们登录的时候,它就自动创建了。

drf的token是保存在我们的服务器数据库当中,但是我们如果是一个分布式的系统, 两套系统想用一个token的话就会出现问题。分布式就得做用户同步。

  • 第二个非常严重的问题,这个token没有过期时间。永久有效。

这两个问题我们下一节课就来结尾。JWT解决

viewsets 配置认证类

我们在setting中配置了一个全局的token auth认证类。这个auth类会对token进行验证 如果验证失败会抛出上面源码中的两种异常

直接访问goods列表页。会返回401的错误。!!!这是一个可以公开的页面

用户登录失败或者token过期等各种情况也应该可以获取到的。

解决方案1: 访问goods列表页不带token。

很多前端写的时候是会全局加token的。

后端解决方案:

将token认证拿到view中来做。

不配置全局的,配置某个具体的viewset的。

删除setting中的auth token配置。

并在列表页中添加单独的auth 认证

    # 设置列表页的单独auth认证
    authentication_classes = (TokenAuthentication,)

验证此时list之中能不能得到user

mark

可以看到依然可以拿到user

因为商品列表页是一个公开的。所以不能配置这个。务必注释掉

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java帮帮-微信公众号-技术文章全总结

Web-第三十一天 WebService学习【悟空教程】

简单的网络应用使用单一语言写成,它的唯一外部程序就是它所依赖的数据库。大家想想是不是这样呢?

2114
来自专栏你不就像风一样

深入理解Spring Boot数据源与连接池原理

在使用Spring Boot数据源之前,我们一般会导入相关依赖。其中数据源核心依赖就是spring‐boot‐starter‐jdbc 如下

1.5K3
来自专栏佳爷的后花媛

php基础(一)

static 是静态变量,在局部函数中存在且只初始化一次,使用过后再次使用会使用上次执行的结果; 作为计数,程序内部缓存,单例模式中都有用到。

2442
来自专栏Java3y

Servlet第六篇【Session介绍、API、生命周期、应用、与Cookie区别】

什么是Session Session 是另一种记录浏览器状态的机制。不同的是Cookie保存在浏览器中,Session保存在服务器中。用户使用浏览器访问服务器的...

5315
来自专栏CSDN技术头条

SpringBoot 核心模块原理剖析

微服务始终一个相对热门的话题,SpringBoot 则以其轻量级、内嵌 Web 容器、一键启动、方便调试等特点被越来越多的微服务实践者所采用。 知其然还要知其所...

4429
来自专栏QQ音乐技术团队的专栏

ContentProvider简介

(一) 基础知识 Content Provider属于Android四大组件之一,相比较而言,它更侧重于共享数据。Android的数据存储方式有以下几种:...

2866
来自专栏武军超python专栏

2018年8月5日对之前学习python中的问题总结

问题: linux中whereis和which的区别: whereis python     which python whereis是一个文件查找命令,...

1005
来自专栏张善友的专栏

利用Windows性能计数器(PerformanceCounter)监控

一、概述 性能监视,是Windows NT提供的一种系统功能。Windows NT一直以来总是集成了性能监视工具,它提供有关操作系统当前运行状况的信息,针对各种...

3229
来自专栏linux、Python学习

案例+解读,来自有道大神的17个常用Linux命令深度解析

命令后带(Mac)标记的,表示该命令在Mac OSX下测试,其它的在Debian下测试。

1516
来自专栏人人都是极客

Linux 程序编译过程的来龙去脉

大家肯定都知道计算机程序设计语言通常分为机器语言、汇编语言和高级语言三类。高级语言需要通过翻译成机器语言才能执行,而翻译的方式分为两种,一种是编译型,另一种是解...

2093

扫码关注云+社区

领取腾讯云代金券