Время для чашки чая, начать разработку Koa2 + MySQL

Node.js
Время для чашки чая, начать разработку Koa2 + MySQL

Эта статья была написана членами сообщества Tuque.mRcнаписано, присоединяйтесьСообщество Туке, чтобы вместе создавать замечательные бесплатные технические руководства, чтобы способствовать развитию индустрии программирования.

Если вы считаете, что мы хорошо поработали, помнитеНравится + Подписаться + КомментарийСанлиан, поощряй нас писать лучшие уроки 💪

Благодаря сложной «луковой модели» и полной поддержке промисов и асинхронного программирования async/await платформа Koa с момента своего создания привлекла бесчисленное количество энтузиастов Node. Тем не менее, Koa сама по себе является простой средой промежуточного программного обеспечения, и для реализации достаточно сложного веб-приложения требуется много сопутствующей экологической поддержки. Этот учебник не только поможет вам разобраться с базовыми знаниями Koa, но также полностью использовать и объяснить необходимые компоненты (маршрутизация, база данных, аутентификация и т. д.) для создания веб-приложений и, наконец, реализовать относительно полную пользовательскую систему.

Начало

Как веб-фреймворк Node.js нового поколения, созданный оригинальной командой Express, Koa привлек большое внимание с момента своего выпуска. Как авторы Коа вДокументацияуказано в:

С философской точки зрения, Koa НАПРАВЛЯЕТ «исправить и заменить узел», в то время как экспресс «дополняет узел» (Express — это усиление узла, а KOA — решить проблему узла и заменить его).

В этой статье мы вместе с вами разработаем простой REST API пользовательской системы, который поддерживает добавление, удаление, изменение пользователей и аутентификацию JWT, а также чувствует суть Koa2 в реальном бою, что является прорывом по сравнению с Express. , изменить. мы выберемTypeScriptВ качестве языка разработки база данных использует MySQL и используетTypeORMВ качестве уровня моста базы данных.

Уведомление

Эта статья не будет включать принципиальный анализ уровня исходного кода Koa, основное внимание будет уделено тому, чтобы дать вам полное представление о том, как использовать Koa и окружающую среду для разработки веб-приложений, и оценить красоту дизайна Koa. также,Этот урок довольно длинный, если на чашку чая не хватило, можно долить~

Предварительные знания

В этом руководстве предполагается, что у вас уже есть следующие знания:

  • Основы языка JavaScript (включая часто используемый синтаксис ES6+)
  • Базовые знания Node.js, а также основы использования npm вы можете найти здесьэтот учебникучиться
  • Базовые знания TypeScript, вам нужно только понимать аннотации простых типов, вы можете обратиться к нашемуСерия руководств по TypeScript
  • *(Не обязательно)* Базовые знания фреймворка Express очень полезны для знакомства с красотами Koa, и в этой статье мы будем много перемежать экспресс-сравнением, вы можете обратиться кэтот учебникучиться

Используемая технология

  • Node.js: 10.x и выше
  • NPM: 6.x и выше
  • Коа: 2.x
  • MySQL: рекомендуемая стабильная версия 5.7 и выше.
  • ТипORM: 0.2.x

цель обучения

После завершения этого урока вы узнаете:

  • Если вы пишете промежуточное ПО Koa
  • пройти через@koa/routerРеализовать конфигурацию маршрутизации
  • Подключайтесь к базам данных MySQL, читайте и записывайте в них через TypeORM (аналогично другим базам данных).
  • Понять принцип аутентификации JWT и внедрить его
  • Механизм обработки ошибок Master Koa

Подготовить исходный код

Мы подготовили каркас проекта для вас, запустите следующую команду, чтобы клонировать наш исходный код:

git clone -b start-point https://github.com/tuture-dev/koa-quickstart.git

Если у вас возникли проблемы с доступом к GitHub, вы можете клонировать наш репозиторий Gitee:

git clone -b start-point https://gitee.com/tuture/koa-quickstart.git

Затем войдите в проект и установите зависимости:

cd koa-quickstart && npm install

Уведомление

Здесь я использовалpackage-lock.jsonУбедитесь, что все зависимости имеют одинаковую версию, если вы используетеyarnЕсть проблема с установочной зависимостью, рекомендуется удалитьnode_modules, повторное использованиеnpm installУстановить.

Самый простой сервер KOA

Создайтеsrc/server.ts, пишем первый сервер Koa, код такой:

// src/server.ts
import Koa from 'koa';
import cors from '@koa/cors';
import bodyParser from 'koa-bodyparser';

