注册 登录
  • 欢迎访问"运维那点事",推荐使用Google浏览器访问,可以扫码关注本站的"微信公众号"。
  • 如果您觉得本站对你有帮助,那么可以扫码捐助以帮助本站更好地发展。

Django REST framework API认证(包含JWT认证)

Python框架 彭东稳 430次浏览 已收录 0个评论

一、背景

在我们学习Django Rest Framework(简称DRF)时,其非常友好地给我们提供了一个可浏览API的界面。很多测试工作都可以在可浏览API界面完成测试。要使用可浏览API界面很简单,只需要在urls.py文件中添加如下部分即可。

其中,r'^api-auth/'部分实际上可以用任何你想使用URL替代。唯一的限制是所包含的URL必须使用'rest_framework'命名空间。在Django 1.9+中,REST framework将自动设置,所以你也无须关心。

配置完成后,如果再次打开浏览器API界面并刷新页面,你将在页面右上角看到一个“Log in”链接。这就是DRF提供的登录和登出入口,可以用来完成认证。

然后进入到’rest_framework.urls’源码,是可以看到提供了’login’和’logout’两个接口,分别用来登入和登录的。代码如下:

其中login接口调用LoginView视图,logout接口调用LogoutView视图。这两个视图都是django.contrib.auth应用提供的。在LogoutView视图中,有这么一个装饰器@method_decorator(csrf_protect),是用来做CSRF code验证的,就是做表单安全验证的,防止跨站攻击。而这个CSRF code是在返回HTML页面的时候Django会自动注册这么一个CSRF code方法,而在template中会自动调用这个方法生成code值。在前端页面元素form部分,可以查看到name="csrfmiddlewaretoken"标识,且在Django返回的 HTTP 响应的 cookie 里,Django 会为你添加一个csrftoken 字段,其值为一个自动生成的token。这就是用来做表单安全验证的,具体关于CSRF原理见Django章节。

这里要说明一个问题就是这个LoginView我们是无法直接拿来用的,因为它需要做CSRF验证,而在前后端分离系统中不需要做CSRF验证,这里不存在站内站外的问题,本身就是跨站访问的。那么在我们前后端分离项目中,如何做API接口的验证呢?其实framework也已经提供了多种验证方式。

二、身份验证

REST framework提供了许多开箱即用的身份验证方案,同时也允许你实施自定义方案。这里需要明确一下用户认证(Authentication)和用户授权(Authorization)是两个不同的概念,认证解决的是“有没有”的问题,而授权解决的是“能不能”的问题。

  • BasicAuthentication

该认证方案使用 HTTP Basic Authentication,并根据用户的用户名和密码进行签名。Basic Authentication 通常只适用于测试。

  • SessionAuthentication

此认证方案使用 Django 的默认 session 后端进行认证。Session 身份验证适用于与您的网站在同一会话环境中运行的 AJAX 客户端。

  • TokenAuthentication

此认证方案使用简单的基于令牌的 HTTP 认证方案。令牌身份验证适用于 client-server 架构,例如本机桌面和移动客户端。

  • RemoteUserAuthentication

这种身份验证方案允许您将身份验证委托给您的 Web 服务器,该服务器设置 REMOTE_USER 环境变量。

默认的认证方案可以使用DEFAULT_AUTHENTICATION_CLASSES全局设置,在settings.py文件配置。在默认情况下,DRF开启了 BasicAuthentication 与 SessionAuthentication 的认证。

关于DRF,几乎所有的配置都定义在MREST_FRAMEWORK变量中。另外,关于认证方式DRF默认会检测配置在DEFAULT_AUTHENTICATION_CLASSES变量中的所有认证方式,只要有一个认证方式通过即可登录成功。这里的DEFAULT_AUTHENTICATION_CLASSES与Django中的MIDDLEWARE类似,在将request通过url映射到views之前,Django和DRF都会调用定义在MREST_FRAMEWORK变量中的类的一些方法。

