很多问题看似明白了,其实还没有明白。Web Api
认证方案显然就属于这个问题。今天花点时间整理一下,总结如下。
HTTP Basic Auth
所谓的 Basic
认证,就是在请求一个URL
的时候,服务端首先返回401 Unauthorized
给客户端,同时在Response Header
中添加一个 WWW-Authenticate
的头,值为 Basic realm="Tomcat Manager Application"
(以tomcat的默认的管理页面举例)。
客户端/浏览器拿到这个头之后,发现认证方式是 Basic
的,于是浏览器就弹出输入用户名密码的对话框。用户名密码输入完成后,客户端在请求头中添加Authorization
,并将值设置为“Basic cm9vdDpyb290
”。
服务端会获取Authorization
中的值,然后进行验证。这就是一次完整的Basic
认证过程。
其中 cm9vdDpyb290
,是用户名加密码使用Base64
编码之后形成的编码。Base64
是对称编码,可以对编码后的内容进行反编码,得到原文。cm9vdDpyb290
解码后的内容是root:root
。
由此可见 Basic
认证的缺点很明显,相当于明文传输,所以 Basic
一定要配合HTTPS
使用,不然敏感信息就是在网上裸奔。
HTTP Digest Auth
鉴于上面 Basic
验证方案明文传输的缺点,Digest
验证做了改进。Digest
验证就是对密码进行摘要加密之后进行传输。
认证过程和 Basic
认证过程基本类似,首先客户端发送一个请求给服务端,在未认证的情况下,服务端返回401 Unauthorized
,同时在Response Header
中添加 WWW-Authenticate
,此时该值由之前得 Basic
替换为 Digest
,表明此次认证为Digest
方式,除此之外还会发送一个nonce
值给客户端,该值是个随机数。
客户端填写完用户名和密码后,将密码用摘要算法,比如MD5
进行加密,发送给服务端。服务端收到请求后,根据用户名从数据库中找到对应的用户名密码,用同样得摘要算法将密码信息也进行加密,将加密之后的信息和请求信息做对比,如果一致证明没问题。
但这里还有个安全问题,假设黑客窃取了摘要后的密文,直接发送到服务端,还是可以通过认证。为了解决这个问题,上面提到 nonce
随机值就起到了作用,客户端每次用这个值加上原始密码一起进行摘要加密计算,因为这个值是服务端返回的,所以服务端也保存着这个值,服务端用数据库存储的密码加上这个值也进行摘要计算,如果算出来的值匹配,那么就验证通过。这就避免加密后的密码被窃取的风险,因为每次请求的nonce
值都是不一样的。
HTTP Cookie/Session Auth
因为HTTP
是无状态的协议,所以即便你之前登录成功,紧随其后的请求如果不携带用户信息,服务器还是不知道这个请求来自谁。我们又不想每次请求都带上用户名密码。所以上面的 Basic
和Digest
方案都不太符合要求。为此引出了Cookie/Session
方案。
Cookie
是存储在客户端的一串信息,当我们通过浏览器访问网站的时候会自动进行Cookie
存储操作。
Session
是存储在服务器端的一串信息。当客户登录成功后,服务端会生成Session
,保存相关信息在服务器中,同时将SessionId
回写到客户端的Cookie
中。客户每次发请求的时候,只要从Cookie
中找到SessionId
附到请求中,服务端读到SessionId
就可以判断出是谁在请求,以及上次的操作状态。
但是使用Cookie
和Session
方案也存在很多问题。
第一个问题就是很多用户都会在浏览器中禁用Cookie
(Cookie
的概念只存在浏览器中)。针对这个问题,我们可以采用两种方案来弥补,第一种方案就是将SessionId
附在URL
里面;第二种方案在页面中增加一个form
表单隐藏项,将SessionId
放到里面。
第二个问题是因为所有Session
都存储在服务器端,如果服务端有多台服务器,前面有个负载均衡服务器,比如F5
或者Nginx
,那么必须保证用户每次请求都负载到同一台服务器,因为它的Session
只存在这台服务器上。这是比较困难的,特别是在进行服务器资源扩容的时候。针对这个问题其实也有两种解决办法,第一个办法就是在Session
量比较小的情况下,可以将Session
同步到所有服务器上。如果量比较大的情况下,我们可以选择将Session
统一集中存储到Redis
中。
第三个问题就是CSRF
(Cross-site request forgery)问题,CSRF
中文名称:跨站请求伪造。所谓的CSRF
就是攻击者通过诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证(通常是利用Cookie
中的信息),绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。具体CSRF
攻击的问题,这里不做更多展开。
HTTP Token Auth
为了弥补Cookie/Session
的问题,大家想到了token
方案。单纯的token
方案就是用户登录成功后返回一串随机且唯一的字符串,服务端将值存储在数据库中,而客户端将值存储在localStroage
中。每次请求的时候都带上该字符串,服务端根据数据库中存储的token
判断用户的状态和相关信息。其实这种方案和sessionId
本质是一样的。
JWT token
无论上面提到是Basic auth
,Cookie/Session
还是普通Token
的方案,我们都要不断的进行查询数据库或者缓存操作;将客户端发过来的值与服务端的存储的值进行比对。我们知道查询数据库是一个很耗时的操作,而且数据库中还要存储大量sessionId
或者密码等信息。这是相当浪费时间和空间的。那么有没有一种方法,不用查询数据库或者缓存呢。答案是肯定,JWT
提供了很好的解决方案。
JWT
全称是JSON Web Token
。JWT
把所有信息都存在自己身上,比如用户名、昵称,角色,加密信息等,并且以JSON
对象存储。
JWT
由三部分组成,第一部分称为头部信息,第二部分称为内容,第三部分叫做签名值。一个完整的JWT
应该是这个样子的 xxxxx.yyyyy.zzzzz
Header
头部信息 一般包含token
类型和加密算法(HMAC SHA256 RSA)。
1 | { |
Payload
传输内容 一般包含用户名,昵称,过期时间等。
1 | { |
Signature
签名值 将Header
信息和Payload
进行base64
编码后用"."
拼接成一个字符串,然后使用一个加密密钥,生成一个签名值。1
Signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
上面这三部分内容生成的JWT Token
为1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU
当客户进行登录操作的时候,服务端根据上面的规则生成token
并返回给客户端。当客户端再次访问服务器的时候,将该token
发送过来,服务端拿到token
之后,只需要将Header
和Payload
部分用base64
反解码,这样就可以知道是哪个用户登录,角色是什么等等。如果想判断这个token
是否是伪造的,我们只需要根据前两部分内容,加上服务器端存储的密钥值进行一次加密运算,只要计算出来的签名值和用户传过来的签名值一致,就可以判断token
是正确的。这样我们就可以避免查询数据库了。这就是典型的用CPU
时间换存储空间的案例。
关于JWT
的更多细节问题可以参考其官网
Token 更新问题
token
一般都是有有效期的,有效期一般不会太长,超过有效期token
就会失效,token
失效后,就要获取新的token
。
token
的更新一般有两种处理策略
token
过期后,重新使用用户名密码获取新的token
。如果token
有效期比较短的话,会频繁进行token
获取操作。在首次获取
token
的时候,除了签发一个Access Token
,还发送一个和Access Token
关联的Refresh Token
,当Access Token
过期之后使用Refresh Token
进行Access Token
的更新。Refresh Token
的过期时间可以设置长一些,并且存储到数据库中,甚至可以在每次使用Refresh Token
的时候都自动延长Refresh Token
的有效期,直至达到一个最大有效期。
有人问为什么不通过重新Login
来获取新的Token
呢,因为重新Login
需要用户输入用户名和密码,对用户体验不好,而使用Refresh Token
的话,可以把Refresh Token
存储在客户端,而不用把用户名密码存到客户端中。
为什么不可以把用户名密码存储在客户端呢?因为相比于用户名密码丢失,Refresh Token
的丢失影响更小,因为很多人的用户名密码都是共用的,比如微博和微信很可能用的是同一套密码,但是Refresh Token
不可能被共用。
针对token
的更新问题,这里写了一个简单的客户端调用伪代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//服务端返回的内容
String resultContext = "";
//发送业务请求
ResponseEntity<String> response = post(request,token);
//token 过期
if (response.getHeader().getStatus == 401){
//重新登录,获取新token
ResponseEntity<String> newToken = postLogin(newTokenRequest);
//更新本地token,以便后续请求直接使用
updateToken(newToken)
//携带新token,继续发送业务请求
ResponseEntity<String> response = post(request, newToken)
resultContext = response.getBody();
} else {
//token 没过期
resultContext = response.getBody();
}
总结
token
方案确实有很多优点,比如客户端不用存储用户密码,服务端不用存储token
,每次验证合法性也不用再查询数据库了,性能有了很大提升;还可以防止CSRF
攻击;扩展性也比Cookie/Session
方案更好。但是实现起来会比其他几种方案更复杂,特别是设计一套优秀的token
认证方案。总的来说,token
方案在安全性和易用性,以及性能方面都全面胜出。此外OAuth2.0
也是基于token
实现的。总之一句话,token
方案你值得拥有。
最后以上所有的认证方案,配合https
协议使用更安全。
推荐阅读
1. Java并发编程那些事儿(十)——最后的总结
2. 聊聊对称加密与非对称加密
3. Awk这件上古神兵你会用了吗
4. 手把手教你搭建一套ELK日志搜索运维平台