// 初始化 Koa 应用实例
const app = new Koa();

// 注册中间件
app.use(cors());
app.use(bodyParser());

// 响应用户请求
app.use((ctx) => {
  ctx.body = 'Hello Koa';
});

// 运行服务器
app.listen(3000);

Весь процесс почти идентичен базовому серверу Express:

  1. Инициализировать экземпляр приложенияapp
  2. Зарегистрируйте связанное промежуточное ПО (междоменноеcorsи запрашивать промежуточное ПО для разбора телаbodyParser)
  3. Добавьте обработчик запросов для ответа на запросы пользователей.
  4. запустить сервер

При ближайшем рассмотрении обработчик запросов на шаге 3 кажется другим. В среде Express обработчик запроса обычно выглядит так:

function handler(req, res) {
  res.send('Hello Express');
}

Два параметра соответствуют объекту запроса (Request) и объекту ответа (Response) соответственно, но в Koa функция обработки запроса имеет только один параметр.ctx(контекст, контекст), а затем просто записать соответствующие свойства в объект контекста (например, вот запись в возвращаемые данныеbodyсередина):

function handler(ctx) {
  ctx.body = 'Hello Koa';
}

Боже мой, Коа, это намеренно срезает углы? Не волнуйтесь, в следующем разделе, посвященном промежуточному программному обеспечению, мы увидим, что уникального в дизайне Koa.

запустить сервер

мы проходимnpm startСервер можно запускать. Мы можем использовать Curl (или Postman и т. д.) для тестирования нашего API:

$ curl localhost:3000
Hello Koa

намекать

Наши леса настроеныNodemon, поэтому нет необходимости выключать сервер в следующий раз, последний код будет автоматически загружен и запущен после сохранения измененного кода.

Первое промежуточное ПО KOA

Строго говоря, Koa просто промежуточный фреймворк, как говорится во введении:

Выразительное промежуточное ПО для node.js с использованием асинхронных функций ES2017 (написание выразительного промежуточного ПО Node.js с асинхронными функциями ES2017).

Следующая таблица лучше может объяснить резкое контрастность KOA и Express:

Видно, что Коа на самом деле нацелен наConnect(промежуточный слой в нижней части Express) и не включает другие функции, которые есть в Express, такие как маршрутизация, механизм шаблонов, отправка файлов и т. д. Далее давайте изучим самые важные точки знаний о Коа:промежуточное ПО.

Знаменитая «луковичная модель»

Возможно, вы никогда не использовали фреймворк Koa, но вы, вероятно, слышали о «луковой модели», а Koa — один из репрезентативных фреймворков луковой модели. Возможно, вам знакомо следующее изображение:

Однако, с моей личной точки зрения, эта картина слишком уж похожа на «луковицу», но понять ее непросто. Далее мы почувствуем красоту дизайна промежуточного программного обеспечения Koa более ясным и интуитивно понятным способом. Сначала давайте посмотрим, как выглядит промежуточное ПО Express:

Запрос (Request) проходит через каждое промежуточное ПО напрямую по очереди и, наконец, возвращает ответ (Response) через функцию обработки запроса, что очень просто. Тогда давайте посмотрим, как выглядит промежуточное ПО Koa:

Видно, что мидлвар Koa не завершает свою миссию после передачи запроса, как мидлвар Express, наоборот, выполнение мидлвара четко делится надва этапа. Давайте посмотрим, как именно выглядит промежуточное ПО Koa.

Определение промежуточного программного обеспечения Koa

Промежуточное ПО Koa представляет собой такую ​​функцию:

async function middleware(ctx, next) {
  // 第一阶段
  await next();
  // 第二阶段
}

Первый параметр — Koa Context, то есть содержимое, передаваемое зеленой стрелкой, которая проходит через все промежуточное ПО и функции обработки запросов на приведенном выше рисунке.Инкапсулирует тело запроса и тело ответа(На самом деле есть и другие атрибуты, но я пока не буду говорить об этом здесь), которые можно передать черезctx.requestа такжеctx.responseЧтобы получить, вот несколько общих атрибутов:

ctx.url    // 相当于 ctx.request.url
ctx.body   // 相当于 ctx.response.body
ctx.status // 相当于 ctx.response.status

намекать

Все свойства запроса и ответа и их псевдонимы см.Документация API контекста.

Второй параметр промежуточного программного обеспечения:nextфункция, этот одноклассник, знакомый с Express, должен знать, что она делает: она используется для передачи управления следующему промежуточному ПО. Но это то же самое, что и ExpressnextСущественное различие между функциями состоит в том, чтоКоаnextФункция возвращает обещание, после того как этот Promise перейдет в состояние выполнено (Fulfilled), он выполнит код второго этапа в middleware.

