随着Web应用程序的出现,直接在客户端上存储用户信息的需求也随之出现。者背后的想象时合法的:与特定用户相关的信息都应该保存在用户的机器上。无论是登录信息、个人偏好、还是其他数据,Web应用程序提供者都需要有办法 将他们保存在客户端。对于这个问题,第一个解决方案就是cookie。
今天cookie只是在客户端存储数据的一个选项。
HTTP cookie也叫cookie,最初用于在客户端存储会话信息。这个规范要求:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value //HTTP响应会设置一个名为“name”,值为“value”的cookie
Other-header: other-header-value
GET /index.jsl HTTP/1.1
Cookie: name=value
Other-header: other-header-value
这些发送给服务器的额外信息,可用于唯一标识发送请求的客户端。cookie是与特定域绑定的。设置cookie后,他会与请求一起发送到创建他的域。这个限制能保证cookie中存储的信息只对被认可的接收者开放,不能被其他域访问。
cookie存储在客户端上,所以保证了它不会被恶意利用,浏览器会施加限制。
它不会占用太多磁盘空间,遵循以下限制,在任何浏览器中都不会碰到问题:
每个域的cookie个数是受限的,但是不同浏览器的限制不同:
如果cookie总数超过了单个域的上线,浏览器就会删除之前的cookie。不同浏览器方案不同,IE和Opera删除最近最少使用的;Firefox随机删除之前的cookie。
磁盘中的cookie超过了最大限制,该cookie会被静默删除(自动删除,不会发起提示)。
这些参数在Set-Cookie头部中使用 分号+空格 分隔开,比如:
HTTP/1.1 200 OK
Content-type: text/html
Set-Cookie: name=value; expires=Mon,22-Jan-07 07:10:24 GMT; domain=.worx.com; path=/; secure
Other-header: other-header-value
为了绕过浏览器对每个域cookie数的限制,有些开发者提出了子cookie的概念。
子cookie是在单个cookie存储的最小块数组,也就是cookie由很多子cookie组成。
本质上是使用cookie的值中存储多个子cookie,最常用的格式:
name=name1=value1&name2=vlue2&name3=value3...
Cookie正确性的验证一般有两种方式:
Web Storage的目的就是为了解决通过客户端存储不需要频繁发送回服务器的数据时使用cookie的问题。
Web Storage第2版定义了两个对象:localStorage(永久储存机制)和sessionStorage(会话储存机制)。这两种浏览器存储API提供了在浏览器中不受页面刷新而影响存储数据的两种方式。
Storage类用于保存键值对数据,直至存储空间上限(由浏览器决定)。Storage的实例与其他对象一样,但增加了以下方法:
注意: Storage只能存储字符串。非字符串会在存储之前自动转换为字符串。这种转换不能在获取数据时撤销。
sessionStorage对象只存储会话数据,这意味着数据只会存储到浏览器或当前标签关闭。存储在sessionStorage中的数据不受页面刷新的影响,可以在浏览器崩溃并重启后恢复。sessionStorage 存储数据作用域为当前标签或嵌套的iframe
。
sessionStorage是Storage类的实例对象,因而可以使用Storage的setIteam()方法或直接给属性赋值给sessionStorage添加数据:
// 使用方法存储数据
sessionStorage.setIteam("name", "value");
// 使用属性存储数据
sessionStorage.book = "value";
同样可以使用Storage的getIteam()方法或属性名来获取数据:
// 使用方法获取数据
let name = sessionStorage.getIteam("name);
// 使用属性名获取数据
let book = sessionStorage.book;
删除数据:
// 使用delete删除
delete sessionStorage.name;
// 使用方法删除
sessionStorage.removeItem("name")
注意: sessionStorage对象主要用于存储只在会话期间有效的小块数据。如果需要跨会话持久存储的话,可以使用localStorage。
在HTML5规范里,localStorage对象取代了globalStorage,作为哭护短持久存储数据的机制。要访问同一个localStorage对象,页面必须来着同一个域(子域不可以)、在相同的端口上使用相同的协议。
因为localStorage是Storage的实例对象,因而localStorage可以使用Storage的方法来操作数据:
// 使用方法存储数据
localStorage.setIteam("name", "value");
// 使用属性存储数据
localStorage.book = "value";
// 使用方法取得数据
let name = localStorage.getIteam("name");
// 使用属性取得数据
let book = localStorage.book;
两种存储机制的区别在于,存储在localStorage中的数据会保留到通过JavaScript代码删除或者用户手动清除浏览器缓存。localStorage数据不受页面刷新的影响,也不会因为关闭窗口、标签或重启浏览器而丢失,只有不手动删除就会一直持久的保存在浏览器。
HTTP是无状态的,为了能够在HTTP协议上保持住状态,比如用户是否登陆接需要一种方案来把用户的一个个无状态HTTP请求关联起来。这种技术就叫Session。
Session的功能就是个一个个分离的HTTP请求关联起来,只要实现这个功能,基本上本能叫Session的一种实现。
把Sessin存储在Web中间件中(比如Tomcat),这种做法正在淘汰,因为这种方案对负载均衡不友好,也不利于快速伸缩。
把Session存在Redis和前端的才是最佳方案,尤其在微服务架构大行其道的情况下。
只要HTTP还是无状态的,只要保存状态的是刚需,Session就不会消失,变化的只是它的实现方式。
session的创建和获取都使用同一个API:request.getSession()
通过isNew()
方法判断是否新创建的会话:
每个会话都有一个唯一ID,可以通过getId()方法获取。
public class SessionServlet extends HttpServlet {
protected void createSession(HttpServletRequest request, HttpServletResponse response) throws Exception{
// 创建和获取Session
HttpSession session = request.getSession();
// 判断当前Session是否最新
boolean isNew = session.isNew();
// 获取SessionID
String id = session.getId();
}
}
public class SessionServlet extends HttpServlet {
protected void getAttribute(HttpServletRequest request, HttpServletResponse response) throws Exception{
// 向session域中保存数据
request.getSession().setAttribute("key","value")
// 获取session域的数据
Object key = resuest.getSession().getAttribute("key")
// 获取session
HttpSession session = request.getSession();
// 设置当前session超时时间
session.setMaxInactiveInterval(35);
// 立刻销毁当前session
session.invalidate();
}
}
session底层是基于cookie技术实现的,因为具有cookie的生命周期特性,浏览器关闭,session也被销毁。
Session正确性的验证一般有两种方式:
Token的意思是“令牌”,是一种网络认证方式,是服务端生成的一串字符串,用于识别客户端的身份,不能用来存储客户端信息,可作为客户端进行请求的一个标识。
当用户第一次登录后,服务器生成一个Token并将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
token的前几位以哈希算法压缩成的一定长度的十六进制字符串。为防止token泄露。
Token无状态,也就是说,Token不会记录客户端之间的状态,也不会存储客户端相关信息,只会记录用户的身份,以保客户端发送的请求是合法的。
使用token机制的身份验证方法,在服务器端不需要存储用户的登录记录。大概的流程:
上面的时序图中并未提到 Refresh Token 过期怎么办。不过很显然,Refresh Token 既然已经过期,就该要求用户重新登录了。
当然还可以把这个机制设计得更复杂一些,比如,Refresh Token 每次使用的时候,都更新它的过期时间,直到与它的创建时间相比,已经超过了非常长的一段时间(比如三个月),这等于是在相当长一段时间内允许 Refresh Token 自动续期。
到目前为止,Token 都是有状态的,即在服务端需要保存并记录相关属性。那说好的无状态呢,怎么实现?JWT来实现。
如果我们把所有状态信息都附加在 Token 上,服务器就可以不保存。
但是服务端仍然需要认证 Token 有效。不过只要服务端能确认是自己签发的 Token,而且其信息未被改动过,那就可以认为 Token 有效—— “签名” 可以作此保证。
平时常说的签名都存在一方签发,另一方验证的情况,所以要使用非对称加密算法。
但是在这里,签发和验证都是同一方,所以对称加密算法就能达到要求,而对称算法比非对称算法要快得多(可达数十倍差距)。
更进一步思考,对称加密算法除了加密,还带有还原加密内容的功能,而这一功能在对 Token 签名时并无必要——既然不需要解密,摘要(散列)算法就会更快。可以指定密码的散列算法,自然是 HMAC。
JWT(json web token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。
JWT真正实现了Token的无状态。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源。比如用户登录。在传统的用户登录认证中,因为http是无状态的,所以都是采用session方式。用户登录成功,服务端会保存一个session,服务端会返回给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。
cookie + session这种模式通常是保存在内存中,而且服务从单服务到多服务会面临的session共享问题。虽然目前存在使用Redis进行Session共享的机制,但是随着用户量和访问量的增加,Redis中保存的数据会越来越多,开销就会越来越大,多服务间的耦合性也会越来越大,Redis中的数据也很难进行管理,例如当Redis集群服务器出现Down机的情况下,整个业务系统随之将变为不可用的状态。而JWT不是这样的,只需要服务端生成token,客户端保存这个token,每次请求携带这个token,服务端认证解析就可。
JWT正确性的验证一般有两种方式:
1、 使用 JWT 的优势
使用 JSON Web Token 保护应用安全,你至少可以获得以下几个优势:
更少的数据库连接:因其基于算法来实现身份认证,在使用 JWT 时查询数据的次数更少(更少的数据连接不等于不连接数据库),可以获得更快的系统响应时间。构建更简单:如果你的应用程序本身是无状态的,那么选择 JWT 可以加快系统构建过程。
跨服务调用:你可以构建一个认证中心来处理用户身份认证和发放签名的工作,其他应用服务在后续的用户请求中不需要(理论上)在询问认证中心,可使用自有的公钥对用户签名进行验证。
无状态:你不需要向传统的 Web 应用那样将用户状态保存于 Session 中。
2、使用 JWT 的弊端
严重依赖于秘钥:JWT 的生成与解析过程都需要依赖于秘钥(Secret),且都以硬编码的方式存在于系统中(也有放在外部配置文件中的)。如果秘钥不小心泄露,系统的安全性将收到威胁。
服务端无法管理客户端的信息:如果用户身份发生异常(信息泄露,或者被攻击),服务端很难向操作 Session 那样主动将异常用户进行隔离。
服务端无法主动推送消息:服务端由于是无状态的,他将无法使用像 Session 那样的方式推送消息到客户端,例如过期时间将至,服务端无法主动为用户续约,需要客户端向服务端发起续约请求。
冗余的数据开销:一个 JWT 签名的大小要远比一个 Session ID 长很多,如果你对有效载荷(payload)中的数据不做有效控制,其长度会成几何倍数增长,且在每一次请求时都需要负担额外的网络开销。
JSON Web Token 很流行,但是它相比于 Session,OIDC(OpenId Connect)等技术还比较新,支持 JSON Web Token 的库还比较少,而且 JWT 也并非比传统 Session 更安全,他们都没有解决 CSRF 和 XSS 的问题。因此,在决定使用 JWT 前,你需要仔细考虑其利弊。
当 Token 无状态之后,单点登录就变得容易了。前端拿到一个有效的 Token,它就可以在任何同一体系的服务上认证通过——只要它们使用同样的密钥和算法来认证 Token 的有效性。就样这样:
当然,如果 Token 过期了,前端仍然需要去认证服务更新 Token:
可见,虽然认证和业务分离了,实际即并没产生多大的差异。当然,这是建立在认证服务器信任业务服务器的前提下,因为认证服务器产生 Token 的密钥和业务服务器认证 Token 的密钥和算法相同。换句话说,业务服务器同样可以创建有效的 Token。
如果业务服务器不能被信任,该怎么办?
遇到不受信的业务服务器时,很容易想到的办法是使用不同的密钥。认证服务器使用密钥1签发,业务服务器使用密钥2验证——这是典型非对称加密签名的应用场景。认证服务器自己使用私钥对 Token 签名,公开公钥。信任这个认证服务器的业务服务器保存公钥,用于验证签名。幸好,JWT 不仅可以使用 HMAC 签名,也可以使用 RSA(一种非对称加密算法)签名。
不过,当业务服务器已经不受信任的时候,多个业务服务器之间使用相同的 Token 对用户来说是不安全的。因为任何一个服务器拿到 Token 都可以仿冒用户去另一个服务器处理业务……悲剧随时可能发生。
为了防止这种情况发生,就需要在认证服务器产生 Token 的时候,把使用该 Token 的业务服务器的信息记录在 Token 中,这样当另一个业务服务器拿到这个 Token 的时候,发现它并不是自己应该验证的 Token,就可以直接拒绝。
现在,认证服务器不信任业务服务器,业务服务器相互也不信任,但前端是信任这些服务器的——如果前端不信任,就不会拿 Token 去请求验证。那么为什么会信任?可能是因为这些是同一家公司或者同一个项目中提供的若干服务构成的服务体系。
但是,前端信任不代表用户信任。如果 Token 不没有携带用户隐私(比如姓名),那么用户不会关心信任问题。但如果 Token 含有用户隐私的时候,用户得关心信任问题了。这时候认证服务就不得不再啰嗦一些,当用户请求 Token 的时候,问上一句,你真的要授权给某某某业务服务吗?而这个“某某某”,用户怎么知道它是不是真的“某某某”呢?用户当然不知道,甚至认证服务也不知道,因为公钥已经公开了,任何一个业务都可以声明自己是“某某某”。
为了得到用户的信任,认证服务就不得不帮助用户来鉴别业务服务。所以,认证服器决定不公开公钥,而是要求业务服务先申请注册并通过审核。只有通过审核的业务服务器才能得到认证服务为它创建的,仅供它使用的公钥。如果该业务服务泄漏公钥带来风险,由该业务服务自行承担。现在认证服务可以清楚的告诉用户,“某某某”服务是什么了。如果用户还是不够信任,认证服务甚至可以问,某某某业务服务需要请求 A、B、C 三项个人数据,其中 A 是必须的,不然它不工作,是否允许授权?如果你授权,我就把你授权的几项数据加密放在 Token 中……
废话了这么多,有没有似曾相识……对了,这类似开放式 API 的认证过程。
看了那么多,我希望下次面试官问我登录页面怎么设计的时候,我可以说一番长篇大论哈哈哈。
角度一:是否有状态
角度二:存储位置
角度三:创建者
角度四:传输方式