参考资料
概述
Koa.js
是基于Node.js的web应用服务端框架,相较于同一团队开发的Express.js
来说它体积更小,功能更简洁,同时也尽可能减少了Node.js中存在的大量的回调。目前通过npm直接安装的
Koa.js
都是2.0版本以上,如果不是需要升级npm到最新版本;如果需要用1.x版本的可以在安装时指定版本号:npm i koa@1.7.0 -S
- 使用
try...catch
结构处理异常,相较于Express.js
中的基于回调函数的组合业务逻辑,异常捕获更加轻松自然。
安装
初始化一个npm目录后执行安装:
1 | node -v # v12.14.0 |
安装的版本是:
1 | "dependencies": { |
Hello World
在目录下创建app.js
,在其中写入:
1 | const Koa = require('koa'); |
这样就搭建好了一个服务器,在命令行输入node app
,打开浏览器对应的地址即可访问。
其实最后监听那边写成Express.js
中的回调风格也是可以的(如下所示),虽然也不是不可以,但是这里是Koa2的教程,所以还是要尽可能地减少使用回调嘛,除了app.use
那边不得不用之外,其他地方最好都不要用。
1 | const server = app.listen(8888, '0.0.0.0', () => { |
中间件
概述
中间件是Koa.js
的核心概念,一个koa实例的最重要的构成就是中间件,这些中间件以函数的形式存放在一个数组中,根据http请求以类似于堆栈的方式执行。
Koa.js
的中间件的最大的特点在于其控制流,根据官网的说法,当存在多个中间件时,调用的顺序是从下游(downstream)开始,循着控制流回到上游(upstream),这一方式称作级联(Cascading),将在下一小节论述。
由于Koa.js
本身功能简洁,诸如路由、静态目录、请求内容解析等功能全都由中间件来实现,其官方提供了一个收录列表,记录了其生态中的中间件列表,若有实际应用需求可以首先来这边寻找。
koa中间件官方收录:
级联
中间件级联也可以理解为嵌套,从一次请求开始,数据流会依次流过不同的中间件,最后返回到客户端。在这个流动的过程中,我们可以根据请求做出一定的响应、添加解析功能、进行拦截、添加分流等。而Koa.js
提供了一个接口,允许开发者自由控制数据流。
控制的关键就在于next
函数,下面是一个官方给出的例子,用于统计一次请求的响应时间,并记录到响应头中、打印到控制台。
1 | const Koa = require('koa'); |
当服务器收到请求时,首先来到logger
中间件,当执行到await next()
语句时,就将当前函数的内容挂起,执行之后的中间件;来到x-response-time
中间件时,记录了一下请求发生的时间,接着执行到await next()
,挂起等待,进入下一个中间件;在执行完最后一个中间件response
之后,发现后续没有中间件了,那么就按照先进后出的顺序,向上回溯,先来到x-response-time
的await next()
之后,执行记录响应时间并写到http响应头的操作,执行完之后,再回溯到logger
,从http响应头中拿到记录的响应时间,打印到控制台。再往上已没有中间件,该请求就被发送回客户端,一次请求的处理流程就走完了。
中间件级联中的控制流的核心在于await next()
语句,由它作为数据下游(downstream)和上游(upstream)的界线。当请求发生时,会根据所写的中间件的顺序,执行await next()
语句之前的语句,当最后一个中间件执行完毕后,下游结束,并按照进来的顺序,反向回溯,执行中间件中写在await next()
之后的语句。就差不多是下面这个意思:
如果参与级联的中间件不包含next()
语句,则认为它就是级联的终点,例如下面语句的执行结果是:请求响应内容只有一个Hello,
:
1 | // response 1 |
要让响应内容为Hello,World!
,需要改写成:
1 | // response 1 |
尝试了一下,好像加不加async/await
都可以发挥next()
的作用:
1 | // response 1 |
响应内容为:Hello,World!12345
。
路由
koajs开发者提供了两个路由中间件:koa-route和koa-router,前者号称超级简易(uber simple),后者则包含了路由的完整特性(full-featured)。
koa-route
由于不打算使用这个,仅在这边贴几行代码说明安装和示例:
1 | npm install koa-route |
1 | const _ = require('koa-route'); |
koa-router
安装:
1 | npm i @koa/router -S |
Adm.js:在独立文件中创建路由、实例化并导出
1 | const Router = require('@koa/router'); |
app.js:在主程序中,设置前缀(嵌套路由)、添加到服务器应用中。
1 | /** |
通过以上的路由配置,打开浏览器访问http://localhost:8888/adm
,即可看到admin home
的内容。
router.prefix() 设置路由前缀/级联路由
可以为一个路由设置前缀,它的返回值是加完前缀的路由,有什么用呢,举个例子。
在Express.js
中,添加路由前缀的方法是:
1 | app.use('/adm', AdmRouter); |
但是Koa.js
中为了做到简洁,不允许这样的用法,app.use
只能接受一个参数,要设置前缀就需要通过router自带的方法:
1 | app.use(AdmRouter.prefix('/adm').routes()); |
router.use() 设置路由中间件
根据路由匹配,添加处理的中间件。可以指定1个或多个路由,也可以不指定;至少指定一个中间件。
1 | // session middleware will run before authorize |
router.redirect() 重定向方法
重定向
1 | router.redirect('/login', '/home'); |
相等于:
1 | router.all('/login', ctx => { |
挂载实例或中间件
koajs/mount:主要被设计来挂载其他Koa实例,但也可以挂载中间件。
挂载在这里的意思是将某些中间件或者将Koa实例作为中间件对应到特定的路由上,类似于koa-router
的prefix()
功能。
安装:
1 | npm i koa-mount -S |
koa-mount可以挂载Koa实例,也可以挂载中间件。
挂载实例的示例代码如下:
1 | const mount = require('koa-mount'); |
以上的a和b都是Koa的实例,通过koa-mount挂载到实例app上,并在app上执行监听,当服务器收到不同的请求时,得到的响应分别是:
1 | GET / |
koa-mount还可以挂载中间件,例如挂载一个路由,或者挂载一些自定义的中间件:
1 | const mount = require('koa-mount'); |
挂载函数的第一个参数是路由地址,可以不写,默认挂载到全路径下。在挂载中间件时,app.use(ctx => {})
等价于app.use(mount(ctx => {}))
。
参数解析
Koa.js
团队提供了一个用于参数解析的中间件koajs/koa-body,支持multipart
,urlencoded
,json
请求体的解析。
该中间件还提供了与expressjs/multer一至的功能(解析multipart/form-data
,主要用于文件上传)。
也有另一个基于cojs/co-body的参数解析中间件koajs/bodyparser,不过已经一年多没更新了,因此这里我们用的是前者。
安装koa-body
1 | npm i koa-body -S |
使用koa-body
使用koa-body
有两种方式,一种是直接设置在Koa的根实例上,对每一个请求都解析一边参数,这样好处是一次配置永久省心,但缺点是会造成不必要的计算浪费,以及不够灵活。另一种方式是设置在koa-router
的路由上,这样足够灵活且不会造成浪费,因此推荐使用第二种。
设置在Koa根实例上:对所有请求都解析参数
1 | const Koa = require('koa'); |
设置在路由中间件中:仅对特定路由生效
1 | const Router = require('@koa/router'); |
需要注意的是,
koa-body
不允许嵌套,即不允许层级调用,否则请求会没有响应(卡死),就像下面这句:
1
2
3 router.post('/login', KoaBody(), KoaBody(), ctx => {
ctx.body = 'succeed!';
});客户端发送请求后迟迟没有收到响应,检查发现第三个中间件函数根本就没有执行。
koa-body的可选配置项
KoaBody()初始化时默认开启了text/urlencoded/json
的解析,不开启multipart
的解析。相关的配置可以通过传入一个选项进行配置,可选的配置项如下所示:
patchNode
{Boolean} Patch request body to Node’sctx.req
, defaultfalse
patchKoa
{Boolean} Patch request body to Koa’sctx.request
, defaulttrue
jsonLimit
{String|Integer} The byte (if integer) limit of the JSON body, default1mb
formLimit
{String|Integer} The byte (if integer) limit of the form body, default56kb
textLimit
{String|Integer} The byte (if integer) limit of the text body, default56kb
encoding
{String} Sets encoding for incoming form fields, defaultutf-8
multipart
{Boolean} Parse multipart bodies, defaultfalse
urlencoded
{Boolean} Parse urlencoded bodies, defaulttrue
text
{Boolean} Parse text bodies, such as XML, defaulttrue
json
{Boolean} Parse JSON bodies, defaulttrue
jsonStrict
{Boolean} Toggles co-body strict mode; if set to true - only parses arrays or objects, defaulttrue
includeUnparsed
{Boolean} Toggles co-body returnRawBody option; if set to true, for form encoded and JSON requests the raw, unparsed request body will be attached toctx.request.body
using aSymbol
(see details), defaultfalse
formidable
{Object} Options to pass to the formidable multipart parseronError
{Function} Custom error handle, if throw an error, you can customize the response - onError(error, context), default will throwstrict
{Boolean} DEPRECATED If enabled, don’t parse GET, HEAD, DELETE requests, defaulttrue
parsedMethods
{String[]} Declares the HTTP methods where bodies will be parsed, default['POST', 'PUT', 'PATCH']
. Replacesstrict
option.
静态页面
安装:
1 | npm i koa-static -S |
使用:
1 | /** |
其他中间件
日志
安装:
1 | npm i koa-logger -S |
使用:
1 | /** |
当有请求发送时,终端会输出一些请求信息:
1 | <-- POST /adm/login |
用户身份验证
基本验证:Basic Auth
基本验证是什么?HTTP Authorization 之 Basic Auth
通过写在HTTP头的用户名:密码
的base64编码后的字符串进行验证的一种手段。安全性极低。
jwt验证:JSON Web Token
JSON Web Token是什么?基于 Token 的身份验证:JSON Web Token
通过加密、签名、指定JSON格式的方式指定一串密钥,当用户第一次登录时得到对应的口令(Token),之后每次请求都发送这一口令,表示用户已知晓服务端的口令,也就是值得信任。不需要涉及到数据库,能减少运算量和存储量(不需要额外存储session),但一旦对方拿到这个加密后的口令,比如登录一次之后,或者被抓包,那么都可以模拟这个口令发送请求。
而且由于口令不与用户关联(也可以关联,但是那就不是JWT的特点),具有通用性,容易泄露,因此要求经常更新。
有一定的加密手段,但一旦被破解或有内鬼泄露,那该加密手段就失效了。
passport.js验证
基于password.js,提供了一串从session中获取用户信息、查询数据库进行验证的接口。
用户权限控制
ACL, RBAC, ABAC 是什么?
ACL/RBAC/ABAC都是用户权限控制的策略,区别在于指定的方式不同,具体可以参考以下三小节的内容(以下内容转载自:使用casbin完成验证授权),ACL对每个用户指定权限,RBAC对一类用户指定,ABAC对具有某种属性的用户指定。用户权限控制首先需要给出权限列表,然后给用户指定列表中的权限,当一个用户想要执行某个权限下的操作时,首先将用户名、请求的权限输入系统进行查询,有权限返回
true
,无权限返回false
。
ACL
ACL
是Access Control List
的缩写,称为访问控制列表. 定义了谁可以对某个数据进行何种操作. 关键数据模型有: 用户, 权限.
ACL规则简单, 也带来一些问题: 资源的权限需要在用户间切换的成本极大; 用户数或资源的数量增长, 都会加剧规则维护成本;
典型应用
- 文件系统
文件系统的文件或文件夹定义某个账号(user)或某个群组(group)对文件(夹)的读(read)/写(write)/执行(execute)权限.
- 网络访问
防火墙: 服务器限制不允许指定机器访问其指定端口, 或允许特定指定服务器访问其指定几个端口.
RBAC
RBAC
是Role-based access control
的缩写, 称为 基于角色的访问控制. 核心数据模型有: 用户, 角色, 权限.
用户具有角色, 而角色具有权限, 从而表达用户具有权限.
由于有角色作为中间纽带, 当新增用户时, 只需要为用户赋予角色, 用户即获得角色所包含的所有权限.
RBAC
存在多个扩展版本, RBAC0
、RBAC1
、RBAC2
、RBAC3
。这些版本的详细说明可以参数这里。我们在实际项目中经常使用的是RBAC1
,即带有角色继承概念的RBAC模型。
ABAC
ABAC
是Attribute-based access control
的缩写, 称为基于属性的访问控制.
权限和资源当时的状态(属性)有关, 属性的值可以用于正向判断(符合某种条件则通过), 也可以用于反向判断(符合某种条件则拒绝):
典型应用
- 论坛的评论权限, 当帖子是锁定状态时, 则不再允许继续评论;
- Github 私有仓库不允许其他人访问;
- 发帖者可以编辑/删除评论(如果是RBAC, 会为发帖者定义一个角色, 但是每个帖子都要新增一条用户/发帖角色的记录);
- 微信聊天消息超过2分钟则不再允许撤回;
- 12306 只有实名认证后的账号才能购票;
- 已过期的付费账号将不再允许使用付费功能;
koa-authz:基于Casbin实现的ACL/RBAC/ABAC控制
koa-authz
是基于Node-Casbin
实现的权限控制中间件,主要控制这么一件事:xx用户是否具有对xx资源进行xx操作的权限。在koa-authz
的描述语言里,就是{subject, object, action}
,其中:
subject
:已经登录了的用户名,xx用户;object
:网络资源的URL,如“dataset1/item1”,xx资源;action
:HTTP方法(GET/POST/PUT/DELETE…)或其他语义化方法(如 read-file, write-blog),xx操作。
具体的使用参考node-casbin/koa-authz和Casbin Tutorials。
会话管理:session
koa-session的安装和使用
安装
1 | npm i koa-session -S |
基本使用。其中app.keys
为必须字段,用于为cookies加密,除非在koa-session
的配置项中把加密的关掉。
1 | /** |
1 | router.get('/', ctx => { |
以上代码实现的是通过session存储某一网页的访问次数。(session存放在浏览器的cookie中)
当浏览器向客户端发送请求时,会附带上cookie,添加了koa-session
中间件后,可以通过ctx.session
拿到存储在浏览器cookie中的session信息(这里称作cookie-based sessions
),当对session其进行修改后,响应这个请求时会把修改后的session发送到浏览器,更新存放在浏览器的cookie数据。
koa-session的配置项
可以通过如下形式为koa-session
传入配置项:
1 | const SessionConfig = {}; |
可选配置项有:
key
: (string) 保存在cookie中的键名,默认为koa.sess
maxAge
: (number || ‘session’),当为一个数字时,表示以毫秒计算的最大session有效时间。默认为86400000
,1天;当为'session'
字符串时,关闭会话或浏览器时,session会过期。autoCommit
: (boolean) 是否自动提交headers,默认为true
。overwrite
: (boolean) 是否重写cookie,默认为true
。httpOnly
: (boolean) 是否开启httpOnly
,默认为true
,带有httpOnly
的Cookie不能通过非http方式来访问。signed
: (boolean) 是否使用app.keys
进行签名,默认为true
,会在cookie中存放一个key.sig
的签名字段,主要目的是防止cookie被篡改。(测试了一下,如果不设置签名,将cookie用base64解码后改成另一个值,还能生效;如果设置了签名,如果发生了修改,那么修改的字段和之前的字段都无效了,会重新设置cookie。如果有人同时复制了数据和签名,那么还是可以篡改数据的,只是不能篡改成他想要的样子,签名的原理和app.keys
的签名原理一至,底层的实现依赖crypto-utils/keygrip和pillarjs/cookies)。rolling
: (boolean) 强制在每个响应上设置会话标识符 cookie。到期重置为原来的maxAge,重置到期倒计时,默认为false
。renew
: (boolean) 当session将要过期时是否更新它,默认为false
。secure
: (boolean) 是否启用安全cookie,默认为false
。sameSite
: (string) session cookie的sameSite
选项,默认为None
,将允许声明Cookie是否仅限于第一方或同一站点上下文。接受三个值:Lax
:Cookies允许与顶级导航一起发送,并将与第三方网站发起的GET请求一起发送,这是浏览器的默认值;Strict
:Cookies只会在第一方上下文中发送,不会与第三方网站发起的请求一起发送;None
:Cookies将在所有上下文中发送,即允许跨域发送。使用None
时,需要设置Secure
。
options中还包括一些其他的自定义配置项,如encode/decode
,externalKey
,store
,ContextStore
,genid/prefix
等,详细的功能和配置方法可以查看koajs/session/index.js。
文件上传:multer
安装:
1 | npm i @koa/multer multer -S |