В чём разница между 401 и 403
Разница между статусами 401 Unauthorized и 403 Forbidden заключается в фундаментальном разделении двух понятий безопасности: Аутентификации (Authentication) и Авторизации (Autho...
🟢 Junior Level
Разница между статусами 401 Unauthorized и 403 Forbidden заключается в фундаментальном разделении двух понятий безопасности: Аутентификации (Authentication) и Авторизации (Authorization).
Простая формулировка
401 Unauthorized(Проблема аутентификации) — «Кто вы? Я вас не знаю»: Сервер не смог установить личность клиента. Запрос отправлен без учетных данных или с недействительными/просроченными учетными данными (токен, логин/пароль).403 Forbidden(Проблема авторизации) — «Я знаю, кто вы, но вам сюда нельзя»: Личность клиента успешно установлена (пользователь вошел в систему), однако у него нет прав (полномочий, ролей) для доступа к данному ресурсу.
Сравнительная аналогия из жизни
- Случай 401: Вы пытаетесь зайти в офисный центр, но забыли пропуск дома. Охранник на входе останавливает вас: «Покажите документ, подтверждающий вашу личность».
- Случай 403: Вы показали охраннику пропуск сотрудника бухгалтерии, но пытаетесь открыть дверь в серверную комнату. Охранник вас узнал, но заявляет: «У вас нет допуска в серверную».
Поведение клиента
- При получении 401: Клиентское приложение пытается обновить сессию (например, через Refresh Token) или перенаправляет пользователя на форму входа.
- При получении 403: Повторный логин с теми же учетными данными бесполезен. Приложение показывает сообщение: «Доступ ограничен администратором».
🟡 Middle Level
Терминологическая путаница в стандарте HTTP
В спецификации HTTP присутствует исторический лингвистический дефект:
- Код назван
401 Unauthorized, хотя фактически он означаетUnauthenticated(отсутствие подтвержденной подлинности). - Настоящая авторизация (проверка прав) выражается кодом
403 Forbidden.
Протокольные требования (RFC 9110)
| Критерий | 401 Unauthorized (RFC 9110 §15.5.2) |
403 Forbidden (RFC 9110 §15.5.4) |
|---|---|---|
| Причина | Отсутствуют или неверны учетные данные | Недостаточно прав, прав доступа или ограничение IP/CORS |
| Обязательный заголовок | Обязан содержать заголовок WWW-Authenticate |
Заголовок WWW-Authenticate не передается |
| Повтор запроса | Имеет смысл после передачи валидных учетных данных | Повтор запроса с теми же данными запрещен |
| OAuth 2.0 / JWT | Токен отсутствует, поврежден или истек (Expired) | Токен валиден, но не содержит нужного scope или роли |
Пример заголовка WWW-Authenticate при 401:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired"
Content-Type: application/problem+json
Архитектура Spring Security 6.x
В экосистеме Spring Security разделение на 401 и 403 заложено на уровне базовых интерфейсов цепочки фильтров (SecurityFilterChain):
HTTP Request
│
┌────────────┴────────────┐
▼ ▼
Неаутентифицирован? Аутентифицирован?
(AuthenticationException) (AccessDeniedException)
│ │
▼ ▼
AuthenticationEntryPoint AccessDeniedHandler
│ │
▼ ▼
HTTP 401 Unauthorized HTTP 403 Forbidden
AuthenticationEntryPoint: Перехватывает исключенияAuthenticationException(анонимный пользователь пытается получить защищенный ресурс).AccessDeniedHandler: ПерехватываетAccessDeniedException(пользователь вошел, но метод аннотирован@PreAuthorize("hasRole('ADMIN')"), а у пользователя только рольROLE_USER).- CSRF Token: Ошибка проверки CSRF токена в Spring Security по умолчанию генерирует 403 Forbidden, а не 401!
🔴 Senior Level
Безопасность vs Утечка метаданных (IDOR и Information Disclosure)
На уровне архитектуры Senior инженер обязан учитывать вектор атак User / Resource Enumeration: Если злоумышленник перебирает идентификаторы закрытых документов:
- Запрос
GET /api/v1/documents/999возвращает404 Not Found. - Запрос
GET /api/v1/documents/1000возвращает403 Forbidden.
Ответ 403 Forbidden мгновенно сообщает злоумышленнику: документ с ID 1000 реально существует в системе, и к нему можно подбирать векторы атаки (IDOR).
Senior-паттерн (Security by Obscurity / Silent Not Found):
В мульти-тенантных системах (Multi-tenant SaaS) и приватных API, если у текущего пользователя нет прав на ресурс другой организации, система возвращает 404 Not Found, а не 403 Forbidden. Это гарантирует, что чужой ресурс выглядит несуществующим.
Production-ready настройка Spring Security 6 + RFC 9457 ProblemDetail
package com.example.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import java.io.IOException;
import java.net.URI;
import java.time.Instant;
@Configuration
@EnableMethodSecurity
public class SecurityConfiguration {
private final ObjectMapper objectMapper;
public SecurityConfiguration(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/public/*").permitAll()
.requestMatchers("/api/v1/admin/*").hasRole("ADMIN")
.anyRequest().authenticated()
)
.exceptionHandling(ex -> ex
// Точка входа для неаутентифицированных (401)
.authenticationEntryPoint(customAuthenticationEntryPoint())
// Обработчик для отказа в доступе (403)
.accessDeniedHandler(customAccessDeniedHandler())
);
return http.build();
}
@Bean
public AuthenticationEntryPoint customAuthenticationEntryPoint() {
return (request, response, authException) -> {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.UNAUTHORIZED,
"Full authentication is required to access this resource"
);
problem.setTitle("Unauthorized");
problem.setType(URI.create("https://api.example.com/errors/unauthorized"));
problem.setProperty("timestamp", Instant.now());
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
response.setHeader("WWW-Authenticate", "Bearer realm=\"api\"");
objectMapper.writeValue(response.getOutputStream(), problem);
};
}
@Bean
public AccessDeniedHandler customAccessDeniedHandler() {
return (request, response, accessDeniedException) -> {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.FORBIDDEN,
"You do not have the required permissions to perform this operation"
);
problem.setTitle("Forbidden");
problem.setType(URI.create("https://api.example.com/errors/forbidden"));
problem.setProperty("timestamp", Instant.now());
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
objectMapper.writeValue(response.getOutputStream(), problem);
};
}
}
Различие OAuth 2.0 Scopes vs Application Roles при 403 ошибках
В распределенных микросервисах с JWT токенами важно различать причину получения 403:
- Недостаточно Role: Пользователь аутентифицирован как оператор (
ROLE_OPERATOR), но эндпоинт требуетROLE_SUPERVISOR. - Недостаточно OAuth Scope: Стороннее приложение аутентифицировалось токеном от имени пользователя, но пользователь при выдаче согласия (Consent Screen) предоставил доступ только на
scope: "read". Попытка отправкиPOSTвызовет отказ поinsufficient_scopeсо статусом 403.
🎯 Шпаргалка для интервью
30-секундный ответ
401 Unauthorizedсигнализирует о сбое аутентификации: сервер не знает клиента. Токен отсутствует, некорректен или истек. По спецификации RFC 9110 ответ обязан содержать заголовокWWW-Authenticate.403 Forbiddenсигнализирует о сбое авторизации: сервер успешно установил личность клиента, но у клиента недостаточно прав, ролей или скоупов для выполнения действия. Повторный вызов с теми же учетными данными бесполезен.- В Spring Security за 401 отвечает
AuthenticationEntryPoint, а за 403 —AccessDeniedHandler.- В целях безопасности (защита от перебора IDOR) вместо 403 часто возвращают 404 Not Found, чтобы скрыть сам факт существования закрытого ресурса.
4 каверзных вопроса с ответами
-
Какой код ответа должен вернуть сервер, если срок действия JWT токена истек (Expired)? Ответ: Строго
401 Unauthorized(с заголовкомWWW-Authenticate: Bearer error="invalid_token", error_description="The token expired"). Ответ 403 был бы архитектурной ошибкой, так как мобильный или веб-клиент использует именно 401 для прозрачного вызова Refresh Token Flow. -
Почему по спецификации RFC 9110 код называется “Unauthorized”, если он про аутентификацию? Ответ: Это историческая терминологическая неточность, допущенная на заре создания HTTP в 1990-х годах. Семантически 401 означает
Unauthenticated, а истинная проверка авторизации (прав) возложена на403 Forbidden. -
В каком случае при наличии валидной аутентификации администратора запрос завершится кодом 403 Forbidden в Spring Security? Ответ: Например, при отсутствии или невалидности CSRF токена (в stateful приложениях), при ограничении доступа по IP (IP filtering), или если JWT токен выдан стороннему приложению без необходимого OAuth
scope. -
Зачем в архитектуре Multi-tenant SaaS намеренно заменять 403 Forbidden на 404 Not Found? Ответ: Для предотвращения утечки информации (Information Disclosure) и атак класса IDOR. Код 403 подтверждает злоумышленнику, что закрытый ресурс существует. Код 404 скрывает само присутствие ресурса в системе.
Красные флаги (чего категорически нельзя говорить)
- ❌ «401 и 403 — это одно и то же, разницы нет» (один про аутентификацию, второй про права доступа).
- ❌ «Если JWT токен истек, нужно возвращать 403 Forbidden» (ломает логику автоматического обновления токенов в HTTP-клиентах).
- ❌ «Если у пользователя 403 ошибка, ему просто нужно еще раз ввести логин и пароль» (повторный логин не дает новых прав доступа).
- ❌ «Ответ 401 не требует никаких обязательных заголовков» (по RFC 9110 заголовок
WWW-Authenticateстрого обязателен).