Поэтому мы не можем не задаться вопросом: есть ли какая-то польза от разделения выполнения промежуточного программного обеспечения на два этапа? Давайте почувствуем это на очень классическом примере: ПО промежуточного слоя ведения журналов (включая расчеты времени отклика).

В действии: ПО промежуточного слоя для ведения журналов

Давайте реализуем простое промежуточное ПО для ведения журналов.logger, который записывает метод, URL-адрес, код состояния и время ответа для каждого запроса. Создайтеsrc/logger.ts, код показан ниже:

// src/logger.ts
import { Context } from 'koa';

export function logger() {
  return async (ctx: Context, next: () => Promise<void>) => {
    const start = Date.now();
    await next();
    const ms = Date.now() - start;
    console.log(`${ctx.method} ${ctx.url} ${ctx.status} - ${ms}ms`);
  };
}

Строго говоря, здесьloggerЯвляетсяфункция фабрики промежуточного программного обеспечения(Factory), результат, возвращаемый после вызова этой фабричной функции, является настоящим промежуточным программным обеспечением Koa. Причина, по которой она написана как фабричная функция, заключается в том, что мы можем лучше контролировать поведение промежуточного программного обеспечения, передавая параметры фабричной функции (конечно,loggerПроще, без всяких параметров).

На первом этапе этого промежуточного программного обеспечения мы проходимDate.now()Сначала получите время поступления запроса, затем передайтеawait next()Отказаться от права на исполнение, дождаться окончания работы нижестоящего промежуточного программного обеспечения, а затем передать расчет на втором этапеDate.now()чтобы получить время, необходимое для обработки запроса.

Подумайте об этом, если вы используете Express для реализации этой функции, как должно быть написано промежуточное программное обеспечение, будет ли оно таким же простым и элегантным, как Koa?

намекать

Здесь через дваDate.now()Неверно рассчитывать время работы по разнице междуprocess.hrtime().

Тогда мыsrc/server.tsцентр прямо сейчасloggerпромежуточное ПО черезapp.useЗарегистрируйтесь, код выглядит следующим образом:

// src/server.ts
// ...

import { logger } from './logger';

// 初始化 Koa 应用实例
const app = new Koa();

// 注册中间件
app.use(logger());
app.use(cors());
app.use(bodyParser());

// ...

В это время посетите наш сервер (через Curl или другие инструменты запроса), и вы должны увидеть выходной журнал:

Контент о самой структуре Koa в основном закончен, но для относительно полного веб-сервера нам все еще нужно больше «оружия» для работы с повседневной бизнес-логикой. В следующих разделах мы рассмотрим две ключевые проблемы с отличными компонентами от сообщества: маршрутизация и база данных, а также продемонстрируем, как использовать их в сочетании с инфраструктурой Koa.

Реализовать конфигурацию маршрутизации

Поскольку KOA — это всего лишь промежуточный фрейм, для реализации маршрута требуется отдельный пакет NPM. Установить первым@koa/router

$ npm install @koa/router
$ npm install @types/koa__router -D

Уведомление

Некоторые учебники используютkoa-router, но из-заkoa-routerВ настоящее время он практически не поддерживается, поэтому здесь мы используем более активно поддерживаемую версию Fork.@koa/router.

планирование маршрута

В этом уроке мы реализуем следующие маршруты:

  • GET /users: все пользовательские запросы
  • GET /users/:id: запрос одного пользователя
  • PUT /users/:id: обновить одного пользователя
  • DELETE /users/:id: удалить одного пользователя
  • POST /users/login: Войти (получить токен JWT)
  • POST /users/register:зарегистрированный пользователь

Реализовать контроллер

существуетsrcсоздан вcontrollersКаталог, используемый для хранения кода, связанного с контроллером. прежде всегоAuthController,Создайтеsrc/controllers/auth.ts, код показан ниже:

// src/controllers/auth.ts
import { Context } from 'koa';

export default class AuthController {
  public static async login(ctx: Context) {
    ctx.body = 'Login controller';
  }

  public static async register(ctx: Context) {
    ctx.body = 'Register controller';
  }
}

затем создайтеsrc/controllers/user.ts, код показан ниже:

// src/controllers/user.ts
import { Context } from 'koa';

export default class UserController {
  public static async listUsers(ctx: Context) {
    ctx.body = 'ListUsers controller';
  }

