聊聊Web Api 认证方案那点儿事

很多问题看似明白了,其实还没有明白。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是无状态的协议,所以即便你之前登录成功,紧随其后的请求如果不携带用户信息,服务器还是不知道这个请求来自谁。我们又不想每次请求都带上用户名密码。所以上面的 BasicDigest方案都不太符合要求。为此引出了Cookie/Session方案。

Cookie是存储在客户端的一串信息,当我们通过浏览器访问网站的时候会自动进行Cookie存储操作。

Session是存储在服务器端的一串信息。当客户登录成功后,服务端会生成Session,保存相关信息在服务器中,同时将SessionId回写到客户端的Cookie中。客户每次发请求的时候,只要从Cookie中找到SessionId附到请求中,服务端读到SessionId就可以判断出是谁在请求,以及上次的操作状态。

但是使用CookieSession方案也存在很多问题。

第一个问题就是很多用户都会在浏览器中禁用CookieCookie的概念只存在浏览器中)。针对这个问题,我们可以采用两种方案来弥补,第一种方案就是将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 authCookie/Session还是普通Token的方案,我们都要不断的进行查询数据库或者缓存操作;将客户端发过来的值与服务端的存储的值进行比对。我们知道查询数据库是一个很耗时的操作,而且数据库中还要存储大量sessionId或者密码等信息。这是相当浪费时间和空间的。那么有没有一种方法,不用查询数据库或者缓存呢。答案是肯定,JWT提供了很好的解决方案。

JWT全称是JSON Web TokenJWT把所有信息都存在自己身上,比如用户名、昵称,角色,加密信息等,并且以JSON对象存储。

JWT由三部分组成,第一部分称为头部信息,第二部分称为内容,第三部分叫做签名值。一个完整的JWT应该是这个样子的 xxxxx.yyyyy.zzzzz

Header 头部信息 一般包含token类型和加密算法(HMAC SHA256 RSA)。

1
2
3
4
{ 
"alg": "HS256",
"typ": "JWT"
}

Payload 传输内容 一般包含用户名,昵称,过期时间等。

1
2
3
4
5
{  
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

Signature 签名值 将Header信息和Payload进行base64编码后用"."拼接成一个字符串,然后使用一个加密密钥,生成一个签名值。

1
Signature = HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

上面这三部分内容生成的JWT Token

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU

当客户进行登录操作的时候,服务端根据上面的规则生成token 并返回给客户端。当客户端再次访问服务器的时候,将该token发送过来,服务端拿到token之后,只需要将HeaderPayload部分用base64反解码,这样就可以知道是哪个用户登录,角色是什么等等。如果想判断这个token是否是伪造的,我们只需要根据前两部分内容,加上服务器端存储的密钥值进行一次加密运算,只要计算出来的签名值和用户传过来的签名值一致,就可以判断token是正确的。这样我们就可以避免查询数据库了。这就是典型的用CPU时间换存储空间的案例。
关于JWT的更多细节问题可以参考其官网

Token 更新问题

token一般都是有有效期的,有效期一般不会太长,超过有效期token就会失效,token失效后,就要获取新的token

token的更新一般有两种处理策略

  1. token过期后,重新使用用户名密码获取新的token。如果token有效期比较短的话,会频繁进行token获取操作。

  2. 在首次获取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日志搜索运维平台

-------------本文结束-------------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%