# 1. 前言

在一个基于 RESTful 规范的应用中,URL 对应的不同路径意味着不同的资源,所以对资源访问的限制即对 URL 访问的限制。常见鉴别用户权限的方式有两种,一种是广泛使用的 Cookie-Based Authentication ( 基于 Cookie 的认证模式),另一种是 Token-Based-Authentication (基于 Token 的认证模式)。在本篇文章中我们以基于 Token 的认证方式为例来介绍如何对接口进行权限控制。

Token 方式最大的优点在于采用了无状态机制,在此基础上,可以实现天然的跨域支持、前后端分离等,同时降低了服务端开发和维护的成本。Token 方式的缺点在于服务器每次都需要对 Token 进行校验,这一步骤会对服务器产生运算压力。另一方面,无状态 API 缺乏对用户流程或异常的控制,为了避免出现一些例如回放攻击的异常情况,应该设置较短的过期时间,且需要对密钥进行严格的保护。对于具有复杂流程的高危场景(如支付等),则要谨慎选择 Token 认证模式。

# 2. Token 认证流程

基于 Token 的前后端互相认证流程通常是这样子的:

  1. 客户端访问 login 登录接口请求登录;
  2. 服务端校验客户端请求携带的参数是否合法,如果合法则以接口返回值形式向客户端颁发 Token
  3. 客户端拿到 Token 后,在之后的请求需要在请求头中的 Authorization 字段中携带 Token ;
  4. 之后服务端在接收到请求后,会首先根据请求携带的 Token 进行认证,如果认证通过,则进行下一步操作,否则,拒绝访问。

其交互流程图如下:

# 3. 结合 koa 实现 Token 认证流程

koa 中,通常情况下,一般会选用 jsonwebtoken ( JWT )这个第三方工具库来实现 Token 的生成、校验和解码。而 Token 的中间件实现会选择 koa-jwtkoa-jwt 会在中间件流程中通过 JWT 完成对 Token 的校验和解码,开发者只需通过 JWT 来实现 Token 的生成即可。

老规矩,使用之前我们先来安装一下这两个库,如下:

# 安装 jsonwebtoken koa-jwt
npm i jsonwebtoken koa-jwt

# 安装 jsonwebtoken 的类型文件
npm i @types/jsonwebtoken -D
1
2
3
4
5

安装完成之后,我们就可以来使用了。

首先,我们需要使用 koa-jwt 来生成一个 Token 校验中间件,为了方便复用,我们在 src/middleware 目录下创建一个名为 jwt.ts 文件,写入如下代码:

import * as JWT from "koa-jwt";
import { JWT_SCERET } from "../config";

export default JWT({ secret: JWT_SCERET });
1
2
3
4

从上面代码中可以看到,我们先在 src/config/index.ts 文件中定义了 JWT_SCERET 变量,用来保存 jwt 的密码。然后调用 koa-jwt 中的 JWT 方法来生成 JWT 中间件。

接下来,我们创建一条登录路由,在 src/router 目录下创建 login.ts 文件,并写入如下内容:

import * as Koa from "koa";
import * as Router from "koa-router";
import LoginController from "../controller/login";

const router = new Router();

export default (app: Koa) => {
  router.post("/login", LoginController.login);

  app.use(router.routes()).use(router.allowedMethods());
};
1
2
3
4
5
6
7
8
9
10
11

接着,我们继续实现 LoginController.login ,在 src/controller 目录下创建 login.ts 文件,并写入如下内容:

import * as Koa from "koa";
import { sign } from "jsonwebtoken";
import { JWT_SCERET } from "../config";
import UserService from "../service/user";

export default {
  login: async (ctx: Koa.Context, next: Koa.Next) => {
    const { name } = ctx.request.body;
    // 校验登录是否合法
    const res = await UserService.findUserByName(name);
    if (res.data) {
      // 如果合法,则生成token 并颁发 token
      const token = sign({ name }, JWT_SCERET, {
        expiresIn: "300s", // token 过期时间为 5 分钟
      });
      ctx.body = {
        msg: "登录成功",
        token,
      };
    } else {
      ctx.body = {
        msg: "登录失败,用户名不正确",
      };
    }
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

从上面代码中可以看到,当客户端登录请求过来后,首先判断请求所携带的参数是否合法(一般会校验登录的用户名和密码是否正确,此处为了简便起见,只校验该用户是否存在于数据库中),如果合法,则根据用户信息生成 Token 并通过返回值颁发给客户端;如果不合法,则返回登录失败信息。

ok,接下来启动服务器,我们就可以尝试发送登录请求来获取 Token 了,打开 postman ,如下:

可以看到,当携带正确的用户名 nlrx 进行登录时, Token 已经被正确返回了。如果用户名错误,则提示登录失败。

接下来,我们给需要进行权限控制的接口添加上前面生成好的 Token 校验中间件,以 /user 为例,如下:

// src/router/user.ts

import * as Koa from "koa";
import * as Router from "koa-router";
import UserController from "../controller/user";
import jwt from "../middleware/jwt";

const router = new Router();

export default (app: Koa) => {
  router.post("/user", jwt, UserController.createUser);

  router.put("/user/:name", jwt, UserController.updateUser);

  router.del("/user/:name", jwt, UserController.deleteUser);

  router.get("/user/:name", jwt, UserController.getUser);

  app.use(router.routes()).use(router.allowedMethods());
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

加上之后,我们保存文件再次重启服务器,此时如果再向之前那样直接访问接口的话,此时接口应该会访问不通,如下:

可以看到,返回了 Authentication Error 即「授权失败」。

那么,我们携带上 Token 访问,再看看:

可以看到,当我们在请求的 Headers 中添加上 Authorization 字段,并把字段值设置为 Bearer <Token> 后,即可返回正确的结果。当没有 TokenToken 过期时,都会提示授权失败。

以上,我们就实现了给接口添加访问权限控制的功能。