  public static async showUserDetail(ctx: Context) {
    ctx.body = `ShowUserDetail controller with ID = ${ctx.params.id}`;
  }

  public static async updateUser(ctx: Context) {
    ctx.body = `UpdateUser controller with ID = ${ctx.params.id}`;
  }

  public static async deleteUser(ctx: Context) {
    ctx.body = `DeleteUser controller with ID = ${ctx.params.id}`;
  }
}

Обратите внимание, что в последних трех контроллерах мы передаемctx.paramsПолучить параметры маршрутаid.

реализовать маршрутизацию

Затем мы создаемsrc/routes.ts, который используется для подключения контроллера к соответствующему маршруту:

// src/routes.ts
import Router from '@koa/router';

import AuthController from './controllers/auth';
import UserController from './controllers/user';

const router = new Router();

// auth 相关的路由
router.post('/auth/login', AuthController.login);
router.post('/auth/register', AuthController.register);

// users 相关的路由
router.get('/users', UserController.listUsers);
router.get('/users/:id', UserController.showUserDetail);
router.put('/users/:id', UserController.updateUser);
router.delete('/users/:id', UserController.deleteUser);

export default router;

можно увидеть@koa/routerв основном используется так же, как Express Router.

зарегистрировать маршрут

Наконец, нам нужноrouterЗарегистрируйтесь как промежуточное ПО. Открытьsrc/server.ts, измените код следующим образом:

// src/server.ts
// ...

import router from './routes';
import { logger } from './logger';

// 初始化 Koa 应用实例
const app = new Koa();

// 注册中间件
app.use(logger());
app.use(cors());
app.use(bodyParser());

// 响应用户请求
app.use(router.routes()).use(router.allowedMethods());

// 运行服务器
app.listen(3000);

Как видите, здесь мы вызываемrouterобъектroutesМетод получает соответствующее промежуточное ПО Koa, а также вызываетallowedMethodsЭтот метод регистрирует промежуточное ПО для обнаружения метода HTTP, поэтому, когда пользователь получает доступ к API через неправильный метод HTTP, автоматически возвращается код состояния 405 Method Not Allowed.

Используем Curl для проверки маршрута (вы также можете сами использовать Postman):

$ curl localhost:3000/hello
Not Found
$ curl localhost:3000/auth/register
Method Not Allowed
$ curl -X POST localhost:3000/auth/register
Register controller
$ curl -X POST localhost:3000/auth/login
Login controller
$ curl localhost:3000/users
ListUsers controller
$ curl localhost:3000/users/123
ShowUserDetail controller with ID = 123
$ curl -X PUT localhost:3000/users/123
UpdateUser controller with ID = 123
$ curl -X DELETE localhost:3000/users/123
DeleteUser controller with ID = 123

В то же время вы можете увидеть вывод журнала сервера следующим образом:

Маршрутизация подключена, теперь давайте доступ к реальным данным!

Доступ к базе данных MySQL

С этого шага мы официально получим доступ к базе данных. Koa сама по себе является промежуточным программным обеспечением, которое теоретически может получить доступ к любому типу базы данных, здесь мы выбираем популярную реляционную базу данных MySQL. И, поскольку мы используем TypeScript-разработку, мы используем здесь сделанный специально для TSORMТип библиотеки.

Подготовка базы данных

Сначала установите и настройте базу данных MySQL двумя способами:

  • Скачайте установочный пакет с официального сайта, вотссылка на скачивание
  • Использование образа MySQL Docker

Убедившись, что экземпляр MySQL запущен, мы открываем терминал и подключаемся к базе данных через командную строку:

$ mysql -u root -p

После ввода предустановленного пароля учетной записи root войдите в интерактивный клиент выполнения MySQL, а затем выполните следующую команду:

--- 创建数据库
CREATE DATABASE koa;

--- 创建用户并授予权限
CREATE USER 'user'@'localhost' IDENTIFIED BY 'pass';
GRANT ALL PRIVILEGES ON koa.* TO 'user'@'localhost';

--- 处理 MySQL 8.0 版本的认证协议问题
ALTER USER 'user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'pass';
flush privileges;

Настройка и подключение TypeORM

Сначала установите соответствующие пакеты npm, а именно драйвер MySQL, TypeORM иreflect-metadata(Библиотека Reflection API для TypeORM для вывода метаданных модели):

$ npm install mysql typeorm reflect-metadata

Затем создайте его в корневом каталоге проекта.ormconfig.json, TypeORM прочитает эту конфигурацию базы данных для подключения, код выглядит следующим образом:

// ormconfig.json
{
  "type": "mysql",
  "host": "localhost",
  "port": 3306,
  "username": "user",
  "password": "pass",
  "database": "koa",
  "synchronize": true,
  "entities": ["src/entity/*.ts"],
  "cli": {
    "entitiesDir": "src/entity"
  }
}

Вот несколько полей для объяснения:

  • databaseэто то, что мы только что создалиkoaбаза данных
  • synchronizeустановить какtrueЭто позволяет нам автоматически синхронизироваться с базой данных каждый раз, когда мы изменяем определение модели* (если вы связались с другими библиотеками ORM, это на самом деле автоматическая миграция данных)*
  • entitiesПоле определяет путь к файлу модели, мы его создадим в ближайшее время

Затем изменитеsrc/server.ts, в котором для подключения к базе данных код выглядит следующим образом:

// src/server.ts
import Koa from 'koa';
import cors from '@koa/cors';
import bodyParser from 'koa-bodyparser';
import { createConnection } from 'typeorm';
import 'reflect-metadata';

import router from './routes';
import { logger } from './logger';

createConnection()
  .then(() => {
    // 初始化 Koa 应用实例
    const app = new Koa();

    // 注册中间件
    app.use(logger());
    app.use(cors());
    app.use(bodyParser());

    // 响应用户请求
    app.use(router.routes()).use(router.allowedMethods());

    // 运行服务器
    app.listen(3000);
  })
  .catch((err: string) => console.log('TypeORM connection error:', err));

Создание определения модели данных

существуетsrcСоздано в каталогеentityКаталог для хранения файлов определения модели данных. создать в немuser.ts, представляющий модель пользователя, код выглядит следующим образом:

// src/entity/user.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ select: false })
  password: string;

  @Column()
  email: string;
}

Как видите, модель User имеет четыре поля, смысл которых легко понять. И TypeORM черездекораторЭтот элегантный способ объединить нашиUserКлассы сопоставляются с таблицами в базе данных. Здесь мы используем три декоратора:

  • EntityИспользуется для украшения всего класса, чтобы сделать его моделью базы данных.
  • ColumnИспользуется для украшения свойства класса, чтобы оно соответствовало столбцу в таблице базы данных, может быть предоставлен ряд необязательных параметров, например, мы даемpasswordуже настроенselect: false, чтобы это поле не выбиралось по умолчанию при запросе
  • PrimaryGeneratedColumnОн предназначен для украшения основного столбца, его значение будет сгенерировано автоматически.

намекать

Все определения TypeORM в декораторах и их подробное использование см.Документация по декоратору.

Работа с базой данных в контроллере

Затем вы можете добавлять, удалять, изменять и запрашивать данные в контроллере. Сначала мы открываемsrc/controllers/user.ts, для реализации всей логики контроллера код выглядит следующим образом:

// src/controllers/user.ts
import { Context } from 'koa';
import { getManager } from 'typeorm';

import { User } from '../entity/user';

export default class UserController {
  public static async listUsers(ctx: Context) {
    const userRepository = getManager().getRepository(User);
    const users = await userRepository.find();

    ctx.status = 200;
    ctx.body = users;
  }

  public static async showUserDetail(ctx: Context) {
    const userRepository = getManager().getRepository(User);
    const user = await userRepository.findOne(+ctx.params.id);

    if (user) {
      ctx.status = 200;
      ctx.body = user;
    } else {
      ctx.status = 404;
    }
  }

  public static async updateUser(ctx: Context) {
    const userRepository = getManager().getRepository(User);
    await userRepository.update(+ctx.params.id, ctx.request.body);
    const updatedUser = await userRepository.findOne(+ctx.params.id);

    if (updatedUser) {
      ctx.status = 200;
      ctx.body = updatedUser;
    } else {
      ctx.status = 404;
    }
  }

  public static async deleteUser(ctx: Context) {
    const userRepository = getManager().getRepository(User);
    await userRepository.delete(+ctx.params.id);

    ctx.status = 204;
  }
}

Работа с моделью данных в TypeORM в основном осуществляется черезRepositoryРеализован в контроллере, вы можете пройтиgetManager().getRepository(Model)получить, тоRepositoryAPI запросов очень похож на другие библиотеки.

намекать

Для получения информации обо всех API запросов репозитория см.Документация здесь.

Осторожно, вы также должны обнаружить, что мы прошлиctx.request.bodyДанные тела запроса получают, что мы настроили на первом шагеbodyParserПромежуточное ПО добавляется в объект Context.

