程序员过关斩将

重复的请求并不好过滤

为什么要做重复请求的过滤呢?不过滤不行吗?

过滤重复请求很难吗?加一个请求ID不就好了吗?

每个技术难点的话题,肯定是由一个产品需求引发的,俗话说:如果没有产品经理,程序员将不需要听诊器,但是会失业!!

产生背景

重复请求能够对系统造成伤害是架构中很难避免的一个设计问题,一般情况下,读请求很少会造成致命性的故障,主要是系统的写请求,很多时候一个重复写的动作,会是我们程序员加班的缘由。比如:用户使用积分兑换物品,重复的请求会造成用户积分的重复扣减,而作为线上系统,如果日志等辅助打的不好的话,排查原因其实需要很多时间。

一般的产品经理设计系统的时候并不会涉及到这类异常情况,但是一旦出现问题,产品经理就会找到程序员骂娘,多么悲哀的故事,人家付出5分精力设计的系统,我们却要花费10分的精力去编码和维护。

重复的业务请求,有的时候对系统造成的影响很大,所以程序员在设计的时候尤其要注意,产生的原因有很多:

  • 黑客进行了拦截,人为的重放了请求
  • 客户端因为某些原因,用户在很短的时间内重放了请求
  • 一些中间件(比如网关)重放了请求
  • 未知的其他情况

道理很简单,用一张图表达的会更清爽一些

图片
image

抽象出来是不是很简单?但是落地却并非像这张图一样简单!!

从这张图上一眼就可以看到,整个过程的重点难点在于过滤器这个逻辑设计部分,这部分可以和业务代码融合在一起,有的时候也可以相分离,比如:有的网关可以内嵌脚本(比如:lua),就完全可以做到和业务无关,但是通常情况下,落地的代码却和业务息息相关。

客户端处理

客户端处理重复请求是一种可以有效过滤正常请求的手段,为什么这么说呢?当一个用户正常操作的时候,客户端完全可以利用loading的方式或者其他过滤重复手段来达到目的,比如:当用户点击一个按钮的时候,弹出loading窗口方式用户再次操作。

再比如:客户端可以设置一个类似于布隆过滤的数据结构,配合对应的过滤算法也可以达到过滤重复请求的效果。

不过,客户端的任何解决方案也只是治标不治本,毕竟,客户端在整个系统架构中,是最不可靠的终端。

请求标识

重复请求过滤的关键在于过滤器的逻辑设计,目前最常用,落地最多当属使用请求ID的方式。大体流程如下:

  1. 客户端发送请求的时候,会生成随机的请求ID,随着业务参数一起传送到服务端
  2. 服务端会根据传送上来的请求ID做是否重复的判断

服务器的判断逻辑其实有很多落地方案了,比如最常见的利用redis来存储请求ID,以下是伪代码(NetCore):

public class Para
{
    public string ReqId{get ;set ;}  

    //其他业务参数
}

public bool IsExsit(Para p)
{
    //利用redis来判断当前的key是否存在
     bool isExsit=redisMethond(p.ReqId);
     //如果存在,则说明是重复请求,如果不存在说明不是重复请求,并且添加到redis
     if(!isExsit){
         AddRedis(p.ReqId);
     }
     return isExsit;

}

一般网上的文章都到此为止了,这种方案有没有问题呢?答案:有

问题1

正常的客户端重复请求,一般情况下真的会根据我们写的代码过滤掉重复请求,为什么说一般情况呢?那是因为分布式的原因,极限情况下也会导致重复的请求到业务处理端,比如以下情况:

  1. 请求被路由到了A服务器,A服务器会去请求Redis,判断是否有相同的请求ID存在,如果是第一次请求,Redis会返回不存在
  2. 同样的时间,客户端或者黑客重放了同样的请求,这个请求被路由到了B服务器,B服务器同样会请求Redis来判断是否存在,这个时候由于A服务器还没回写Redis,所以B服务器得到的结果也是不存在该请求
  3. 这样就导致了业务端收到了两次同样的请求,会导致业务不可预期的结果

可见,一个小小重复过滤请求,可能还需要分布式锁的出场才可以

问题2

即便请求中加了唯一的请求ID,但是这个ID并没有安全保证,或者说,这个ID是可以篡改的。当黑客拦截到请求,随便改一下请求ID,在重放就搞定你了。所以,加的请求ID,还需要一个安全机制来保证安全,不然这个参数其实意义不大。

