Node RESTful

753 阅读7分钟

一 RESTful API 理论

1.1 REST 是什么?

    • web软件架构风格
      • 用来创建网络服务
    • Representational State Transfer
      • Representational: 数据的表现形式( 最佳实践JSON )
      • State: 当前状态或数据
      • Transfer: 数据传输
  • 6个限制
    • 客户-服务器(Client - Server)
      • 关注点分离
      • 服务端专注数据存储,提升了简单性
      • 前端专注用户界面,提升了可移植性
    • 无状态 (Stateless)
      • 所有用户会话信息都保存在客户端
      • 每次请求必须包括所有信息,不能依赖上下文信息
      • 服务端不保存会话信息:提升了简单性、可靠性、可见性
    • 缓存(Cache)
      • 所有服务端响应都要被标为可缓存或不可缓存
      • 缓存减少前后端交互,提升了性能
    • 统一接口 (Uniform Interface)
      • 统一指接口设计尽可能统一通用,提升了简单性、可见性
      • 接口与实现解耦,使前后端可独立开发迭代
      • 统一接口的子限制
        • 资源的标识
          • 资源是任何可命名的事物,如用户、评论等
          • 每个资源可被URI唯一标识
        • 通过表述来操作资源
          • 表述就是Representation 如JSON XML等
          • 客户端不能直接操作SQL服务端资源
          • 客户端应通过表述(如JSON)来操作资源
        • 自描述消息
          • 每个消息(请求或响应)必须提供足够的信息让接受者理解
          • 通过媒体类型(application/json , application/xml)
          • HTTP方法:GET 、 POST 、 DELETE
          • 是否缓存: Cache-control
        • 超媒体作为应用状态引擎
          • 超媒体:带文字的链接
          • 应用状态:一个网页
          • 引擎:驱动、跳转
          • 合起来:点击链接跳转到另一个网页
    • 分层系统(Layered System)
      • 每层只知道相邻的一层,后面隐藏的就不知道了
      • 客户端不知道是和代理还是真实服务器通信
      • 其他层:安全层、负载均衡、缓存层等
    • 按需代码(Code-On-Demand) 可选
      • 客户端可下载运行服务端传来的代码(如JS)
      • 通过减少一些功能,简化了客户端

1.2 RESTful API

  • 即符合REST架构风格的API
  • 基本的URI
  • 标准HTTP方法
    • GET / POST / PUT / PATCH / DELETE
  • 传输的数据媒体类型
    • JSON / XML
  • 实例
    • GET /users ---- 获取user列表
    • GET /users/12 ---- 查看某个具体的 user
    • POST /users ---- 新建一个 user
    • PUT /users/12 ---- 整体替换更新 user 12
    • PATCH /users/:name ---- 部分更新,只更新name
    • DELETE /users/12 ---- 删除 user12

1.3 RESTful API 最佳实践

  • 请求设计规范
    • URI使用名词,尽量用复数,如/users
    • URI使用嵌套表示关联关系,如/users/12/repos/5
    • 使用正确的HTTP方法,如GET / POST / PUT / DELETE
    • 不符合CRUD情况:POST/action/子资源
  • 响应设计规范
    • 查询
      • https://api.github.com/users?since=100
    • 分页
      • https://api.github.com/user/repos?page=2&per_page=100
    • 字段过滤
      • https://api.github.com/users/username?fileds=name
    • 状态码
      • Status
    • 错误处理
      •   HTTP/1.1 422  
          {  
            “message”: “Validation Failed”,  
            “errors”: [  
              {  
                “resource”: ‘Issue’,  
                “field”: “title”,  
                “code”: “missing_field”     
              }  
            ]  
          }  
        
  • 安全
    • HTTPS
    • 鉴权
    • 限流
  • 增删改查应返回什么响应?
    • 增改:应返回修改的对象
    • 删:应返回 204 (没有内容但成功了)

二 Koa2

2.1 简介

  • 基于NodeJS下一代web框架
  • Web应用和API开发领域
  • 更小、更富有表现力、更健壮
  • 利用async函数,丢弃了回调函数
  • 增强错误处理:try catch
  • 没有捆绑任何中间件

2.2 Hello world

  • npm init

  • npm i koa --save

  • npm i nodemon --save-dev

  • package.json

    "scripts": {  
        "start": "nodemon index.js"  
      },  
    
  • index.js

    const Koa = require("koa");  
    const app = new Koa();  
      
    app.use((ctx) => {  
      ctx.body = "hello world";  
    });  
      
    app.listen(3000);  
    
  • npm start

