最近做一个对外提供服务的平台,涉及到一些需要支持多平台多方式登录的可扩展设计。 公司内部的身份认证方案比较成熟,回想起来自己很久没有做过登录与认证相关的事情了,初接手时对原理性的东西竟感受到了一些陌生。
诚如初中高中时老师们说的,好记性不如烂笔头,之前做过的学过的东西疏于总结,慢慢的就忘记了。干脆上网查查资料,把相关概念的理解在这里做个归档吧,日后再忘记了,看起自己的文字来总还是好回忆一点。

1. 简单账号密码登录

最传统的登录方法当然是用户名密码校验登录了
image.png
流程虽然简单,也有许多细节,目前能记起来一部分:

  • cookie存储要http only,避免因前端xss漏洞导致cookie泄露
  • 密码不能存储明文,可存为加盐后的哈希值
  • 所有服务器节点/进程需要能共享session,所以需要一个统一的session存储层
  • 尽量使用https协议,防止中间人攻击

这种登录模式下,登录态只对当前域名起作用,如当前应用是 app1.oa.com。假设企业内有其它的应用app2.oa.com、app1.asd.com,那么得另外开发两套登录系统,这显然是不合适的。那么就需要一套多应用共用同一套登录逻辑的系统。

2. SSO —— 单点登录

2.1 同顶域单点登录

由账号密码单应用登录流程,我们可以很容易想到,想要共享登录态,只需要客户端能共享cookie,服务端能共享session存储就行了。

服务端共享session存储很简单,而客户端,只需要在写入cookie的时候,将cookie写到顶域中去就行了,例如app1.oa.com,cookie写到oa.com下面,这样app2.oa.com也可以读取到cookie,然后在后端与app1.oa.com使用同一套session存储即可。

image.png

同域名下有很多app时,可以抽出一个app,如 sso.oa.com,所有的appx.oa.com未登录时都跳转到sso.oa.com,在sso.oa.com上验证登录态,再重定向回appx.oa.com并种.oa.com的cookie,这样就实现了同域名下的单点登录。

image.png

2.2 不同顶域单点登录

上面讨论的是同顶域下的情况,假设企业内此时还有一个应用,app1.asd.com,也想用sso.oa.com来登录,咋办呢?
现在的问题在于,访问sso.oa.com的时候,sso应用不能往asd.com下跨域写cookie了,难么sso.oa.com就得换一种方案来告诉app1.asd.com,用户是否在sso上登录、登录者身份等信息。

image.png

ticket的实现有很多,例如在sso.oa.com中将用户名、有效期等信息对称加密成ticket,或者将一条存储记录的索引id作为ticket,在sso.oa.com/valid校验成功后返回给app1.asd.com,这样asd.com域下的app也能使用sso.oa.com作为单点登录服务了。

sso在实践的过程中可能会有很多变种,例如访问sso.oa.com/valid这一步不再有appx.asd.com这些应用服务来实现,而由一个统一的接入层服务来做,并把验证后的结果通过header传递给appx.asd.com服务等等。

2.3 单点登录实现——CAS

前面说到的是原理的简单理解,真正的业界企业级开源单点登录解决方案叫 CAS(Central Authentication Service,中央认证服务),上面的sso.oa.com鸟枪换炮一把,就是CAS解决方案中的cas server,而各种app服务中校验的模块就是cas client。

CAS协议 中很详细的描述了CAS单点登录实现的各个细节,因时间有限这里不展开看了。

3. Oauth2.0授权

单说单点登录,使用oauth2.0授权,也是可以做CAS解决方案中登录校验的部分的,但是oauth2.0除了做身份校验外,还有个很重要的特性,是可以做细粒度的,用户级别的授权,这个是CAS做不到的。

oauth2.0将整个系统中的角色分为 HTTP服务提供商(认证服务器 + 资源服务器 )、第三方应用、用户、用户代理(如浏览器),协议要解决的问题是:
http服务提供商(如微信),授权第三方应用(如某h5活动页),在用户代理(如浏览器或微信客户端)中,由用户(经过认证服务器)允许后,从资源服务器中获取某些用户资源(如昵称、头像)

oauth2.0有4种基础认证方式,分别是:授权码模式、简化模式、账号密码模式、客户端授权模式。详见rfc6749,这里搬运一下,另外写点简单的理解,以备忘。

3.1 授权码模式

image.png

A) 访问第三方应用,第三方应用重定向到认证服务器页面

B) 用户在用户代理中授权,并发送给认证服务器 (说人话:用户在网页上勾选允许访问资源并点击确认按钮,发送请求给认证服务器)

