何为认证
计算机本身无法判断坐在显示器前的使用者的身份。进一步说,也无法确认网络的那头究竟有谁。可见,为了弄清是谁在访问及服务器,就得让对方的客户端自报家门。
可是,就算是正在访问服务器的地方声称自己是ueno ,身份是否属实,这点却也无从谈起。为了确认ueno本人是否真的具有访问系统的权限,就需要核对“登录者本人才知道的信息”、“登录者本人才会有的信息”。
核对的信息通常是指以下这些。
- 密码:只有本人才会知道的字符串信息。
- 动态密码:仅限本人持有的设备内显示的一次性密码。
- 数字证书:仅限本人(终端)持有的信息。
- 生物认证:指纹和虹膜等本人的生理信息。
- IC卡等:仅限本人持有的信息。
但是,这个世界还是比较复杂的,除了用户访问,还有用户委托的第三方的应用,还有企业和企业间的调用,这里,我把业内常用的一些 API认证技术相对系统地总结归纳一下,这样可以让大家更为全面的了解这些技术。注意,这是一篇长文!
HTTP使用的认证方式
HTTP/1.1使用的认证方式如下所示:
- BASIC认证(基本认证)
- DIGEST认证(摘要认证)
- JWT – JSON Web Tokens
- OAuth 1.0 – 3 legged & 2legged
- OAuth 1.0 – 3 Authentication Code & Client Credential
此外,还有Windows 统一认证(Kerberos认证、NTLM认证),不做讲解。
BASIC 认证
BASIC认证(基本认证)是从HTTP1.0 就定义的认证方式。是非常传统的API认证技术。也是一种比较简单的技术。即使现在仍然有一部分网站会使用这种认证方式。这个技术也就是使用了username 和 password 来进行登录。
技术原理
假如用户名为 guest,密码是 guest
1、把guest 和 guest 连接起来,构成 guest:guset 这样的字符串。然后经过Base64编码,最后结果是Z3Vlc3D6Z4Vlc8Q=
2、然后把这窜字符串(Z3Vlc3D6Z4Vlc8Q=)写入HTTP头中的 Authorization 后,发送到服务端。
3、接收到包含HTTP首部字段 Authorization 请求的服务器。会对认证的信息的正确性进行验证。如验证通过,则返回一条包含Request-URI资源的响应。
服务端如果没有接收到 HTTP首部字段 Authorization ,服务器会随状态码401 Authorization Required,返回带 WWW-Authenticate 首部字段的响应。该字段内包含认证的方式(BASIC)及Request-URI安全域字符串(realm)。
小结
我们可以看到,BASIC认证虽然采用Base64 编码方式,但这不是加密处理。不需要任何附加信息即可对其解码。换言之,明文解码就是用户名和密码。 使用Base64的目的无非就是为了把一些特殊的字符给搞掉,这样就可以放在HTTP协议里传输了。在HTTP等非加密通信的线路上进行BASIC认证过程中,如果被人窃听,被盗的可能性极高。所以,一般要配合TLS/SSL的安全加密方式来使用。
DIGEST认证
也称“HTTP摘要认证”,为弥补BASIC认证存在的弱点,可以看做是基本认证的增强版本,不包含密码的明文传递。
从 HTTP1.1 起就有了 DIGEST 认证,其基本思路是请求方把用户名口令和域做一个MD5 – MD5(username:realm:password) 然后传给服务器,这样就不会在网上传用户名和口令了,其中,用户名和口令基本不会变,所以,这个MD5的字符串也是比较固定的,这个认证过程就是在首部字段 WWW-Authenticate 内加入了两个重要字段,一个是 nonce ,另一个是 realm。
认证步骤
步骤一
首先,请求需认证的资源时,服务器会随状态码401 Authorization Required,返回带 WWW-Authenticate 首部字段的响应。该字段内包含认证所需的临时质询码(随机数,nonce)。
客户端请求 (无认证):
GET /dir/index.html HTTP/1.0
Host: localhost
服务端返回的首部字段WWW-Authenticate必须包含realm和nonce这两个字段的信息。客户端就是依靠向服务器回送这两个值进行认证的。
服务器响应:
HTTP/1.0 401 Unauthorized
WWW-Authenticate: Digest realm="testrealm@host.com", //认证域
qop="auth,auth-int", //保护质量
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", //服务器密码随机数
opaque="5ccc069c403ebaf9f0171e9517f40e41"
nonce是一种每次随返回的 401 响应生成的任意随机字符串。该字符串通常推荐由Base64编码的十六进制的组成形式。但实际内容依赖服务器的具体实现。
说明
客户端请求一个需要认证的页面,但是不提供用户名和密码。通常这是由于用户简单的输入了一个地址或者在页面中点击了某个超链接。
服务器返回401 “Unauthorized” 响应代码,并提供认证域(realm),以及一个随机生成的、只使用一次的数值,称为密码随机数 nonce。
此时,浏览器会向用户提示认证域(realm)(通常是所访问的计算机或系统的描述),并且提示用户名和密码。用户此时可以选择取消。
一旦提供了用户名和密码,客户端会重新发送同样的请求,但是添加了一个认证头包括了响应代码。
注意:客户端可能已经拥有了用户名和密码,因此不需要提示用户,比如以前存储在浏览器里的。
步骤二
接收到401状态码的客户端,返回的响应中包含DIGEST认证必须的首部字段Authorization信息。 请求方的首部字段 Authorization 内必须包含username、realm、nonce、uri 和 response 的字段信息。其中,realm 和 nonce 就是之前从服务器接收到的响应中的字段。
- username是realm限定范围内可进行认证的用户名
- uri(digest-uri)即 Request-URI 的值,但考虑到经代理转发后的 Request-URI
值可能被修改,因为事先会复制一份副本保存在uri内。 - response 也可叫做 Request-Digest。存在经过MD5运算后的密码字符串,形成响应码。
response说明
response 值由三步计算而成。当多个数值合并的时候,使用冒号作为分割符:
1、对用户名、认证域(realm)以及密码的合并值计算 MD5 哈希值,结果称为 HA1。
HA1 = MD5(A1) = MD5(username:realm:password)
即:HA1 = MD5( “guest:testrealm@host.com:guest” ) = 939e7578ed9e3c518a452acee763bce9
2、对HTTP方法以及URI的摘要的合并值计算 MD5 哈希值,例如,”GET” 和 “/dir/index.html”,结果称为 HA2。
如果qop值为“auth”或未指定,那么HA2为:HA2 = MD5(A2) = MD5(method:digestURI)
; 如果qop值为“auth-int”,那么HA2为 HA2 = MD5(A2) = MD5(method:digestURI:MD5(entityBody));
即:HA2 = MD5( “GET:/dir/index.html” ) = 39aff3a2bab6126f332b942af96d3366
3、对HA1、服务器密码随机数(nonce)、请求计数(nc)、客户端密码随机数(cnonce)、保护质量(qop)以及 HA2 的合并值计算 MD5 哈希值。结果即为客户端提供的
response 值。
如果qop值为“auth”或“auth-int”,那么response = MD5(HA1:nonce:nonceCount:clientNonce:qop:HA2)
如果qop值为未指定, 那么response = MD5(HA1:nonce:HA2
即:Response = MD5( “939e7578ed9e3c518a452acee763bce9:\
dcd98b7102dd2f0e8b11d0f600bfb0c093:\
00000001:0a4f113b:auth:\
39aff3a2bab6126f332b942af96d3366” )
= 6629fae49393a05397450978507c4ef1
关于nonce
此时客户端可以提交一个新的请求,重复使用服务器密码随机数(nonce)(服务器仅在每次“401”响应后发行新的nonce),但是提供新的客户端密码随机数(cnonce)。在后续的请求中,十六进制请求计数器(nc)必须比前一次使用的时候要大,否则攻击者可以简单的使用同样的认证信息重放老的请求。由服务器来确保在每个发出的密码随机数nonce时,计数器是在增加的,并拒绝掉任何错误的请求。显然,改变HTTP方法和/或计数器数值都会导致不同的 response值。
服务器应当记住最近所生成的服务器密码随机数nonce的值。也可以在发行每一个密码随机数nonce后,记住过一段时间让它们过期。如果客户端使用了一个过期的值,服务器应该响应“401”状态号,并且在认证头中添加stale=TRUE,表明客户端应当使用新提供的服务器密码随机数nonce重发请求,而不必提示用户其它用户名和口令。
服务器不需要保存任何过期的密码随机数,它可以简单的认为所有不认识的数值都是过期的。服务器也可以只允许每一个服务器密码随机数nonce使用一次,当然,这样就会迫使客户端在发送每个请求的时候重复认证过程。需要注意的是,在生成后立刻过期服务器密码随机数nonce是不行的,因为客户端将没有任何机会来使用这个nonce。
步骤三
最后呢,我们的客户端对服务端发起如下请求—— 注意HTTP头的 Authorization: Digest …
客户端请求 (用户名 "guest", 密码 "guest"):
GET /dir/index.html HTTP/1.0
Host: localhost
Authorization: Digest username="guest",
realm="testrealm@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
uri="/dir/index.html",
qop=auth,
nc=00000001, //请求计数
cnonce="0a4f113b", //客户端密码随机数
response="6629fae49393a05397450978507c4ef1",
opaque="5ccc069c403ebaf9f0171e9517f40e41"
3.接受到包含首部字段Authorizaiton 请求的服务器,会确认认证信息的正确性。认证通过后,则返回包含Request-URI资源的响应。
并且这时会在首部字段 Authorizaiton-Info 写入一些认证成功的相关信息。
小结
DIGEST认证提供了高于BASIC认证的安全等级。但是和HTTPS的客户端认证相比仍旧很弱。DIGEST认证提供防止密码被窃听的保护机制,但并不存在防止用户伪装的保护机制。
最后你可以发现,整个过程其实关键是用户的password,这个password如果不够得杂,其实是可以被暴力破解的,而且,整个过程是非常容易受到中间人攻击——比如一个中间人告诉客户端需要的 Basic 的认证方式 或是 老旧签名认证方式
JWT 认证
JWT – JSON Web Tokens,JWT是一个比较标准的认证解决方案,这个技术在Java圈里应该用的是非常普遍的。特别适用于分布式站点的单点登录(SSO)场景。(说起JWT,应该对比基于token的认证和传统的session认证的区别。)
JWT签名也是一种MAC(Message Authentication Code)的方法。
JWT的签名流程一般是下面这个样子:
- 用户使用用户名和口令到认证服务器上请求认证。
- 认证服务器验证用户名和口令后,以服务器端生成JWT Token,这个token的生成过程如下:
- 认证服务器还会生成一个 Secret Key(密钥)
- 对JWT Header和 JWT Payload分别求Base64。在Payload可能包括了用户的抽象ID和的过期时间。
- 用密钥对JWT签名
HMAC-SHA256(SecertKey, Base64UrlEncode(JWT-Header)+'.'+Base64UrlEncode(JWT-Payload));
- 然后把
base64(header).base64(payload).signature
作为 JWT token返回客户端。 - 客户端使用JWT Token向应用服务器发送相关的请求。这个JWT Token就像一个临时用户权证一样。
当应用服务器收到请求后:
- 应用服务会检查 JWT Token,确认签名是正确的。
- 因为只有认证服务器有这个用户的Secret Key(密钥),所以,应用服务器得把JWT Token传给认证服务器。
- 认证服务器通过JWT Payload 解出用户的抽象ID,然后通过抽象ID查到登录时生成的Secret Key,然后再来检查一下签名。
- 认证服务器检查通过后,应用服务就可以认为这是合法请求了。
我们可以看以,上面的这个过程,是在认证服务器上为用户动态生成 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 2.0
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。
- 一个是Authorization Code Flow, 这个是 3 legged 的
- 一个是Client Credential Flow,这个是 2 legged 的。
Authorization Code Flow
Authorization Code 是最常使用的OAuth 2.0的授权许可类型,它适用于用户给第三方应用授权访问自己信息的场景。这个Flow也是OAuth 2.0四个Flow中我个人觉得最完整的一个Flow,其流程图如下所示。
Authorization Code Grant Flow 流程解释:
- 当用户(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 ID
- response_type=code为告诉认证服务器,我要走Authorization Code Flow。
- redirect_uri意思是我跳转回第三方应用的URL
- scope意是相关的权限
- state 是一个随机的字符串,主要用于防CSRF攻击。
- 当Authorization Server收到这个URL请求后,其会通过 client_id来检查 redirect_uri和 scope是否合法,如果合法,则弹出一个页面,让用户授权(如果用户没有登录,则先让用户登录,登录完成后,出现授权访问页面)。
- 当用户授权同意访问以后,Authorization Server 会跳转回 Client ,并以其中加入一个 Authorization Code。 如下所示:
https://example-client.com/callback?
code=Yzk5ZDczMzRlNDEwYlrEqdFSBzjqfTG
&state=xcoiv98CoolShell3kch
我们可以看到,
- 请流动的链接是第 1)步中的 redirect_uri
- 其中的 state 的值也和第 1)步的 state一样。
- 接下来,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
- 如果没什么问题,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,其中一般会包含用户的OpenID
- 接下来就是用 Access Token 请求用户的资源了。
GET /v1/user/pictures
Host: https://example.resource.com
Authorization: Bearer iJKV1QiLCJhbGciOiJSUzI1NiI
Client Credential Flow
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”),
总结
概念和术语
Authentication(认证) 和 Authorization (授权),前者是证明请求者是身份,就像身份证一样,后者是为了获得权限。身份是区别于别人的证明,而权限是证明自己的特权。Authentication为了证明操作的这个人就是他本人,需要提供密码、短信验证码,甚至人脸识别。Authorization 则是不需要在所有的请求都需要验人,是在经过Authorization后得到一个Token,这就是Authorization。就像护照和签证一样。
编码Base64Encode、签名HMAC、加密RSA。编码是为了更的传输,等同于明文,签名是为了信息不能被篡改,加密是为了不让别人看到是什么信息。
明白一些初衷
- 使用复杂地HMAC哈希签名方式主要是应对当年没有TLS/SSL加密链路的情况。
- JWT把 uid 放在 Token中目的是为了去掉状态,但不能让用户修改,所以需要签名。
- OAuth 1.0区分了两个事,一个是第三方的Client,一个是真正的用户,其先拿Request Token,再换Access Token的方法主要是为了把第三方应用和用户区分开来。
- 用户的Password是用户自己设置的,复杂度不可控,服务端颁发的Serect会很复杂,但主要目的是为了容易管理,可以随时注销掉。
- OAuth 协议有比所有认证协议有更为灵活完善的配置,如果使用AppID/AppSecret签名的方式,又需要做到可以有不同的权限和可以随时注销,那么你得开发一个像AWS的IAM这样的账号和密钥对管理的系统。
注意事项
- 无论是哪种方式,我们都应该遵循HTTP的规范,把认证信息放在 Authorization HTTP 头中。
- 不要使用GET的方式在URL中放入secret之类的东西,因为很多proxy或gateway的软件会把整个URL记在Access Log文件中。
- 密钥Secret相当于Password,但他是用来加密的,最好不要在网络上传输,如果要传输,最好使用TLS/SSL的安全链路
- HMAC中无论是MD5还是SHA1/SHA2,其计算都是非常快的,RSA的非对称加密是比较耗CPU的,尤其是要加密的字符串很长的时候。
- 最好不要在程序中hard code 你的 Secret,因为在github上有很多黑客的软件在监视各种Secret,千万小心!这类的东西应该放在你的配置系统或是部署系统中,在程序启动时设置在配置文件或是环境变量中。
- 使用AppID/AppSecret,还是使用OAuth1.0a,还是OAuth2.0,还是使用JWT,我个人建议使用TLS/SSL下的OAuth 2.0。
- 密钥是需要被管理的,管理就是可以新增可以撤销,可以设置账户和相关的权限。最好密钥是可以被自动更换的。
- 认证授权服务器(Authorization Server)和应用服务器(App Server)最好分开。
全文完