http、浏览器相关

本文来自于我的github转载时请注明出处

0.前言

单机玩多了,localhost是最亲近的伙伴,然后到了面试,问起http、cookie相关的,一些安全处理、错误处理,就不行了,所以现在开始慢慢总结,恶补这方面。

1.url

1.1url的?和#有什么效果和区别

  • #后面的内容是网页位置标识符,一般是锚点<a name='xx'>或id属性<div id='xx'>。通过location.hash可以取到该值,常见的返回顶部也可以利用href=‘#’。改变#后面的内容不会引起页面重新刷新,但是会有历史记录,所以可以后退。这对于ajax应用程序特别有用,可以用不同的#值,表示不同的访问状态,然后向用户给出可以访问某个状态的链接。但是IE 6和IE 7不会有历史记录。#后面的内容不会提交到服务器。
    对于a标签,平时有一个常规的操作:
    想要在某个点击按钮变成a标签的那个cursor(手指),一般就用a标签包住文字,
    <a href="#">按钮</a>但是这样子是会有历史记录,所以我们应该改成
    <a href="javascript:void 0">按钮</a>

  • ?不调用缓存的内容,而认为是一个新地址,重新读取。?后面的内容一般是get请求的内容或者页面传参,以键值对的形式。通过location.search可以取到该字符串。 改变?后面的内容会引起页面刷新,?后面的内容会提交到服务器。

2.cookie

常用的会话跟踪技术是Cookie与Session。Cookie通过在客户端记录信息确定用户身份,只有过期才能消失。
HTTP协议是无状态的协议,一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接。这就意味着服务器无法从连接上跟踪会话。有了cookie就可以实现会话跟踪,可以说是刚刚好弥补http由于无状态带来的缺陷。客户端再向服务器发送请求的时候,都会把相应的Cookie再次发回至服务器,Cookie信息则存放在HTTP请求头(req)

服务器的响应头如果包含Set-Cookie这个头部时,就在客户端新建一个cookie,并且在后续的http请求中自动发送这个cookie到服务器端,直到这个cookie过期,比如在node中返回cookie可以这样设置

 res.setHeader('Set-Cookie', 'isVisit=true;path=/;max-age=1000;httponly');

domain:主域(可以灵活设置www.test.com、.test.com)
max-age:多久后过期
expires:具体过期的时间(UTC、GMT)
path:cookie所在的目录,默认/
secure:在https和ssl传输
httponly:设置后不能通过js操作

浏览器会将domain和path都相同的cookie保存在一个文件里,cookie间用*隔开

3.token

token也是用户身份的验证方式,最简单的token组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,由token的前几位+哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器)。还可以把不变的参数也放进token,避免多次查库,甚至也可以拿数据库那个唯一的_id来加密形成token。

在前端的概念中,并没有原生的token这种东西,这是一种约定俗成的而产生的一种东西,功能大概就是这样子,验证身份、保证安全,类似于cookie。

基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。客户端登录,服务端收到请求,去验证用户名与密码,在验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端,客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者Storage 里,客户端每次向服务端请求资源的时候需要带着服务端签发的 Token。服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据。其实,看起来像是cookie,但是他一般就是为了保证cookie安全而存在的。因为cookie有时候会被窃取,被xss、csrf攻击,有了这个唯一的token,就可以让cookie更加安全。
第一次登录后,以后每次发送请求时,凡是需要验证的地方都要带上该token,然后服务器端验证token,成功返回所需要的结果,失败返回错误信息,让他重新登录。其中服务器上token设置一个有效期,每次APP请求的时候都验证token和有效期。

在node上,也可以自己实现一个token创建、验证、删除的函数。下面我们基于express的generator构建一个token验证。首先搭建好express脚手架,然后加上下面的依赖:

//package.json配置,npm安装不了就cnpm
{
  "name": "token",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "basic-auth": "^2.0.0",
    "body-parser": "~1.18.2",
    "cookie": "^0.3.1",
    "cookie-parser": "~1.4.3",
    "cookie-signature": "^1.1.0",
    "debug": "~2.6.9",
    "ee-first": "^1.1.1",
    "ejs": "~2.5.7",
    "express": "~4.15.5",
    "finalhandler": "^1.1.1",
    "media-typer": "^0.3.0",
    "merge-descriptors": "^1.0.1",
    "morgan": "~1.9.0",
    "ms": "^2.1.1",
    "on-headers": "^1.0.1",
    "proxy-addr": "^2.0.3",
    "raw-body": "^2.3.2",
    "send": "^0.16.2",
    "serve-favicon": "~2.4.5",
    "setprototypeof": "^1.1.0",
    "utils-merge": "^1.0.1",
    "vary": "^1.1.2"
  }
}

新建一个token.js,然后封装我们的token的函数

var crypto=require("crypto");
var token={
    createToken:function(obj,timeout){
        console.log(parseInt(timeout)||0);
        var obj2={
            data:obj,//payload
            created:parseInt(Date.now()/1000),//token生成的时间的,单位秒
            exp:parseInt(timeout)||10//token有效期
        };

        //payload信息
        var base64Str=Buffer.from(JSON.stringify(obj2),"utf8").toString("base64");

        //添加签名,防篡改
        var secret="hel.h-five.com";
        var hash=crypto.createHmac('sha256',secret);
            hash.update(base64Str);
        var signature=hash.digest('base64');
        return  base64Str+"."+signature;
    },
    decodeToken:function(token){

        var decArr=token.split(".");
        if(decArr.length<2){
            //token不合法
            return false;
        }

        var payload={};
        //将payload json字符串 解析为对象
        try{
            payload=JSON.parse(Buffer.from(decArr[0],"base64").toString("utf8"));
        }catch(e){
            return false;
        }
        //检验签名
        var secret="hel.h-five.com";        
        var hash=crypto.createHmac('sha256',secret);
            hash.update(decArr[0]);
        var checkSignature=hash.digest('base64');
        return {
            payload:payload,
            signature:decArr[1],
            checkSignature:checkSignature
        }
    },
    checkToken:function(token){
        var resDecode=this.decodeToken(token);
        if(!resDecode){
            return false;
        }

        //是否过期
        var expState=(parseInt(Date.now()/1000)-parseInt(resDecode.payload.created))>parseInt(resDecode.payload.exp)?false:true;
        if(resDecode.signature===resDecode.checkSignature&&expState){
            return true;
        }

        return false;
    }
}
module.exports=token;

接下来,就随便玩了。如果token明文传输的话会非常的危险,所以建议一定要使用HTTPS,并且用post方法token放在请求体里。

4. 会话追踪

既然讲了cookie,而且能实现会话跟踪,加上token更加保证安全性。那么我们还有常见的一些其他方法进行不重要信息的会话跟踪

4.1页面传值(重写url)

如果是一些不是特别重要的信息,我们就可以直接把他带上url参数里面,比如分页插件的页数跳页保留、日期跳页保留、前一页设置参数后一页发请求等等。这些东西一般也并不需要出动cookie、sessionstorage、localstorage这些,显得大材小用。

4.2隐藏表单

这种方法,其实就是把用户提交的信息放在一个隐藏的表单里面,以供后面的用户验证页面利用。比如那些,xxx您好,这个xxx是用户名或者昵称之类的,我们可以隐藏这个,提供给后续页面。不过如果放重要的信息,是不安全的,前端怎么防御都是一层纸,所以一般也是不要放上重要的信息。这个方法最适用于大量数据存储的会话应用。

4.3session、localstorage

如果用过三大框架,都有一个感觉:做spa,当数据多的时候,页面复杂而庞大的时候,一些数据想在很深层的路由页面用,就可能想到这两个storage。其实也是一种方法,上面的隐藏表单是另一种方法,而对于框架又产生了一种单向数据流的概念,接着就有vuex、redux、ngrx的产生,他们基本都是:v层用户行为→触发action→后端交互或者v层要改变→mutations突变→改变state→重新渲染v层
主要的是,他们把这些曾经我们想用两个storage、隐藏表单的东西,放在了这个state(或者说像是store的中转站里面)。用组件化思维开发,其实这些共用的状态管理场所,就放在全局里面,其他组件不管是多少级的,都可以利用。

