Что такое JSON Web Tolen?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
JSON Web Token (JWT): Безопасность и практическое применение
JSON Web Token (JWT) — это стандартный способ для передачи информации между системами через URL-safe, компактные токены. Это один из самых важных инструментов в modern backend разработке для аутентификации и авторизации.
Структура JWT
JWT состоит из 3 частей разделенных точками: header.payload.signature
Пример:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Header (закодировано в base64url):
{
"alg": "HS256",
"typ": "JWT"
}
Payload (data, тоже base64url):
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022,
"exp": 1516242622
}
Signature (криптографическая подпись):
HMAC_SHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)
Как работает JWT
- Пользователь логинится:
@Post('/auth/login')
async login(@Body() credentials: LoginDto) {
const user = await this.userService.validateCredentials(
credentials.email,
credentials.password
);
const payload = {
sub: user.id,
email: user.email,
role: user.role,
};
const token = this.jwtService.sign(payload, {
expiresIn: '24h',
secret: process.env.JWT_SECRET,
});
return { access_token: token };
}
- Клиент отправляет токен в каждом запросе:
const response = await fetch('/api/users/me', {
headers: {
'Authorization': `Bearer ${access_token}`
}
});
- Сервер верифицирует токен:
@UseGuards(JwtAuthGuard)
@Get('/me')
async getCurrentUser(@Request() req) {
// req.user содержит распакованный payload если токен валиден
return req.user;
}
Преимущества JWT
1. Stateless аутентификация
// JWT: нет нужды хранить сессии на сервере
const token = jwt.sign({ userId: 123 }, secret);
// Любой сервер может верифицировать токен
const decoded = jwt.verify(token, secret); // { userId: 123 }
Это критично для микросервисов и масштабирования.
2. CORS-friendly
JWT отлично работает с CORS потому что это просто строка в header:
// Работает cross-domain
const response = await fetch('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${token}`
}
});
3. Мобильные приложения
JWT perfect для мобильных apps потому что нет cookies:
// React Native
const token = await SecureStore.getItemAsync('token');
const response = await fetch(url, {
headers: { 'Authorization': `Bearer ${token}` }
});
Недостатки и security concerns
1. JWT нельзя invalidate сразу
Токен валиден до истечения срока, даже если пользователь захотел выйти:
// ❌ Плохое решение: сохранить список logout-ов (опять stateful!)
const logoutTokens = new Set();
@Post('/logout')
logout(@Request() req) {
logoutTokens.add(req.headers.authorization);
}
// ✅ Хорошее решение: использовать refresh tokens
@Post('/logout')
logout(@Request() req) {
// Invalidate refresh token в БД
await this.refreshTokenService.invalidate(req.user.id);
}
2. Payload виден любому (base64 не encryption)
// ВАЖНО: JWT зашифрован ПОДПИСЬЮ, а не содержимым
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXQiOiJQYXNzd29yZDEyMyJ9.xxx';
// Декодируем payload
const decoded = jwt.decode(token); // {secret: 'Password123'} — видно!
// НИКОГДА не кладите в JWT чувствительные данные (пароли, кредитные карты)
3. Key rotation сложный
Если приватный ключ скомпрометирован, все issued токены скомпрометированы:
// Решение: использовать JWKS (JSON Web Key Set)
const keys = await fetch('https://example.com/.well-known/jwks.json');
// Сервер может кэшировать и обновлять ключи
Access Token + Refresh Token паттерн
В production я всегда использую двухуровневую систему:
@Post('/auth/login')
async login(@Body() credentials: LoginDto) {
const user = await this.validateUser(credentials);
// Short-lived access token (15 minutes)
const accessToken = this.jwtService.sign(
{ sub: user.id, email: user.email },
{ expiresIn: '15m' }
);
// Long-lived refresh token (7 days) - stored in DB
const refreshToken = this.jwtService.sign(
{ sub: user.id, tokenVersion: user.tokenVersion },
{ expiresIn: '7d' }
);
// Сохраняем refresh token в БД
await this.refreshTokenService.save(user.id, refreshToken);
return {
accessToken,
refreshToken,
expiresIn: 15 * 60 // 15 minutes in seconds
};
}
@Post('/auth/refresh')
async refresh(@Body() body: { refreshToken: string }) {
// Верифицируем refresh token
const decoded = this.jwtService.verify(body.refreshToken);
// Проверяем что токен еще в БД
const isValid = await this.refreshTokenService.verify(
decoded.sub,
body.refreshToken
);
if (!isValid) {
throw new UnauthorizedException('Invalid refresh token');
}
// Выдаем новый access token
const accessToken = this.jwtService.sign(
{ sub: decoded.sub },
{ expiresIn: '15m' }
);
return { accessToken };
}
Best Practices
1. Use HTTPS только
// ❌ Никогда не отправляйте JWT по HTTP
// ✅ Всегда HTTPS для защиты от man-in-the-middle
2. Хранение токена на frontend
// ❌ localStorage (уязвимо для XSS)
localStorage.setItem('token', token);
// ✅ HttpOnly cookie (защищено от XSS)
response.cookie('token', token, {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
});
3. Signature algorithm
// ❌ Плохо: используют RS256 без причины
const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
// ✅ Хорошо: HS256 для простых случаев
const token = jwt.sign(payload, secret, { algorithm: 'HS256' });
// ✅ RS256/ES256 когда нужна асимметричная криптография
4. Claim validation
// Всегда валидируйте все claims
const decoded = jwt.verify(token, secret);
if (!decoded.sub || !decoded.email) {
throw new UnauthorizedException('Invalid token claims');
}
if (decoded.exp < Date.now() / 1000) {
throw new UnauthorizedException('Token expired');
}
Real-world implementation
// jwt.strategy.ts
import { Strategy } from 'passport-jwt';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: any) {
// Passport автоматически верифицирует подпись
// validate() вызывается только если подпись валидна
const user = await this.userService.findById(payload.sub);
if (!user) {
throw new UnauthorizedException();
}
return user; // Доступна как req.user
}
}
// auth.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// controller.ts
@Controller('api/v1/users')
export class UserController {
@UseGuards(JwtAuthGuard)
@Get('/me')
getProfile(@Request() req) {
return req.user; // User из validate()
}
}
Вывод
JWT — это powerful tool для modern аутентификации, особенно в микросервисной архитектуре. Главное — правильно используйте:
- Access + Refresh tokens
- HTTPS
- HttpOnly cookies
- Валидация всех claims
- Никогда не кладите sensitive данные
Это foundation современной backend безопасности.