Затем мы модифицируемAuthController, чтобы реализовать определенную логику регистрации. Поскольку пароль не может храниться в базе данных в виде обычного текста, его необходимо зашифровать с помощью асимметричного алгоритма.Здесь мы используем пароль, выигравший чемпионат конкурса по шифрованию паролей.Argon2алгоритм. Установите соответствующий пакет npm:

npm install argon2

Затем реализуйте конкретныеregisterКонтроллер, изменитьsrc/controllers/auth.ts, код показан ниже:

// src/controllers/auth.ts
import { Context } from 'koa';
import * as argon2 from 'argon2';
import { getManager } from 'typeorm';

import { User } from '../entity/user';

export default class AuthController {
  // ...

  public static async register(ctx: Context) {
    const userRepository = getManager().getRepository(User);

    const newUser = new User();
    newUser.name = ctx.request.body.name;
    newUser.email = ctx.request.body.email;
    newUser.password = await argon2.hash(ctx.request.body.password);

    // 保存到数据库
    const user = await userRepository.save(newUser);

    ctx.status = 201;
    ctx.body = user;
  }
}

Убедившись, что сервер работает, мы можем запустить тестовую волну. Первый — зарегистрировать пользователей (здесь я использую Postman для демонстрации, он более интуитивен):

Вы можете пойти дальше и зарегистрировать несколько пользователей, а затем продолжить посещение/usersСоответствующие маршруты должны иметь возможность успешно получать, изменять и удалять соответствующие данные!

Реализовать JWT-аутентификацию

JSON Web Token (JWT) — это популярная схема аутентификации RESTful API. Здесь мы проведем вас за руку, чтобы узнать, как использовать аутентификацию JWT в рамках Koa, но не будем слишком много объяснять принцип (см.эта статьяучиться).

Сначала установите соответствующие пакеты npm:

npm install koa-jwt jsonwebtoken
npm install @types/jsonwebtoken -D

Создайтеsrc/constants.ts, используемый для хранения константы JWT Secret, код выглядит следующим образом:

// src/constants.ts
export const JWT_SECRET = 'secret';

В реальной разработке замените его на достаточно сложную строку, и лучше всего вводить ее через переменные среды.

перенаправлять

Для некоторых маршрутов мы хотим, чтобы только зарегистрированные пользователи имели разрешение на просмотр (защищенная маршрутизация), а остальные маршруты доступны для всех запросов (незащищенная маршрутизация). В модели лука Коа мы можем сделать это так:

Все запросы могут напрямую обращаться к незащищенному маршруту, но защищенный маршрут размещается за промежуточным ПО JWT (или «внутри» с точки зрения луковой модели), так что запрос без токена JWT возвращается напрямую, а не продолжается.

После того, как идея ясна, открывайтеsrc/routes.tsФайл маршрутизации, измените код следующим образом:

// src/routes.ts
import Router from '@koa/router';

import AuthController from './controllers/auth';
import UserController from './controllers/user';

const unprotectedRouter = new Router();

// auth 相关的路由
unprotectedRouter.post('/auth/login', AuthController.login);
unprotectedRouter.post('/auth/register', AuthController.register);

const protectedRouter = new Router();

// users 相关的路由
protectedRouter.get('/users', UserController.listUsers);
protectedRouter.get('/users/:id', UserController.showUserDetail);
protectedRouter.put('/users/:id', UserController.updateUser);
protectedRouter.delete('/users/:id', UserController.deleteUser);

export { protectedRouter, unprotectedRouter };

Выше мы достиглиprotectedRouterа такжеunprotectedRouter, соответствующие маршрутам, требующим защиты промежуточного ПО JWT, и маршрутам, не требующим защиты, соответственно.

Зарегистрировать промежуточное ПО JWT

Следующий шаг — зарегистрировать промежуточное ПО JWT и прописать маршруты, не требующие защиты, до и после него.unprotectedRouterи маршруты, которые необходимо защититьprotectedRouter. Изменить файлы сервераsrc/server.ts, код показан ниже:

// src/server.ts
// ...
import jwt from 'koa-jwt';
import 'reflect-metadata';

import { protectedRouter, unprotectedRouter } from './routes';
import { logger } from './logger';
import { JWT_SECRET } from './constants';

createConnection()
  .then(() => {
    // ...

    // 无需 JWT Token 即可访问
    app.use(unprotectedRouter.routes()).use(unprotectedRouter.allowedMethods());

    // 注册 JWT 中间件
    app.use(jwt({ secret: JWT_SECRET }).unless({ method: 'GET' }));

    // 需要 JWT Token 才可访问
    app.use(protectedRouter.routes()).use(protectedRouter.allowedMethods());

    // ...
  })
  // ...