业务签名

由于单纯添加请求ID,并不能解决问题,所以我们需要一种保证请求ID的机制,目前来看,普遍的落地方案是根据业务参数生成摘要,也就是所谓的加签操作。加签操作可以有效的防止参数被篡改。如果你做过微信相关的开发,你会发现和微信服务器的交互也是基于加签操作的。而生成的签名可以作为请求ID,以下是伪代码:

    //客户端生成签名
    string sigh=MD5($"参数1=值1&参数2=值2&time=当前时间戳")

以上只是例子,虽然MD5算法有产生重复数据的可能性,但是对于当前这个业务场景来说足够了。细心的同学会发现,参数当中加了一个时间戳的参数,这个是我故意加的,这个时间戳在这个场景下会出现问题,什么问题呢?

时间戳问题

当前的请求场景是要过滤重复的请求,什么样的请求算是重复请求呢?关键是这个定义要明确,我看了很多重复过滤请求的文章,重复请求这个概念其实定义的不好,这个是和具体业务场景相关的。举个栗子:当用户一秒内重复点击某个按钮算是重复请求,那10秒内重复点击呢?用户一秒之内对同一个商品下单算重复请求,那10秒内呢?

这个定义就涉及到了上面所说的时间戳参数的问题,时间戳是否要参与生成签名,要根据具体的业务场景来定义,不过,我还是要建议,请求的参数中带上时间戳,无论它参不参与签名,至于为什么这么做,当时间长了你就知道了

写在最后

过滤重复请求这个需求,并没有像想象中那么容易,并非只要加上一个请求ID就完事了,它涉及到安全以及分布式的问题,在某些场景下(比如:秒杀)还会涉及到性能以及高可用等非功能性问题,所以那些说:只需要一个请求ID就能过滤的同学,请不要再误导别人了,技术是神圣不可侵犯的。

还是那句话:具体的业务影响到具体的代码实现,脱离业务讲架构其实就是耍流氓

领导说我的类的职责不单一

为什么类的职责要单一化?

类的职责单一化很容易吗?

首先,我要提醒一下看到这篇文章的同学,我认为保证类(一定是类吗?)的单一职责并不容易

软件开发过程中,自古就流传着几大规则,无论如何这里都要和大家阐述一遍

单一职责原则

一个类应该只有一个发生变化的原因

开闭原则

软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。这个原则是诸多面向对象编程原则中最抽象、最难理解的一个。

里氏替换原则

所有引用基类的地方必须能透明地使用其子类的对象,换句话说,子类在任何引用基类的地方都可以替换成子类。

依赖倒置原则

这个原则说的详细一点其实可以概括为两点:

  1. 高层模块不应该直接依赖于底层模块,应该依赖于抽象
  2. 抽象不应该依赖于具体实现,具体实现应该依赖于抽象
接口隔离原则

程序不依赖于不使用的接口,换句话说,一个程序只依赖于它需要的接口。

单纯从概念上讲呢,单一职责原则可算是最简单易懂的一种原则了,就像设计模式中的单例模式一样无趣,是这样吗?

谁的职责

说实话,看过不少讲解“职责单一”设计原则的文章,都是以类来阐述。其实我觉得不对,职责单一设计原则本质上是软件设计原则的一种思想,具有指导意义。至于谁的职责需要单一,是一个伪命题,不仅仅指面向对象编程中的类,系统的模块,甚至于微服务在架构设计中也应该遵循此规则。

在面向对象设计的理解中,程序最基本的组成单位是类(class),多个类组成模块(module),多个模块组成服务(service),多个服务组成系统(system),一般的软件系统都会存在以上几个概念。

无论是类,还是模块,还是服务,还是系统,我认为设计的时候都要保证“单一职责”。

单一真的容易吗

说到“单一”职责,每个人都有不同的看法

class UserInfo
{
    //用户id
    public int UserId{get ;set ;}
    //用户登录账号
    public string Account{get;set ;}
    //用户登录密码
    public string Pwd{get ;set;}
    //用户姓名
    public string Name{get ;set ;}

}

以上是最常见的用户信息实体,你认为它职责单一吗?说一说,我自己的看法:

站在用户信息的角度来说,这个类代表的是用户信息,它就是单一的,这也是大多数人的看法,有错吗?其实没错。因为在当前场景下,它确实是这样。

