美文网首页Android技术知识Android开发经验谈Android开发
浅谈 RESTful Api 对APP身份校验的一些思考

浅谈 RESTful Api 对APP身份校验的一些思考

作者: 李晨玮 | 来源:发表于2017-12-20 12:45 被阅读773次

    写这篇文章的初衷来源于最近心血来潮想独立开发一款自己的产品,由于本人一直从事Android开发,但产品又需要数据的支持,迫于无奈又捡起了我那半桶水的服务端开发,在做架构设计的时候遇到了一些问题,下面想谈谈在Servier/APP通讯中对用户身份安全认证的一些思考,也希望可以抛砖引玉,和大家一起探讨出更好的解决方案。

    本文章打算从下面几个点切入:
    1、认识什么是Session和Token认证?
    2、Session和Token认证分别会引发些什么问题?
    3、如何解决这些问题,还有更深层次的思考?

    Session认证

    由于HTTP请求协议是无状态的,用户每次访问服务器,对服务器而言都是全新的访客,服务器没有办法识别是哪个用户,更不用说去记录用户上一次的活动状态了,所以我们需要某种机制来维持这个关系,就这样Session诞生了,每一个Session都对应着一个SessionId(唯一标识),在这个Session里可以保存用户的各种状态信息,那服务器要怎么识别Session具体属于哪个用户的呢,此时Cookie就派上用场了,它是存放在客户端的,里面记录着一个SessionId,只要用户在请求服务器的时候,带上这个Cookie,也就可以和服务器“相认”了。

    Session认证会引发的一些问题

    1. 由于Session是存放在服务器的内存中的,随着用户量的增加,会导致服务器内存吃紧,压力和开销也明显增大。

    2. 当业务量足够大,单台服务器吃不消的时候,我们往往会做负载均衡来分担单台服务器的压力,此时就会面临到一些问题,比如用户在A服务器登录建立起Session会话,当再次访问服务器的时候,被负载均衡策略代理到了B服务器上,此时B服务器上是没有该用户的Session会话的,会导致用户需要重新登录等。

    3. 在移动端上,每次请求服务器都要带上Cookie,有点显得“过重”,并且也不是很好“维护”。

    当然,以上提到的这些点,对于现在来说已经有很多成熟的解决方案了:

    1. 比如内存吃紧,单台服务器压力过大,自动扩容就可以解决这个问题。

    2. 在做服务器负载均衡的时候,可能会导致用户丢失Session会话,只要处理好Session的保持、复制或者共享即可,比如用Nginx 做负载均衡的Session保持,Tomcat来做Session的复制,通过KV数据库来做Session的共享,比如Redis等。

    3. 要在移动端上维护Cookie,GitHub上也有一些开源框架,可以很轻松的帮我们处理这个问题。

    既然问题都可以得到解决,为什么我还想去谈Token认证,其实这只是“另外一种”解决问题的思路,能让问题更加友好,更加不费力的解决。

    Token认证

    Token认证也是无状态的,它的出现使得服务器不再需要为每个用户在内存中开辟一个Session会话,简单点说,就是在用户登录成功的时候,服务器会给用户颁发一个特殊的令牌(Token),这个令牌类似于身份证,每个用户都是不一样的,每次用户访问服务器的时候,只需要带上这个令牌,服务器便可知道具体是哪个用户,对于客户端而言,它只是一串字符串,维护起来很轻松,比如可以存放在内存,缓存文件,数据库,或者so文件中,对于服务端而言,很大程度上节约了内存的开销,只需要将用户id和token做KV的关联即可。

    Token认证会引发的一些问题

    1、如何传递Token才是比较安全的做法?
    2、如何防范当传输信息被非法盗取后发起的恶意请求?

    针对以上的问题,我们一步步的来探讨:
    首先我们对Token认证做了简单的介绍,这时候我们应该去考虑,当用户去访问服务器的时候,如果只是简单的将Token拼接在URL之后,或者附带在HTTP请求头里,这样是很容易被窃取的,由于Token是客户端和服务器之间唯一识别的凭证,只要知道了这个Token和它所携带的方式,那么就可以模拟用户的一些操作了,比如修改用户信息,虚拟财产交易,更改订单状态等。

    如何传递Token才是比较安全的做法?

    1、这里引入一个比较主流的规范:JWT

    JSON WEB Token(JWT),是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成:头信息(header),消息体(payload)和签名(signature)。

    关于JWT一些更具体的信息,大家可以在https://jwt.io上了解,这里我就不做过多的介绍了,我直接切入主题。

    JWT它是由三个部分组成的,分别是头部(Header),载体(Payload),签名(Signature),它们之间用字符串“.”进行连接,也就是:

    头部(Header).载体(Payload).签名(Signature)
    

    官方对每个部分应该包含什么信息也给出了一些建议(并不是强制要求),这里我们来简单看一下:

    头部(Header):
    由两部分组成:令牌类型,加密算法,比如:

    {
      "alg": "HS256",//加密算法为HS256
      "typ": "JWT"//令牌类型为JWT
    }
    

    载体(Payload):
    由一些有效信息组成(不应该包含敏感信息,比如用户的手机,密码等),比如:

    {
      "sub": "1234567890",//面向的用户编号为1234567890
      "name": "John Doe",//面向的用户名为John Doe
      "admin": true//是否是管理员
    }
    

    到这里,可能有朋友会问,这信息也太简单了吧,一个布尔值就可以确定用户是不是管理员,那么我手动修改下信息,那是不是也可以变成管理员了,别着急,我们继续往下看。

    关键部分,签名(Signature),它是由头部和 载体两部分经过一些编码处理和加密组成的,官方给了一个示例:

      HS256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      secret)
    

    我们可以看出这个签名是是由:
    经过Base64编码的头部 + 字符串“.” + 经过Base64编码的载体一起拼成后的字符串,再和秘钥secret进行加密算法(头部指定的HS256算法)组成。


    JWT生成示例

    如上图所示,可以我们就可以得到JWT了,我们再来梳理一下流程:
    当用户登录服务器的时候,服务器会根据用户的信息生成JWT,并保存在KV数据库里,然后回传给用户生成的JWT,用户下一次再访问服务器的时候只需要带上这个JWT,即可完成身份的验证。


    JWT交互时序图

    为什么这样可以保证信息的安全,因为签名(Signature)是由服务器通过头部和载体和加密算法组成,虽然头部和载体是可以通过Base64解码成明文的,但是我们在生成签名的时候已经确定好的里面的内容,加入用户篡改头部或者载体的信息,比如把管理员权限字段设置为true,那么和我们服务器所存的签名肯定是对应不起来的,这时候服务器就会拒绝掉该用户的请求。

    关于JWT的优点:
    1. 由JSON格式组成,所以具有跨平台性,可以支持各种编程语言。

    2. 减少了服务器的压力,相比在内存中开辟Session,存储字符串明显来的省力许多,但是这里需要注意用户的规模,虽然这里我们减少了I/O的开销,但是我们却多了加解密的过程,两者需要权衡。

    3. 解决了分布式共享Session的问题,不需要再去考虑文章开头所提到的负载均衡策略所带来的问题。

    关于JWT的缺点:

    没有什么东西是完美无瑕的,JWT虽然解决了一些问题,但是也同时带来了一些新的问题,我列举几个:

    1. 无法控制单点登录,假设用户在A设备登录了服务器,服务器正常返回JWT,用户又在B设备登陆了服务器,服务器又返回一个JWT,此时2个JWT都是可用的,服务器没办法踢出其中任意设备的用户。

    2. 当用户退出登录或者修改密码的时候,JWT没有办法回收,之前和朋友讨论过也看了一些开源项目,大多数的做法就是直接把JWT丢掉,类似删Cookie形式,但是这里还是存在安全隐患,如果被窃取,依旧会造成一些问题,因为这个JWT还是有效可用的。
      可能有些朋友会说,在载体里面设置JWT的有效时间不就好了,但是这样又会导致新的问题,这个有效时间多久合适?太短,会造成用户频繁的过期登录,太长,当发生以上问题的时候,还是要忍耐有效期内的操作。

    3. 关于安全操作,回到文章开头的问题,当JWT被抓包窃取后,是不是就可以模拟用户(有效期内)操作了?比如修改用户的订单信息,频繁的调用验证码接口等,这里涉及到URL的重放等攻击,先不做深究,有时间再写一篇文章来详细说明。

    如何防范当传输信息被非法盗取后发起的恶意请求?

    2、另外一种加密Token的方式

    整体执行流程还是不变的,只是在处理这个Token的时候,让它变得更安全(加密,时效,可控)
    具体流程:


    登录时序图
    1. 客户端在登录的时候除了账号密码一些信息外,还要额外的带上客户端的本地时间戳。

    2. 服务器验证账号密码,并求出当前服务器时间戳减去客户端时间戳的时间差,并生成一个Token,返回给客户端的信息为:用户编号(不一定是数据表的自增id,出自一些安全性的考虑,文章篇幅有限,这里就暂不拓展讲了),时间差,Token等信息,并在KV数据库(比如Redis)建立用户编号->Token关系。

    3. 客户端存储用户编号,Token和时间差,再下一次访问服务器的时候,利用访问的URL,用户编号,Token,访问时间(客户端本地时间戳 + 时间差)加密(可以是对称或者非对称加密)形成一个签名(signature),然后再重新拼接url+用户编号+访问时间+签名 去访问服务器。

    4. 服务器接收到客户端请求,首先判断访问时间是否超过当前时间30秒(自定义),如果有则拒绝请求,如果没有,根据用户编号到KV数据库获取Token,然后进行同样的算法得出签名与客户端传递的签名相比对,如果一样则方可通过,否则拒绝。

    这种方案的好处:
    1. 让访问链接具备时效性,为什么要去判断这个访问时间?就是防止链接被重用(URL重放),避免接口被抓包获取,被重复使用,这样一来,就算完整的链接暴露了,这个链接的有效访问时间也只有30秒。

    2. 解决了用户单点登录,修改密码,登出的问题,因为Token是由服务器维护的,在用户信息以上操作的时候,我们对Token进行刷新即可,利用旧的Token再生成签名自然校验就不通过了。

    关于客户端Token的保存

    这里针对客户端而外再提一下关于Token的保存,毕竟客户端存在被反编译的风险的,因为本人对Android开发比较熟悉,这里就拿Android来举例:

    1. 做好代码的混淆和加固是最根本的。

    2. 不要把这些比较重要的信息写在配置文件,缓存文件,数据库里等,因为Android系统是可以通过Root拿到高级权限的。

    3. 我们可以把Token的生成策略存放so文件中,然后用JNI去调用,因为so文件是由C++编写的,反编译难度大大增大,可能有人会说那我拷贝so文件,不就依旧可以在其他地方使用了吗?所以我们也可以采取点其他方法,比如添加包名的验证等。

    好了,文章到这里就收尾了,以上是本人对服务端身份校验的一些理解与思考,由于篇幅的限制,有些地方没有办法讲的太细,也肯定存在不足的地方(比如在登录,发送验证码,下订单等涉及到安全操作的时候使用HTTPS),希望有更好方案的朋友一起交流。

    相关文章

      网友评论

        本文标题:浅谈 RESTful Api 对APP身份校验的一些思考

        本文链接:https://www.haomeiwen.com/subject/fsnvwxtx.html