这章内容实在太多,每小节我可能只会提到我个人认为需要注意的点,但是最终还是需要各位童鞋自己去看去跟着敲代码体会其中MVC的思想与基础功能的封装过程,我在看本章的过程中,实际上我看了3遍,第一遍是大概浏览理解,第二遍我开始尝试自己去封装路由、中间件等基础功能,原因就是我觉得有些地方作者写复杂了,结果封装到后来自己发现自己设计错误。。。朴老师很多设计思想其实是相对最优解~~~~导致我不可能将自己的错误设计贴出来给大家看。。。。。。于是,最后一遍我就只总结下述几个需要注意的点给大家参考了。
- http server Request 事件的两个参数,
http.IncomingMessage
(req)类与http.ServerResponse
(res)类如何接受不同方法的参数与响应对应MIME到客户端,这里必须区分了解 requestListener 客户端请求这两个东西的区别,requestListener 会被挂载到 request 事件中,监听每次请求,简单来说这点的关键是 路径解析、参数解析 两样; - cookie 与 session 的关系;
- web基础攻防的几个概念:XSS攻击、csrf攻击的意义与防范;
- 为什么需要路由映射;
- Restful 规范怎么用;
- 中间件的概念(请求与响应的过滤器);
- 使用模板渲染前端;(非前端路由渲染);
一、路径解析与参数解析(简单介绍 get 与 post 的情况,其他请求方式类似)
首先我不会像书上一样写上完整的封装过程,我只会写一个demo来说明如何取到
路由解析:我们直接取到创建 server 类的 req 参数,取出 req.url
,然后使用 node 提供的 url 模块去解析即可,不需要自己手动解析啦,例如
GET 请求的路由解析
var server = http.createServer(function (req, res) {
console.log(req.method)
console.log(url.parse(req.url))
}).listen(8181)
// 结果
GET
Url {
protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '?name=YOLO',
query: 'name=YOLO',
pathname: '/test',
path: '/test?name=YOLO',
href: '/test?name=YOLO' }
由此我们可以获得解析后的协议类型、主机名、端口、请求路径、query参数等等,当然这是 get 请求的情况,大家都知道 get 请求的 query 参数是被直接解析的,但是做前端的都知道,get 请求是简单请求,是会被浏览器限制参数长度的,所以我们如果是需要传递 base64 那样巨长的字符串是不可能的。
POST请求的参数解析,注意实际情况需要根据 url.parse()
解析请求地址后指定对应匹配路由进行处理,否则将会响应所有请求。
var server = http.createServer(function (req, res) {
var bufferList = [], len = 0, body
req.on('data', function (chunk) {
bufferList.push(chunk)
len += chunk.length
})
req.on('end', function () {
body = Buffer.concat(bufferList, len)
res.writeHead(200, {'Content-Type': 'json;charset=utf8'})
res.end(body)
})
}).listen(8181)
我这个 demo 用户传递什么就会响应什么给客户端,由于 POST 是复杂请求,其中由客户端传递的参数是可以自定义长度限制的,是有可能传递很长的参数的,这时我们需要用流的方式去读取 body ,此时的客户端参数将会以 buffer 的方式一段一段传过来,我们需要手动接受后进行拼接,在请求处理完成时将其进行拼接后进行逻辑处理,至于其中的逻辑处理就是业务代码了,这里我们不做相关处理。(如果不用流有可能会出现参数获取出现 “断层” 的情况,这点前面的 buffer 章节已经讲过,简单请求 get 是不会传递非常长的数据的,所以我们直接解析没有关系)
至于其他 IncomingMessage
类所挂载的参数,如头部、请求方式、socket等,大家可以直接查看文档 http_class_http_incomingmessage 文档 => request 类(http.server)的 req 参数
而 res 响应的 ServerResponse
类,文档中的例子也已经列的非常清楚了 http_class_http_serverresponse 文档 => http.server 的 response ,需要注意每次响应记得响应状态码,如 200、304、404、500等,熟悉前端的童鞋应该不用我解释为什么了吧~
二、cookie 与 session 的关系
Cookie
我们知道 http 是一种无状态的短连接协议,无法保存状态信息与服务器进行信息交互,这会影响一些我们日常看到的如登录态保存、历史记录等功能,因为我们必须每次请求都能携带之前保存的状态才能在服务端进行判断,此时 cookie 因此而生,客户端与服务端都可以操作与修改它,处理分为以下几步。
- 服务器向客户端发送Cookie;
- 浏览器将Cookie保存;
- 之后每次浏览器都会将Cookie发现服务端;
- 切勿使用Cookie保留敏感信息,因为客户端可操纵,即使保留,还请进行加密处理;
- 与session实现映射后,可利用ip或其他客户端唯一标识实现客户端用户校验,如 jwt 做单点登录的原理
- Cookie不宜保存大量信息,会浪费带宽,因为每次请求都会携带,但是并不是每次都要使用。
- 返回 cookie 时注意设置
Expires
与Max-Age
字段,告知客户端 cookie 何时失效过期,而需要重新获取,若不设置则在客户端关闭会话时就会失效,如关闭浏览器
服务端可在req.headers
中看到 cookies ,然后使用如 cookie-parser 这类工具做解析,或自己手动解析。
Session
自动登录的应用
利用 cookie 与 session 实现用户数据映射关系,然后进行登录态的判断,这可能是我们使用最多的情况,Session 数据是直接位于内存中的,而第五章我们提到过 V8 内核是有内存限定的,所以我们不宜保留过多的数据,我们普遍会将用户的登录态以加密的形式由服务端发送到客户端作保留,当用户下次打开客户端时,只要 cookie 没过期,我们则会获取到之前保留的登录态从而实现自动登录的功能,但有重要的前提:
- 生成登录态口令时,必须加上客户端唯一标识(以防被别人猜出规律还可加上一个规定的密钥拼接后)进行加密,服务端拿到后与当前请求的设备再进行一次同样的加密过程后,与cookie传过来的进行对比,这就杜绝了不同设备盗用 cookie 的可能性了,如 IP、User-agent 等(代理也可以伪造,还是 IP 安全= =),否则就别怪别人拿你 cookie 伪造登录囖~;
- session 同步 cookie 的过期时间,因为我们不可能无限让客户实现自动登录,而且用户有可能会进行密码的修改,此时我们要手动改变或删除cookie,以保证重新生成新的登录态;
- 在用户量过多或太多需要保留 session 的情况下,我们应将 session 集中化管理,session与cookie的用户数据映射是按照我们的需求手动映射,并不会自动映射,因为 session 中我们可能需要保留很多其他数据或状态以当缓存使用,最终提高 qps,session 集中化管理方案我们就可以引入我们耳熟能详的 Redis 或 Memcached 了,前端同学不了解的请自行了解,后端同学应该就很熟悉了~
三、XSS(跨站脚本攻击)、CSRF(跨站请求伪造)
XSS:一般是因为前端渲染模板,在直接使用参数时没有进行转义,从而导致被注入可执行脚本,如书中所提到的,有些网站会直接拿 query 参数去作为 html 内容而没有进行转义,如果此时我们直接将参数改为一个 script 脚本然后将这个 url 地址进行短链压缩处理后发送给其他用户使用,那么这个脚本中就可以拿到该用户的 cookie 信息,这名攻击者在脚本中可以随意对 cookie 信息进行保存,从而伪造用户信息。
所以前端朋友们直接注入 html 时一定要注意是否已经进行转义,不要随意注入(转义值对某些符号或中文进行转义,如 < => < > => >
,中文被转义为 unicode 等等)
CSRF:在一般情况下,每个域名对应的服务都有自己的cookie,在请求对应服务时,请求会携带对应服务的cookie,但是若在另一服务(暂时称为 B 服务)下对其他有 csrf 漏洞的服务(A服务)进行 post 请求,如果这个用户在此之前在A服务已经登录过由留存cookie,则在B服务伪造A服务请求时会一起将本地的 A 服务 cookie 携带发送,如果 A 服务未进行防范就会被请求成功,以下为防范方法:
- 首先注意这个情况复杂请求(如 post)已经跨域了~除非服务端 cors 白名单配置是允许所有域名进行请求,否则不存在此情况= =,需要注意的是简单请求是不会跨域的(如 GET ,TCP 只会发一次包),原因就是复杂请求 TCP 会发 2 次包()如 post 是会先发起一次 options 请求去试探接口存在性的,options 成功后才是 post 请求本身)。
- 如果没有跨域问题,那就真需要服务端对 csrf 进行防范了 ,一般我们的方式也是像书中一样,由服务端生成一个随机值在进行简单请求(如 GET 请求,在渲染模板时即可返回,如果是前后端分离,由前端控制路由的情况,则先请求任意一简单请求即可生成)时返回给客户端 cookie,客户端每次进行复杂请求时必须携带此 cookie,服务端对此客户端的 csrf cookie 进行校验,如果不正确则直接返回安全错误。
四、为什么要抽象出路由映射
这个太简单了,当然是因为如果不封装,则每次请求都要去判断请求的 method 与 请求的接口地址,然后对应到业务处理函数去处理,这个过程实在是麻烦,再加上业务稍微复杂一点点的情况下,我们需要大量中间件(如获取 cookie 挂载、处理 session 后响应挂载、判断是否是访问静态资源等)来支持请求与响应的过滤处理,如果不封装。。。怕是重复代码要写傻。。。
五、Restful 规范怎么用
按我个人的理解,实际上就是语义化接口请求方式去对应增删查改的操作,书上的例子也十分明显的
// 增加用户
app.post('/user/:username', addUser)
// 删除用户
app.delete('/user/:username', removeUser)
// 查询用户
app.get('/user/:username', getUser)
// 修改用户
app.put('/user/:username', updateUser)
简单易懂,就是一个接口名对应多种请求方式处理~
六、中间件的概念
中间的概念其实很简单,就是过滤器,在执行真正请求的业务函数前,先依次执行完过滤函数,而这个处理过程就是职责链模式的调用方式。书中已经有具体抽象中间件的源码可供参考,所以我就不贴代码了。而 koa 在 express 这种中间件的基础上添加了后置处理这个概念,使得我们可以在业务函数返回响应后先进行处理再真正返回,而 egg 作为一个对 koa 实现高度封装的企业级框架,当然也继承了这一特征,贴一张 egg 文档中我觉得最形象的 koa 的洋葱图,希望大家可以理解这个过程。(这里对于没使用 generator 的童鞋可能要补一下了 generator 函数的语法)
七、关于模板渲染的一些注意点
在使用 js 的渲染模板之前,我想大家可能早就听说过万恶之源 asp、jsp 了,没错他们一个是 .net 另一个是 java 的前端渲染模板,而 js 出的 ejs 语法也是类似 asp 与 jsp 的,就是为了让大家平滑使用,我写代码的时候已经是 jsp 的时代了,所以我也是写过 jsp 的,开着 eclipse 吭哧吭哧的等着热编译完成,仿佛回到大一写 C 语言照着书上的例子敲都编译不过的日子,what the fuck。。。好,吐槽已过,说正事。
前端模板本质上就是利用正则写各种过滤器,然后把我们规定的一些语法替换成 html ,原理就是这样~再加上不同语法分别对应执行语言,转义 html 和不转义直接输出 html 字符串就完成了,没错就是这么简单,书上已经写出了一个涵盖 ejs 3种语法的 demo 了,大家看那个就好了~唯一需要注意的是别被 XSS 了,怎么不被 XSS,上面有~
其实模板渲染的注意点就是前段优化的几点
- 尽量模块化渲染模板(注意不是组件化);
- 后端渲染模板时尽量异步吞吐模块化的模板,避免整屏都是空白的情况;
- 有必要时可以在前端异步请求数据,只要能提升用户体验,减少白屏时间就行;
- 开发资源多的话,还是前后端分离吧,不要执着于模板输出;
第八章实在是内容太多,个人总结的几点只是自己觉得比较重要的,可能具有一定的针对性,本章无法抽象出书中所有的内容,因为书中封装过程的代码量太大了,不可能每行 review 给大家,所以还是需要大家自己去看 中间件模式、路由插件、基础模板解释器 等几个简易的封装过程,其中的思想真的很重要,一定要好好看,好好写,就酱~