测开小记
基于前期DRF学习成果。
1、认证与授权
1.1 Django的登录接口
设置认证授权的类:
在全局urls里设置登录退出路由(rest_framework自带的,返回的是html页面,不是借口):
命令行创建超级管理员账户:
python manage.py createsuperuser
根据提示输入用户名,邮箱,密码,确认密码,就创建成功了。django会将创建的用户保存在数据库的auth_user表中,这个表本不存在,但在做数据迁移的时候如果不指定app名称,就会对所有的子应用进行数据迁移,包括django.contrib.auth,从而自动生成auth_user等表。
然后访问api/login去登录,登录成功就能请求或者操作数据了。
当然这个登录接口返回的是页面,不能作为项目的实际登录接口,仅作为测试参考。后面要自己实现登录功能。
1.2 Django的Session会话认证
上面的登录流程的认证方式用的是Session会话认证机制。客户端提交用户名密码后,django后端拿到用户名密码,经过加密,生成session_key和session_data,保存在指定的数据库中的django_session表中:
然后在响应头中的set_cookies字段,以sessionid=session_key的形式将session_key响应给客户端,其中还包括会话的过期时间expire_date。客户端收到响应后将set_cookies字段中的数据保存到Cookies中。
下次访问服务端发送请求时带上sessionid,django拿到sessionid后在数据库django_session表中比对看是否存在,是否过期,若存在且未过期就正常响应请求,若不存在或过期则返回403。
1.3 Session认证的缺陷
- session_key是保存在数据库的,如果用户量巨大会增加服务器的开销;
- 在分布式架构中,难以维持Session会话同步;
- 存在CSRF攻击的风险。
1.4 csrf_token问题
1、删除cookie中的csrf_token为何也能正常访问?
2、django每次返回的csrf_token为何都不变?
2、实现JWT认证
django提供的登录接口(api/login/)属于前后端不分离的,返回的是html页面,用的是session鉴权。真实项目是前后端分离的,后端登录接口返回的是JWT。JWT相较于session和传统的token更具有优势。不用查数据库,可跨域等。jwt直接放在响应数据中,前端提取后存于浏览器,下次访问受限资源只需在请求头带上jwt即可。本项目的登录接口地址为user/login/ 。
1、安装:pipenv install djangorestframework-jwt
2、指定认证类:
3、在全局路由表中添加路由
4、访问登录接口user/login/ 登录成功后会返回一个token
5、之后访问其他页面都要在请求头中添加Authorization(默认)字段,其值为”JWT token值“,注意JWT后面有个空格。
6、也可以修改token前缀JWT
我这里改为bearer,配合postman鉴权前缀bearer。
7、修改包含token的响应数据
3、关于JWT的状态码
当我将权限类设置为IsAuthenticated时,也就表明身份认证通过的用户才有资格访问,此时若我在未登录的情况下访问项目资源,应该是返回401未认证,而实际是返回的却是403?why?
根据RFC的标准:
401(未经授权)状态码表示该请求尚未应用,因为它缺少针对目标资源的有效身份验证凭据……用户代理可以使用新的或替换的Authorization标头字段重复该请求。
403(禁止)状态码表示服务器理解了请求但拒绝对其进行授权……如果请求中提供了身份验证凭据,则服务器认为它们不足以授予访问权限。
根据上面的标准,我的访问确实应该返回401呀,如果说我以普通用户的身份登录了网站却去访问VIP的资源那应该返回403才对。实在是不解。
按照django的设置,那什么情况下返回401呢?是登录认证过程失败吗?
而实际上登录失败返回的是400!
这也可以通过rest_framework_jwt的最上层的视图函数JSONWebTokenAPIView的post方法里查看:序列化器校验失败返回400.
我还没看到返回401的情况。记录一下。
ps:
如果用户登陆失败(身份验证失败,即authenticaiton失败),则服务端返回401错误。
如果用户身份验证成功,但是权限验证失败,则返回403错误。
网上有人说官方把HTTP 401叫Unauthorized(未授权),不是很好,叫Unauthenticated(未认证)更好,也是有些道理的。
4、指定局部权限类
不应该设置全局权限类,应该根据每个接口的功能去设置权限类,如登陆接口和注册接口就不应该需要什么权限。
设置项目接口为登录认证后才能访问:
也可以使用authentication_classes指定局部的认证类,优先级都高于全局的。不过通常没必要为单个视图指定认证类。
5、项目登录注册接口
5.1 接口实现
上面解决了用户登录的问题,现在解决的是注册问题。
在django.contrib.auth.models.py里有User模型,
里面有用户名密码邮箱,身份标识等等字段,我们可以直接拿来当做自己的用户模型,在创建序列化器的时候加以个性化修改,如邮箱必须输入且唯一,需要确认密码等。
对auth/validators.py的校验器做了修改:
序列化器:
视图:
5.2 CORS跨域
前后端联调:前端代码有柠檬的和自己写的,暂时用自己写的。
将我的前端登录注册UI界面与后端额登录注册API接口进行联调,首先试试登录功能。
由于浏览器的同源策略,限制了非同源网站对目标服务器的访问,导致在前后端联调时报错:
此处解决方法是用CORS(跨域资源共享)。
安装django-cors-headers报错:
找到报错原因:缺少C++ 14
安装Microsoft Visual C++ 14.0
1、下载Microsoft Visual C++ Build Tools
下载
链接: https://pan.baidu.com/s/1UCeazxT_nOTCX4M4UEm9xA?pwd=9my7 提取码: 9my7
下载完毕双击安装后出现安装包丢失或损坏的情况,选择第二项,自己提供搜索包。
2、下载离线包
链接: https://pan.baidu.com/s/1zt4v9hoRts2YgojF90zYow?pwd=gcz2 提取码: gcz2
解压后:
在1中选择上图标出的.msi搜索包,安装完毕。
再次安装mysqlclient还是报错:
我又将虚拟环境的pip版本升级到最新版后去安装
回到原点,网上看到:Django默认使用的是MySQLdb,但是MySQLdb不支持python3.x ,取而代之的是pymysql。所以应该是安装pymysql后没有引入导致的或者是没有安装mysqlclient导致的错误。
我看自己的虚拟环境中安装了mysqlclient,但是没有pymysql。
先安装pymysql,然后在项目根目录下的同名目录中的__init__.py中添加:
然后启动项目成功。
注意:这里我自己的前端登录代码需要作很多修改,包括username和password字段名(之前用的是user和pwd),服务器地址,还有对登录成功后token的处理。
在前端登录页面输入正确的用户名和密码点击登录后,发现前端发出了两个请求给服务器:
第一个请求方法是OPTIONS,第二个是POST。第一个请求类型是preflight,第二个是xhr,很显然,第一个是预检请求。第二个才是实际真实请求。
CORS官方解释如下:
跨源资源共享 (CORS)(或通俗地译为跨域资源共享)是一种基于 HTTP 头的机制,该机制通过允许服务器标示除了它自己以外的其它 origin(域,协议和端口),使得浏览器允许这些 origin 访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的"预检"请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。
首先需要搞清楚,CORS是一个解决跨域访问的方案,那么为何有跨域的情况存在呢?
跨域问题的本质原因是:浏览器的同源策略。
那么什么是同源策略呢?同源是指:协议、域名、端口都相同,非同源的资源之间不能相互通信。同源策略是为了保证用户信息的安全,防止恶意的网站窃取数据。
然而,同源策略的存在也导致不同源之间的合理通信受到了限制:
- Cookie、LocalStorage 和 IndexDB 无法读取;
- DOM 无法获得;
- AJAX 请求无法发送
为了解决浏览器同源策略给我们的限制,就有了各种跨域方案。CORS就是其中的一种。
CORS标准新增了一组 HTTP 首部字段,客户端和服务器通过使用该首部字段来处理权限,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。另外,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型 的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,浏览器才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证 相关数据)。
例如我的第二个login请求:
注意:当发出跨源请求时,第三方 cookie 策略仍然会强制执行。客户端仍然不能向服务端发送cookie。即使发送了,如果服务器端的响应中未携带 Access-Control-Allow-Credentials: true,浏览器将不会把响应内容返回给请求的发送者。CORS预检请求不能包含cookie等身份凭证,预检请求的响应必须指定 Access-Control-Allow-Credentials: true 来表明后续实际的真实请求可以携带cookie等身份凭证。
另外,对于附带身份凭证的请求(通常是 Cookie),服务器不得设置 Access-Control-Allow-Origin 的值为“*”,而要设置为具体的源,否则请求会失败。
在我的项目的settings.py中添加:
此时预检请求和真实登录请求响应头都会携带Access-Control-Allow-Credentials: true和AccesAccess-Control-Allow-Origin: http://localhost:8080。
http://localhost:8080
Cookie 策略还受 SameSite 属性和SameParty属性控制。如果你设置SameSite=Strict,浏览器将阻止所有三方 Cookie 携带,此时在CORS中自然也不会有cookie发送出去。
5.3 预检请求
什么情况下会发送预检请求呢?
当一个请求的请求方法不属于【GET,POST,HEAD】,或者请求头字段Content-Type的值不属于【text/plain,multipart/form-data,application/x-www-form-urlencoded】,或者请求头字段不属于对CORS安全的请求头字段集合,这种请求就不是简单请求,会触发CORS预检请求。
在上面的两次请求中,预检请求只是一个检查的过程,它不会携带任何请求的参数;预检通过后的请求才会真正的携带请求参数与服务器进行数据通信。
若服务器对预检请求没有任何响应,那么浏览器不知道服务器是否支持CORS而不会发送后续的实际请求;或者服务器不支持当前的Origin跨域访问也不会发送后续请求。
关于分页
服务器端返回的项目默认只有四个,不满足要求,不能动态修改:
由于在前后端都实现了分页功能,所以我暂时使用了前端的分页,前端分页是element-UI提供的页面组件实现的,后端分页是通过重写PageNumberPagination(_PageNumberPagination)实现的,暂时保留后端分页设置,仅仅改动一个地方:将page_size设置为足够大即可。
这样分页就搞定了。
注册
修改用户名格式校验器:
6、集成HttpRunner
6.1 安装运行
安装:pipenv install httprunner==2.5.7
在项目根目录下创建httprunner项目:hrun --startproject dj01
发现报错:
原因:MarkupSafe 2.1版本中的“soft_unicode”已重命名为“soft_str”。我的虚拟环境中安装的是2.1.1版本,所以要降版本。
pipenv install markupsafe==2.0.1
再去创建项目:成功
项目根目录下多了dj01文件夹,里面有多个子文件夹如api,reports等。
在api目录新增login_api.yml文件,测试一下登录接口,使用正确的用户名和密码:
运行测试用例:hrun dj01/api/login_api.yml
生成测试报告:
也可以通过HttpRunner实例去运行,run里面跟绝对路径:
6.2 关于yml
yml文件里的request中的key与request库是对应的,如果要发form表单数据,则只需将Content-Type改为application/x-www-form-urlencoded格式,数据写成data而不是json。
扩展:什么是application/x-www-form-urlencoded?
它是一种编码类型。当URL地址里包含非西欧字符的字符串时,系统会将这些字符转换成application/x-www-form-urlencoded字符串,然后以?key=value&key=value的形式拼接到URL后面,表单里提交时也是如此,当包含非西欧字符的字符串时,系统也会将这些字符转换成application/x-www-form-urlencoded字符串,然后在服务器端自动解码。FORM元素的enctype属性指定了表单数据向服务器提交时所采用的编码类型,默认的缺省值是“application/x-www-form-urlencoded。
然而,在向服务器发送大量的文本、包含大量非ASCII字符的文本或二进制数据时这种编码方式效率很低。这个时候我们就要使用另一种编码类型“multipart/form-data”,比如在我们在做上传的时候,表单的enctype属性一般会设置成“multipart/form-data”。 Browser端<form>表单的ENCTYPE属性值为multipart/form-data,它告诉我们传输的数据要用到多媒体传输协议,由于多媒体传输的都是大量的数据,所以规定上传文件必须是post方法,<input>的type属性必须是file。
也可以将url: "http://127.0.0.1:8000/user/login/"中的域名和端口号提取出来放到base_url中:
设置全局变量:
设置完毕在下方就可以通过 $变量名 调用变量了:
当然这个变量不能跨yml文件调用,但是环境变量可以。在.env中设置环境变量,然后在所有的yml文件都可以调用。调用方式:"${ENV(变量名)}"
运行测试用例之后会首先加载并设置环境变量:
6.3 关于validate断言
具体的简写标准在httprunner库的parser.py中有说明:有lt,le,gt,ge等
方括号内为实际值和期望值,实际值提取自响应属性response attributes中,默认有:
status_code, cookies, elapsed, headers, content, text, json, encoding, ok, reason, url.
如果yml文件中填写的不是上述响应属性,运行测试用例时就会报错ParamsError:
当然也可以自行添加响应属性。
content, text, json指的是响应体数据,当一个接口返回的数据为json格式时,content、text和json三者获取的结果相同,均为字典类型。
断言条件也可以用contains:如 - contains: ["json", 'token'] ,判断 返回的json数据是否包含“token”字符串。
结果自然通过了。注意,这里的包含关系仅限于json字典的key当中,而不是value。
如:- contains: ["json", 'tqs'] ,虽然我的用户名是tqs,但还是会断言失败:
那如何取出json字典的value值呢?直接用"."的方式去取。
如:- lt: ["json.userID", 2] ,判断userID的值是否小于2:断言通过
如果取出的value是个列表[1,2,3,4],则直接通过索引json.key.0就能获取到列表的第一个值1。
断言的完整写法:
6.4 testcases
如果要获取项目列表,直接api的yml是不行的,因为没有提供登录成功后返回的token,提示身份认证信息未提供。
此时就需要用到testcases文件夹了。它里面的yml能实现接口依赖和数据驱动测试,执行比较复杂的逻辑。
testcases提供的demo中可以看到,有config和teststeps两大块:
由于在api中已经写好了url、变量等,如果再重复写那就回合并覆盖。这里可以简化一下:
提取响应中的token值存放到token这个全局变量中,供其下方的测试步骤调用。再在GET请求中添加一个头部Authorization,通过$token调用全局变量token:
有了token认证,测试用例自然就通过了:
6.5 数据驱动
对于动态数据,可以在debugtalk.py模块里面写函数或者类来实现。如随机登录用户名:
在yml文件里通过${random_username()}来调用:
如果要处理更复杂的动态数据,可以在根目录下自定义模块,再在debugtalk.py中去调用。
关于参数化数据驱动,详见我的笔记httprunner基操。
当我对登录接口进行数据驱动的时候,报错429:请求频繁导致被限流。
看来要研究一下DRF的限流组件了。
7、数据库设计
8、继续
8.1 新建各个子应用,但是子应用多了都放到项目根目录下很杂乱,新建一个包apps,存放各个子应用,此时子应用在项目中的路径已经改变,python搜索模块的时候会找不到,所以要添加apps路径到sys.path里面。
注册子应用:
此时会发现之前注册的三个子应用引用有问题,再注册新的子应用也没了补全提示。
解决办法:将apps包标记为Sources Root
更新:2022.11
很遗憾,我决定停止对测试平台的开发,主要是自己精力不够,python掌握的程度不够,django框架以及drf没学透,很多源码看不懂,对于整个平台的架构,我也没什么经验,项目、接口、测试用例、配置等模块之间的关联,包括数据库的设计,模型、序列化器、视图函数的构建,中间有很多问题没有很好的得到解决。至于前端vue,学过但过了一段时间没用又忘了...感觉力不从心,太累了,要学的东西太多了~以后有机会再继续吧!