我们知道,HTTP是无状态的,所以,当我们需要获得用户是否在登录的状态时,我们需要检查用户的登录状态,一般来说,用户的登录成功后,服务器会发一个登录凭证(又被叫作Token),就像你去访问某个公司,在前台被认证过合法后,这个公司的前台会给你的一个访客卡一样,之后,你在这个公司内去到哪都用这个访客卡来开门,而不再校验你是哪一个人。在计算机的世界里,这个登录凭证的相关数据会放在两种地方,一个地方在用户端,以Cookie的方式(一般不会放在浏览器的Local Storage,因为这很容易出现登录凭证被XSS攻击),另一个地方是放在服务器端,又叫Session的方式(SessonID存于Cookie)。
但是,这个世界还是比较复杂的,除了用户访问,还有用户委托的第三方的应用,还有企业和企业间的调用,这里,我想把业内常用的一些 API认证技术相对系统地总结归纳一下,这样可以让大家更为全面的了解这些技术。注意,这是一篇长文!
本篇文章会覆盖如下技术:
目录
HTTP Basic 是一个非常传统的API认证技术,也是一个比较简单的技术。这个技术也就是使用 username
和 password
来进行登录。整个过程被定义在了 RFC 2617 中,也被描述在了 Wikipedia: Basic Access Authentication 词条中,同时也可以参看 MDN HTTP Authentication
其技术原理如下:
username
和 password
做成 username:password
的样子(用冒号分隔)Base64("username:password")
得到一个字符串(如:把 haoel:coolshell
进行base64 后可以得到 aGFvZW86Y29vbHNoZWxsCg
)aGFvZW86Y29vbHNoZWxsCg
放到HTTP头中 Authorization
字段中,形成 Authorization: Basic aGFvZW86Y29vbHNoZWxsCg
,然后发送到服务端。: Basic Realm='HelloWorld'
之类的头要求客户端进行认证。之后如果没有认证通过,则返回一个401错。如果服务端认证通过,那么会返回200。我们可以看到,使用Base64的目的无非就是为了把一些特殊的字符给搞掉,这样就可以放在HTTP协议里传输了。而这种方式的问题最大的问题就是把用户名和口令放在网络上传,所以,一般要配合TLS/SSL的安全加密方式来使用。我们可以看到 JIRA Cloud 的API认证支持HTTP Basic 这样的方式。
但我们还是要知道,这种把用户名和密码同时放在公网上传输的方式有点不太好,因为Base64不是加密协议,而是编码协议,所以就算是有HTTPS作为安全保护,给人的感觉还是不放心。
中文称“HTTP 摘要认证”,最初被定义在了 RFC 2069 文档中(后来被 RFC 2617 引入了一系列安全增强的选项;“保护质量”(qop)、随机数计数器由客户端增加、以及客户生成的随机数)。
其基本思路是,请求方把用户名口令和域做一个MD5 – MD5(username:realm:password)
然后传给服务器,这样就不会在网上传用户名和口令了,但是,因为用户名和口令基本不会变,所以,这个MD5的字符串也是比较固定的,因此,这个认证过程在其中加入了两个事,一个是 nonce
另一个是 qop
GET /coolshell/admin/ HTTP/1.1
WWW-Authenticate
包含如下信息:WWW-Authenticate: Digest realm="[[email protected]]", qop="auth,auth-int", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"
nonce
为服务器端生成的随机数,然后,客户端做 HASH1=MD5(MD5(username:realm:password):nonce:cnonce)
,其中的 cnonce
为客户端生成的随机数,这样就可以使得整个MD5的结果是不一样的。qop
中包含了 auth
,那么还得做 HASH2=MD5(method:digestURI)
其中的 method
就是HTTP的请求方法(GET/POST…),digestURI
是请求的URL。qop
中包含了 auth-init
,那么,得做 HASH2=MD5(method:digestURI:MD5(entityBody))
其中的 entityBody
就是HTTP请求的整个数据体。response = MD5(HASH1:nonce:nonceCount:cnonce:qop:HASH2)
如果没有 qop
则 response = MD5(HA1:nonce:HA2)
Authorization: Digest ...
GET /dir/index.html HTTP/1.0 Host: localhost Authorization: Digest username="Mufasa", realm="[[email protected]]", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="%2Fcoolshell%2Fadmin", qop=auth, nc=00000001, cnonce="0a4f113b", response="6629fae49393a05397450978507c4ef1", opaque="5ccc069c403ebaf9f0171e9517f40e41"
维基百科上的 Wikipedia: Digest access authentication 词条非常详细地描述了这个细节。
摘要认证这个方式会比之前的方式要好一些,因为没有在网上传递用户的密码,而只是把密码的MD5传送过去,相对会比较安全,而且,其并不需要是否TLS/SSL的安全链接。但是,别看这个算法这么复杂,最后你可以发现,整个过程其实关键是用户的password,这个password如果不够得杂,其实是可以被暴力破解的,而且,整个过程是非常容易受到中间人攻击——比如一个中间人告诉客户端需要的 Basic 的认证方式 或是 老旧签名认证方式(RFC2069)。
先说HMAC技术,这个东西来自于MAC – Message Authentication Code,是一种用于给消息签名的技术,也就是说,我们怕消息在传递的过程中被人修改,所以,我们需要用对消息进行一个MAC算法,得到一个摘要字串,然后,接收方得到消息后,进行同样的计算,然后比较这个MAC字符串,如果一致,则表明没有被修改过(整个过程参看下图)。而HMAC – Hash-based Authenticsation Code,指的是利用Hash技术完成这一工作,比如:SHA-256算法。
(图片来自 Wikipedia – MAC 词条 )
我们再来说App ID,这个东西跟验证没有关系,只是用来区分,是谁来调用API的,就像我们每个人的身份证一样,只是用来标注不同的人,不是用来做身份认证的。与前面的不同之处是,这里,我们需要用App ID 来映射一个用于加密的密钥,这样一来,我们就可以在服务器端进行相关的管理,我们可以生成若干个密钥对(AppID, AppSecret),并可以有更细粒度的操作权限管理。
把AppID和HMAC用于API认证,目前来说,玩得最好最专业的应该是AWS了,我们可以通过S3的API请求签名文档看到AWS是怎么玩的。整个过程还是非常复杂的,可以通过下面的图片流程看个大概。基本上来说,分成如下几个步骤:
CanonicalRequest
,作个SHA-256的签名,然后再做一个base16的编码AWS4-HMAC-SHA256
、时间戳、Scop,再打一个包,叫 StringToSign
。AWSSecretAccessKey
来对日期签一个 DataKey
,再用 DataKey
对要操作的Region签一个 DataRegionKey
,再对相关的服务签一个DataRegionServiceKey
,最后得到 SigningKey
.SigningKey
来对第二步的 StringToSign
签名。最后,发出HTTP Request时,在HTTP头的 Authorization
字段中放入如下的信息:
Authorization: AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7
其中的 AKIDEXAMPLE
是 AWS Access Key ID, 也就是所谓的 AppID,服务器端会根据这个AppID来查相关的 Secret Access Key,然后再验证签名。如果,你对这个过程有点没看懂的话,你可以读一读这篇文章——《Amazon S3 Rest API with curl》这篇文章里有好些代码,代码应该是最有细节也是最准确的了。
这种认证的方式好处在于,AppID和AppSecretKey,是由服务器的系统开出的,所以,是可以被管理的,AWS的IAM就是相关的管理,其管理了用户、权限和其对应的AppID和AppSecretKey。但是不好的地方在于,这个东西没有标准 ,所以,各家的实现很不一致。比如: Acquia 的 HMAC,微信的签名算法 (这里,我们需要说明一下,微信的API没有遵循HTTP协议的标准,把认证信息放在HTTP 头的 Authorization
里,而是放在body里)
JWT是一个比较标准的认证解决方案,这个技术在Java圈里应该用的是非常普遍的。JWT签名也是一种MAC(Message Authentication Code)的方法。JWT的签名流程一般是下面这个样子:
HMAC-SHA256(SecertKey, Base64UrlEncode(JWT-Header)+'.'+Base64UrlEncode(JWT-Payload));
base64(header).base64(payload).signature
作为 JWT token返回客户端。当应用服务器收到请求后:
我们可以看以,上面的这个过程,是在认证服务器上为用户动态生成 Secret Key的,应用服务在验签的时候,需要到认证服务器上去签,这个过程增加了一些网络调用,所以,JWT除了支持HMAC-SHA256的算法外,还支持RSA的非对称加密的算法。
使用RSA非对称算法,在认证服务器这边放一个私钥,在应用服务器那边放一个公钥,认证服务器使用私钥加密,应用服务器使用公钥解密,这样一来,就不需要应用服务器向认证服务器请求了,但是,RSA是一个很慢的算法,所以,虽然你省了网络调用,但是却费了CPU,尤其是Header和Payload比较长的时候。所以,一种比较好的玩法是,如果我们把header 和 payload简单地做SHA256,这会很快,然后,我们用RSA加密这个SHA256出来的字符串,这样一来,RSA算法就比较快了,而我们也做到了使用RSA签名的目的。
最后,我们只需要使用一个机制在认证服务器和应用服务器之间定期地换一下公钥私钥对就好了。
这里强烈建议全文阅读 Anglar 大学的 《JSW:The Complete Guide to JSON Web Tokens》
OAuth也是一个API认证的协议,这个协议最初在2006年由Twitter的工程师在开发OpenID实现的时候和社交书签网站Ma.gnolia时发现,没有一种好的委托授权协议,后来在2007年成立了一个OAuth小组,知道这个消息后,Google员工也加入进来,并完善有善了这个协议,在2007年底发布草案,过一年后,在2008年将OAuth放进了IETF作进一步的标准化工作,最后在2010年4月,正式发布OAuth 1.0,即:RFC 5849 (这个RFC比起TCP的那些来说读起来还是很轻松的),不过,如果你想了解其前身的草案,可以读一下 OAuth Core 1.0 Revision A ,我在下面做个大概的描述。
根据RFC 5849,可以看到 OAuth 的出现,目的是为了,用户为了想使用一个第三方的网络打印服务来打印他在某网站上的照片,但是,用户不想把自己的用户名和口令交给那个第三方的网络打印服务,但又想让那个第三方的网络打印服务来访问自己的照片,为了解决这个授权的问题,OAuth这个协议就出来了。
整个授权过程是这样的:
oauth_token
)和 Request Token Secret (oauth_token_secret
)oauth_token
)和 Verification Code(oauth_verifier
)oauth_token
)和 Access Token Secret (oauth_token_secret
)下图附上一个Yahoo!的流程图可以看到整个过程的相关细节。
因为上面这个流程有三方:User,Consumer 和 Service Provide,所以,又叫 3-legged flow,三脚流程。OAuth 1.0 也有不需要用户参与的,只有Consumer 和 Service Provider 的, 也就是 2-legged flow 两脚流程,其中省掉了用户认证的事。整个过程如下所示:
oauth_token
)和 Request Token Secret (oauth_token_secret
)oauth_token
)和 Access Token Secret (oauth_token_secret
)最后,再来说一说OAuth中的签名。
&
作连接符。假设 Consumer Secret 为 j49sk3j29djd
而 Token Secret 为dh893hdasih9
那个,签名密钥为:j49sk3j29djd&dh893hdasih9
下图是整个签名的示意图:
图片还是比较直观的,我就不多解释了。
在前面,我们可以看到,从Digest Access, 到AppID+HMAC,再到JWT,再到OAuth 1.0,这些个API认证都是要向Client发一个密钥(或是用密码)然后用HASH或是RSA来签HTTP的请求,这其中有个主要的原因是,以前的HTTP是明文传输,所以,在传输过程中很容易被篡改,于是才搞出来一套的安全签名机制,所以,这些个认证的玩法是可以在HTTP明文协议下玩的。
这种使用签名方式大家可以看到是比较复杂的,所以,对于开发者来说,也是很不友好的,在组织签名的那些HTTP报文的时候,各种,URLEncode和Base64,还要对Query的参数进行排序,然后有的方法还要层层签名,非常容易出错,另外,这种认证的安全粒度比较粗,授权也比较单一,对于有终端用户参与的移动端来说也有点不够。所以,在2012年的时候,OAuth 2.0 的 RFC 6749 正式放出。
OAuth 2.0依赖于TLS/SSL的链路加密技术(HTTPS),完全放弃了签名的方式,认证服务器再也不返回什么 token secret 的密钥了,所以,OAuth 2.0是完全不同于1.0 的,也是不兼容的。目前,Facebook 的 Graph API 只支持OAuth 2.0协议,Google 和 Microsoft Azure 也支持Auth 2.0,国内的微信和支付宝也支持使用OAuth 2.0。
下面,我们来重点看一下OAuth 2.0的两个主要的Flow:
Authorization Code 是最常使用的OAuth 2.0的授权许可类型,它适用于用户给第三方应用授权访问自己信息的场景。这个Flow也是OAuth 2.0四个Flow中我个人觉得最完整的一个Flow,其流程图如下所示。
下面是对这个流程的一个细节上的解释:
1)当用户(Resource Owner)访问第三方应用(Client)的时候,第三方应用会把用户带到认证服务器(Authorization Server)上去,主要请求的是 /authorize
API,其中的请求方式如下所示。
https://login.authorization-server.com/authorize? client_id=6731de76-14a6-49ae-97bc-6eba6914391e &response_type=code &redirect_uri=http%3A%2F%2Fexample-client.com%2Fcallback%2F &scope=read &state=xcoiv98CoolShell3kch
其中:
client_id
为第三方应用的App IDresponse_type=code
为告诉认证服务器,我要走Authorization Code Flow。redirect_uri
意思是我跳转回第三方应用的URLscope
意是相关的权限state
是一个随机的字符串,主要用于防CSRF攻击。2)当Authorization Server收到这个URL请求后,其会通过 client_id
来检查 redirect_uri
和 scope
是否合法,如果合法,则弹出一个页面,让用户授权(如果用户没有登录,则先让用户登录,登录完成后,出现授权访问页面)。
3)当用户授权同意访问以后,Authorization Server 会跳转回 Client ,并以其中加入一个 Authorization Code。 如下所示:
https://example-client.com/callback? code=Yzk5ZDczMzRlNDEwYlrEqdFSBzjqfTG &state=xcoiv98CoolShell3kch
我们可以看到,
redirect_uri
state
的值也和第 1)步的 state
一样。4)接下来,Client 就可以使用 Authorization Code 获得 Access Token。其需要向 Authorization Server 发出如下请求。
POST /oauth/token HTTP/1.1 Host: authorization-server.com
code=Yzk5ZDczMzRlNDEwYlrEqdFSBzjqfTG &grant_type=code &redirect_uri=https%3A%2F%2Fexample-client.com%2Fcallback%2F &client_id=6731de76-14a6-49ae-97bc-6eba6914391e &client_secret=JqQX2PNo9bpM0uEihUPzyrh
5)如果没什么问题,Authorization 会返回如下信息。
{ "access_token": "iJKV1QiLCJhbGciOiJSUzI1NiI", "refresh_token": "1KaPlrEqdFSBzjqfTGAMxZGU", "token_type": "bearer", "expires": 3600, "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciO.eyJhdWQiOiIyZDRkM..." }
其中,
access_token
就是访问请求令牌了refresh_token
用于刷新 access_token
id_token
是JWT的token,其中一般会包含用户的OpenID6)接下来就是用 Access Token 请求用户的资源了。
GET /v1/user/pictures Host: https://example.resource.com
Authorization: Bearer iJKV1QiLCJhbGciOiJSUzI1NiI
Client Credential 是一个简化版的API认证,主要是用于认证服务器到服务器的调用,也就是没有用户参与的的认证流程。下面是相关的流程图。
这个过程非常简单,本质上就是Client用自己的 client_id
和 client_secret
向Authorization Server 要一个 Access Token,然后使用Access Token访问相关的资源。
请求示例
POST /token HTTP/1.1 Host: server.example.com Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials &client_id=czZCaGRSa3F0Mzpn &client_secret=7Fjfp0ZBr1KtDRbnfVdmIw
返回示例
{ "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3", "token_type":"bearer", "expires_in":3600, "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk", "scope":"create" }
这里,容我多扯一句,微信公从平台的开发文档中,使用了OAuth 2.0 的 Client Credentials的方式(参看文档“微信公众号获取access token”),我截了个图如下所谓。我们可以看到,微信公众号使用的是GET方式的请求,把AppID和AppSecret放在了URL中,虽然这也符合OAuth 2.0,但是并不好,因为大多数网关代理会把整个URI请求记到日志中。我们只要脑补一下腾讯的网关的Access Log,里面的日志一定会有很多的各个用户的AppID和AppSecret……
讲了这么多,我们来小结一下(下面的小结可能会有点散)
uid
放在 Token中目的是为了去掉状态,但不能让用户修改,所以需要签名。Authorization
HTTP 头中。(全文完)