mirror of
https://github.com/NohamR/knowledge-kit.git
synced 2026-05-24 20:00:37 +00:00
docs: 批量博文
This commit is contained in:
155
Chapter3 - Server/3.1.md
Normal file
155
Chapter3 - Server/3.1.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# 利用分页和模糊查询技术实现一个App接口
|
||||
|
||||
摘要: 模糊查询技术、分页技术、App接口
|
||||
|
||||
1、分页问题
|
||||
|
||||
关键技术点:php执行sql语句。示例:“SELECT * FROM hotel WHERE subject='10' and address like '%杭州%' LIMIT 0,4”。注意:LIMIT后需要跟2个数字,1个是起始位置,2是结束位置。
|
||||
|
||||
可以对limit函数进行封装:select *from user limit (page-1)*size,page*size;
|
||||
|
||||
单例模式编写App接口的注意点:
|
||||
|
||||
(1)、php后台获取参数,之后根据参数转换成SQL语句;
|
||||
|
||||
(2)、php执行SQL语句,将结果转换成JSON,返回给客户端
|
||||
|
||||
2、模糊搜索
|
||||
|
||||
SQL匹配模式(开发中应用最多的一种):
|
||||
|
||||
(1)、使用SQL匹配模式,不能使用操作符 = 或 != ,而是使用操作符LIKE或者NOT LIKE
|
||||
|
||||
(2)、使用SQL匹配模式,MySQL提供了2种通配符。
|
||||
|
||||
%:表示任意数量的任意字符(包括0个)
|
||||
|
||||
_:表示任意的单个字符
|
||||
|
||||
例子:u_name为“张三”,“张猫三”、“三脚猫”,“唐三藏”。如果要找回包含“三”和“猫”的纪录
|
||||
|
||||
“select *from user where u_name LIKE '%三%' and u_name LIKE '%猫%';
|
||||
|
||||
若使用“select *from user where u_name LIKE '%三%猫%';”只能找出:三脚猫
|
||||
|
||||
(3)、使用SQL匹配模式,如果匹配格式当中不包含以上2种通配符中的任意1个,其查询效果等同于= 或 !=
|
||||
|
||||
(4)、使用SQL匹配模式,默认情况下是不区分大小写的
|
||||
|
||||
|
||||
|
||||
正则表达式匹配模式(不推荐):
|
||||
|
||||
(1)、 []:表示括号内所列字符中的一个:指定一个字符、字符串或范围,要求所匹配对象为它们中的任意一个
|
||||
|
||||
比如 SELECT * FROM [user] WHERE u_name LIKE '[张李王]三'。将找出“张三”、“李三”、“王三”(而不是“张李王三”);
|
||||
|
||||
比如[ ] 内有一系列字符(01234、abcde之类的)则可略写为“0-4”、“a-e”。SELECT * FROM [user] WHERE u_name LIKE '老[1-9]'-> 将找出“老1”、“老2”、……、“老9”;
|
||||
|
||||
(2)、[^]:表示不在括号所列之内的单个字符。其取值和[]相同,要求匹配对象为指定字符以外的任意一个字符
|
||||
|
||||
比如 SELECT * FROM [user] WHERE u_name LIKE '[^张李王]三'。将找出不姓“张”、“李”、“王”的“赵三”、“孙三”等
|
||||
|
||||
(3)、.:匹配任意的单个字符
|
||||
|
||||
(4)、^:表示以某个字符或字符串开头。^a:以a开头
|
||||
|
||||
(5)、$:表示以某个字符或字符串结尾。s$:以s结尾
|
||||
|
||||
(6)、*:匹配0个或多个在它前面的字符
|
||||
|
||||
使用正则表达式匹配的操作符是:REGEXP和NOT REGEXP(RLIKE或NOT RLIKE)
|
||||
|
||||
注意啊:正则表达式和SQl匹配模式工作原理不一样:正则只要匹配出符合条件就马上算成功;而SQl需要严格匹配。比如同样需要写从酒店表中拿出地址中包含“杭州”的酒店
|
||||
|
||||
SQL:SELECT *FROM hotel WHERE address LIKE '%杭州%';
|
||||
|
||||
正则:SELECT *FROM hotel WHERE address REGEXP '^[杭][州]*’;
|
||||
|
||||
|
||||
需要特别注意的是:如果所查询的内容包含通配符时,导致我们查询"%","_","["的语句无法正常实现。因此我们需要特殊处理。
|
||||
|
||||
需要特别注意的是:如果所查询的内容包含通配符时,导致我们查询"%","_","["的语句无法正常实现。因此我们需要特殊处理。
|
||||
|
||||
|
||||
```php
|
||||
function sql_encode($sql){
|
||||
$sql = replace($sql,"[","[[]");
|
||||
$sql = replace($sql,"%","[%]");
|
||||
$sql = replace($sql,"_","[_]");
|
||||
return $sql;
|
||||
}
|
||||
```
|
||||
|
||||
最后贴一个利用模糊查询实现分页查询的接口。
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: geek
|
||||
* Date: 2017/2/20
|
||||
* Time: 上午9:15
|
||||
*/
|
||||
|
||||
header('content-type:text.html;charset=utf-8');
|
||||
error_reporting(0);
|
||||
require_once '../../model/PdoMySQL.class.php';
|
||||
require_once '../../model/config.php';
|
||||
require_once 'Response.php';
|
||||
class HotelList
|
||||
{
|
||||
private $tableName = "hotel";
|
||||
private $telephone = "";
|
||||
private $cityName= "";
|
||||
private $subjectId = "";
|
||||
private $page = 0;
|
||||
private $size = 0;
|
||||
protected static $_instance = null;
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected function __clone()
|
||||
{
|
||||
// TODO: Implement __clone() method.
|
||||
}
|
||||
public function getInstance()
|
||||
{
|
||||
if (self::$_instance === null) {
|
||||
self::$_instance = new self();
|
||||
}
|
||||
return self::$_instance;
|
||||
}
|
||||
|
||||
function getHotels()
|
||||
{
|
||||
self.$this->telephone = $_REQUEST["telephone"];
|
||||
self.$this->subjectId = $_REQUEST["subjectId"];
|
||||
self.$this->cityName = $_REQUEST["cityName"];
|
||||
self.$this->page = $_REQUEST["page"];
|
||||
self.$this->size = $_REQUEST["size"];
|
||||
|
||||
$mysqlPdo = new PdoMySQL();
|
||||
|
||||
if($this->telephone == ""){
|
||||
Response::show(201,"fail","非安全的数据请求","json");
|
||||
}
|
||||
$userRows = $mysqlPdo->find("user","telephone='$this- >telephone'");
|
||||
if($userRows[0]["telephone"] != $this->telephone){
|
||||
Response::show(201,"fail","非安全的数据请求","json");
|
||||
}
|
||||
$city = str_replace("市","",$this->cityName);
|
||||
$allrows = $mysqlPdo->find($this->tableName,"subject='$this->subjectId' and address like '%$city%'","","","","",[(intval($this->page)-1)*intval($this->size),intval($this->page)*intval($this->size)]);
|
||||
Response::show(200,'酒店列表获取成功',$allrows,'json');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$lister = HotelList::getInstance();
|
||||
$lister->getHotels();
|
||||
```
|
||||
|
||||
其中:Respone和PdoMySQL的2个类分别是接口数据展示的类和PDO操作的一个封装。
|
||||
46
Chapter3 - Server/3.2.md
Normal file
46
Chapter3 - Server/3.2.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# 网页二维码,App 扫码登录实现原理
|
||||
|
||||
|
||||
|
||||
### 需求介绍
|
||||
|
||||
首先,介绍下什么是扫码登录。现在,大部分同学手机上都装有qq和淘宝,天猫等这一类的软件。而开发这些app的企业,都有他们相对应的网站。为了让用户在使用他们的网站时,登录更加方便和安全。这些企业提供了, 使用手机,扫一扫,就可以登录的服务。网页登录时的效果如下:
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
有很多小伙伴可能会感到很神奇,网页上只是显示了个二维码,它怎么就知道是哪个手机扫到了二维码,并且进行登录的呢?而且,登录完成以后,还能直接把用户信息显示给用户,真的是很神奇啊。
|
||||
|
||||
### 原理解释
|
||||
|
||||
**网页端+服务器**
|
||||
|
||||
接下来就是对于这个服务的详细实现。首先,大概说一下原理:用户打开网站的登录页面的时候,向浏览器的服务器发送获取登录二维码的请求。服务器收到请求后,随机生成一个uuid,将这个id作为key值存入redis服务器,同时设置一个过期时间,再过期后,用户登录二维码需要进行刷新重新获取。同时,将这个key值和本公司的验证字符串合在一起,通过二维码生成接口,生成一个二维码的图片(二维码生成,网上有很多现成的接口和源码,这里不再介绍。)然后,将二维码图片和uuid一起返回给用户浏览器。
|
||||
|
||||
浏览器拿到二维码和uuid后,会每隔一秒向浏览器发送一次,登录是否成功的请求。请求中携带有uuid作为当前页面的标识符。这里有的同学就会奇怪了,服务器只存了个uuid在redis中作为key值,怎么会有用户的id信息呢?
|
||||
|
||||
这里确实会有用户的id信息,这个id信息是由手机服务器存入redis中的。具体操作如下:
|
||||
|
||||
**手机端+服务器**
|
||||
|
||||
话说,浏览器拿到二维码后,将二维码展示到网页上,并给用户一个提示:请掏出您的手机,打开扫一扫进行登录。用户拿出手机扫描二维码,就可以得到一个验证信息和一个uuid(扫描二维码获取字符串的功能在网上同样有很多demo,这里就不详细介绍了)。由于手机端已经进行过了登录,在访问手机端的服务器的时候,参数中都回携带一个用户的token,手机端服务器可以从中解析到用户的userId(这里从token中取值而不是手机端直接传userid是为了安全,直接传userid可能会被截获和修改,token是加密的,被修改的风险会小很多)。手机端将解析到的数据和用户token一起作为参数,向服务器发送验证登录请求(这里的服务器是手机服务器,手机端的服务器跟网页端服务器不是同一台服务器)。服务器收到请求后,首先对比参数中的验证信息,确定是否为用户登录请求接口。如果是,返回一个确认信息给手机端。
|
||||
|
||||
手机端收到返回后,将登录确认框显示给用户(防止用户误操作,同时使登录更加人性化)。用户确认是进行的登录操作后,手机再次发送请求。服务器拿到uuId和userId后,将用户的userid作为value值存入redis中以uuid作为key的键值对中。
|
||||
|
||||
**登录成功**
|
||||
|
||||
然后,浏览器再次发送请求的时候,浏览器端的服务器就可以得到一个用户Id,并调用登录的方法,声成一个浏览器端的token,再浏览器再次发送请求的时候,将用户信息返回给浏览器,登录成功。这里存储用户id而不是直接存储用户信息是因为,手机端的用户信息,不一定是和浏览器端的用户信息完全一致。
|
||||
|
||||
**登录原理图如下:**
|
||||
|
||||

|
||||
|
||||
97
Chapter3 - Server/3.3.md
Normal file
97
Chapter3 - Server/3.3.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# 从 Node.js 看看服务端框架的一些感想
|
||||
|
||||
> 为什么写这篇文章?因为早上在思考一个问题「想获取一家公司的数据(内容型公司),反爬措施做的比较好(VIP会员制度,访问次数太多会锁掉账号)。有几个方向想去尝试:1.App 逆向破解看看网络请求部分的参数是如何生成的;2.Charles 抓包破解参数部分看看能否模拟;3.查看小程序是否有漏洞。最后想来想去还是算了,因为反爬虫措施即使破解了,但是当请求的次数较多的时候还是会封锁 VIP 账号。最后想的是找出封锁账号的请求次数的临界值,然后用爬虫手段去获取数据,但是不能超过临界值就可以。因为 VIP 账号价格实在太高就放弃了。过了几天我想到了用浏览器插件的方式去做这个事情。也就是在对方的网站里面注入我们的 JS 脚本,脚本会在网站上面添加一个按钮,点击按钮就可以将数据同步到我们自己的数据中心。废话说了一大堆,因为 JS 在浏览器环境里面不具备服务端的能力所以想到的是通过接口将数据让一个 Node.js 的服务去处理,将数据入库等操作。问题正式进入主题,要开发微服务的过程中选择用 Node 的 express 还是 Koa 还是 eggjs 等问题困扰了我一会儿。这篇文章不针对这些具体的库进行讨论,而是对于服务端的一些思考」
|
||||
|
||||
## 优秀的后端框架?
|
||||
|
||||
一个什么样的框架算得上是优秀或者合格?有个需求让你写一个 HTTP 服务,借助于 express 你可能初始化项目、安装依赖、写完代码都用不了6分钟?觉得似乎很简单,哥们儿你想想这是一个服务,而不是说让你能跑就行了。计算机学生的平时作业差不多满足了。但是你说你这个东西能打吗?可以说“战五渣”。一旦部署到线上环境,可能瞬间就被大量涌入的请求击垮,更何况有些人要攻击你。换个角度思考问题。加入你的线上程序需要升级,你该怎么办?停止当前的服务让用户等待一段时间吗?
|
||||
|
||||
所以一个后端服务必须满足2个特性:
|
||||
|
||||
- 容错性强(Fault tolerate)
|
||||
- 可拓展性高(Scalability)
|
||||
|
||||
其他的特性也很重要,比如程序的健壮性、接口设计友好、代码修改起来灵活等等特性。但是容错性、可拓展性是服务正常运行的基本保障。至少得向用户保证服务是可用的。无论代码写的多优雅它都是为业务所服务的。
|
||||
|
||||
|
||||
## 拓展性(Scalability)
|
||||
|
||||

|
||||
|
||||
- X轴:纯粹对服务的实例进行拓展。为了响应更多的请求
|
||||
- Y轴:未服务添加新的给你。功能性拓展
|
||||
- Z轴:按照业务数据对服务进行拓展
|
||||
|
||||
## 实例拓展
|
||||
|
||||
|
||||
增加服务实例包括两类:横向拓展、纵向拓展。横向拓展表示利用更多的及其。纵向拓展表示在一台及其上挖掘它的潜力。
|
||||
|
||||
NodeJS 程序是单进程运行的。32位机器上最多只有 1GB 内存的实用权限(在 64GB 机器上的最大内存权限扩大到 1.7GB)。目前绝大部分线上服务器 CPU 都是多核并且至少 16GB。如此 Node 便无法发挥机器的最大能力。Node 早就意识到这一点,它允许创建多个子进程运行多个实例。
|
||||
|
||||

|
||||
|
||||
|
||||
有一个主进程 master,但是 master 进程并不实际处理业务逻辑,但是除了业务逻辑之外的事情它都负责。它是 manager,负责启动子进程、管理子进程(如果进程挂到了则需要重启)。同时扮演 Router 的角色,也就是对程序的所有访问请求都先到达主进程,主进程分配请求给子进程 worker(子进程负责处理业务逻辑)
|
||||
|
||||
这个机制下有两条细节需要处理。
|
||||
1. 如何把外界的任务平均分配给不同的 worker 处理?这里的平均并不是指数量上的平均(因为每个请求的工作量可能不同)。不能让某个子进程太闲,也不能让某个子进程太忙,而是始终处于工作的状态。也就是「负载均衡(load-balancing)」。默认情况下 Clust 模块采用的是 **round robin** 负载均衡算法,说白了就是依次按照顺序把请求指派给列表上的子进程,到结尾之后重头开始。
|
||||
|
||||
这个算法只能保证每个子进程收到的请求个数是是平均的。但如果某个进程本来的任务很复杂,后来又由于不断的收到被平均指配的任务,那么这个子进程的压力就很大了。除此之外我们需要考虑超时、重做机制,所以主进程 master 作为路由时不仅仅需要转发请求,还需要智能的分配请求
|
||||
|
||||
|
||||
另一个问题是状态共享问题,假如某个用户第一次访问该服务时是分配给了线程A上的实例A处理,并且用户在这个实例上进行了登陆,而没有过几秒钟之后当用户第二次访问时分配给了线程B上的实例B处理,如果此时用户在A上的登陆状态没有共享给其他实例的话,那么用户不得不重新登陆一次,这样的用户体验是无法接受的。如下图所示
|
||||
|
||||

|
||||
|
||||
解决方案1:将状态共享
|
||||
|
||||

|
||||
|
||||
解决方案2:新增一个模块专门用于记录用户第一次访问的实例。并在之后当用户访问服务时始终指派访问该实例
|
||||
|
||||

|
||||
|
||||
|
||||
主进程-子进程的模式思路不仅可以用于「纵向拓展」,还适用于「横向拓展」。当单台机器已经无法满足你需求的时候,你可以把单实例子进程的概念拓展为单台机器:我们将在多台机器上部署多个进行实例,用户的访问请求也并非直接到达它们,而是先到达前方的代理机器,它也是负责负载均衡的机器,负责将请求转发给部署了应用实例的机器。这样的模式我们也通常称为反向代理模式:
|
||||
|
||||

|
||||
|
||||
这个模式仍然可以继续改进:动态的启动或者关闭机器上的若干实例用于节省资源、移除负载均衡这一环节用于提高通讯的效率。
|
||||
|
||||
对于所有的开发来说,很多道理都是通用的。比如设计模块、解耦思想。上面说的负载均衡、反向代理等等不只是 Java、Node、PHP、.Net 等都存在。所以只要是服务端的概念,Node 里面一样存在。(有的 Node 工程师是从前端开发转过来的,所以在此强调。)虽然 Node.js 较新,但是解决思路或者方案可以借鉴传统的服务端方案。跳出语言的限制去看待问题、解决问题、寻找思路和方案
|
||||
|
||||
|
||||
## 功能拓展
|
||||
|
||||
你也许会问新增功能有什么难点?每个程序员的日常就是不断的进行功能迭代。但在这里我们希望解决一个问题,就是既然我们无法保证功能不会出错,那我们有没有办法保证当一个功能出错之后不会影响整个程序的正常运行?这也是我们所说的容错性。
|
||||
|
||||
道理都懂,我们都明白程序需要容错,所以try/catch是从编码上解决这个问题。但问题是try/catch不是万能的,万无一失的程序也是不存在的,所以我们要换个思路解决这个问题,我们允许程序出错,但是要及时把错误隔离,并且不再影响程序的运行。这个就要从架构上解决这个问题。例如使用微服务(Microservices)架构。
|
||||
|
||||
在介绍微服务架构之前,我们要了解其它架构为什么没法满足我们的要求。例如我们常用的单体(monolithic)架构。单体架构这个词你可能不熟悉,但几乎我们每天都在和它打交道,大部分的后端服务都归属于单体架构,对它的解释我翻译Martin Fowler的描述:
|
||||
|
||||
企业级应用通常分为三个部分:用户界面(包含运行在用户浏览器上的html页面和javascript脚本),数据库(通常是包含许多表的关系数据库),和服务端应用。服务端应用将会处理http请求,执行业务逻辑,从数据库中取得数据,生成html视图返回给浏览器。这样的服务端应用就被称为单体(monolith)——单个具有逻辑性的执行过程。任何针对系统的修改都会导致重新构建和部署一个新版本的服务端应用。
|
||||
|
||||
(注:以上这段描述摘自Martin Fowler的文章Microservices,我认为这是对微架构描述最全面的文章,如果想对这一小节做更深入的了解可以把这篇文章细读。 这也是我读到的Martin Fowler所写的文章中最通俗的文章。个人认为Martin Fowler的文章读起来比较晦涩,John Resig紧随其后)
|
||||
|
||||
单体架构是一种很自然的搭建应用的方式,它符合我们对业务处理流程的认知。但单体应用也存在问题:任何一处,无论大小的修改都会导致整个应用被重新构建和重新部署。随着应用规模和复杂性的不断增大,参与维护的人数增多,每一轮迭代修改的模块增多,对上线来说是极大的考验,对于内部单个模块的拓展也是极为不利的。例如当图片压缩请求剧增时,需要新增图片压缩模块的实例,但实际上不得不扩展整个单体应用的实例。
|
||||
|
||||
微服务架构解决的就是这一系列问题。顾名思义,微服务架构下软件是由多个独立的服务组成。这些服务相互独立互不干预。以拆分上面所说的单体应用为例,我们可以把处理HTTP请求的模块和负责数据库读写的模块分离出来成为独立的服务,这两个模块从功能上看是没有任何交集。这样的好处就是,我们可以独立的部署,拓展,修改这些服务。例如应用需要添加新的接口时,我们只需要修改处理HTTP请求的服务,只公开这部分代码给修改者,只上线这部分服务,拓展时也只需要新添这部分服务的实例。
|
||||
|
||||
微服务和我们通常编写的模块(以文件为单位,以命名空间为单位)相比更加独立,更像是一个五脏俱全的“小应用”,如果你读完了我之前推荐的Martin Fowler关于微服务的文章的话,你会对这点更深有感触:微服务除了在运维上独立以外,它还可以拥有独立的数据库,还应该配备独立的团队维护。它甚至可以允许使用其他的语言进行开发,只要对外接口正常即可。
|
||||
|
||||
当然微服务也存在不足,例如如何将诸多的微服务在大型架构中组织起来,如何提高不同服务之间的通信效率都是需要在实际工作中解决的问题。
|
||||
|
||||
微服务说到底还是解耦思想的实践。从这个意义上来说,React下的Flux架构某种意义上也属于微服务。如果你了解Flux的起源的话,Flux架构其实来源于后端的CQRS,即Command Query Responsibility Segregation,命令与查询职责分离,也就是将数据的读操作和写操作分离开。这么设计的理由有很多,举例说一点:在许多业务场景中,数据的读和写的次数是不平衡,可能上千次的读操作才对应一次写操作,比如机票余票信息的查询和更新。所以把读和写操作分开能够有针对性的分别优化它们。例如提高程序的scalability,scalability意味着我们能够在部署程序时,给读操作和写操作部署不同数量的线上实例来满足实际的需求。
|
||||
|
||||
|
||||

|
||||
|
||||
如果你也有Unity编程经验的话会对解耦更有感触,在Unity中我们已经不能称之为解耦,而是自治,这是Unity的设计模式。举个例子,屏幕上少则可能有十几个游戏元素,例如玩家、敌人还有子弹。你必须为它们编写“死亡”的规则,“诞生”的规则,交互的规则。因为你根本无法预料玩家在何时何种位置发射出子弹,也无法预料子弹何时在什么位置碰撞上什么状态敌人。所以你只能让它们在规则下自由发挥。这和微服务有异曲同工之妙:独立,隔离,自治。
|
||||
|
||||
## 总结
|
||||
|
||||
Node 作为服务端的新人,应该学习前辈的经验。借用奔驰广告的一句话:经典是对经典的继承、经典是对经典的背叛。只有站在前人的肩膀上,我们才有可能创新,看的更远
|
||||
|
||||
|
||||
(以上文章部分参考自网络,因为本人看到后相见恨晚,和我思想观念一致,所以搬运总结于此,望共勉)
|
||||
5
Chapter3 - Server/3.4.md
Normal file
5
Chapter3 - Server/3.4.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Let's Encrypt
|
||||
|
||||
https://imququ.com/post/letsencrypt-certificate.html
|
||||
|
||||
可以创建免费的HTTPS证书
|
||||
19
Chapter3 - Server/3.5.md
Normal file
19
Chapter3 - Server/3.5.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# PHP、Mysql 乱码
|
||||
|
||||
|
||||
- 查看 Mysql 数据表字符编码
|
||||
- 查看表字段编码
|
||||
- 查看 PDO 连接编码
|
||||
- 查看 Mysql 数据库服务器编码
|
||||
|
||||
这次吃亏就是在 Mysql 数据库服务器编码导致。
|
||||
|
||||
- 查看数据库服务器编码
|
||||
```SQL
|
||||
show variables like 'character%';
|
||||
```
|
||||
|
||||
所以知道其他编码都正常,问题定位到数据库服务器编码问题。
|
||||
- vim /etc/my.cnf
|
||||
- character-set-server=utf8
|
||||
- 重启服务器
|
||||
377
Chapter3 - Server/3.6.md
Normal file
377
Chapter3 - Server/3.6.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# YAML
|
||||
|
||||
我司基础平台技术组整合了一套 cli 和相关工具,React Native、iOS、Android、Node、前端项目统一进行依赖管理、打包、提测、发布等,项目工程中有 `.yml` 文件,不太了解作用,所以本文对其进行了解和学习。
|
||||
|
||||
## 一、 YAML 是什么
|
||||
|
||||
> YAML (a recursive acronym for "YAML Ain't Markup Language") is a human-readable data-serialization language. It is commonly used for configuration files and in applications where data is being stored or transmitted. YAML targets many of the same communications applications as Extensible Markup Language (XML) but has a minimal syntax which intentionally differs from SGML .[1] It uses both Python-style indentation to indicate nesting, and a more compact format that uses [] for lists and {} for maps[1] making YAML 1.2 a superset of JSON.[2] ~[维基百科](https://en.wikipedia.org/wiki/YAML)
|
||||
|
||||
|
||||
编程难免需要写配置文件,怎么写配置有很多种方式,XML、JSON、YAML...
|
||||
YAML 是用来专门写配置文件的语言,非常简洁、强大,比 JSON 格式更简洁。
|
||||
YAML 是一种人类可读的数据序列化语言,通常用于配置文件和用于存储或传输数据的应用程序中。
|
||||
|
||||
基本语法:
|
||||
|
||||
- 大小写敏感
|
||||
- 使用缩进表示层级关系
|
||||
- 缩进时不允许使用 Tab 键,只允许使用空格键
|
||||
- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
|
||||
|
||||
`#` 表示注释,从这个字符串开始到这一行结束,都会被解析器认为是注释而忽略
|
||||
|
||||
YAML 支持3种数据结构
|
||||
|
||||
- 对象:键值对的集合,又称为映射(mapping)、哈希(hashes)、字典(dictionary)
|
||||
- 数组:一组按次序排列的值,又称为序列(sequence)/列表(list)
|
||||
- 纯量:单个的、不可再分的值
|
||||
|
||||
|
||||
## 二、对象
|
||||
|
||||
对象:一组键值对,用冒号结构表示
|
||||
|
||||
```yaml
|
||||
hobby: coding
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ hobby: 'coding' }
|
||||
```
|
||||
|
||||
Yaml 也允许另一种写法,将所有键值对写成一个行内对象
|
||||
|
||||
```yaml
|
||||
person: { age: 24, hobby: coding, title: 资深工程师 }
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ person: { age: 24, hobby: 'coding', title: '资深工程师' } }
|
||||
```
|
||||
|
||||
|
||||
## 三、数组
|
||||
|
||||
一组连词线开头的行,构成一个数组
|
||||
|
||||
```yml
|
||||
- movie
|
||||
- coding
|
||||
- music
|
||||
- bicycle
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
[ 'movie', 'coding', 'music', 'bicycle' ]
|
||||
```
|
||||
|
||||
数据结构的子成员是一个数组,则可以在该项下面缩进一个空格
|
||||
|
||||
```yml
|
||||
- music
|
||||
- coding
|
||||
- swift
|
||||
- Javascript
|
||||
- shell
|
||||
- php
|
||||
- ruby
|
||||
- python
|
||||
- cat
|
||||
- movie
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
['music', ['swift', 'Javascript', 'shell', 'php', 'ruby', 'python'], 'cat', 'movie']
|
||||
```
|
||||
|
||||
数组也可以用行内表示法
|
||||
|
||||
```yml
|
||||
coding: [swift, Javascript, shell, php, ruby, python]
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ coding: ['swift', 'Javascript', 'shell', 'php', 'ruby', 'python']}
|
||||
```
|
||||
|
||||
|
||||
## 四、复合结构
|
||||
|
||||
对象和数组可以结合使用,形成复合结构。
|
||||
|
||||
|
||||
```yml
|
||||
hobby:
|
||||
- coding
|
||||
- music
|
||||
- movie
|
||||
- bicycle
|
||||
skills:
|
||||
swift: iOS
|
||||
ruby: cocoapod
|
||||
php: server
|
||||
python: spider
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{
|
||||
hobby: ['coding', 'music', 'movie', 'bicycle'],
|
||||
skills: {
|
||||
swift: 'iOS',
|
||||
ruby: 'cocoapod',
|
||||
php: 'server',
|
||||
python: 'spider'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 五、纯量
|
||||
|
||||
纯量是最基本、不可再分的值。以下数据类型都属于 Javascript 的纯量
|
||||
|
||||
- 字符串
|
||||
- 布尔值
|
||||
- null
|
||||
- 整数
|
||||
- 浮点数
|
||||
- 日期
|
||||
- 时间
|
||||
|
||||
数值直接以字面量的形式表示:
|
||||
|
||||
```yml
|
||||
number: 12.30
|
||||
```
|
||||
|
||||
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ number: 12.30}
|
||||
```
|
||||
|
||||
布尔值用 true 和 false 表示
|
||||
```yml
|
||||
isDeveloper: true
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ isDeveloper: true}
|
||||
```
|
||||
|
||||
null 用 ~ 表示
|
||||
|
||||
```yml
|
||||
pet: ~
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ pet: null }
|
||||
```
|
||||
|
||||
时间采用 ISO8601 格式
|
||||
|
||||
```yml
|
||||
iso8601: 2019-11-27-14t21:59:43.10-05:00
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ iso8601: new Date('2019-11-27-14t21:59:43.10-05:00') }
|
||||
```
|
||||
|
||||
YAML 允许使用2个感叹号,强制数据类型转换。
|
||||
|
||||
```yml
|
||||
age: !!str 24
|
||||
isDeveloper: !!str true
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ age: '24', isDeveloper: 'true' }
|
||||
```
|
||||
|
||||
## 六、字符串
|
||||
|
||||
字符串是 YAML 中最常见,也是最复杂的一种数据类型。
|
||||
|
||||
字符串默认不使用引号表示。
|
||||
|
||||
```yml
|
||||
str: 这是一行字符串
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ str: '这是一行字符串' }
|
||||
```
|
||||
|
||||
如果字符串之中包含空格或特殊字符,需要放在引号之中。
|
||||
|
||||
```yml
|
||||
str: '内容: 字符串'
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ str: '内容: 字符串' }
|
||||
```
|
||||
|
||||
单引号和双引号都可以使用,双引号不会对特殊字符转义。
|
||||
|
||||
```yml
|
||||
s1: '姓名\n杭城小刘'
|
||||
s2: "姓名\n杭城小刘"
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ s1: "姓名\\n杭城小刘", s2: "姓名\n杭城小刘" }
|
||||
```
|
||||
|
||||
单引号之中如果还有单引号,必须连续使用2个单引号转义
|
||||
|
||||
```yml
|
||||
str: 'labor''s day'
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ s1: "labor's day" }
|
||||
```
|
||||
|
||||
|
||||
字符串可以写成多行,从第二行开始,必须有一个单空格缩进。换行符会被转为空格。
|
||||
|
||||
```yml
|
||||
str: 这是第一段
|
||||
多行
|
||||
字符串
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ str: "这是第一段 多行 字符串" }
|
||||
```
|
||||
|
||||
|
||||
多行字符串可以使用 | 保留换行符,也可以使用 > 折叠换行。
|
||||
|
||||
```yml
|
||||
this: |
|
||||
Foo
|
||||
Bar
|
||||
that: >
|
||||
Foo
|
||||
Bar
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ this: "Foo\nBar\n", that: "Foo Bar\n" }
|
||||
```
|
||||
|
||||
+ 表示保留文字末尾的换行,- 表示删除字符串末尾的换行
|
||||
|
||||
```yml
|
||||
name: |
|
||||
LBP
|
||||
hobby: |+
|
||||
Coding
|
||||
Movie
|
||||
|
||||
|
||||
sports: |-
|
||||
Bicycle
|
||||
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{ name: "LBP\n", hobby: "Coding\nMovie\n\n\n", sports: "Bicycle" }
|
||||
```
|
||||
|
||||
字符串之中还可以插入 HTML 标记
|
||||
|
||||
```yml
|
||||
message: |
|
||||
|
||||
<p style="color:red">
|
||||
yaml
|
||||
</p>
|
||||
```
|
||||
|
||||
转换为 Javascript
|
||||
|
||||
```Javascript
|
||||
{message: "\n <p style="color:red">\n yaml\n</p>\n"}
|
||||
```
|
||||
|
||||
|
||||
## 七、引用
|
||||
|
||||
锚点 `&` 和别名 `*` 用来表示引用
|
||||
|
||||
```yml
|
||||
engineer: &engineer
|
||||
name: 杭城小刘
|
||||
age: 24
|
||||
title: 资深工程师
|
||||
iOSer:
|
||||
kind: iOS
|
||||
<<: *engineer
|
||||
```
|
||||
|
||||
等同于下面的 yml 代码
|
||||
|
||||
```yml
|
||||
engineer:
|
||||
name: 杭城小刘
|
||||
age: 24
|
||||
title: 资深工程师
|
||||
iOSer:
|
||||
kind: iOS
|
||||
name: 杭城小刘
|
||||
age: 24
|
||||
title: 资深工程师
|
||||
```
|
||||
|
||||
其中: `<<` 表示合并到当前数据,`&` 用来建立锚点, `*` 用来表示引用锚点。
|
||||
|
||||
|
||||
## 其他
|
||||
|
||||
1. 有个 [NodeJS 库](https://github.com/nodeca/js-yaml)可以将 YAML 函数和正则转换为 JS 对象。反过来也可以
|
||||
2. [Ruby](https://yaml.org/YAML_for_ruby.html) 可以将 YAML 转换为 Ruby,反过来也可以
|
||||
|
||||
|
||||
参考链接:
|
||||
|
||||
- [YAML 语言教程:阮一峰](http://www.ruanyifeng.com/blog/2016/07/yaml.html)
|
||||
126
Chapter3 - Server/3.7.md
Normal file
126
Chapter3 - Server/3.7.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# Node 单元测试
|
||||
|
||||
## 为什么做这个事情
|
||||
|
||||
我在我写的文章里面多次提到单元测试的重要性。重要的事情说三遍“单测很重要”、“单测很重要”、“单测很重要”。
|
||||
单纯说这句话没公信力和权威性,那我举例子来说明吧。
|
||||
|
||||
场景1
|
||||
|
||||
某业务线在不断的版本迭代,在版本6的时候发现功能 A 的代码太乱太多了。小刘同学打算重构,他辛辛苦苦解决后,打算提交给测试工程师进行测试。测试工程师说“小刘,你这个代码全是 Bug 呀,我点进去就 Crash”。小刘听到后很尴尬,心里想“功能 A 的代码我写的很小心,一行行检查过去的,不可能有问题”。测试说“我点击商品页,点击加入购物车,马上 Crash 了”。你这个测试被打回,阻塞主流程了。小刘想了想才发现他自己开发的模块是没有问题的,但是他在重构功能 A 的时候不小心把依赖功能 A 的地方少传递了配置参数 😅
|
||||
|
||||
场景2
|
||||
|
||||
某公司基础普通组有小刘同学,在设计某个新技术方案的时候,辛辛苦苦花了3天时间出了技术方案,他去找老板聊,老板让他把思路描述下,再把设计的测试用例给一下。小刘吞吞吐吐讲完了设计思路,但是他说还没有设计测试用例。老板说你没测试用例,我怎么 review 你的设计,一行行看代码理解逻辑吗?一句句听你的设计判断有没有问题吗?以后你找我听技术设计,你理好设计思路,和测试用例。我看看主流程和一些边界的输入输出,确保这些东西正确那就是没问题的。我没有那么多时间一行行看代码。(说的也是,组长是 P9 忙得很)
|
||||
|
||||
|
||||
场景3
|
||||
|
||||
某公司基础普通组有小刘同学在做了移动端的 APM 监控和数据上报 SDK 的第一版,但是他很乖,写好了单元测试。忘了说了小刘同学是负责 iOS 端的,同事做 Android 端的小张同学和他对应,不同就是他没有写单元测试的代码。 需求下来了,需要迭代版本2,小刘和小张都开发好代码了,需要进行测试。哈哈哈小刘乐坏了,他花了0.5天就测试结束,小张花了1.5天进行测试。为什么呢?小刘写代码都要写单元测试代码,小张不写。虽然写单元测试代码可能需要花一点点时间。但是当新版本迭代的时候就不需要回头继续**全量测试**。他只要按下 `Command + U`, Xcode 就会跑单元测试相关的代码。
|
||||
|
||||
|
||||
那单元测试的好处?😂 什么?你还问好处,上面那么清晰明了,那我再总结下:
|
||||
- 确保你写的代码的每个分支都可以被覆盖,防止线上代在用户端,不小心执行到未知的分支里面
|
||||
- 在进行新版本迭代或者重构的时候,可以集中精力到新逻辑里面,旧的逻辑可以用 UT 测试覆盖率来确保
|
||||
- 在团队内进行 code review 或者 merge review 的时候,review 的人可以看代码中主要逻辑,结合 ut 来判断。
|
||||
|
||||
|
||||
另外,测试来说一般是结合 CI、CD 的,像我们公司有自己的工具 cli 工具, iOS、Android、RN、React、Vue、Node 项目都一起处理,分析依赖、打包构建(打包系统根据工程特点调度特定打包机)、测试、hot fix、埋点统计、APM等等。
|
||||
|
||||
所以如果公司规模小,就写好 UT 然后结合 lint 做一些处理,公司规模大、开发有能力则需要结合 ci、cd 将测试的能力结合进去。
|
||||
|
||||
|
||||
另外 UT 是一道工序,最好在每个开发者写代码的时候做 MR,团队内 MR +1 数大于3才可以合并到分支,且 +1 的人里面必须有一个同一个项目的同学,必须有一个同技术栈且比你高水平的人,MR 指出的问题修改好才可以合并。且 MR 代码不能太大,因为太大,给你做 MR 的同学会很耗费精力。人家阅读你代码时间成本太大。
|
||||
|
||||
## Node 侧如何进行 UT 开发
|
||||
|
||||
Node 在大学三年级的时候就听说了,也写过,之前也用来写过爬虫、自动化脚本、cli 等,之前学习过如何在 Node 侧写 ut,这篇文章用来总结下。
|
||||
|
||||
|
||||
|
||||
举个例子,有个 Node 工程,一个模块的主要功能是获取该目录下的所有文件。开发代码如下
|
||||
|
||||
```javascript
|
||||
// fetchCodeFiles.js
|
||||
const fs = require('fs-extra'),
|
||||
glob = require('glob')
|
||||
|
||||
const fetchCodeFiles = async (dirPath) => {
|
||||
return new Promise((reslove, reject) => {
|
||||
glob('**/*.?(sh|pch|json|xcconfig|mm|cpp|h|m)', { root: dirPath, cwd: dirPath, realpath: true }, (err, files) => {
|
||||
if (err) reject(err)
|
||||
reslove(files)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = fetchCodeFiles
|
||||
```
|
||||
|
||||
单元测试该怎么做?
|
||||
1. 在终端下切换到当前工程目录,安装 `npm install yamljs --save`
|
||||
2. 在工程根目录下新建 `test` 文件夹
|
||||
3. 为你需要的开发文件写测试代码。文件命名建议 `模块名称.test.js`
|
||||
4. 测试代码需要引入 `assert`。
|
||||
5. 通过 `describe` 方法、 `it` 方法、`assert` 方法描写测试代码
|
||||
6. 为了方便测试,在 `package.json` 文件中的 `scripts` 下添加描述 `"test": "mocha"`
|
||||
7. 为了更方便,我使用的是 iterm2,在 .zshrc 文件里设置别名 `alias nt="node test"`
|
||||
|
||||
提升效率的配置 .zshrc 可以查看文章: [Mac 终端效率神技](./../Chapter7 - Geek Talk/7.10.md)
|
||||
|
||||
上面开发代码的测试代码如下:
|
||||
|
||||
```javascript
|
||||
// fetchCodeFiles.test.js
|
||||
const fetchCodeFiles = require('./../src/fetchCodeFiles'),
|
||||
assert = require('assert')
|
||||
|
||||
describe("fetch all code files", () => {
|
||||
describe("fetch all code files", () => {
|
||||
it("should return 12 when code directory is '/Users/liubinpeng/Workspace/search_key/test/code'", (done) => {
|
||||
done(assert(fetchCodeFiles('/Users/liubinpeng/Workspace/search_key/test/code')) === 12)
|
||||
})
|
||||
it("should return 4 when code directory is '/Users/liubinpeng/Workspace/search_key/test/code/Classes'", (done) => {
|
||||
done(assert(fetchCodeFiles('/Users/liubinpeng/Workspace/search_key/test/code/Classes')) === 4)
|
||||
})
|
||||
it("should return 0 when code directory is '/Users/liubinpeng/Workspace/search_key/test/code/EmptyCodeFiles'", (done) => {
|
||||
done(assert(fetchCodeFiles('/Users/liubinpeng/Workspace/search_key/test/code/EmptyCodeFiles')) === 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
```json
|
||||
// package.json
|
||||
|
||||
{
|
||||
"name": "search",
|
||||
"version": "1.0.0",
|
||||
"description": "des",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "mocha",
|
||||
"start": "node src/index.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fs-extra": "^8.1.0",
|
||||
"glob": "^7.1.6",
|
||||
"yamljs": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^6.2.2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
运行结果
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
## Mocha
|
||||
|
||||
本文主要想说明的是 UT 的重要性,以及 Node 测如何做单元测试。当然 UT 没这么简单,具体深入的不是本文的终点,感兴趣的可以查看 Mocha 这个项目的[官方文档](https://mochajs.org)。
|
||||
112
Chapter3 - Server/3.8.md
Normal file
112
Chapter3 - Server/3.8.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# 数据安全(反爬虫)之「防重放」策略
|
||||
|
||||
> 在[大前端时代的安全性](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.56.md)一文中讲了 Web 前端和 Native 客户端如何从数据安全层面做反爬虫策略,本文接着之前的背景,将从 **API 数据接口**的层面讲一种技术方案,实现数据安全。
|
||||
|
||||
|
||||
|
||||
|
||||
## 一、 API 接口请求安全性问题
|
||||
|
||||
API 接口存在很多常见的安全性问题,常见的有下面几种情况
|
||||
1. 即使采用 HTTPS,诸如 Charles、Wireshark 之类的专业抓包工具可以扮演证书颁发、校验的角色,因此可以查看到数据
|
||||
2. 拿到请求信息后原封不动的发起第二个请求,在服务器上生产了部分脏数据(接口是背后的逻辑是对 DB 的数据插入、删除等)
|
||||
|
||||
|
||||
所以针对上述的问题也有一些解决方案:
|
||||
1. HTTPS 证书的双向认证解决抓包工具问题
|
||||
2. 假如通过网络层高手截获了 HTTPS 加证书认证后的数据,所以需要对请求参数做签名
|
||||
2. 「防重放策略」解决请求的多次发起问题
|
||||
|
||||
关于 HTTPS 证书双向认证和 Web 端反爬虫技术方案均在[大前端时代的安全性](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter1%20-%20iOS/1.56.md)一文中有具体讲解。接下来引出本文主角:防重放
|
||||
|
||||
|
||||
|
||||
|
||||
## 二、 请求参数防篡改
|
||||
|
||||
在之前的文章也讲过,HTTPS 依旧可以被抓包,造成安全问题。抓包工具下数据依旧是裸奔的,可以查看[Charles 从入门到精通](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter7%20-%20Geek%20Talk/7.2.md)文中讲的如何获取 HTTPS 数据。
|
||||
|
||||
假如通过网络层高手截获了 HTTPS 加证书认证后的数据,所以需要对请求参数做签名。步骤如下
|
||||
- 客户端使用约定好的密钥对请求参数进行加密,得到签名 signature。并将签名加入到请求参数中,发送给服务端
|
||||
- 服务端接收到客户端请求,使用约定好的密钥对请求参数(不包括 signature)进行再次签名,得到值 autograph
|
||||
- 服务器对比 signature 和 autograph,相等则认为是一次合法请求,否则则认为参数被篡改,判定为一次非法请求
|
||||
|
||||
因为中间人不知道签名密钥,所以即使拦截到请求,修改了某项参数,但是无法得到正确的签名 signature,这样构造的一个请求,会被服务器判定为一次非法请求。
|
||||
|
||||
|
||||
|
||||
|
||||
## 三、 防重放策略
|
||||
|
||||
在工程师文化中,我们要做一个事情,就首先要对这个事情下个定义。我们才能知道做什么、怎么做。
|
||||
|
||||
理论上,一个 API 接口请求被收到,服务会做校验,但是当一个合法请求被中间人拦截后,中间人原封不动得重复发送该请求一次或多次,这种重复利用合法请求进行得攻击被成为**重放**。
|
||||
|
||||
重放会造成服务器问题,所以我们需要针对重放做防重放。本质上就是如何区别去一次正常、合法的请求。
|
||||
|
||||
|
||||
|
||||
### 3.1 基于 timestamp 的方案
|
||||
|
||||
理论上,客户端发起一次请求,到服务端接收到这个请求的时间,业界判定为不超过60秒。利用这个特征,客户端每次请求都加上 timestamp1,客户端将 timestamp1 和其他请求参数一起签名得到 signature,之后发送请求到服务器。
|
||||
|
||||
- 服务器拿到当前时间戳 timestamp2,timestap2 - timestamp1 > 60s,则认为非法
|
||||
- 服务端接收到客户端请求,使用约定好的密钥对请求参数(不包括 signature、timestamp1)进行再次签名,得到值 autograph。比对 signature 和 autograph,若不相等则认为是一次非法请求
|
||||
|
||||
假如中间人拦截到请求,修改了 timestamp 或者其他的任何参数,但是不知道密钥,所以服务器依旧判定为非法请求。
|
||||
中间人从抓包、篡改参数、发起请求的过程一般来说大于60秒,所以服务器依旧会判定为非法请求。
|
||||
|
||||
基于 timestamp 的设计缺陷也很明显,种种原因下,60秒内的请求,会钻规则漏洞,服务器判定为一次合法请求。
|
||||
|
||||
|
||||
|
||||
### 3.2 基于 nonce 的方案
|
||||
|
||||
既然时间戳会有漏洞,那么新方案是基于随机字符串 nonce。也就是说每次请求都加入一个随机字符串,然后将其他参数一起利用密钥加密得到签名 signature。服务端收到请求后
|
||||
- 先判断 nonce 参数是否能存在于某个集合中,如果存在则认为是非法请求;如果不存在,则将 nonce 添加到当前的集合中
|
||||
- 服务端将客户端请求参数(除 nonce)结合密钥加密得到 autograph,将 signature 和 autograph 比对,不相等则认为非法请求
|
||||
|
||||
但是该方案也有缺点,因为当次的请求都需要和集合中去搜索匹配,所以该集合不能太大,不然匹配算法特别耗时,接口性能降低。所以不得不定期删除部分 nonce 值。但是这样的情况下,被删除的 nonce 被利用为重放攻击,服务器判定为合法请求。
|
||||
|
||||
假设服务器只保存24小时内请求的 nonce,该存储仍旧是一笔不小的开销。
|
||||
|
||||
|
||||
|
||||
### 3.3 基于 timestamp + nonce 的方案
|
||||
|
||||
根据 timestamp 和 nonce 各自的特点:timestamp 无法解决60秒内的重放请求;nonce 存储和查找消耗较大。所以结合2者的特点,便有了 「timestamp + nonce 的防重放方案」。
|
||||
|
||||
- 利用 timestamp 解决超过60秒被认为非法请求的问题
|
||||
- 利用 nonce 解决 timestamp 60秒内的漏网之鱼
|
||||
|
||||
步骤:
|
||||
|
||||
1. 客户端将当前 timestamp1、随机字符串和其他请求参数,按照密钥,生成签名 signature
|
||||
2. 服务端收到请求,利用服务端密钥,将除 timestamp1、随机字符串之外的请求参数,加密生成签名 autograph
|
||||
3. 服务端对比 signature 和 autograph,不相等则认为非法请求
|
||||
4. 拿到服务端时间戳, timestamp2 - timestamp1 < 60,则判定为一次合法请求,然后保存 nonce
|
||||
5. 服务端只保存60秒内的 nonce,定时将集合内过期的 nonce 删除
|
||||
|
||||
该集合不应该直接操作文件或者数据库,否则服务端 IO 太多,造成性能瓶颈。可以是 mmap 或者其他内存到文件的读写机制。根据场景可以选择乐观锁、悲观锁。
|
||||
|
||||
其中有一个 timestamp 的问题,服务器会将请求参数中的 timestamp 判断差值,其中一个致命的缺点是服务器的时间和客户端的时间是存在时间差的,当然你也可以通过校验时间戳解决此问题。时间同步请继续看下面部分。
|
||||
|
||||
|
||||
|
||||
|
||||
## 四、 计算机网络时间同步技术原理
|
||||
|
||||
客户端和服务端的时间同步在很多场景下非常重要,比如秒杀系统(页面打开,各个类目的商品展示倒计时秒杀功能。如果直接请求接口页面数据,然后拿到服务器时间进行倒计时,则会因为网络传输的耗时,导致时间不精确)、接口 timestamp 等。
|
||||
|
||||
1. 如果精度要求不高的情况下:先请求服务器上的时间 ServerTime,然后记录下来,同时记录当前的时间 LocalTime1;需要获取当前的时间时,用最新的当前时间 (LocalTime2 - LocalTime1 + ServerTime)
|
||||
|
||||
拿 iOS 端举例:
|
||||
- App 启动后通过接口获取服务器时间 ServerTime,保存本地。并同时记录当前时间 LocalTime1
|
||||
- 需要使用服务器时间时,先拿到当前时间 LocalTime2 - LocalTime1 + ServerTime
|
||||
- 若获取服务器时间接口失败,则从缓存中拿到之前同步的结果(初始的时间在 App 打包阶段内置了)
|
||||
- 使用 `NSSystemClockDidChangeNotification` 监测系统时间发生改变,若变化则重新获取接口,进行时同步
|
||||
|
||||
|
||||
2. 如果需要精度更高,比如 100纳秒的情况,则需要使用 NTP(Network Time Protocol)网络时间协议、PTP (Precision Time Protocol)精确时间同步协议了。
|
||||
|
||||
NTP、PTP 不在本文的范畴,不懂得可以查看这篇[文章](https://segmentfault.com/a/1190000005337116)
|
||||
13
Chapter3 - Server/chapter3.md
Normal file
13
Chapter3 - Server/chapter3.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# 第三部分
|
||||
|
||||
第三部分主要记录在后端技术的经验或学习心得。包括 Node、PHP、Python等
|
||||
|
||||
|
||||
* [1、利用分页和模糊查询技术实现一个App接口](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.1.md)
|
||||
* [2、网页端扫码登录实现原理](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.2.md)
|
||||
* [3、从Node.js看看服务端框架的感想](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.3.md)
|
||||
* [4、免费的配置HTTPS证书](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.4.md)
|
||||
* [5、PHP、Mysql 中文乱码](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.5.md)
|
||||
* [6、YAML](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.6.md)
|
||||
* [7、Node单元测试](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.7.md)
|
||||
* [8、数据安全(反爬虫)之「防重放」策略](https://github.com/FantasticLBP/knowledge-kit/blob/master/Chapter3%20-%20Server/3.8.md)
|
||||
Reference in New Issue
Block a user