2.3 koa中间件与洋葱模型

  •   app.use(async (ctx, next) => {  
        console.log(1);  
        await next();  
        console.log(3);  
      });  
      app.use((ctx) => {  
        console.log(2);  
      });  
    

2.4 路由

  • 处理不同的URL
  • 处理不同的HTTP方法
  • 解析URL上的参数
  • 在koa中,是一个中间件
    •   const Koa = require("koa");  
        const app = new Koa();  
          
        app.use(async ctx => {  
          if (ctx.url === "/") {  
            ctx.body = "this is home page";  
          } else if (ctx.url === "/users") {  
            if (ctx.method === "GET") {  
              ctx.body = "this is users list page";  
            } else if (ctx.method === "POST") {  
              ctx.body = "this is create new user page";  
            } else {  
              ctx.status = 405;    
              // 方法不允许 Method Not Allowed  
            }  
          } else if (ctx.url.match(/\/users\/\w+/)) {  
            const userId = ctx.url.match(/\/users\/(\w+)/)[1];  
            ctx.body = `this is user id ${userId}`;  
          } else {  
            ctx.status = 404;  
            // 未找到 Not Found  
          }  
        });  
          
        app.listen(3000);  
      
  • 使用koa-router实现路由
    •   const Koa = require("koa");  
        const Router = require("koa-router");  
        const app = new Koa();  
        const router = new Router();  
        const usersRouter = new Router({ prefix: "/users" });  
          
        app.use(router.routes());  
        app.use(usersRouter.routes());  
          
        const auth = async (ctx, next) => {  
          if (ctx.url !== "/users") {  
            ctx.throw(401);   
             // 未受权 Unauthorized  
          }  
          await next();  
        };  
          
        router.get("/", (ctx) => {  
          ctx.body = "this is home page";  
        });  
          
        usersRouter.get("/", auth, (ctx) => {  
          ctx.body = "this is users list page";  
        });  
          
        usersRouter.post("/", auth, (ctx) => {  
          ctx.body = "this is create new user";  
        });  
          
        usersRouter.get("/:id", auth, (ctx) => {  
          ctx.body = `this is user ${ctx.params.id}`;  
        });  
          
        app.listen(3000);  
      

2.5 HTTP options

  • 检测服务器所支持的请求方法
    • 响应中 headers => allow 中会列举支持的HTTP方法
  • CORS中的预检请求
  • koa-router中的allowedMethods
    • app.use(usersRouter.allowedMethods());
    • 响应options方法,告诉它所支持的请求方法
    • 自动返回405(不允许)
      • 比如users未实现delete,但发送了DELETE请求,将返回405
    • 自动返回501(没实现)
      • koa不支持的方法会返回501

2.6 增删改查响应

  •   usersRouter.get("/", (ctx) => {  
        // this is users list page  
        ctx.body = [{ name: "jon" }, { name: "lily" }];  
      });  
        
      usersRouter.post("/", (ctx) => {  
        // this is create new user  
        ctx.body = { name: "jon" };  
      });  
        
      usersRouter.get("/:id", (ctx) => {  
        // this is user jon  
        ctx.body = { name: "jon" };  
      });  
        
      usersRouter.put("/:id", (ctx) => {  
        // this is after change  
        ctx.body = { name: "jon2" };  
      });  
        
      usersRouter.delete("/:id", (ctx) => {  
        // this is after del  
        ctx.status = 204;  
      });  
    