Разве это не кажется очень интуитивным в соответствии с чертежом «луковой модели» только что?

намекать

После регистрации промежуточного программного обеспечения JWT, если пользовательский запрос содержит действительный токен, следующееprotectedRouterты можешь пройтиctx.state.userПолучите содержимое токена (точнее, полезной нагрузки, которая обычно представляет собой ключевую информацию о пользователе, такую ​​как идентификатор); в противном случае, если токен отсутствует или недействителен, промежуточное ПО JWT напрямую и автоматически вернет ошибку 401. Дополнительные сведения об использовании koa-jwt см.Документация.

Выпуск токена JWT при входе в систему

Нам нужно предоставить порт API, чтобы пользователи могли получить токен JWT, наиболее подходящий, конечно, интерфейс входа в систему./auth/login. Открытьsrc/controllers/auth.ts,существуетloginЛогика выдачи JWT Token реализована в контроллере, код выглядит следующим образом:

// src/controllers/auth.ts
// ...
import jwt from 'jsonwebtoken';

// ...
import { JWT_SECRET } from '../constants';

export default class AuthController {
  public static async login(ctx: Context) {
    const userRepository = getManager().getRepository(User);

    const user = await userRepository
      .createQueryBuilder()
      .where({ name: ctx.request.body.name })
      .addSelect('User.password')
      .getOne();

    if (!user) {
      ctx.status = 401;
      ctx.body = { message: '用户名不存在' };
    } else if (await argon2.verify(user.password, ctx.request.body.password)) {
      ctx.status = 200;
      ctx.body = { token: jwt.sign({ id: user.id }, JWT_SECRET) };
    } else {
      ctx.status = 401;
      ctx.body = { message: '密码错误' };
    }
  }

  // ...
}

существуетlogin, мы сначала основываемся на имени пользователя (в теле запросаnameПоле) Запросить соответствующего пользователя, если пользователь не существует, вернуть напрямую 401; если есть, передатьargon2.verifyдля проверки открытого текста пароля в теле запросаpasswordСоответствует ли он зашифрованному паролю, хранящемуся в базе данных, если он соответствует, передатьjwt.signВыпустите токен и верните 401, если он несовместим.

Полезная нагрузка токена здесь — это объект, который идентифицирует идентификатор пользователя.{ id: user.id }, так что после успешной аутентификации вы можете пройтиctx.user.idчтобы получить идентификатор пользователя.

Добавить контроль доступа в пользовательский контроллер

После того, как промежуточное ПО и выпуск токена завершены, последним шагом является проверка токена пользователя в подходящем месте, чтобы убедиться, что у него есть достаточные разрешения. Наиболее типичный сценарий: при обновлении или удалении пользователей мы хотимУбедитесь, что это сам пользователь. Открытьsrc/controllers/user.ts, код показан ниже:

// src/controllers/user.ts
// ...

export default class UserController {
  // ...

  public static async updateUser(ctx: Context) {
    const userId = +ctx.params.id;

    if (userId !== +ctx.state.user.id) {
      ctx.status = 403;
      ctx.body = { message: '无权进行此操作' };
      return;
    }

    const userRepository = getManager().getRepository(User);
    await userRepository.update(userId, ctx.request.body);
    const updatedUser = await userRepository.findOne(userId);

    // ...
  }

  public static async deleteUser(ctx: Context) {
    const userId = +ctx.params.id;

    if (userId !== +ctx.state.user.id) {
      ctx.status = 403;
      ctx.body = { message: '无权进行此操作' };
      return;
    }

    const userRepository = getManager().getRepository(User);
    await userRepository.delete(userId);

    ctx.status = 204;
  }
}

Логика аутентификации двух контроллеров в основном одинакова, мы сравниваемctx.params.idа такжеctx.state.user.idЯвляются ли они одинаковыми, если нет, возвращайте ошибку 403 Forbidden и продолжайте выполнять соответствующие операции с базой данных, если они совпадают.

После написания кода мы используем информацию о только что зарегистрированном пользователе для доступа к API входа:

Токен JWT успешно получен! Затем копируем полученный Токен, при дальнейшем тестировании защищенного маршрута нам нужно добавитьAuthorizationзаголовок, значениеBearer <JWT_TOKEN>,Как показано ниже:

Затем вы можете протестировать защищенный маршрут! Здесь он опущен из-за ограничения места.

обработка ошибок

