文章目录
Restify简介
1. Restify是什么?
Restify是一个专注于RESTful API的nodejs框架。
2. 安装Restify
npm init -y
npm i restify -S
3. 实现一个最简单的服务器
var restify=require('restify')
var server=restify.createServer()
server.get('/hello/:name',(req,res,next)=>{
res.send('hello '+req.params.name)
return next()
})
server.listen(8000,()=>{
console.log(server.name)
console.log(server.url)
})
$ curl -is http://localhost:8000/hello/world
HTTP/1.1 200 OK
Server: restify
Content-Type: application/json
Content-Length: 13
Date: Sat, 20 Jan 2024 09:02:45 GMT
Connection: keep-alive
Keep-Alive: timeout=5
"hello world"
处理程序链
1. 什么是处理程序链
所谓处理程序链,是指一组处理程序,按顺序依次执行。
如下,定义了针对’/hello/:name’路由的一组路由处理程序链:
server.get('/hello/:name',
function(req,res,next){
console.log('first step')
return next() //程序链式执行的关键,不可省略
},
function(req,res,next){
console.log('second step')
res.send('how are you!') //在一条处理链上,只能向客户端返回一次数据。
return next()
},
function(req,res,next){
console.log('third step')
return next()
}
)
2. next()函数
处理程序链中的每个程序执行之后,都需要手工调用next(),这将移动到链中的下一个函数继续执行。调用res.send()不会自动触发next(),send()之后可能还要继续处理,因此也需要手动调用next()。
正常情况下,next()不需要任何参数。若由于某种原因(条件),需要提前终止程序链向后继续执行,可调用next(false)。
next()也接受Error类型对象,这将导致Restify向客户端发送错误状态码,错误状态码根据Error类型进行推断,Error类型为500,NotFoundError类型为404。关于错误处理,参见后文错误处理
server.get('/hello/:name',(req,res,next)=>{
if(req.params.name=='') return next(new Error('no name!')
res.send('hello '+req.params.name)
return next()
})
3. 三类处理程序链
在Restify服务器中有三类处理程序链:
- pre 在路由之前的处理程序链
- use 在路由之后的处理程序链,对所有路由都生效
- {httpVerb} 对特定路由执行的处理程序链,见上例
3.1. 通用预处理程序链:pre
pre对传入的所有请求都执行,即便请求的路由未注册。这可以用于记录日志或在路由之前清理传入的请求。
server.pre(
function(req,res,next){
console.warn('run before route! 1')
return next()
},
function(req,res,next){
console.warn('run before route! 2')
return next()
}
)
3.2. 通用处理程序链:use
use对所有被匹配的路由都执行。
server.use(
function(req,res,next){
console.warn('run for all routers! 1')
return next()
},
function(req,res,next){
console.warn('run for all routers! 2')
return next()
}
)
3.3. 路由处理程序链
Restify使用HTTP动词和参数来确定路由处理程序链。参数值可以通过req.params获取。
function send(req,res,next){
res.send("hello "+req.params.name)
return next()
}
server.post('/hello',function(req,res,next){
res.send(201,Math.random().toString(36).substr(3,8))
return next()
})
server.put('/hello',send)
server.get('/hello/:name',send)
server.del('/hello/:name',function(req,res,next){
res.send(204)
return next()
})
路由可以指定的HTTP动词包括:get、post、del、put、head、opts、patch。
4. 错误处理
Restify可以通过事件方式来处理错误。通过next(error)抛出错误事件,并用on()函数为特定错误事件添加侦听器。
restify-errors模块封装了一组常见HTTP和REST错误的构造函数。
const errs=require('restify-errors')
server.get('/hello/:name',function(req,res,next){
//找不到资源
return next(new errs.NotFoundError())
})
server.on('NotFound',function(req,res,err,callback){
//不能调用res.send(),因为现在处于错误处理上下文,不在路由处理程序链中。
//在此处可以记录错误日志或执行善后处理。
//当完成错误处理后,调用回调函数回到路由处理程序链,Restify将根据错误类型自动发送包含错误代码的响应报文。
return callback()
})
常用的错误事件如下:
- 400 BadDigestError
- 405 BadMethodError
- 500 InternalError
- 409 InvalidArgumentError
- 400 InvalidContentError
- 401 InvalidCredentialsError
- 400 InvalidHeaderError
- 400 InvalidVersionError
- 409 MissingParameterError
- 403 NotAuthorizedError
- 412 PreconditionFailedError
- 400 RequestExpiredError
- 429 RequestThrottledError
- 404 ResourceNotFoundError
- 406 WrongAcceptError
插件
restify.plugins模块中提供过了一系列有用的插件,这些插件可以通过server.pre()或server.use()加载。
1. pre插件
pre类插件会在路由之前使用。
1.1. context
该插件给req添加了设置键值对req.set(key.val)和读取值req.get(key)方法。
server.pre(restify.plugins.pre.context())
server.get('/',
function(req,res,next){
req.set(myMessage,'hello world')
return next()
},
function(req,res,next){
res.send(req.get(myMessage))
return next()
}
)
1.2. dedupeSlashes
该插件会删除URL中重复斜杠,比如会将请求"/hello/abc"转换为"/hello/abc"。
server.pre(restify.plugins.pre.dedupeSlashes())
server.get('/hello/:name',function(req,res,next){
res.send(200)
return next()
})
1.3. sanitizePath
该插件修复URL,比如会将"/foo//bar//“转化为”/foo/bar"
server.pre(restify.plugins.pre.sanitizePath())
server.get('/hello/:name',function(req,res,next){
res.send(200)
return next()
})
2. use插件
use类插件会对所有路由生效。
2.1. acceptParser
解析Accept头,确保服务器可以提供客户要求的内容。若请求一个无法处理的类型,该插件会返回NotAcceptableError(406)错误。通过server.acceptable可获取restify服务器可接受的类型。
> server.acceptable
[
'application/json',
'text/plain',
'application/octet-stream',
'application/javascript'
]
通常,该插件都应该被加载:
server.use(restify.plugins.acceptParser(server.acceptable))
2.2. queryParser
解析HTTP查询字符串(如:/hello?name=abc&job=doctor
)。若使用该插件,解析的内容在req.query中可用。
该插件接受一个选项参数mapParams,可配置是否将req.query合并到req.params字段中(req.params字段是针对/hello/:sex
这类参数),若将mapParams配置为true,则类似/hello/male?name=abc&job=doctor
这类路由,解析后的req.query为。
...
server.use(restify.plugins.queryParser({mapParams:true}))
server.get(
'/foo/:sex',
(req,res,next)=>{
res.send({'req.query':req.query,'req.params':req.params})
return next()
}
)
...
>curl -is -l "http://localhost:8000/foo/male?name=abc&title=def"
HTTP/1.1 200 OK
Server: restify
Content-Type: application/json
Content-Length: 72
Date: Sun, 21 Jan 2024 13:11:52 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"req.query":{"name":"abc","title":"def"},"req.params":{"sex":"male","name":"abc","title":"def"}}
2.3. bodyParser
解析HTTP请求主体,解析后生成req.body字段。
...
server.use(
restify.plugins.queryParser({mapParams:true}),
restify.plugins.bodyParser()
)
server.get(
'/foo/:sex',
(req,res,next)=>{
res.send(
{
'req.query':req.query,
'req.params':req.params,
'req.body':JSON.parse(req.body)
})
return next()
}
)
...
>curl -is -X GET -d "{\"a\":1,\"b\":2,\"c\":{\"d\":3,\"e\":\"asdf\"}}" -l "http://localhost:8000/foo/male"
HTTP/1.1 200 OK
Server: restify
Content-Type: application/json
Content-Length: 92
Date: Sun, 21 Jan 2024 14:06:09 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"req.query":{},"req.params":{"sex":"male"},"req.body":{"a":1,"b":2,"c":{"d":3,"e":"asdf"}}}
2.4. serveStatic
提供静态文件。该插件需要映射到路由上:
server.get('/static/*',restify.plugins.serveStatic({
directory:__dirname,
default:'index.html'
}))
实际路径为主程序所在目录(__dirname)+‘/static/’。
鉴权机制
Restify-auth是基于jwt的鉴权中间件。使用前需要先安装:
npm i restify-auth -S
1. 基本使用
const restify=require('restify')
const {restifyAuth} =require('restify-auth')
const server=restify.createServer()
const auth=restifyAuth({
secret:'test-secret' //设定一个salt密码
})
server.pre(restify.plugins.pre.sanitizePath())
server.use(
restify.plugins.queryParser(),
restify.plugins.bodyParser()
)
server.get(
'/getauth',
auth.authorizer, //在需要鉴权的路由添加
async (req,res)=>{
res.send(req.auth)
}
)
server.post(
'/login',
async (req,res)=>{
var user=req.body.user||''
var password=req.body.password||''
if(user=="test"&&password=="test"){ //用户验证
var token=await auth.sign({user})
res.header('authorization',token)
res.send({success:true})
}else{
res.send({success:false})
}
}
)
server.on('restifyError',(req,res,err,cb)=>{
console.log(err.toString())
return cb()
})
server.listen(8000,()=>{
console.log(server.name)
console.log(server.url)
})
客户端请求:
>curl -is -d "user=test&password=test" -l "http://localhost:8000/login"
HTTP/1.1 200 OK
Server: restify
authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdCIsImlhdCI6MTcwNTg0MDUzOCwiZXhwIjoxNzA1ODQxNzM4fQ.NK9F-FntUQHqXuEACQ94T2XKmTO3U8UxpZdaLegT8ko
Content-Type: application/json
Content-Length: 16
Date: Sun, 21 Jan 2024 12:35:38 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"success":true}
在应答报文头中,authorization字段包含了从服务器返回的token,使用该token可访问服务器需要鉴权的路由:
>curl -is -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidGVzdCIsImlhdCI6MTcwNTg0MDUzOCwiZXhwIjoxNzA1ODQxNzM4fQ.NK9F-FntUQHqXuEACQ94T2XKmTO3U8UxpZdaLegT8ko" -l "http://localhost:8000/getauth"
HTTP/1.1 200 OK
Server: restify
Content-Type: application/json
Content-Length: 49
Date: Sun, 21 Jan 2024 12:38:20 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"user":"test","iat":1705840538,"exp":1705841738}
若token错误,或不提供token,则无法访问该路由:
>curl -is -l "http://localhost:8000/getauth"
HTTP/1.1 401 Unauthorized
Server: restify
Content-Type: application/json
Content-Length: 53
Date: Sun, 21 Jan 2024 12:40:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5
{"code":"Unauthorized","message":"token is required"}
2. token过期和刷新
可设置token有效期和刷新周期:
...
const auth=restifyAuth({
secret:'test-secret',
expiresIn:'20m', //有效期20分钟
refreshRange:0.6 //刷新时间为超过有效期60%时,向客户端发送新token,新token在报文头的authorization字段中,客户端检测到新token后,应及时更新token。
})
...