2.7 控制器

  • 拿到路由分配的任务,并执行
  • 在koa中,控制器也是中间件
  • 获取HTTP请求参数
    • Query String
      • 如?q=keyword
        • ctx.query:{q:li}
    • Router Params
      • 如/users/jon
        • ctx.params:{id:’jon’}
    • Body
      • 如{name: ‘jon’}
        • npm i koa-bodyparser -S
          1. Postman设置body -> row => JSON
          2. postman Headers添加数据 {"name": “jon”}
        • ctx.request.body
    • Header
      • 如Accept, Cookie
        • ctx.header
    • VSCode 调试API
        1. 停止服务
        2. 点击Run -> Run Script: start
        3. 打断点
        4. Postman中发送请求
        5. 调试
        • 可右键 add to watch
  • 处理业务逻辑
  • 发数HTTP响应
    • 发送Status
      • 如200 / 400 等
        • ctx.status = 204
    • 发送Body
      • 如{name:’jon’}
        • ctx.body = { name: "jon" };
    • 发送Header
      • 如Allow, Content-Type
        • ctx.set(‘Allow’, ‘GET, POST’)
  • 最佳实践
    • 每个资源可控制器放在不同的文件里
      • 批量挂载路由
        • src/routes/index.js 批量挂载路由

          const fs = require("fs");  
            
          module.exports = (app) => {  
            fs.readdirSync(__dirname).forEach((file) => {  
              if (file === "index.js") {  
                return;  
              }  
              const route = require(`./${file}`);  
              app.use(route.routes()).use(route.allowedMethods());  
            });  
          };  
          

          src/index.js

          const routing = require("./routes");  
          routing(app);  
          
      • 将路由单独放在一个目录
        •   src  
            --routes  
            ----index.js  
            ----home.js  
            ----users.js  
          
        • routes/users.js

          const Router = require("koa-router");  
          const router = new Router({ prefix: "/users" });  
          const {find, delete: del } = require("../controllers/users");  
            
          router.get("/", find);  
          router.delete("/:id", del);  
          //...  
            
          module.exports = router;  
          
      • 将控制器单独放在一个目录
        •   src  
            --controlles  
            ----home.js  
            ----users.js  
          
    • 尽量使用类+类方法的形式编写控制器
      • controlles/users.js
        class UsersCtl {  
          find(ctx) {  
            ctx.body = db;  
          }  
          delete(ctx) {  
            db.splice(ctx.params.id * 1);  
            ctx.status = 204;  
          }  
          //...  
        }  
          
        module.exports = new UsersCtl();  
        
    • 严谨的错误处理
      • 运行时错误,都返回 500
      • 逻辑错误
        • 找不到 404
        • 先决条件失败 412
          • ctx.throw(412)
        • 无法处理的实体(参数格式不对)422
          • npm i koa-parameter -S
          • src/index.js
            const parameter = require("koa-parameter");  
            app.use(parameter(app));  
            
            controlles/users.js
            create(ctx){  
              // 检验请求 (name 为string 必选)  
              // 如不满足自动返回 422  
              ctx.verifyParams({  
                name: {type: ‘string’, required: true}  
              })  
            }  
            
      • 手写错误处理中间件
        • src/index.js 确保挂载在第一个中间件

          app.use(async (ctx, next) => {  
            try {  
              await next();  
            } catch (err) {  
              ctx.status = err.status || err.statusCode || 500;  
              ctx.body = { message: err.message };  
            }  
          });  
          
            • 能够捕获ctx.throw()
              • 对于500, 需增加短路条件
              • 对于404, 中间件无法捕获
      • koa-json-error
        • npm i koa-json-error -S
        • src/index.js
          const error = require("koa-json-error");  
          app.use(  
            error({  
              postFormat: (err, { stack, ...rest }) =>  
                process.env.NODE_ENV === "production" ? rest : { stack, ...rest },  
            })  
          );  
          

三 登录认证

3.1 session

  • 1 客户端发送用户名密码给服务端
    2 服务端登录,生成sessionID保存到redis中,并返回给客户端存入cookie,设置httpOnly 和 expires
    3 因客户端每次请求都会带上cookie,所以之后的每次请求sessionID 也会跟到服务端
    4 服务端验证 sessionID,以此判断是否登录

3.2 JWT

  • JSON Web Token
    • 定义了一种紧凑且独立的方式,可将各方之间的信息作为 JSON对象进行安全传输
    • 该信息可验证和信任,因为是经过数字签名的
  • 构成
    • 头部 Header
      • typ: token的类型,这里固定为JWT
      • alg: 使用的hash算法
    • 有效载荷 Payload
      • 存储需要传递的信息,如用户ID、用户名
      • 还包含元数据,如过期时间,发布人等
      • 与Header不同,Payload可加密
    • 签名 Signature
      • 对Header和Payload部分进行签名
      • 保证Tonken在传输工程中没有被篡改或损坏
  • 工作原理
    • 1 客户端发送用户名密码给服务端
      2 服务端登录,生成Token,并返回给客户端存入 localStorage 或 sessionStorage
      3 之后的每次将Token加入请求头中的Authorization:Bearer
      4 服务端验证 Token,以此判断是否登录

3.3 session 对比 JWT

  • 可拓展性
    • JWT多服务器更好,无需redis
  • 安全性
    • 都会受的攻击,不要把敏感数据方式cookie后JWT中
  • RESTful API
    • REST要求程序无状态,session违反了此要求
  • 性能
    • JWT每次传输量更大
    • session需要查询数据库
  • 时效性
    • JWT无法实时更新,需等过期

3.4 在node中使用JWT

  • npm i jsonwebtoken -S
    启动node
    node
    设置环境变量
    jwt = require(‘jsonwebtoken’);
    然后签名
    token = jwt.sign({name:’jon’}, ‘secret’);
    解码
    jwt.decode(token);
    验证是否被窜改
    jwt.verify(token, 'secret');