C) 认证服务器浏览器中重定向到第三方应用的callback页面,并携带授权码

D) 第三方应用在服务器端将授权码和重定向uri发给认证服务器

E) 认证服务器将access_token和refresh_token返回给第三方服务器

至此,认证流程完成,后面第三方服务器就可以拿着access_token愉快的去资源服务器拉数据了。

其中A)步骤中,授权请求时携带的参数包括:

  • response_type REQUIRED. Value MUST be set to “code”.
  • client_id REQUIRED. The client identifier as described in Section 2.2.
  • redirect_uri OPTIONAL. As described in Section 3.1.2.
  • scope OPTIONAL. The scope of the access request as described by Section 3.3.
  • state RECOMMENDED. An opaque value used by the client to maintain
    state between the request and callback.  The authorization
    server includes this value when redirecting the user-agent back
    to the client.  The parameter SHOULD be used for preventing
    cross-site request forgery as described in Section 10.12.  
    

这里要注意的是,state参数是用来防止csrf攻击的,当我们作为第三方应用的开发者时,这一点要尤其注意,攻击与防范的流程如下:

image.png

图中的state举例是存在localStorage中的随机数, 实际实现的时候可以有很多中,例如存在客户端cookie中、存在后端session中等,其目的是保证每个用户产生的会话都有不同的state数据,这样攻击者诱导受害用户时,第三方应用就可以发现两者state的不一致,从而拒绝服务,提高流程安全性。

D) 步骤中,一般需要认证服务器分配给第三方应用的appid/appsecret来对认证请求做个请求签名,appsecret只有第三方服务器和认证服务器知道,提高请求的安全性。

有的开放平台例如微信等,会给用户提供多层级的应用管理,第三方开发者可以在认证服务器中申请一个父应用,副应用下可以有很多子应用。 每个用户在每个子应用上有唯一的openid,在父应用上有统一的unionid。

3.2 简化模式

简化模式下,不需要由第三方应用的服务器再拿着授权码找认证服务器换取access_token,而是再浏览器中直接返回access_token,流程如下:

image.png

rfc中的流程描述已经是比较清晰了,这里有一点要注意的是,C步骤中,认证服务器把access_token给到三方应用的方式是:

HTTP/1.1 302 Found
Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=example&expires_in=3600  

有个细节是token不是用url query参数传递的,而是用哈希传递的。这里是因为oauth2.0协议本身没有规定传输协议,用户可能用的不安全的http协议,为了利用浏览器不会把哈希参数传递到服务器,来规避中间人攻击,从协议上尽量让简化认证的流程更安全一些。

image.png

上图是一个针对不安全的http第三方应用中间人攻击,在协议使用url query参数传递token时攻击的流程,用hash传递参数则可以从协议层面上避免此问题,只要认证/资源服务器都使用安全协议,理论上第三方应用在中间人攻击的情况下也可以保证token通信的安全性。

3.3 密码模式

image.png

这种模式下,用户把账号密码直接交给第三方应用,第三方应用通过账号密码直接找认证服务器换取tokne。

一般只有在极度信任第三方应用时才采取此模式,例如三方应用是操作系统本身,或者极高可信度的应用。这种模式有时候也用于互联网服务自身的客户端登录方式,由之前的http账号密码登录,切换为账号密码换token,后续用token操作的oauth模式。

3.4 客户端模式

image.png

这种模式下客户端直接在认证服务器中注册,然后以客户端名义无条件获取access_token,这种模式不知道有啥应用场景,反正一般用不到就是了,后面知道了在这里补充下。

3.5 token的类别

前面我们了解到,oauth2.0的整个流程里边,流程中的人有三个角色:用户、第三方应用开发者、互联网服务开发者。用户只需要点授权按钮就可以了;第三方应用开发者则需要开发接入鉴权服务器的代码,包括授权码的传递、state一致性验证等细节;而互联网服务开发者,则需要关注认证服务器本身的实现,例如简化模式下通过hash传递session等。前面环节提到的token,在认证服务器中的实现还有点区别。

token一般需要包含clientId、userid、scope(权限范围)、有效期、token类型等等信息,在认证服务器中由不公开的秘钥对称加密,然后下发给客户端

Bearer Type Access Token

rfc6750文档对Bearer Type Access Toke的语法与使用提供了协议参考,rfc6750是对oauth2.0协议的rtf6749文档的一份补充。

bearer token应该使用安全的TLS连接来通信,这也以为这,认证服务器必须用https协议来提供服务,用http协议的话就会有很大的安全风险。

bearer token的语法:

b64token    = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
credentials = "Bearer" 1*SP b64token

