Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Что такое CSRF токен
Определение
CSRF (Cross-Site Request Forgery) токен — это уникальный, непредсказуемый и невоспроизводимый код, который предотвращает атаки типа CSRF (подделка межсайтовых запросов).
Это один из наиболее эффективных механизмов защиты веб-приложений от атак, при которых злоумышленник заставляет аутентифицированного пользователя выполнить нежелательные действия на другом сайте.
Проблема: CSRF атака
Сценарий без защиты:
- Пользователь залогинился на
bank.com - Пользователь посещает
evil.com(не закрыв вкладку банка) - На
evil.comесть скрытый запрос:
<!-- На зло
умышленническом сайте evil.com -->
<img src="http://bank.com/api/transfer?to=attacker&amount=1000" style="display:none;" />
- Браузер автоматически отправляет этот запрос с cookies пользователя
- Банк трансферит деньги, думая, что запрос от самого пользователя
Решение: CSRF токен
CSRF токен гарантирует, что запрос пришёл со своего сайта, а не с чужого. Вот как это работает:
Шаг 1: Сервер генерирует токен
@RestController
@RequestMapping("/api")
public class CsrfController {
private final CsrfTokenRepository tokenRepository;
@GetMapping("/csrf-token")
public ResponseEntity<CsrfTokenDTO> getCsrfToken(
HttpServletRequest request,
HttpSession session
) {
// Генерируем уникальный CSRF токен
String csrfToken = UUID.randomUUID().toString();
// Сохраняем в сессии
session.setAttribute("_csrf_token", csrfToken);
return ResponseEntity.ok(
new CsrfTokenDTO(csrfToken)
);
}
}
Шаг 2: Клиент отправляет форму с токеном
<!-- Форма на сайте bank.com -->
<form action="/api/transfer" method="POST">
<!-- Скрытое поле с CSRF токеном -->
<input type="hidden" name="_csrf" value="abc123xyz..." />
<input type="text" name="to" placeholder="Получатель" />
<input type="number" name="amount" placeholder="Сумма" />
<button type="submit">Отправить</button>
</form>
Шаг 3: Сервер проверяет токен
@PostMapping("/transfer")
public ResponseEntity<?> transferMoney(
@RequestParam String to,
@RequestParam BigDecimal amount,
@RequestParam("_csrf") String csrfToken,
HttpSession session
) {
// Проверяем, что токен совпадает с сохранённым в сессии
String expectedToken = (String) session.getAttribute("_csrf_token");
if (!csrfToken.equals(expectedToken)) {
// CSRF атака!
return ResponseEntity.status(403).body("CSRF validation failed");
}
// Выполняем операцию
transferService.transfer(to, amount);
return ResponseEntity.ok("Transfer successful");
}
Почему токен защищает
Злоумышленник не может угадать токен:
// На evil.com он пытается отправить запрос
// <img src="http://bank.com/api/transfer?to=attacker&amount=1000&_csrf=??? " />
// Но он НЕ знает значение токена!
Токен уникален для каждого пользователя и сессии:
public class CsrfTokenGenerator {
public String generateToken() {
// Генерируем 256-битный токен
SecureRandom random = new SecureRandom();
byte[] tokenBytes = new byte[32];
random.nextBytes(tokenBytes);
// Кодируем в hex для удобства
return HexFormat.of().formatHex(tokenBytes);
}
}
Реализация в Spring Security
Spring Security автоматически управляет CSRF токенами:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/public/**") // Исключения
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
Передача токена в Headers (для AJAX)
Получение токена:
// В HTML
<meta name="_csrf" content="{{ csrfToken }}" />
<meta name="_csrf_header" content="X-CSRF-TOKEN" />
// JavaScript
const token = document.querySelector('meta[name="_csrf"]').getAttribute('content');
const header = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
Отправка запроса:
fetch('/api/transfer', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': token,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: 'recipient',
amount: 1000
})
});
Проверка на сервере:
@PostMapping("/transfer")
public ResponseEntity<?> transfer(
@RequestBody TransferRequest request,
@RequestHeader("X-CSRF-TOKEN") String csrfToken,
HttpSession session
) {
String expectedToken = (String) session.getAttribute("_csrf_token");
if (!csrfToken.equals(expectedToken)) {
return ResponseEntity.status(403).build();
}
// Выполняем операцию
return ResponseEntity.ok(transferService.transfer(request));
}
Типы хранения CSRF токена
| Тип | Где хранится | Плюсы | Минусы |
|---|---|---|---|
| Session | В сессии сервера | Безопасно | Требует сессию |
| Cookie | В cookie браузера | Не требует сессию | Может быть украден XSS |
| Token store | В памяти на сервере | Масштабируемо | Сложнее |
Лучшие практики
- Генерируй токен на каждый запрос (или на сессию)
- Никогда не передавай токен в URL (только в headers или body)
- Используй HTTPS для передачи токена
- Исключай безопасные методы (GET, HEAD) из проверки
- Устанавливай краткое время жизни для токена
- Комбинируй с другими защитами (SameSite cookies, CSP)
SameSite Cookies — современная защита
@Configuration
public class CookieConfig {
@Bean
public DefaultHttpFirewall httpFirewall() {
DefaultHttpFirewall firewall = new DefaultHttpFirewall();
// SameSite автоматически защищает от CSRF
return firewall;
}
}
Одновременно с установкой:
response.addHeader(
"Set-Cookie",
"SESSION=abc123; Path=/; HttpOnly; Secure; SameSite=Strict"
);
Заключение
CSRF токен — это критически важный механизм защиты веб-приложений. Он гарантирует, что все состояние-меняющие операции (POST, PUT, DELETE) выполняются только с разрешения владельца сеанса. Современные фреймворки вроде Spring Security управляют CSRF автоматически, но понимание механизма работы необходимо каждому разработчику для безопасной разработки приложений.