5.浏览器缓存机制

前面我们讲了基于应用层实现缓存的sessionstorage和localstorage,现在我们讨论一下基于http协议实现的缓存,先看一下他们的概念:
我们会用物理的时间(具体某一个时间点:2018年1月11日 12:00:00)和时间间隔(10小时)来辅助描述他们的概念。强制缓存是expires和cache-control,协商缓存是etag和last-modified。

强制缓存:强制缓存不需要与服务器发生交互,返回结果是200,from cache
缓存命中:客户端请求数据,现在本地的缓存数据库中查找,如果本地缓存数据库中有该数据,且该数据没有失效。则取缓存数据库中的该数据返回给客户端。
缓存未命中:如果数据失效,就向服务器请求该数据,此时服务器返回该数据和该数据的缓存规则返回给客户端,客户端收到该数据和缓存规则后,一起放到本地的缓存数据库中留存。以备下次使用。

协商缓存需要访问服务器,如果命中缓存的话,返回结果是304。

  • Cache-Control:本地缓存有效时间间隔。最常见的,Cache-Control:max-age=100(单位:秒) 表示文件在本地缓存,从发出请求算起,有效时长是100秒。在接下来100秒内,如果再次请求这个资源,浏览器不会发出 HTTP 请求,而是直接使用本地缓存的文件。
    另外还有Expires,Expires则是一个具体的时间:
    2018年1月11日 12:00:00,在这个时间前都是有效的。当然,时间格式一般是GMT(类似new Date()生成的那种)。在实际应用,建议用max-age,因为对于不同的用户,虽然有相同expire但是实地时间不一样。Expires 是 HTTP1.0 标准中的字段,Cache-Control 是 HTTP1.1 标准中新加的字段,功能一样,都是控制缓存的有效时间。
    我们来看一下例子:
    node服务器:
  if (req.url === '/' || req.url === '' || req.url === '/index.html') {
    fs.readFile('./index.html', function(err, file) {
      console.log(req.url)
      res.setHeader('Cache-Control', "no-cache, max-age=" + 5);
      res.setHeader('Content-Type', 'text/html');
      res.writeHead('200', "OK");
      res.end(file);
    });
  }
  if (req.url === '/pic') {
    fs.readFile('./static/1.png', function(err, file) {
      res.setHeader('Cache-Control', "max-age=" + 5);//缓存五秒
      res.setHeader('Content-Type', 'images/png');
      res.writeHead('200', "Not Modified");
      res.end(file);
    });
  }

第一次进去的时候,我们打开network可以看见
1
1
5秒内再次刷新的话:
1

1

对比可以看见他们的区别,缓存不过期的时候,直接读取

  • Last-Modified: 文件在服务器上的最新更新时间(比如1月1日)。再次请求时,如果文件缓存过期,浏览器通过 If-Modified-Since 字段带上这个时间(1月1日),发送给服务器,由服务器比较时间戳来判断文件是否有修改。如果没有修改(服务器最后更新时间:1月1日),服务器返回304告诉浏览器继续使用缓存;如果有修改(服务器最新更新时间:1月2日),则返回200,同时返回最新的文件。一般我们会给Cache-Control设置一个合理的时间间隔,因为如果太长,服务器文件更新用户这边就不能及时更新;如果太短,缓存的存在意义也不大。因此Cache-Control 通常与 Last-Modified 一起使用。Cache-Control 与 Last-Modified 是浏览器内核的机制,一般是默认进行,不能更改或者设置。

  • Etag :和 Last-Modified 差不多,但Etag 值是利用一个对文件进行标识的字串,对文件进行标识。在向服务器查询文件是否有更新时,浏览器通过 If-None-Match 字段把特征字串发送给服务器,由服务器和文件最新特征字串进行匹配,来判断文件是否有更新。没有返回304,有更新返回200。
    我们在node服务器演示一下,对于上面的cache-control部分的/pic那段代码,我们加以修改

  if (req.url === '/pic') {
    fs.readFile('./static/1.png', function(err, file) {
    if (!req.headers['if-none-match']) {//第一次
      res.setHeader('Cache-Control', "no-cache, max-age=" + 5);
      res.setHeader('Content-Type', 'images/png');
      res.setHeader('Etag', "abc");
      res.writeHead('200', "Not Modified");
      res.end(file);
    } else {
      if (req.headers['if-none-match'] === 'abc') {//第二次请求并匹配成功
        res.writeHead('304', "Not Modified");
        res.end();
      } else {//不匹配就和第一次请求一样的处理
        res.setHeader('Cache-Control', "max-age=" + 5);
        res.setHeader('Content-Type', 'images/png');
        res.setHeader('Etag', "abc");
        res.writeHead('200', "Not Modified");
        res.end(file);
      }
    }
    });
  }

第一次
1

再次
1

过程:
1.客户端请求一个页面。
2.服务器返回页面,并在给A加上一个Last-Modified/ETag。
3.客户端展现该页面,并将页面连同Last-Modified/ETag一起缓存。
4.客户再次请求页面,并将上次请求时服务器返回的Last-Modified/ETag一起传递给服务器。
5.服务器检查该Last-Modified或ETag,并判断出该页面自上次客户端请求之后还未被修改,直接返回响应304和一个空的响应体。

5.离线缓存manifest

其实就是Application Cache。在HTML 头中通过 manifest 属性引用 manifest 文件。manifest 文件列出了需要缓存的文件以及相应的设置。manifest文件不一定要manifest结尾,只要能达到效果即可。
浏览器在首次加载 HTML 文件时,会解析 manifest 属性,并读取 manifest 文件,获取 CACHE MANIFEST 下要缓存的文件列表,再对文件缓存。
工作流程:
1

html文件:

<!DOCTYPE html>
<html manifest="index.manifest">
<head>
    <meta charset="utf-8">
    <title></title>
</head>
<body>
<!--注意这里,一个缓存一个没有缓存-->
    <img src="1.jpg">
    <img src="http://localhost:3000/animal/2.jpg">
</body>
<script type="text/javascript">
window.applicationCache.addEventListener('updateready', function(e) {
    if (window.applicationCache.status == window.applicationCache.UPDATEREADY) {
     window.applicationCache .swapCache();
      if (confirm('A new version of this site is available. Load it?')) {
        window.location.reload();
      }
    } else {
      console.log('Manifest didnt changed. Nothing new to server.')
    }
  }, false);
</script>
</html>

manifest文件设置:

CACHE MANIFEST
#version 0.0

CACHE:
#需要缓存的文件
http://localhost:3000/animal/2.jpg

NETWORK:
#每次重新拉取的文件,*是全部
*

FALLBACK:
#离线状态的代替文件
/1.png

我这里开启两个服务器,一个是3000端口的express,第二张图片(2.jpg)来源于这里(另外的文件夹)。一个服务器是1000端口的python服务器,启动主页是index.html页面,1.jpg就在当前路径
第一次进入的时候:

1

1

再次进入
1

1

断掉两个服务器,直接用img标签的图片1读取失败(NETWORK写了*,所以当前路径下所有的文件是需要联网的),而图片2因为缓存所以还能加载

1

1

如果想更新缓存文件,只能改变manifest文件,就算我们把2.jpg换掉,不改manifest是没效果的。当manifest文件里面改动(比如改个版本号,一般也是加个空格、改个版本号来刷新缓存文件),就重新走一遍上面的流程。但是更新缓存过程中,如果有一个文件更新失败,则整个更新会失败。另外,manifest 和引用它的HTML要在相同 HOST下。

总结:(放大看全图)
1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值