这是一个ABNF语法格式描述规范描述的格式,认证服务器应该按照此格式来生成bearer token。

规定bearer token使用只能由三种方式传输

  • 使用http header Authorization: Bearer mF_9.B5f-4.1JqM,这种方式应该优先支持
  • 使用http post application/x-www-form-urlencoded表单 access_token=mF_9.B5f-4.1JqM
  • 使用uri query参数 GET /resource?access_token=mF_9.B5f-4.1JqM,这种方法客户端必须指定Cache-Control头为no-store,认证服务器在response的时候也要指定Cache-Control头为private

例如:

GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer mF_9.B5f-4.1JqM  
POST /resource HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded

access_token=mF_9.B5f-4.1JqM
GET /resource?access_token=mF_9.B5f-4.1JqM HTTP/1.1
Host: server.example.com

这三种方法由先到后优先级由高到低,客户端在高优先级的无法使用时才能使用低优先级的,认证服务器实现时应该三种都支持。

另外就是一些协议的细节,例如认证失败的返回之类,这里不赘述。

MAC Type Access Token

oauth-v2-mac-token草案中提到了一些mac token的实现细节。前面提到bearer token必须在安全的网络环境下使用,而现在https虽然慢慢变得主流,但是仍不排除部分场景下会使用http通信。 mac token就是为了解决这个问题。

它的实现关键点在于认证服务器下发的token中引入了下面这些属性

MAC key identifier:  h480djs93hd8
MAC key:  489dks293j39
MAC algorithm:  hmac-sha-1
Issue time:  Thu, 02 Dec 2010 21:39:45 GMT

客户端利用这些属性,以及客户端生成的随机字符串来计算签名。利用token请求资源时,认证服务器中用同样的算法来校验签名,保证传输的可靠性。

例如:

认证服务器下发mac token时,附带

MAC key identifier:  h480djs93hd8
  MAC key:  489dks293j39
  MAC algorithm:  hmac-sha-1
  Issue time:  Thu, 02 Dec 2010 21:39:45 GMT

客户端计算用于签名的字符串元素

Age:  264095
Random string:  dj83hs9s
Nonce:  264095:dj83hs9s

拼接签名串:
264095:dj83hs9s\n
GET\n
/resource/1?b=1&a=2\n
example.com\n
80\n
\n
\n

使用hmac-sha1算法,489dks293j39作为秘钥,计算得签名串为

SLDJd4mg43cjQfElUs3Qub4L6xE=

最终客户端请求资源时

GET /resource/1?b=1&a=2 HTTP/1.1
Host: example.com
Authorization: MAC id="h480djs93hd8",
               nonce="264095:dj83hs9s",
               mac="SLDJd4mg43cjQfElUs3Qub4L6xE="

认证服务器收到请求后,验证签名是否正确、token是否在有效期内,并根据结果给出对应response

3.6 oauth2.0 服务开发

一般情况下我们只会作为用户,或者第三方应用开发者来使用oauth2.0协议,假如我们处于一个需要做oauth2.0开放平台的互联网服务团队,或者想深入了解oauth2.0细节的实现,可以基于一个oauth2.0的开源模块实现个认证服务器,并阅读下模块源码,对照着rfc文档参考各个逻辑的实现。

例如
oauth2-server模块

4. 登录态保持 —— JSON Web Token (JWT)

前面提到的各种登录/认证方法,第三方应用的服务器端总是有状态的,需要维护登录用户的session对象。要么前置一个无状态接入层,通过sessionid hash到各服务节点,服务节点lru缓存session,要么每个节点就得从存储层中读取session信息,请求量大的时候这种io压力不小。

很自然的想法是,把用户信息存放在客户端,每次请求的时候随cookie或http头、http表单等渠道发送到服务器上,这样就可以让服务器变成无状态的存在了。

JWT就是实现这种想法的一个标准协议。

token有header、claims、signature三部分组成: token=header.claims.signature

header中存储加密算法、token类型,claims中存储用户相关的token签发信息,signature是根据head中指定的加密算法,及存在服务端的秘钥计算出来的加密串。具体算法在这里不赘述了,详见rfc7519

发送请求到服务器时,由某种渠道,如authration头、cookie等渠道发送给服务器,服务器用对应的认证方法计算签名认证即可。

JWT的优势在于让服务器成为无状态的,减少session管理的压力;劣势在于数据完全在客户端,无法做手动让一个session失效等操作。另外,由于JWT本身并未规定数据是否要加密,body部分的数据要么需要加密后下发给客户端,要么使用https协议传输。

☞ 参与评论