【SSO单点登录】ticket+token+redis 实现sso单点登录 && 防重放、防盗用、防篡改

【SSO单点登录】ticket+token+redis 实现sso单点登录 && 防重放、防盗用、防篡改

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 10 天,点击查看活动详情

🍳引言

大家好,我是melo,sso篇断更很久了,本次带来 sso:ticket+token+redis 的实现方案~

最近断断续续冷面翻炒redis、MySQL、sso,知识大杂烩属于是hhh

🎏本篇速览脑图

单点登录.png

🍙名词约定

全局会话

在SSO登录页面登录后,我们就认为建立起了全局会话

判定标志

SSO页面的session存在且未过期

局部会话

在各个子系统,是否已经登录过,这个我们称为局部会话

判定标志

子系统存在可行的token【未过期且有效】

ticket

SSO系统颁发给子系统的凭证,有此凭证且有效的话,表明SSO系统允许子系统去建立局部会话【生成token】

token

子系统的访问凭证,各个子系统的token是不一样的,具体视业务而定,子系统也需要配置相应的拦截器来检测token

🎯🎈SSO登录

当SSO登录页面登录成功后,会存储一份session,建立起会话,表示全局会话已存在 session我们这里就不再过多赘述,想了解更多的话可以参考本专栏往期内容

🎯用户访问流程

直接访问子系统A分成了两种情况:SSO已登录或未登录【全局会话是否存在】

我们先列出时序图,后续分析起来对照时序图会更清晰一点

ps:时序图里边涉及到了一些代码细节,不清晰的地方可以先跳过,后续讲解代码的时候再着重解读

SSO已登录

注意:这里我们分析的是SSO已登录,也就是全局会话已存在的情况

  1. melo直接访问子系统A,此时会优先判断局部会话是否存在且有效【token】

    1. 若有效则直接放行,没有SSO什么事情了,业务正常执行
    2. 若无效,说明局部会话不存在,此时去判断全局会话
  2. 接下来会跳转到SSO页面,SSO页面调用SSO服务接口,判断全局会话是否存在,发现session存在且有效

现在全局会话校验成功了,接下来的问题就是如何建立局部会话?

  1. 局部会话依赖于全局会话,所以需要全局会话去颁发ticket给子系统A【相当于一个授权的过程,允许子系统去登录】
  2. 子系统拿到ticket后,校验是否是SSO颁发的且有效,有效则解析出ticket里边的凭证【比如学号】,然后根据学号在子系统A生成局部会话token,至此局部会话也建立成功了。

SSO未登录

SSO未登录的流程,跟上边大体是相同的,只不过在SSO页面判断全局会话不存在时【session为空】,此时需要跳转到SSO登录页面,登录成功生成session后再去给子系统颁发ticket

🎯细节

SSO如何跳转回子系统A

子系统A跳转到SSO的时候,需要传递参数redirect_url,后续根据这个url就能跳转回去

ticket如何传递给子系统A

一般是从SSO跳转到子系统A的时候,拼接在地址栏后边,比如melo.com?ticket=xxx

🎈不同系统需要共用redis吗?

不需要的,我们用远程调用的方式,去调用各个系统的接口,各个系统接口内部,就能访问各自主机redis,而不需跨系统去访问其他主机的redis

🎯安全优化

ticket如何防范被篡改?

ticket里边是有用户凭证的,黑客如果篡改了ticket里边的用户凭证,比如改成黑客自己的,那到子系统A登录的时候,登录的就是黑客的身份了。

此处melo的解决方法,其实是类似jwt,颁发ticket的时候,是用jwt的加密方式【结合数字签名

此处不清晰的同学,可以参考本专栏往期内容

只要加密数字签名的私钥不泄露,黑客就没办法自行篡改凭证后,捏造出对应的数字签名。

🎈ticket如何防盗用?

ticket拼接在地址栏,安全风险还是蛮高的,如果黑客拿到我们的ticket,岂不是能直接去子系统A登录了?

此处melo的解决方法是:SSO颁发ticket的时候,获取此时用户的ip,采用JWT机制,并在payload里边绑定用户的ip,到子系统A的时候,校验ticket有效性的时候,再次获取用户ip,并解析出payload里边的ip,对照两个ip是否一致,一致说明是同一个用户。

此方法的缺点是:黑客能够看到payload里边的ip,如果黑客伪造自己ip为payload里边的,这样就能通过我们的校验了。

如何应对ip伪造

伪造ip一般是通过伪造 X-Forwarded-For 来实现的,但是客户端请求来源IP其实是不能被伪造的,因为在客户端和服务端进行通信的时候,我们需要进行三次握手,如果这个来源IP是假的,那么我们的握手是不会成功的。

此时我们可以通过nginx来获取到请求的来源IP:

proxy_set_header X-Real-IP $remote_addr
复制代码

$remote_addr 是 nginx 的内置变量,代表客户端的真实IP。

对于最外层的代理服务器,我们可以进行如下配置:

proxy_set_header X-Forwarded-For $remote_addr
复制代码

而内部的代理服务器则保留原有的 X-Forwarded-For 配置,这样,我们就可以获得真实的客户端IP了。

ticket如何设置只允许使用一次?

ticket如果可以被多次使用,也会带来一定的风险。

此处melo的解决方法是:SSO颁发ticket的时候,就把ticket存储在redis中,并设置1min过期

子系统A验证ticket有效性的时候,远程调用SSO的verify接口【接口的功能是:判断SSO的redis中ticket是否已过期】,未过期则说明有效,并删除掉该ticket

  • 若已过期,则说明已失效了,需要重新颁发

    细心的同学也许会发现,远程调用其实就解决了,不同系统需要共用redis的问题,如果是直接在子系统A去验证的话,需要去访问SSO的redis【但由于防火墙问题,redis一般都是设置仅本机可访问的】

  • 而如果用远程调用的话,是去调用sso的verify接口,此接口只需要判断本机上的redis是否存在ticket即可

🎯🎈防重放

比如我们有这样一个接口,查询支付订单的接口,调用该接口时出于安全性,需要绑定一个签名,签名由订单的场地id+时间段等信息,末尾再加上通讯双方约定好的私钥 secret ,用md5加密生成sign,作为接口的签名传递到服务端去校验

服务端用同样的信息和规则,结合 secret ,用md5加密后,判断两个签名是否一致

虽然这样解决了接口信息篡改的问题,黑客无法自行伪造签名来发请求,但如果黑客抓取到我们的一个合法请求后,其实是可以一直用这个合法请求,去疯狂调用我们接口的~

什么是重放攻击

所以,重放攻击就是:黑客拦截了我们的请求,获得我们发给服务端的一个合法请求,然后重复的发送该合法请求,若该请求耗时过长,极端调用可能会搞崩我们的系统

注意 -- 跟幂等的区别

幂等是允许执行多次,只不过执行多次后的结果依然是一样的 而防重放,是不允许执行多次,从根头上就遏制住了

🎈如何防止重放攻击?

原理其实很简单,就是让合法的请求,只能被执行一次,保证每次请求的唯一性

时间戳 timestamp

我们认为一次HTTP请求从发出到到达服务器的时间是不会超过60s的,当你发送一个请求时必须携带一个时间戳timestamp,假设值为20 。

当请求发送到服务器之后,服务器会取出服务器的当前时间, 假设为 now =100, 很明显 now -timestamp>60s,那么服务器就认为请求是不合法的。

防止时间戳被更改

一般情况下,黑客从抓包重放请求耗时远远超过了60s,但如果黑客修改了时间戳为当前时间,使得 now-timestamp < 60s 的话,似乎就能绕过我们的校验了诶?

所以此时我们会结合上文的 防篡改 机制 , 具体流程如下:

  1. 客户端对 传输的参数信息【时间戳等】+约定好的secret 进行 md5 加密,得到sign,一并传递给服务端
  2. 服务端重复该过程,对 收到的参数信息【时间戳等】+约定好的secret 进行 md5 加密,得到sign
  3. 判断是否跟客户端发送过来的sign一致,若一致则说明接口参数信息没有被篡改过

存在问题

如果黑客在60s再次发起请求的话,那还是防范不了的

随机数nonce

nonce的意思是仅一次有效的随机字符串,所以需要做到每次请求的 nonce 都不同,可以由时间戳来生成,结合 ip 地址等,进一步确保唯一性。

客户端请求接口的时候,生成随机数,发送到服务端,服务端吧 nonce 存储在redis里边,当重放请求到达时,验证发现 nonce 已经存在,则认为请求是非法的

存在问题

要保证历史全局唯一,有点麻烦,并且redis存储那么多,开销有点大

🎈时间戳+随机数

综合来看,时间戳的问题是没法防止60s内的攻击,而随机数的问题在于要做到全局唯一,而且要存储很多 nonce ,耗费空间大

所以我们其实可以结合两者,具体流程如下:

  1. 客户端生成时间戳和随机数,并且作为sign的加密参数,传递给服务端
  2. 服务端先判断时间戳是否合法,若不合法,则直接返回【省去存储随机数的开销】
  3. 若时间戳合法,再将 nonce 存储到redis里边,并且设置 1min过期

注意这里设置 1min过期 的好处在于,只需要保证在这 1min内 ,nonce不会重复即可,这样客户端生成nonce也不用太过复杂

后续那些在60s内的攻击,虽然能绕过时间戳校验,但却因为 nonce 的存在【1min过期,所以能防1min内的,且占用内存不会过多】,会被认为是非法的。

而60s以外的攻击,在时间戳校验就直接GG了。

  • 所以此方案,综合解决了两者的缺点:无法防止60s的攻击,以及存储开销大