Наконец, давайте кратко поговорим об обработке ошибок в Koa. Поскольку Koa использует асинхронные функции и промисы в качестве решений для асинхронного программирования, обработка ошибок, естественно, очень проста — ее можно легко сделать напрямую с помощью синтаксиса try-catch, который поставляется с JavaScript.

Реализовать пользовательскую ошибку (исключение)

Во-первых, давайте реализуем некоторые пользовательские классы ошибок (или исключений, в этой статье они не различаются). Создайтеsrc/exceptions.ts, код показан ниже:

// src/exceptions.ts
export class BaseException extends Error {
  // 状态码
  status: number;
  // 提示信息
  message: string;
}

export class NotFoundException extends BaseException {
  status = 404;

  constructor(msg?: string) {
    super();
    this.message = msg || '无此内容';
  }
}

export class UnauthorizedException extends BaseException {
  status = 401;

  constructor(msg?: string) {
    super();
    this.message = msg || '尚未登录';
  }
}

export class ForbiddenException extends BaseException {
  status = 403;

  constructor(msg?: string) {
    super();
    this.message = msg || '权限不足';
  }
}

Здесь указан тип ошибкиNest.jsдизайн. В целях обучения это упрощено и реализует только те ошибки, которые нам нужно использовать.

Использовать пользовательские ошибки в контроллере

Затем мы можем использовать пользовательскую ошибку прямо сейчас в контроллере. Открытьsrc/controllers/auth.ts, измените код следующим образом:

// src/controllers/auth.ts
// ...
import { UnauthorizedException } from '../exceptions';

export default class AuthController {
  public static async login(ctx: Context) {
    // ...

    if (!user) {
      throw new UnauthorizedException('用户名不存在');
    } else if (await argon2.verify(user.password, ctx.request.body.password)) {
      ctx.status = 200;
      ctx.body = { token: jwt.sign({ id: user.id }, JWT_SECRET) };
    } else {
      throw new UnauthorizedException('密码错误');
    }
  }

  // ...
}

Как видите, мы изменили код установки кода состояния и тела ответа вручную на простой бросок ошибки, и код стал намного понятнее.

намекать

Объект Koa Context предоставляет удобный методthrow, который также может вызывать исключения, например.ctx.throw(400, 'Bad request').

Так же модифицироватьUserController相关的逻辑。 Исправлятьsrc/controllers/user.ts, код показан ниже:

// src/controllers/user.ts
// ...
import { NotFoundException, ForbiddenException } from '../exceptions';

export default class UserController {
  // ...

  public static async showUserDetail(ctx: Context) {
    const userRepository = getManager().getRepository(User);
    const user = await userRepository.findOne(+ctx.params.id);

    if (user) {
      ctx.status = 200;
      ctx.body = user;
    } else {
      throw new NotFoundException();
    }
  }

  public static async updateUser(ctx: Context) {
    const userId = +ctx.params.id;

    if (userId !== +ctx.state.user.id) {
      throw new ForbiddenException();
    }

    // ...
  }
 // ...
  public static async deleteUser(ctx: Context) {
    const userId = +ctx.params.id;

    if (userId !== +ctx.state.user.id) {
      throw new ForbiddenException();
    }

    // ...
  }
}

Добавить промежуточное ПО для обработки ошибок

Наконец, нам нужно добавить промежуточное ПО для обработки ошибок, чтобы перехватывать ошибки, возникающие в контроллере. Открытьsrc/server.ts, для реализации промежуточного программного обеспечения обработки ошибок код выглядит следующим образом:

// src/server.ts
// ...

createConnection()
  .then(() => {
    // ...

    // 注册中间件
    app.use(logger());
    app.use(cors());
    app.use(bodyParser());

    app.use(async (ctx, next) => {
      try {
        await next();
      } catch (err) {
        // 只返回 JSON 格式的响应
        ctx.status = err.status || 500;
        ctx.body = { message: err.message };
      }
    });

    // ...
  })
  // ...

Как видите, в этом промежуточном программном обеспечении для обработки ошибок мы конвертируем возвращаемые данные ответа в формат JSON (вместо предыдущего простого текста), который выглядит более единообразным.

На этом урок окончен. Контента много, надеюсь он будет вам полезен. Наша пользовательская система смогла обработать большинство случаев, но по-прежнему плохо обрабатывает некоторые крайние случаи (можете ли вы вспомнить какие-нибудь?). Но опять же, я полагаю, вы уже решили, что Koa — отличный фреймворк, верно?

Хотите узнать больше интересных практических технических руководств? ПриходитьСообщество ТукеМагазин вокруг.