Вход по JWT
Сейчас создать или удалить статью может кто угодно. Для API обычно используют JWT — подписанный токен, строку, в которой сервер записывает, кто пользователь, и ставит подпись секретным ключом. Клиент получает токен при входе и присылает его в каждом запросе, а сервер проверяет подпись и не хранит никаких сессий.
Модуль авторизации
Секрет берётся из переменной JWT_SECRET в файле .env. Создайте src/auth.ts:
// src/auth.ts
import { jwt } from 'hono/jwt'
const secret = process.env.JWT_SECRET
if (!secret) {
throw new Error('Не задана переменная JWT_SECRET')
}
export const jwtSecret = secret
export const requireAuth = jwt({ secret, alg: 'HS256' })Если переменной нет, сервер не запустится и сообщит об этом. requireAuth — middleware, которое пропускает запрос только с правильным токеном. HS256 — алгоритм подписи общим секретом.
Выдача токена
Создайте src/routes/auth.ts с маршрутом входа. Для простоты пароль администратора тоже лежит в .env как ADMIN_PASSWORD, в настоящем проекте пользователи хранятся в базе с хешами паролей:
// src/routes/auth.ts
import { Hono } from 'hono'
import { sign } from 'hono/jwt'
import { jwtSecret } from '../auth.js'
const auth = new Hono()
auth.post('/login', async (c) => {
const { username, password } = await c.req.json<{ username: string; password: string }>()
if (username !== 'anna' || password !== process.env.ADMIN_PASSWORD) {
return c.json({ error: 'Неверный логин или пароль' }, 401)
}
const token = await sign(
{ sub: username, exp: Math.floor(Date.now() / 1000) + 60 * 60 },
jwtSecret,
)
return c.json({ token })
})
export default authПодключите группу в src/index.ts: app.route('/auth', auth). Поле exp — время окончания действия токена в секундах: через час токен перестанет приниматься.
Защита маршрутов
Добавьте requireAuth в маршруты, которые меняют данные, в src/routes/posts.ts:
// src/routes/posts.ts
import { requireAuth } from '../auth.js'
posts.post('/', requireAuth, zValidator('json', postSchema), (c) => {
const user = c.get('jwtPayload')
console.log(`Статью создаёт ${user.sub}`)
// ...создание статьи, как в прошлом уроке
return c.json({ author: user.sub }, 201)
})То же сделайте для put и delete. c.get('jwtPayload') возвращает данные из токена. Чтение списка остаётся открытым.
Проверка
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"username": "anna", "password": "пароль-из-env"}'В ответе придёт {"token":"eyJ..."}. Запрос без токена получит 401, а с токеном пройдёт:
curl -X POST http://localhost:3000/posts \
-H "Authorization: Bearer eyJ..." \
-H "Content-Type: application/json" \
-d '{"title": "Защищённая статья", "body": "Текст"}'Вместо eyJ... подставьте полученный токен целиком.
Частая ошибка
В токен кладут секретные данные, например пароль или адрес. Подпись защищает токен от подделки, но не прячет содержимое: любую его часть можно прочитать, раскодировав base64. Храните в токене только идентификатор пользователя, роль и срок действия.