Дино (Deno): Создать API для отдыха с помощью JWT

от автора

В преддверии старта курса «Node.JS Developer» приглашаем всех желающих посмотреть открытый урок на тему «Докеризация Node.js приложений».

А сейчас делимся традиционным переводом полезного материала. Приятного чтения.


Со времен первой версии Deno стал модным словом для разработчиков Javascript/TypeScript/Node. Давайте погрузимся в эту технологию, создав защищенный с помощью JWT REST API.

Желательно уже иметь некоторые основы в Node и его экосистеме (Express, Nodemon, Sequelize и т.д.), чтобы следовать этому руководству.

Что такое Дино (Deno)?

Deno — это простая, современная и безопасная среда выполнения JavaScript и TypeScript, которая использует V8 и встроена в Rust.

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

Вступление

С момента официального релиза V1, Deno стал «модным словечком» в течение нескольких недель (для фана, вот кривая популярности «deno» поиска в Google).

Что можно сделать с безопасной средой выполнения для Typescript and Javascript»?

Чтобы лучше понять и высказать свое мнение об этом растущем проекте, я решил создать защищенный с помощью JWT REST API и поделиться с вами своими чувствами.

Я привык работать с Node.js и Express.

Цель

Целью данного руководства будет создание защищенного REST API, что подразумевает:

  • Настройка сервера

  • Создание модели с ORM и базой данных

  • CRUD-пользователь

  • Реализация защищенной маршрутизации с JWT

Предпосылка

Для создания нашего REST API с JWT, я буду использовать :

  • Deno (рекомендую официальную документацию для установки: здесь)

  • VSCode и плагин поддержки Deno, доступный по ссылке

А также следующие пакеты (я буду возвращаться к этому на протяжении всего урока):

Установка

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

|-- DenoRestJwt     |-- controllers/     |   |-- database/     |   |-- models/     |-- helpers/     |-- middlewares/     |-- routers/     |-- app.ts

Если бы мы были на Node + Express приложении, я бы использовал Nodemon для облегчения разработки, Nodemon перезапускает сервер автоматически после изменений в коде.

Nodemon — это инструмент, который помогает разрабатывать приложения на основе node.js, автоматически перезапуская приложение Node при обнаружении изменений в файле в каталоге.

Чтобы сохранить тот же «комфорт разработки», я решил использовать Denon, его аналог для Deno.

deno install --allow-read --allow-run --allow-write -f --unstable  https://deno.land/x/denon/denon.ts

Давайте немного изменим конфигурацию Denon. Это будет полезно позже (особенно для управления переменными окружения).

// into denon.json {   "$schema": "https://deno.land/x/denon/schema.json",   "env": {},   "scripts": {     "start": {       "cmd": "deno run app.ts"     }   } }

Теперь мы готовы начать кодирование в хороших условиях! Чтобы запустить Denon, просто введите в консоле denon start:

➜ denon start [denon] v2.0.2 [denon] watching path(s): *.* [denon] watching extensions: ts,js,json [denon] starting `deno run app.ts` Compile file:///deno-crashtest/app.ts [denon] clean exit - waiting for changes before restart

Вы видите, что наш сервер работает… но он ломается! Это нормально, у него нет кода для выполнения в app.ts.

Давайте инициализируем наш сервер

Я решил использовать фреймворк  Oak.

Oak — это промежуточный фреймворк для http-сервера Deno, включая промежуточное ПО маршрутизатора. Этот промежуточный фреймворк вдохновлен Koa, а промежуточный маршрутизатор вдохновлен @koa/router.

Давайте инициализируем наш сервер с помощью Oak :

// app.ts import { Application, Router, Status } from "https://deno.land/x/oak/mod.ts";  // Initialise app const app = new Application();  // Initialise router const router = new Router();  // Create first default route router.get("/", (ctx) => {     ctx.response.status = Status.OK;     ctx.response.body = { message: "It's work !" }; });  app.use(router.routes()); app.use(router.allowedMethods());  console.log("? Deno start !"); await app.listen("0.0.0.0:3001");

Теперь, если мы запустим наш сервер с denon start.

error: Uncaught PermissionDenied: network access to "0.0.0.0:3001",  run again with the --allow-net flag

Это одно из больших различий между Deno и Node: Deno по умолчанию безопасен и не имеет доступа к network. Вы должны авторизовать его:

// into denon.json "scripts": {     "start": {       // add --allow-net       "cmd": "deno run --allow-net app.ts"     }   }

Теперь вы можете получить доступ из браузера (хотя я советую использовать Postman) к localhost:3001 :

{     "message": "It's work !" }

Установка базы данных

Я буду использовать DenoDB в качестве ORM (в частности, потому что он поддерживает Sqlite3). Более того, он очень похож на Sequelize (к которому я привык).

