Fork me on GitHub

鉴权二三事

 知晓事务的发展,才能理解其本质。

  聊到鉴权,其实可以当成一本编年史来看…

  我们都知道HTTP协议的一个特性是无状态。在早期web应用中,基本都是在不同页面跳转然后请求对应静态资源,这种无状态并不会造成什么影响。但随着互联网的高速发展,应用场景马上变得复杂起来,比如服务端须要根据不同用户区分定制状态(如保持用户登陆态、商城购物车记录等等),彼时并没有一个能够识别用户身份的方案,于是一系列的鉴权方案应运而生…

  先说Cookie,它是服务器(设置响应头键Set-cookie,值<cookie名>=<cookie值>)发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。主要应用如下(via MDN):

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等),可以看看这篇第三方cookie应用,其中有一个混淆点需要搞清楚:第三方Cookie依旧只能获取第三方对应服务端返回的Cookie信息(同Domain),当前页面即第一方Cookie是无关联的;具体实现主要是通过在不同页面投放广告来收集。

问题&处理

存储

  在早期web开发中,其还充当过一阵浏览器端的数据存储方案,不过本身空间有局限(4KB),现已被sessionStoragelocalStorageIndexedDB替代。

安全

  另外,Cookie容易被利用进行XSS攻击(反射、DOM、存储),不过本质上还是去拿document.cookie。我们可以在后端返回响应头设置HttpOnly属性来防范。HttpOnly可以限制浏览器端的document.cookie返回,指定了HttpOnly属性的Cookie值将不会出现在返回值中;

  CSRF我认为是一个比较容易混淆的概念,大概理解下就是诱导用户进入恶意页面,触发一个同源指向的接口,比如用户之前登陆了http://bank.example.com,有一个cookie的登陆态,在第三方的接口被触发了一个提款的操作http://bank.example.com/withdraw?account=bob&amount=1000000&for=mallory,由于是同源,之前的cookie就会被携带,攻击者就可以在用户不知情的情况下成功提现。对于这种情况一般有验证码Referrer检测的方案规避。

流量

  在访问web应用后,随后对服务端的请求,会携带服务端返回的Cookie信息(Header),无形间增大了对服务器的流量消耗。

跨域

  Cookie的跨域发送须客户端和服务端同时支持,客户端设置xhrwithCredentialstrue,服务端设置Access-Control-Allow-Origin为对应Domain(不能为*),Access-Control-Allow-Credentialstrue。见逼乎一文

属性

  Cookie的属性如下(via Chrome控制台):

  • Name: 键。
  • Value: 值。
  • Domain: 标识定义Cookie应该发送给哪些URL 若没有指定,默认为当前文档的主机(不包含子域名);如果指定了Domain,则一般包含子域名。
  • Path: 标识指定了主机下的哪些路径可以接受Cookie(该URL路径必须存在于请求URL中)。以字符 %x2F (“/“) 作为路径分隔符,子路径也会被匹配(即主机中服务端对Cookie的共享访问)。
  • Expires/Max-Age: 有效时间(绝对),受客户端时间影响。
  • HttpOnly:前文已述。
  • Secure:由于HTTP本身对内容是明文传输的,若是有人劫持了中间信息,Cookie就会被盗用,设置Secure就是限制对应Cookie仅在HTTPS下才能够传输。
  • SameSite: 处于实验阶段,使用请考虑兼容性。跨站请求的Cookie不会被发送,用来防止 CSRF 攻击和用户追踪,设置配置参考阮老师这篇

Session

  Session其实是和Cookie配合食用的。区分于Cookie存储在浏览器端,Session会在服务端存储。存储方式取决于实际业务场景,一般会在全局空间中维护,当用户登出或超过服务端设置有效时长后主动进行清理,亦或极端(断电)情况下,被动清空。所以在大型应用中,为了保证可用性,通常会在Redis或自身库中维护,亦有Nginx根据访问IP定制固定服务IP的做法(最终目的就是分享、容错、高可用)。

  简单理解Cookie和Session联系就是:用户发起登陆,向服务端传递usernamepassword,服务端验证通过后根据一定加密规则(SHA-256摘要处理、加盐等)返回一个sessionId串,客户端接收该值存到Cookie内,在之后的每次请求中携带。图示如下:

Token & JWT

  从前文梳理下来,可以发现用Cookie-Session做鉴权大致存在三个问题。其一是安全性,XSS、CSRF都可能会泄露Cookie;其二就是在大型应用中需要共享这个Session(如阿里登陆天猫再访问其他阿里的网页同样会保持登陆态),工程量大,容灾还得做得屌;最后则是跨域的限制。

  那有没有更好的方案呢?JWT(JSON Web Token)就是一个不错的选择。不过在开始前,我们先理一下Token和JWT的关联。先从字面上看,显然Token表述的范畴更大,它是一个凭证令牌,至于生成方式并没有标准定义,用什么算法、生成长度、格式都没有要求。以我自己实际经历的项目而言,就是使用Token鉴权(一个项目是JWT、一个是普通TOKEN)的。

  先看普通TOKEN,后台通过定制的规则对用户登陆信息进行处理,然后全局维护一个字典结构(后端小哥讲的,咱也不敢多说),将生成的accToken返回给前端,前端在之后的每一次请求头内携带…所以似乎这样的做法只是相当于将凭证从Cookie里改到了请求头的自定义属性中,提升了一定的安全性以及消除了跨域不能传递的问题。

  再看JWT,一言概之就是它是TOKEN这个范畴下的一种具体实现。它与前文中鉴权方式的最大不同点在于它用于鉴权的数据都是存储在客户端的,服务端不会在鉴权上消耗额外空间,自然也省了一大部分开发和维护成本。

  JWT的原理也很简单,就是在服务端认证后,返回一个JSON对象。前端对该JWT进行存储,通常是localStorage,之后请求放入头的Authorization中,交互流程其实都差不多,见下图:

  再看JWT的结构:

  • Header(头部)是一个JSON对象,内容是描述 JWT 的元数据,通常是定义签名的算法和Token的类型。最后会通过BASE64URL算法(同BASE64转法基本相同,不过额外有一个替换规则,=被省略、+替换成-/替换成_)转为字符串。

  • Payload(负载)存放实际需要传递的数据 默认是不加密的,不要把秘密信息放这 最后会通过BASE64URL算法转为字符串。

  • Signature(签名)对前两部分的签名,防止数据篡改。

1
2
3
4
5
6
// Header 里面指定的签名算法(默认是 HMAC SHA256)
// 服务器 指定一个 密钥secret
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

  上面三个部分字符串通过.连接后返回给用户:

OAuth2.0

  OAuth 2.0 是一个行业的标准授权协议。OAuth 2.0 专注于简化客户端开发人员,同时为 Web 应用程序,桌面应用程序,手机和客厅设备提供特定的授权流程。

  它的最终目的是为第三方应用颁发一个有时效性的令牌 token。使得第三方应用能够通过该令牌获取相关的资源。比较常用的场景就是第三方登录,举个例子,当我初次登陆掘金社区时,需要注册账号,但我又不想直接在里面注册,下面就有些第三方账户登陆方案,比如github,通过从第三方授权(token)的方案登陆该页面(虽然也可能要绑定一些东西)。之后再登陆,可以不必输入账号密码,直接选择第三方登陆途径即可登陆(前提是你第三方的账号也处在登陆态)。

  这其中的权限又是如何获取和传递的呢?可以见下图:

  Resource Owner可以理解为我们的掘金社区平台,Client是我们的浏览器,充当一个中间人的角色。Authorization Server是github的服务端,所以综合来说就是掘金经我们的浏览器向github申请了一个短时Token,然后再到自身的服务里注册构建联系。