随着业务的发展,用户的信息字段会越来越多,比如:用户的年龄,性别,学历....等等。看着越来越大的UserInfo类,是否该拆分呢?

这个时候我觉得你可以根据用户信息的类型来进行拆分,毕竟大而全的类其实并不好。怎么拆分呢?比如:可以根据用户登录场景拆分出用户认证的类型

class UserAuth
{
     //用户id
    public int UserId{get ;set ;}
    //用户登录账号
    public string Account{get;set ;}
    //用户登录密码
    public string Pwd{get ;set;}
}

可以根据用户信息在系统中的出现频率和重要度拆分出用户基本信息和用户扩展信息

class UserBasicInfo
{
     //用户id
    public int UserId{get ;set ;}
     //用户姓名
    public string Name{get ;set ;}
    //用户手机号
    public string Phone{get ;set ;}

    //其他基本属性
}
class UserExtendnfo
{    
     //用户邮箱
    public string Email{get ;set ;}
    //用户QQ号
    public string QQ{get ;set ;}

    //其他属性
}

当然这里我只是举个栗子,如果用户的Email和手机号一样常用,可以把Email属性提到基本属性中。

以上只是以用户信息为例,根据不同的用途进行拆分的一个栗子。在不同的业务背景下,不同的业务阶段,对同一个类的拆分可能会有很大不同。有的时候,你所认为的"正确“会随着系统的发展慢慢变成”错误“,当然这种”错误“并不可怕,毕竟系统的架构都是慢慢迭代出来的。

总之呢,评价一个类是否一定满足单一原则,并没有一个统一标准和规范,在实际的开发中,也没有必要进行过度设计,在项目初级,完全可以是一个满足业务需求的大而全的类,随着业务的发展,你必然会经历拆分的过程,这也是软件发展的必然阶段。

以上只是针对类这个最基本的面向对象单位来聊了聊,上升到模块以及系统也是一样的道理,微服务也是随着软件开发的不断演进而出现的,其实从职责上来看,微服务也是职责单一原则的产物,而这个这则单一更多的是倾向于业务单一性,并非功能单一性。

那职责拆分的越细越好吗?我不这么认为,当一个类或者模块甚至系统,被拆分过细的时候,就会面临着维护的问题,拿微服务来说,当微服务的数量过多,就会面临着治理等一系列问题,这也是K8s要解决的问题之一。

拆分原则

说到底,虽然职责单一很难在主观上给予准确判断,但是还是有一些通用规则可以借鉴,这里以类为例

  • 高内聚。系统在修改任一功能的时候,只需要修改一处地方,如果你需要修改多处才能满足某个需求,很有可能你的职责划分的不合理
  • 属性过多。当一个类属性过多的时候,可以考虑把这个类进行职责的拆分。而至于多少个才算多呢?当查找某个属性令你头疼的时候,说明已经到了可以拆分的程度了(自己杜撰)
  • 依赖过多。当一个类型中依赖的资源过多的时候,可以进行拆分
  • 独立变化。当一个类的某些属性被大量使用而且会经常变化的时候,可以考虑把这些属性进行拆分成独立的类。

说到职责单一,这里顺便提一下接口的设计,接口的设计更要遵循职责单一的原则,接口本质上是对业务的抽象,不同的业务应该抽象成不同的接口,以保证每个类,每个模块,每个系统都可以独立扩展。

论系统设计的高可扩展性

写在最后

没有绝对好的方法可以让所有人都认为你的拆分是正确的“职责单一”,有的时候,怎么样才能职责单一真是要靠“灵感”

少年派登录安全的奇幻遐想

据说,这篇也是快餐,完全符合年轻人口味

说到登录,无人不知无人不晓。每一个有用户体系的相关系统都会有登录的入口,登录是为了确认操作人的正确性。说到登录安全,其实是一个很伟大的命题,不过常用的手段也不过尔尔。

避免明文

这个设计到用户凭证信息的表设计,切记避免明文存储用户的密码信息。还记得以前很多大厂的密码泄露事件吗?

在数据表的设计中,除了用户密码的摘要列之外,需要添加所谓的“salt”列,其实是随机生成的一个字符串,用于和用户密码的摘要联合生成最终的摘要。

loginName salt pwd
182xxxxxxxx 随机字符串 散列值
182xxxxxxxx 随机字符串 散列值

如果非要写一个过程的话:

  • 当用户首次注册的时候,系统随机生成salt,然后和密码按照规则拼接成一个字符串,然后求散列值,并存储在pwd列中
  • 客户端请求登陆接口,上传用户的账号和密码,这里的密码推荐md5的摘要(js也可以生成md5)
  • 服务端接收到请求,根据用户的账号查询对应的salt
  • 把上传的密码和salt根据规则拼接,然后生成摘要
  • 把上一步生成的摘要和数据库的pwd进行对比,相同则登录成功,不同则登录失败

为什么非要加入salt呢?有了salt不仅可以加大黑客破解的难度,而且同样密码的用户存储的pwd列也不相同,在用户信息安全性上又提高了一点。

验证码

验证码是一种比较廉价的但是很有效的防止别人乱搞的手段,它通过一种只有真人才能识别的防伪手段来阻止危险。

图片
image

以上是12306的登录界面,看验证码的方式,是不是已经骚到了极点。如果你的登录接口不希望别人暴力破解的话,验证码是必须的。

对于普通的网站,验证码程序其实可以做的很简单就足够用了,就像以下

图片
image

用到的技术是服务端把验证码的内容绘制在一张带有纹路的图片上,把码的内容存储在一个地方,并分配一个key,把这个key返回客户端,当客户端登录的时候携带者这个key和用户填入的验证码内容来确定验证码是否正确。

我曾经看过有人把验证码的校验放到客户端,要记住,客户端其实是无安全可言的,哪怕是那些做了混淆的App。

手机验证码

目前几乎所有的系统都支持手机验证码登录,为什么这么普及是有原因的。

  • 首先,这种方式便捷,用户无需记住密码,试想一下,用户要记住自己常用的几十个网站密码是很难的,而且手机现在几乎都不离身
  • 其次,手机验证码方式安全系数比较高,因为手机号现在都采用了实名制,手机号被盗的可能性比较小,而且现在的手机都有指纹锁,就算手机丢了也不怕
  • 最后,系统都采用手机号登录,可以高效的拉进和用户的距离,而且也有利于国家的监管工作,毕竟根据手机号就可以追踪到用户的所有信息了
设备号

登录的时候把当前设备的标识上传到服务端进行识别,我觉得对于登录来说很重要。为什么呢?

在现在App漫天飞的时代,在App上是要实现自动登录的,换句话说,用户登录过一次这个App,当用户下次打开的时候,需要实现自动登录,这在用户体验上会比每次都登录好很多。但是这就面临着一个问题:需要把用户登录的凭证保存在本地,切换到浏览器中,这些凭证信息可能会保存在Cookie中或者local storage中,当然凭证肯定是要加密的。我们要保证的是这些凭证就算是被黑客知道了,也不能正常登录。

那怎么才能保证呢?答案是设备。在用户的登录请求中一定要上传设备号(浏览器也可以用js生成的),服务端存储着用户的有效设备列表,当然这个有效设备需要产品经理给出明确的定义,比如最常见的:登录过5次的设备。当然说到设备还有一个主设备的概念,至于怎么样才能定义主设备,也是需要产品方给出定义的,像最常见的:手机端是主设备。像微信现在登录pc端是需要手机端扫码的,切换到业务,可以看做需要主设备确认的请求才能执行。

安全设备概念在多点登录的场景下非常有用,尤其是需要互踢的需求下。

登录时间

服务端一定要记住用户最后一次的登录时间,在很多情况下需要记住用户在某个设备上的最后登录时间。这样做不止是为了记录分析用户的登录行为,还可以分析长期未登录的用户,使他的登录凭据失效,强制他重新登录。

HTTPS

虽然一个证书每几个钱,但是https起到的作用在安全性上还是很大的。本质上它采用的也是加密算法,比http要耗费cpu,传输速度上要慢一些。但是它可以有效的防止中间人劫持,防止用户信息外漏,而且可以防止被钓鱼网站攻击,有效识别网站真实身份,像其他的有利于SEO,地址栏出现安全锁等就不说了。

写在最后

以上所说只是一些最常见的手段,除此之外,比如IP黑名单机制,限流机制等都可以加固登录的安全。

快不快?希望各位把登录安全在留言区做补充!!

 

posted @ 2022-08-27 18:11  CharyGao  阅读(51)  评论(0编辑  收藏  举报