另外,你还可以使用基于APIView类的视图,在每个视图或每个视图集的基础上设置身份验证方案。

需要明白的一点是,DRF的认证是在定义有权限类(permission_classes)的视图下才有作用,且权限类(permission_classes)必须要求认证用户才能访问此视图。如果没有定义权限类(permission_classes),那么也就意味着允许匿名用户的访问,自然牵涉不到认证相关的限制了。所以,一般在项目中的使用方式是在全局配置DEFAULT_AUTHENTICATION_CLASSES认证,然后会定义多个base views,根据不同的访问需求来继承不同的base views即可。

另外,在前后端分离项目中一般不会使用 BasicAuthentication 与 SessionAuthentication 的认证方式。所以,我们只需要关心 TokenAuthentication 认证方式即可。

三、TokenAuthentication

要使用TokenAuthentication方案,你需要将认证类配置为包含TokenAuthentication

并在INSTALLED_APPS设置中另外包含 rest_framework.authtoken

 注意: rest_framework.authtoken应用一定要放到INSTALLED_APPS,并且确保在更改设置后运行python manage.py migrate。 rest_framework.authtoken应用需要创建一张表用来存储用户与Token的对应关系。

数据库迁移完成后,可以看到多了一个authtoken_token表,表结构如下:

其中“user_id”字段关联到了用户表。

  • 配置URLconf

使用TokenAuthentication时,你可能希望为客户提供一种机制,以获取给定用户名和密码的令牌。 REST framework 提供了一个内置的视图来支持这种行为。要使用它,请将obtain_auth_token视图添加到您的 URLconf 中:

其中,r'^api-token-auth/'部分实际上可以用任何你想使用URL替代。

  • 创建Token

你还需要为用户创建令牌,用户令牌与用户是一一对应的。如果你已经创建了一些用户,则可以为所有现有用户生成令牌,例如:

你也可以为某个已经存在的用户创建Token:

创建成功后,会在Token表中生成对应的Token信息。

如果你希望每个用户都拥有一个自动生成的令牌,则只需捕捉用户的post_save信号即可。

请注意,你需要确保将此代码片段放置在已安装的models.py模块或 Django 启动时将导入的其他某个位置。

  • 获取Token

上面虽然介绍了多种创建Token的方式,其实我们最简单的就是只需要配置一下urls.py,然后就可以通过暴露的API来获取Token了。当使用表单数据或 JSON 将有效的usernamepassword字段发布到视图时,obtain_auth_token视图将返回 JSON 响应:

请注意,缺省的obtain_auth_token视图显式使用 JSON 请求和响应,而不是使用你设置的默认的渲染器和解析器类。

当我们正常获取到Token后,obtain_auth_token视图会自动帮我们在Token表中创建对应的Token。源码如下:

默认情况下,没有权限或限制应用于obtain_auth_token视图。 如果您希望应用throttling,则需要重写视图类,并使用throttle_classes属性包含它们。

如果你需要自定义obtain_auth_token视图,你可以通过继承ObtainAuthToken视图类来实现,并在你的urls.py中使用它。例如,你可能会返回超出token值的其他用户信息:

还有urls.py:

  • 认证Token

当我们获取到Token后,就可以拿着这个Token来认证其他API了。对于客户端进行身份验证,令牌密钥应包含在 Authorization HTTP header 中。关键字应以字符串文字 “Token” 为前缀,用空格分隔两个字符串。例如:

注意: 如果你想在 header 中使用不同的关键字(例如Bearer),只需子类化TokenAuthentication并设置keyword类变量。

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

  • request.user是一个User实例,包含了用户名及相关信息。
  • request.auth是一个rest_framework.authtoken.models.Token实例。

未经身份验证的响应被拒绝将导致HTTP 401 Unauthorized的响应和相应的 WWW-Authenticate header。例如:

测试令牌认证的API,例如:

注意: 如果您在生产中使用TokenAuthentication,则必须确保您的 API 只能通过https访问。

四、认证源码

使用 TokenAuthentication 认证方式,当认证成功后,在 request 中将提供了 request.user 和 request.auth 实例。其中 request.user 实例中有用户信息,比如用户名及用户ID,而 request.auth 实例中有Token信息。那么DRF是如何把 Token 转换为用户信息呢?通过下面的源码部分就可以看到它们是如何转换的。

基于 DRF 的请求处理,与常规的 url 配置不同,通常一个 Django 的 url 请求对应一个视图函数,在使用 DRF 时,我们要基于视图对象,然后调用视图对象的 as_view 函数,as_view 函数中会调用 rest_framework/views.py 中的 dispatch 函数,这个函数会根据 request 请求方法,去调用我们在 view 对象中定义的对应的方法,就像这样:

这里虽然直接调用 views.obtain_auth_token 方法,但进入到 views.obtain_auth_token 方法后还是 DRF 模式,源码如下:

ObtainAuthToken 方法是继承 DRF 中的 APIView 的 View 类:

如果你是用 POST 方法请求 ObtainAuthToken,那么 as_view() 函数会调用 dispatch 函数,dispatch 根据 request.METHOD,这里是 POST,去调用 ObtainAuthToken 类的 POST 方法,这就跟通常的 url->view 的流程一样了。

这里需要注意的一点就是,DRF 中的 APIVIEW 是继承 Django View 的,重写了部分 as_view 方法,而调用 dispatch 函数是在 Django View 的 as_view 方法中做的事情,源码部分如下:

但是用户认证是在执行请求 View 之前做的,所以其实就是在 dispatch 函数之中做的,具体见源码 rest-framework/views.py 中 APIView 类中的 dispatch 函数:

这里的 self.initialize_request 也可以关注一下,因为这里的 request 对象,后面也会有调用的地方。

其中 self.get_authenticators() 方法就是用来取 self.authentication_classes 变量。

关于 authentication_classes 变量,上面已经给出了,就在 APIView 里面 authentication_classes 字段。

然后就到了认证,重点在于 self.initial(request, *args, **kwargs) 函数,对于这个函数:

这里关注 self.perform_authentication(request) 验证某个用户,其实可以看到权限检查及限流也是在这里做的。

这里 request.user 其实是一个 @property 的函数,加 @property 表示调用 user 方法的时候不需要加括号“user()”,可以直接调用 request.user 。而这里的 request 对象就是上面 initialize_request 方法返回的,其中还返回了 DRF 定义的 request 对象,在 request 对象中有被 @property 装饰的 user 方法。

重点来了,到了真正认证的方法了,关注 self._authenticate() 函数即可。此方法会循环尝试每个 DRF 认证方式。

那么 self.authenticators 从哪儿来的呢?就是上面展示的,在 APIVIEW 类中的 authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES 得到的。我们上面在介绍 DRF 身份验证时也说了,可以把认证类定义在全局 settings 文件中,你还可以使用基于 APIView 类的视图,在每个视图或每个视图集的基础上设置身份验证方案。如下方式:

当基于 APIView 类的视图定义验证或权限类时,相当于覆盖了原生 APIVIEW 中的相关变量,自然就使用覆盖后的变量了。authentication_classes 里面放的就是可以用来验证一个用户的类,他是一个元组,验证用户时,按照这个元组顺序,直到验证通过或者遍历整个元组还没有通过。同理 self.check_permissions(request) 是验证该用户是否具有API的使用权限。关于对view控制的其他类都在rest-framework/views.py的APIView类中定义了。

由于我们这里只是拿 TokenAuthentication 认证说明,所以忽略 BasicAuthentication 和 SessionAuthentication 这两种认证,其原理与TokenAuthentication 一样。这样,就进入到了 TokenAuthentication 认证,其源码部分如下:

PS:DRF自带的TokenAuthentication认证方式也非常简单,同时弊端也很大,真正项目中用的较少。由于需要存储在数据库表中,它在分布式系统中用起来较为麻烦,并且每次都需要查询数据库,增加数据库压力;同时它不支持Token的过期设置,这是一个很大的问题。在实际前后端分离项目中使用JWT(Json Web Token)标准的认证方式较多,每个语言都有各自实现JWT的方式,Python也不例外。

五、JWT认证

了解完DRF自带的TokenAuthentication认证方式的弊端之后,再来看JWT(Json Web Token)认证方式。它们两个的原理是一样的,就是认证用户Token,然后取出对应的用户。但JWT解决了两个较大的问题。

第一,是不需要把Token存储到数据库表中了,而是根据一定的算法来算出用户Token,然后每次用户来验证时再以同样的方式生成对应的Token进行校验。当然,实际JWT生成Token的方式还是较为复杂的,具体可以看JWT协议相关文章。

第二,JWT对于生成的Token可以设置过期时间,从而在一定程度提高了Token的安全性。

JWT的原理还是稍稍有点麻烦的,里面涉及了一些对称加密和非对称加密的算法。但是JWT使用起来确是非常简单,Python中有PyJWT库,而在DRF中也有对应的开源项目django-rest-framework-jwt

  • 安装

直接使用pip安装即可,目前支持Python、Django、DRF主流版本:

  • 使用

settings.py文件中,将JSONWebTokenAuthentication 添加到REST framework框架的DEFAULT_AUTHENTICATION_CLASSES.

同样,你还可以使用基于APIView类的视图,在每个视图或每个视图集的基础上设置身份验证方案。与上面演示的 Token 认证一样,这里就不贴代码了,尽可能使用基于APIView类的视图认证方式。

但使用基于APIView类的视图认证方式时,不要忘记导入类。

在你的urls.py文件中添加以下URL路由,以便通过POST包含用户名和密码的令牌获取。

如果你使用用户名admin和密码admin123456创建了用户,则可以通过在终端中执行以下操作来测试JWT是否正常工作。

或者,你可以使用Django REST framework支持的所有内容类型来获取身份验证令牌。例如:

现在访问需要认证的API时,就必须要包含Authorization: JWT <your_token> 头信息了:

  • 刷新Token

如果JWT_ALLOW_REFRESH为True,可以“刷新”未过期的令牌以获得具有更新到期时间的全新令牌。像如下这样添加一个URL模式:

使用方式就是将现有令牌传递到刷新API,如下所示: {"token": EXISTING_TOKEN}。请注意,只有非过期的令牌才有效。另外,响应JSON看起来与正常获取令牌端点{"token": NEW_TOKEN}相同。

  • 认证Token

在一些微服务架构中,身份验证由单个服务处理。此服务负责其他服务委派确认用户已登录此身份验证服务的责任。这通常意味着其他服务将从用户接收JWT传递给身份验证服务,并在将受保护资源返回给用户之前等待JWT有效的确认。添加以下URL模式:

将Token传递给验证API,如果令牌有效,则返回令牌,返回状态码为200。否则,它将返回400 Bad Request以及识别令牌无效的错误。

  • 手动创建Token

有时候你可能希望手动生成令牌,例如在创建帐户后立即将令牌返回给用户。或者,你需要返回的信息不止是Token,可能还有用户权限相关值。你可以这样做:

  • 其他设置

你可以覆盖一些其他设置,比如变更Token过期时间,以下是所有可用设置的默认值。在settings.py文件中设置。

一般除了过期时间外,其他配置参数很少改变。具体参数意义当用到时可以查询官网。

JWT唯一的一个不算缺点的缺点就是Token太长了,180位。

<延伸>

Token 认证的来龙去脉

基于cookie的django-rest-jwt认证


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (0)or分享 (0)
关于作者:

您必须 登录 才能发表评论!