Давайте добавим первый контроллер Database и файл Sqlite3.

|-- DenoRestJwt     |-- controllers/ 	|   |-- Database.ts         |   |-- database/ 	|   |   |-- db.sqlite     |   |-- models/     |-- app.ts
// Database.ts import { Database } from "https://deno.land/x/denodb/mod.ts";  export class DatabaseController {   client: Database;  	/**    * Initialise database client    */   constructor() {     this.client = new Database("sqlite3", {       filepath: Deno.realPathSync("./controllers/database/db.sqlite"),     });   }    /**    * Initialise models    */   async initModels() {     this.client.link([]);     await this.client.sync({});   } }

Наш ORM инициализирован. Вы можете заметить, что я использую realPathSync, который требует дополнительного разрешения. Давайте добавим --allow-read недописанное и --allow-write недописанное в denon.json:

"scripts": {     "start": {       "cmd": "deno run --allow-write --allow-read --allow-net app.ts"     }   }

Все, что осталось сделать, это создать модель пользователя через наш ORM:

|-- DenoRestJwt     |-- controllers/     |   |-- models/     |       |-- User.ts     |-- app.ts
// User.ts import { Model, DATA_TYPES } from "https://deno.land/x/denodb/mod.ts"; import nanoid from "https://deno.land/x/nanoid/mod.ts";  export interface IUser {   id?: string;   firstName: string;   lastName: string;   password: string; }  export class User extends Model {   static table = "users";   static timestamps = true;      static fields = {     id: {       primaryKey: true,       type: DATA_TYPES.STRING,     },     firstName: {       type: DATA_TYPES.STRING,     },     lastName: {       type: DATA_TYPES.STRING,     },     password: {       type: DATA_TYPES.TEXT,     },   };    	// Id will generate a nanoid by default   static defaults = {     id: nanoid(),   }; }

Здесь нет ничего нового, так что я не буду останавливаться на этом. (ps: Я использую nanoid для управления моим UUID, я позволю вам прочитать эту очень интересную статью об этом).

Я пользуюсь этой возможностью, чтобы добавить функцию, которая будет полезна позже: хэш пароля. Для этого я использую Bcrypt:

// inside User's class import * as bcrypt from "https://deno.land/x/bcrypt/mod.ts";  // ... static async hashPassword(password: string) {     const salt = await bcrypt.genSalt(8);     return bcrypt.hash(password, salt); }

И наконец, давайте свяжем нашу модель с нашим ORM :

// Database.ts import { User } from "./models/User.ts";  export class DatabaseController { //...   initModels() {       // Add User here       this.client.link([User]);       return this.client.sync({});   } }

Хорошо! Теперь, когда наш сервер и база данных на месте, пришло время инициализировать маршруты создания аккаунтов… 

User controller

Нет ничего более основного, чем хороший CRUD:

|-- DenoRestJwt     |-- controllers/     |   |-- Database.ts     |   |-- UserController.ts
import * as bcrypt from "https://deno.land/x/bcrypt/mod.ts"; import { IUser, User } from "./models/index.ts";  export class UserController {   async create(values: IUser) {     // Call static user method     const password = await User.hashPassword(values.password);      const user: IUser = {       firstName: values.firstName,       lastName: values.lastName,       password,     };      await User.create(user as any);      return values;   }   async delete(id: string) {     await User.deleteById(id);   }    getAll() {     return User.all();   }    getOne(id: string) {     return User.where("id", id).first();   }    async update(id: string, values: IUser) {     await User.where("id", id).update(values as any);     return this.getOne(id);   }    async login(lastName: string, password: string) {     const user = await User.where("lastName", lastName).first();     if (!user || !(await bcrypt.compare(password, user.password))) {       return false;     }      // TODO generate JWT    } }

Я просто использую методы, предоставляемые ORM. Теперь нам осталось только управлять генерацией JWT.

Настройка маршрутизации

Теперь пришло время создать наши различные пути и вызвать наш свежезакодированный контроллер.

|-- DenoRestJwt     |-- routers         |-- UserRoute.ts
import { Router, Status } from "https://deno.land/x/oak/mod.ts"; import { UserController } from "../controllers/UserController.ts"; import { BadRequest } from "../helpers/BadRequest.ts"; import { NotFound } from "../helpers/NotFound.ts";  // instantiate our controller const controller = new UserController();  export function UserRoutes(router: Router) {   return router     .get("/users", async (ctx) => {       const users = await controller.getAll();        if (users) {         ctx.response.status = Status.OK;         ctx.response.body = users;        } else {         ctx.response.status = Status.NotFound;         ctx.response.body = [];       }        return;     })     .post("/login", async (ctx) => {       if (!ctx.request.hasBody) {         return BadRequest(ctx);       }       const { value } = await ctx.request.body();        // TODO generate JWT        ctx.response.status = Status.OK;       ctx.response.body = { jwt };     })     .get("/user/:id", async (ctx) => {       if (!ctx.params.id) {         return BadRequest(ctx);       }        const user = await controller.getOne(ctx.params.id);       if (user) {         ctx.response.status = Status.OK;         ctx.response.body = user;         return;       }        return NotFound(ctx);     })     .post("/user", async (ctx) => {       if (!ctx.request.hasBody) {         return BadRequest(ctx);       }        const { value } = await ctx.request.body();       const user = await controller.create(value);        if (user) {         ctx.response.status = Status.OK;         ctx.response.body = user;         return;       }        return NotFound(ctx);     })     .patch("/user/:id", async (ctx) => {       if (!ctx.request.hasBody || !ctx.params.id) {         return BadRequest(ctx);       }        const { value } = await ctx.request.body();       const user = await controller.update(ctx.params.id, value);        if (user) {         ctx.response.status = Status.OK;         ctx.response.body = user;         return;       }        return NotFound(ctx);     })     .delete("/user/:id", async (ctx) => {       if (!ctx.params.id) {         return BadRequest(ctx);       }        await controller.delete(ctx.params.id);        ctx.response.status = Status.OK;       ctx.response.body = { message: "Ok" };     }); }

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

Я использую методы HTTP для четкого разделения маршрутов. Я также создал хелперы управления возвращаемыми ошибками. Исходники можно найти прямо из проекта GitHub! Все, что нам нужно сделать, это вызвать наш маршрутизатор в нашем приложении:

// app.ts import { DatabaseController } from "./controllers/Database.ts"; import { UserRoutes } from "./routers/UserRoute.ts";  const userRoutes = UserRoutes(router); app.use(userRoutes.routes()); app.use(userRoutes.allowedMethods());  await new DatabaseController().initModels();

Безопасность и JWT

Пришло время добавить безопасности к этому проекту! Я использую JWT для этого.

1. Создать защищенный маршрут

Прежде всего, мы собираемся установить промежуточный слой:

  • Проверяет, существует ли в запросе заголовок «Authorization».

  • Достает заголовок

  • Валидирует заголовок 

  • Возвращает ошибку / Принимает запрос и вызывает приватный маршрут

Я воспользуюсь библиотекой Djwt.

|-- DenoRestJwt     |-- middlewares/     |   |-- jwt.ts

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

import { Context, Status } from "https://deno.land/x/oak/mod.ts"; import { validateJwt } from "https://deno.land/x/djwt/validate.ts";  /**  * Create a default configuration  */ export const JwtConfig = {   header: "Authorization",   schema: "Bearer", 	// use Env variable   secretKey: Deno.env.get("SECRET") || "",   expirationTime: 60000,   type: "JWT",   alg: "HS256", };  export async function jwtAuth(   ctx: Context<Record<string, any>>,   next: () => Promise<void> ) {     // Get the token from the request     const token = ctx.request.headers       .get(JwtConfig.header)       ?.replace(`${JwtConfig.schema} `, "");          // reject request if token was not provide     if (!token) {       ctx.response.status = Status.Unauthorized;       ctx.response.body = { message: "Unauthorized" };       return;     }          // check the validity of the token     if (       !(await validateJwt(token, JwtConfig.secretKey, { isThrowing: false }))     ) {       ctx.response.status = Status.Unauthorized;       ctx.response.body = { message: "Wrong Token" };       return;     }          // JWT is correct, so continue and call the private route     next();   }

Обратите внимание, что нам нужен секретный ключ, чтобы зашифровать наш токен. Для этого я использую переменные окружения Deno. Так что нам нужно внести несколько изменений в конфигурацию Denon: добавить нашу переменную и разрешить Deno получать переменные окружения.

{   "$schema": "<https://deno.land/x/denon/schema.json>",   // Add env variable   "env": {     "SECRET": "ADRIEN_IS_THE_BEST_AUTHOR_ON_MEDIUM"   },   "scripts": {     "start": {       // add the permission with --allow-env       "cmd": "deno run --allow-env --allow-read --allow-net app.ts"     }   } }

(ps: если вы хотите обезопасить переменные окружения, я рекомендую это учебное пособие)

Тогда давайте создадим наш приватный маршрут.

|-- DenoRestJwt     |-- routers         |-- UserRoute.ts         |-- PrivateRoute.ts

Просто вызовите наш метод перед вызовом нашего маршрута:

import { Router, Status } from "https://deno.land/x/oak/mod.ts"; import { jwtAuth } from "../middlewares/jwt.ts";  export function PrivateRoutes(router: Router) {   // call our middleware before our private route   return router.get("/private", jwtAuth, async (ctx) => {     ctx.response.status = Status.OK;     ctx.response.body = { message: "Conntected !" };   }); }

Не забудьте добавить его в наше приложение:

import { Router, Status } from "https://deno.land/x/oak/mod.ts"; import { jwtAuth } from "../middlewares/jwt.ts";  export function PrivateRoutes(router: Router) {   // call our middleware before our private route   return router.get("/private", jwtAuth, async (ctx) => {     ctx.response.status = Status.OK;     ctx.response.body = { message: "Conntected !" };   }); }

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

{     "message": "Unauthorized" }

2.  JWT поколение

Теперь пришло время настроить генерацию токенов при входе пользователей в систему. Помните, что мы оставили // TODO generate JWT в нашем контроллере. Перед его завершением мы сначала добавим статический метод в нашу модель User, чтобы сгенерировать токен.

// User.ts import {   makeJwt,   setExpiration,   Jose,   Payload, } from "https://deno.land/x/djwt/create.ts"; import { JwtConfig } from "../../middlewares/jwt.ts"; // ...  export class User extends Model { // ... 	static generateJwt(id: string) { 	    // Create the payload with the expiration date (token have an expiry date) and the id of current user (you can add that you want) 	    const payload: Payload = { 	      id, 	      exp: setExpiration(new Date().getTime() + JwtConfig.expirationTime), 	    }; 	    const header: Jose = { 	      alg: JwtConfig.alg as Jose["alg"], 	      typ: JwtConfig.type, 	    };  	    // return the generated token 	    return makeJwt({ header, payload, key: JwtConfig.secretKey }); 	  } 	// ... }

Вызовем этот метод в нашем контроллере :

// UserController.ts  export class UserController { // ... 	async login(lastName: string, password: string) { 			const user = await User.where("lastName", lastName).first(); 			if (!user || !(await bcrypt.compare(password, user.password))) { 				return false; 			} 			// Call our new static method 			return User.generateJwt(user.id); 	 } }

Наконец, давайте добавим эту логику в наш маршрутизатор:

// UserRoute.ts  // ... .post("/login", async (ctx) => {       if (!ctx.request.hasBody) {         return BadRequest(ctx);       }       const { value } = await ctx.request.body(); 			       // generate jwt       const jwt = await controller.login(value.lastName, value.password);       if (!jwt) {         return BadRequest(ctx);       }        ctx.response.status = Status.OK;       // and return it       ctx.response.body = { jwt };     }) // ...

Теперь, если мы попытаемся подключиться, у нас есть :

// localhost:3001/login {     "jwt":  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IlEyY0ZZcUxKWk5Hc0toN0FWV0hzUiIsImV4cCI6MTU5MDg0NDU2MDM5MH0.drQ3ay5_DYuXEOnH2Z0RKbhq9nZElWCMvmypjI4BjIk" }

(Не забудьте создать аккаунт раньше)

Давайте добавим этот токен в наши заголовки Authorization и снова вызовем наш приватный маршрут:

// localhost:3001/private with token in headers {     "message": "Connected !" }

Здорово! Есть наш защищенный API ?.

Вы можете найти этот проект на моем Github: здесь (я добавляю коллекцию почтальона, чтобы сделать запрос).

Мои впечатления о Deno

Я решил поделиться с вами своими впечатлениями относительно Depo, что вам даст некое представление о нем:

Импорт модулей по URL в начале немного контр-интуитивно понятен: всегда хочется сделать npm i или  yarn add. Более того, нам приходится запускать Deno, чтобы кэшировать наши импорты, и только после этого мы имеем доступ к автозавершению.

The remote module XXX has not been cached
  • Я всегда использую TypeScript в своих проектах на Javascript, так что в начале я совсем не потерялся. Напротив, я довольно хорошо знаком с ним.

  • Интересный момент: permissions. Я думаю, хорошо, что Deno, например, требует permissions на доступ к сети. Это заставляет нас, как разработчиков, быть в курсе доступа и прав нашей программы. (более безопасно)

  • Сначала мы немного запутались в том, где искать пакеты (https://deno.land/x → 460 пакетов и NPM → + 1 миллион).

  • Вы никогда не можете быть уверены, что пакет также работает на Deno или нет. Вы +всегда хотите быть ближе к тому, что знаете и используете на Node, чтобы перенести его на Deno. Я не знаю, хорошо это или плохо, это всё ещё javascript…


Узнать подробнее о курсе «Node.JS Developer».

Посмотреть открытый урок на тему «Докеризация Node.js приложений».


ЗАБРАТЬ СКИДКУ

ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/532582/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *