八、Web应用
1. 基础
1.1 Cookie
-
Cookie的处理分为几步:
服务器向客户端发送Cookie;
浏览器将Cookie保存;
之后每次浏览器都会将Cookie发送到服务器端。
-
由于Cookie的实现机制,当服务器端向客户端发送了设置Cookie的意图,除非Cookie过期,否则客户端每次发送请求都得带上Cookie,当设置的Cookie过多,会导致报头过长,浪费了部分带宽。
1.2 Session
- Session的数据保存在服务器端,客户端无法随意修改,这样数据的安全性能够得到保障,数据也无需一直在协议中流动;
- 有两种常见的将每个客户端与服务器一一对应起来。
1.2.1 基于Cookie来实现用户和数据的映射
- 口令放在Cookie中,即使被篡改,发送到服务器端的时候就失去了映射关系,因此就无法对数据进行操作;
- Session的默认时间一般设置为20分钟。一旦20分钟内客户端和服务器端无交互行为,服务器端就会将数据删除。
- 只要Session设置的过期时间设置的比较短,安全性就越高。
- 一旦服务器端启动了Session,就会约定生成一个键值作为Session口令,这个值可以随意约定。
- 一旦服务器检测到客户端发来的Cookie中没有对应的键值对,就会自动生成一个值与之对应,这个值是唯一的,并设定好过期时间。
- 这个方案依赖Cookie实现,也是大多数Web应用的实现方法。一旦禁止了Cookie使用,绝大多数登录功能就会失效。
1.2.2 通过查询字符串来实现客户端和数据的对应
- 原理是去检查请求的查询字符串,一旦没有对应的值,就会自动生成新的带值的URL。
- 例如:当用户访问http://localhost/index时,服务器端检查到URL中不带有值,会自动生成session_id,拼接到URL后,把用户重定向到http://localhost/index?session_id=xxxxxxxx。这样,新来的请求就带有了session_id。
- 这种方式相比于Cookie,风险更大。一旦有人复制了这个带session_id网址给你,你也可以以他的身份来访问数据。
1.3 Session与内存
-
上述方法中,我们将session存储在Node内存中,根据Node的内存管理,如果用户过多,可能会超出内存限制,触发垃圾回收,从而引起性能隐患。
-
另一个问题是我们可能启动了多个进程,用户的请求可能被随机分配到某个进程中,这样可能会导致session错乱。
-
为了解决性能问题,我们通常将这些数据进行集中化,统一转移到数据存储中,最常用的就是Redis、Memcached这些高效缓存。
-
这些高速缓存会引起网络访问的问题。网络访问没有直接访问磁盘那么高速,因此可能会有延迟的隐患。但是我们依然采用这些高效缓存的方法,原因是:
Node与这些高效缓存保持长连接,握手的延迟只会影响初始化;
高速缓存直接在内存中进行存储和访问;
缓存服务通常和Node服务运行在同一网络环境下,因此网络延迟很小。
1.4 缓存
-
为了提高浏览器性能,YSlow提到了几条规则:
添加Expires或Cache-Control到报文头中;
让Ajax可缓存
配置Etags
-
通常来说,POST、DELETE、PUT这些请求是不做缓存的,大多数缓存只在GET请求中。
-
当浏览器发送一个请求前,它会先检测本地是否有未过期的文件,如果有文件就直接采用;否则就向服务器端发送请求,获取响应文件。
-
在上述步骤中,检测本地文件是否过期,浏览器会发送一次条件请求,所谓条件请求,就是在get请求中添加一个请求头——If-Modified-Since字段。
If-Modified-Since: Sun,01 Jan 2021 00:00:00 GMT
它向服务器询问这个文件的最后修改日期是否大于这个请求字段,如果超过则返回新的文件,否则就只需要响应一个304状态码,直接使用本地文件。
-
但是,时间戳只能精确到秒,对于频繁更新的内容无法生效。因此HTTP1.1引入了ETags来解决这个问题:
If-None-Match: '123-131312312'
ETags是由服务器自行生成的散列值,根据文件内容进行生成,一旦文件内容改变,就会生成新的散列值;
一旦散列值不匹配,就会响应新的文件,因此可以解决时间精度问题。
-
尽管以上的方法可以在文件内容无修改的情况下节省带宽,但它仍然会发起一个HTTP请求。
-
要做到不发送请求,就是在服务器端响应内容时,设置过期时间Expire。每次发送请求前,检测这个文件是否过期,再进行后续的操作。
1.5 Basic认证
- Basic认证是当客户端与服务器端进行请求时,通过用户名密码来实现的一种身份认证方式。
- 如果一个页面需要Basic认证,会在请求头中添加Authorization字段,这个字段由
用户名:密码
拼接后经过Base64编码而成; - 若用户首次访问,浏览器可以弹出对话框向用户寻求用户名和密码,认证通过后,服务器端返回200状态码,浏览器保存用户名和密码,之后的每一次请求都会带上Authorization字段。
- 由于Authorization时明文的,因此一般只会在HTTPS中使用。
2. 数据上传与安全
2.1 内存限制
-
在解析表单、JSON、XML数据时,我们是先保存起来,然后进行解析后传递给业务逻辑。这种策略只适合小数据量的请求,一旦数据量过大,还是会造成内存问题。
-
解决这些问题有两个方案:
- 限制内容大小。一旦超过限制便返回400状态码,停止数据接收。
- 通过流式解析,将数据流保存到本地磁盘,Node只保存文件路径。
2.2 CSRF
-
CSRF全称是Cross-site Request Forgery,跨域请求伪造。
-
用户提交数据时,攻击者可以引诱用户前往另一个域名提交数据,并发送原域名的Cookie到服务器进行校验。
-
例如,b_domain.com/form下有这样一个表格,可以以a_domain.com/form的名义进行数据传输。
<form action="a_domain.com/form"> <input type="password" placeholder="password"/> </form>
3. 路由解析
3.1 MVC
-
MVC的主要思想是将业务逻辑与职责分离:
控制器,一组行为的集合
模型,数据相关的操作和封装
视图,视图的渲染
-
MVC的工作模式如下:
路由解析,根据URL寻找到对应的控制器行为
行为调用相关的模型,进行数据操作
数据操作后调用视图和数据进行页面渲染,输出到客户端
3.2 RESTful
- REST的全称是Representational State Transfer,意为表现层状态转化。符合REST的设计称为RESTful;
- 它的设计哲学是将服务器端的内容实体看作一个资源,表现在URL上;
4. 中间件
-
中间件可以简化隔离基础操作与业务逻辑之间的细节,让开发者能够关注在业务的开发上,以达到提升开发效率的效果;
-
中间件封装了底层的细节,为上层提供更为方便的服务;
-
由于Node异步的原因,我没需要提供一种机制,在当前中间件执行完之后执行下一个中间件,这里可以采用尾触发的方式来实现
var middleware = function(req,res,next) { // TODO next(); }
-
当中间件出现错误时,可能会导致node终止进程,因此在设计使用中间件的方法时需要捕获异常。
-
可以使用
async
函数进行,对await
进行try操作,当出现异常时,及时抛出异常。
5. 页面渲染
5.1 响应内容
5.1.1 MIME
-
如果想要客户端以正确的方式来响应内容,需要对
Content-Type
进行设置,我们称为MIME值。 -
MIME的全称为Multipurpose Internet Mail Extensions,最早用于电子邮件,后来也应用到浏览器中;
-
Content-Type除了MIME值以外,还可以包含一些参数:
{ "Content-Type" : 'application/json;charset=utf-8' }
5.1.2 附件下载
-
在一些场景下,我们希望不打开页面直接弹出下载框,对此有一个专门的字段来满足这个需求——
Content-Disposition
。 -
这个字段影响的行为是客户端会根据它的值判断是应该将数据直接渲染还是作为附件下载;
-
当只需要查看时,这个字段的值为
inline
; -
当为附件下载时:
{ "Content-Disposition" : 'attachment;filename="filename.txt"' }
5.1.3 响应跳转
-
当URL出现某些问题不能直接处理请求时,需要将用户跳转到其他页面去,此时也可以通过
Location
字段来实现:res.redirect = function (url) { res.setHeader("Location",url); res.writeHead(302); res.end('Redirect to ' + url); }