1.cookie 与 localStorage
- cookie是客户端用来保存用户状态的(处理session会话),是完成交互式应用的必备条件。当然也可以存储kv键值对(例如存储token等),但是cookie的大小限制是4k,只能存储有限的数据。
cookie在第一次发出请求时(或者登录或者请求页面),响应会在Set-Cookie中返回一定的信息,用作记录用户的状态,该状态会保存在客户端浏览器中,一般在对同一个域名发请求时会携带此cookie,服务端验证请求中的cookie并判断如何响应。过程如下
cookie中的kv对还有其他的一些属性
①Expires属性:设置Cookie的生存期。在java中可以通过setMaxAge属性设置cookie的生存期Cookie cookie = new Cookie("name","jianshu"); // 设置生命周期为Integer.MAX_VALUE,永久有效,会持久化到浏览器中 cookie.setMaxAge(Integer.MAX_VALUE); // 设置maxAge为正数,持久化到浏览器中指定时间,不管是否关闭浏览器 cookie.setMaxAge(Integer.MAX_VALUE); //当maxAge属性为负数,则表示该Cookie只是一个临时Cookie,不会被持久化,仅在本浏览器窗口或者本窗口打开的子窗口中有效,关闭浏览器后该Cookie立即失效 cookie.setMaxAge(Integer.MAX_VALUE); //当maxAge为0时,表示立即删除同名的Cookie cookie.setMaxAge(0); resp.addCookie(cookie);
②Path属性:定义了访问例如“/test”路径时是否可以携带该cookie
③Domain属性:指定了可以访问该 Cookie 的 Web 站点或域。Cookie 机制并未遵循严格的同源策略,可以设置父域的Cookie。当设置domain为域.jianshu.com,aa.jianshu.com和bb.jianshu.com使用同样的cookie。单点登录时会有用处,然而也增加了 Cookie受攻击的危险,比如攻击者可以借此发动会话定置攻击。
④Secure属性:指定是否使用https安全协议发送Cookie。使用HTTPS安全协议,可以保护Cookie在浏览器和Web服务器间的传输过程中不被窃取和篡改。
⑤HttpOnly 属性 :防止客户端脚本通过document.cookie属性访问Cookie,有助于保护Cookie不被跨站脚本攻击窃取或篡改。但是,HTTPOnly的应用仍存在局限性,一些浏览器可以阻止客户端脚本对Cookie的读操作,但允许写操作;此外大多数浏览器仍允许通过xml对象读取响应中的Set-Cookie
- localStorage/sessionStorage:cookie由于存储大小有限,浏览器设计了专门用于存储数据量的仓库,大概为几兆(各个浏览器提供的不同),虽然有点不是特别大,但是对于一般的应用基本够用了。它们对于单页面应用的制作有一定的作用。
sessionStorage用于存储会话信息,localStorage用于永久性存储。它们对于大多数浏览器都是可用的,而且使用也非常简单,详细使用可以查看localStorage使用总结
- WebSql/IndexedDB:localStorage虽然能够完成了小数据量的存储,但是不能实现大数据量和条件查询。为了完成上述两个功能,浏览器又创造了IndexedDB与webSql,这两个对象都可以在本地创建一个前端数据库。目前IndexedDB相比WebSql兼容的浏览器较多,IndexedDB不仅提供查找接口,还能建立索引。
IndexedDB有数据库和数据仓库的概念分别对应关系型数据库中的数据库和表,不同的是数据库有版本的区别,同一个时刻,只能有一个版本的数据库存在。如果要修改数据库结构(新增或删除表、索引或者主键),只能通过升级数据库版本完成
下面是IndexedDB的简单使用//新建数据库与打开数据库都是这步操作,但是如果是新建数据库会发生版本升级可以从onupgradeneeded事件中得到db对象,而打开数据库会从onsuccess事件中得到db //注意:打开数据库时如果指定的版本号,大于数据库的实际版本号,也会走onupgradeneeded事件。 var request = window.indexedDB.open(databaseName, version); //新建数据库时监听的事件 request.onupgradeneeded = function(event) { var db = event.target.result;//得到数据库对象 } //打开数据库时监听的事件 request.onsuccess = function (event) { var db = request.result//得到数据库对象 } //新建表和主键 var objectStore = db.createObjectStore(tableName, { keyPath: 'id' }); //新建索引,参数索引名称、索引所在的属性、配置对象 //主键(key)是默认建立索引的属性。比如,数据记录是{ id: 1, name: '张三' },那么id属性可以作为主键 var index = objectStore.createIndex(indexName, indexColumn, { unique: true }); //数据的增删改查都需要使用事务,所以执行增删改查时先新建一个事务对象,其中的第二个参数是操作模式("只读"或"读写") var transaction= db.transaction([tableName], 'readwrite') //通过事务得到数据表对象执行增删改查的操作 var objectStore = transaction.objectStore(tableName); objectStore .add({ id: 1, name: 'aa', age: 24, email: 'aa@123.com' })//增加数据 var request = objectStore.get(1);//通过主键查询 request.onsuccess = function( event) { if (request.result) { console.log('Name: ' + request.result); } }; objectStore .put({ id: 1, name: 'bb', age: 35, email: 'bb@123.com'});//修改主键为1的数据 objectStore .delete(1);//删除主键为1的数据
IndexedDB与WebSql不同,它不使用sql语句,属于NoSql的一种,而且有很多优点,例如支持事务,异步等等,详情可以查看浏览器数据库 IndexedDB 入门教程
2.cors跨域请求
1.同源策略
为了保存用户的登陆状态,我们使用浏览器cookie+服务器端session的方式。用户登陆成功之后,服务器会生成对应的session保存会话状态,然后将session保存在Set-Cookie中返回给我们;下次请求携带cookie,服务器就知道是谁在访问了。
但是,如果A网站的cookie被其他网站拿到,那么其他网站就有可能使用我们的cookie在A网站做一些危险操作,比如转账等。这种利用其他网站登录状态的操作叫做跨域请求伪造(csrf)。所以为了防止跨域攻击,浏览器只会发送同源(协议相同,域名相同,端口相同)网站的cookie,例如,访问http://www.baidu.com网站时,只会发送该网站的cookie数据,不会发送http://www.qq.com的cookie。这就叫做同源策略。同源策略包括:
①无法读取其他网站的Cookie、LocalStorage 和 IndexDB 数据。为了让a网站读取b网站的cookie,可以利用上面说的domain属性
②iframe页面嵌套不同源页面时,DOM元素无法获得(window.open打开的跨源窗口也不可以。iframe页面可以使用window.postMessage方法完成通信同源策略与请求
③不能发送不同源的ajax请求。为了支持ajax的跨源操作,需要用到跨域资源共享(cors)。跨域资源共享与普通的ajax请求相同,但是浏览器在了解到是跨域请求后便会自动执行,不需要用户,但是服务端需要根据请求源判断时候同意跨域。跨域资源共享分为简单请求与非简单请求:
- 如果是get,post,head请求方而且请求参数只包括Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type(只限于application/x-www-form-urlencoded、multipart/form-data、text/plain)时称为简单请求。简单请求是为了兼容表单提交模式。对于简单请求,浏览器会增加一个Origin字段(指明请求的源),这个字段是服务器来判断跨域是否被允许的依据。如果跨域请求被允许,则会返回
//该字段必须存在,表示发起请求的域名,如果允许所有的网站跨域访问则写成*; ***需要注意的是:java中response可能不能设置多个域名,所以先只能循环判断,在设置请求的域名*** Access-Control-Allow-Origin: http://www.otherhome.com //该字段可选。表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。 //除此之外xhr也需要设置成可以发送cookie,使用***xhr.withCredentials = true***。需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名 Access-Control-Allow-Credentials: true //该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('myheader')可以返回myheader字段的值。 Access-Control-Expose-Headers: myheader Content-Type: text/html; charset=utf-8
- 如果非get,post,head请求则统称为非简单请求,非简单请求需要在请求之前发起一次预检请求,预检请求使用OPTIONS方法。整个过程也只不需要用户参与。预检请求形如:
//预检请求的请求数据 OPTIONS /cors HTTP/1.1 //预检请求需要传递Origin Origin: http://www.otherhome.com //非简单请求的方法 Access-Control-Request-Method: PUT //非简单请求的需要传递的请求头参数 Access-Control-Request-Headers: Custom-Header Host: api.alice.com User-Agent: Mozilla/5.0 //预检请求的响应数据 HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) //请求端允许请求的域,与简单请求的使用方法相同。如果不允许该网站跨域,则不返回有关信息 Access-Control-Allow-Origin: http://www.myhome.com //请求端允许使用的方法 Access-Control-Allow-Methods: GET, POST, PUT //请求端允许使用的请求头参数 Access-Control-Allow-Headers: Custom-Header //一次预检的有效期 Access-Control-Max-Age: 1728000 Content-Type: text/html; charset=utf-8
预检请求成功后就与简单请求的过程相同了,也会有Origin和Access-Control-Allow-Origin字段,详情可以查看跨域资源共享讲解。我们经常使用nginx来实现负载均衡,但是nginx也可以配置后用来解决前后端分离开发的问题,例如下图
使用场景:
如果前后项目部署在多个服务器或者图片使用第三方存储的时候,可能会经常使用跨源。
3.token
3.1来源:
为了记录用户登陆的状态,我们发明了sesion,但是目前为止session已经不能适应时代的发展。原因:
- 用户量的增加,导致服务器需要记录的数据量变大,服务器不堪重负;
- 分布式系统中,各服务器不共享session。假如用户在a服务器登录,但是不久之后,请求被发送到b服务器,可以用户的登录记录只存在a中,向b服务器发送的请求就会返回未登录;
- 移动设备不支持cookie和session;
- token可以很好的解决跨域问题;
基于以上原因,token应运而生。session的目的就是为了检验登陆状态,而token的设计就带有检验功能。而且token不需要存储在服务器端,可以存储在客户端的任何地方(cookie,localStorage等),只需要在请求时设置在请求头中即可,服务器端会提取请求头中的token并检验,如果通过则说明登录。
使用jwt完成登录校验流程:
- 用户使用用户名,密码来请求服务器
- 服务器验证用户的信息,通过验证发送给用户一个token
- 客户端存储token,并在每次请求时附送上这个token值
- 服务端验证请求的token,如果验证通过返回数据,否则显示未认证
3.2构成
我们经常使用的token是Json web token(jwt),也是实现上面登录流程的token。
它由三部分构成:第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature)。
①头部有两部分信息:声明类型,也就是是jwt;声明加密的算法 例如 HMAC SHA256{ 'typ': 'JWT', 'alg': 'HS256' }
②载荷就是存放有效信息的地方。包含三个部分:标准中注册的声明;公共的声明;私有的声明。其中
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
{ "sub": "1234567890", "name": "John Doe", "admin": true }
note:公共的声明和私有的声明可以添加其他任何信息,但是,token中的信息可以被解密出来,所以不建议存放敏感信息
③第三部分是一个签证信息(signature),这部分需要将base64加密后的header和base64加密后的payload连接组成的字符串(头部在前),然后通过header中声明的加密方式进行加盐secret组合加密,就构成了jwt的第三部分signature = encrypt(base64(header) + base64(payload), salt);
最后,jwt=base64(header)+","+base64(payload)+","+signature。形如:
//base64(header),base64(payload),signature; eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
note:需要注意的是由于jwt的header和payload只使用了base64加密,这种方式是不安全的,所以不建议将敏感信息存储在token中;另外为了防止第三方恶意伪造token,网站会加密token后返回给客户端,如果客户端请求的token解密失败,则说明被修改或者伪造。
3.3token的使用
token的使用场景非常广泛,除了上面提到的jwt,还有pc端二维码登录,第三方授权登录等。
我们的app在登陆一次之后就在需要登录了,即使关机的情况下也不需要,其实就用到了上面的jwt。
pc端二维码登录
主要分为三步:1.pc端生成登陆二维码;2.用户使用app扫描;3.用户点击app上的确认登陆。具体步骤分为
①PC端向服务端发起请求,告诉服务端,我要生成用户登录的二维码,并且把PC端设备信息也传递给服务端
②服务端收到请求后,它生成二维码ID,并将二维码ID与PC端设备信息进行绑定,然后把二维码ID返回给PC端
③PC端收到二维码ID后,生成二维码(二维码中肯定包含了ID)
④为了及时知道二维码的状态,客户端在展现二维码后,PC端不断的轮询服务端,比如每隔一秒就轮询一次,请求服务端告诉当前二维码的状态及相关信息
⑤⑥用户用手机去扫描PC端的二维码,通过二维码内容取到其中的二维码ID
再调用服务端API将移动端的身份信息与二维码ID一起发送给服务端
⑦服务端接收到后,它可以将身份信息与二维码ID进行绑定,生成临时token。然后返回给手机端
⑧因为PC端一直在轮询二维码状态,所以这时候二维码状态发生了改变,它就可以在界面上把二维码状态更新为已扫描
⑨⑩手机端在接收到临时token后会弹出确认登录界面,用户点击确认时,手机端携带临时token用来调用服务端的接口,告诉服务端,我已经确认
服务端收到确认后,根据二维码ID绑定的设备信息与账号信息,生成用户PC端登录的token
最后:这时候PC端的轮询接口,它就可以得知二维码的状态已经变成了"已确认"。并且从服务端可以获取到用户登录的token。到这里,登录就成功了,后端PC端就可以用token去访问服务端的资源了。具体信息可以查看二维码登录简介
第三方授权登录
我们在登陆一些网站时,经常为了方便使用微信或者微博账号登录,这样不仅不需要注册大量信息,而且还可以减少撞库的风险。这种方式登录第三方的行为就使用了token。
撞库:我们经常为了方便将多个网站的密码设置成相同或者相似的,如果一个网站的密码泄露时,黑客可能会得到会得到我们很多网站的权限,这种行为叫做撞库撞库简介
第三方登录的流程(以OAuth的授权码模式为例):
Third-party application:第三方应用
HTTP service:服务提供商,本文中指的是微信或者微博
Resource Owner:用户/资源拥有者,本文指的是在微信中注册的用户
Authorization server:认证服务器,在资源拥有者授权后,向客户端授权(颁发 access token)的服务器
Resource server:资源服务器,务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器
具体流程(详细流程可以查看OAuth第三方授权登录):
①用户登录第三方网站,并使用微信授权的方式登录;
②这是会跳转到微信的登录授权页面,跳转地址形如:https://www.weixin.com/oauth/authorize?response_type=code&client_id=CLIENT_ID& redirect_uri=CALLBACK_URL&scope=read
- response_type参数表示授权类型(code)
- client_id参数让微信知道是哪个第三方应用在请求,每个支持微信授权登录的第三方都需要先到微信平台申请,得到相应的client_id和client_secert
- redirect_uri参数是微信接受或者拒绝授权后的跳转网址,这里我们假设是https://music.163.com/callback
- scope参数表示要求的授权范围(这里是只读)
③如果用户同意授权就会从微信跳转到redirect_uri所指向的地址,并且会传回授权码,请求形如:
https://music.163.com/callback?code=AUTHORIZATION_CODE
④第三方网站就会使用这个授权码(code)和第三方身份信息(client_id,client_secret)向授权服务器申请token,请求形如:
https://b.com/oauth/token?client_id=CLIENT_ID&client_secret=CLIENT_SECRET& grant_type=authorization_code&code=AUTHORIZATION_CODE&redirect_uri=CALLBACK_URL
- client_id参数和client_secret参数用来确认第三方应用的身份(client_secret参数是保密的,因此只能在后端发请求)
- grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码
- code参数是上一步拿到的授权码
- redirect_uri参数是微信生成token后的回调网址
微信方会生成授权token,并通过redirect_uri返回一段json
{ "access_token":"ACCESS_TOKEN", "token_type":"bearer", "expires_in":2592000, "refresh_token":"REFRESH_TOKEN", "scope":"read", "info":{...} }
其中access_token就是授权token,expires_in是令牌的有效期。最后第三方网站就可以使用该token请求用户的微信数据。
更新令牌:令牌的有效期到了,如果让用户重新走一遍上面的流程会比较麻烦。所以我们设计了自动更新令牌的方法,需要用到上一步的REFRESH_TOKEN,更新令牌请求形如:https://b.com/oauth/token?grant_type=refresh_token&client_id=CLIENT_ID& client_secret=CLIENT_SECRET&refresh_token=REFRESH_TOKEN
- grant_type参数为refresh_token表示要求更新令牌
- client_id参数和client_secret参数用于表示第三方应用身份
- refresh_token参数就是用于更新令牌的令牌,也就是上面返回的json段中的REFRESH_TOKEN
参考:
localStorage使用总结
HTML5前端数据库——Web SQL Database
深入理解Cookie
数据库 IndexedDB 入门教程
IndexedDB
http请求头
session, token