Главная / Статьи / Аутентификация против авторизации: где каждая из них должна находиться в вашем коде

Аутентификация против авторизации: где каждая из них должна находиться в вашем коде

Узнайте, в чем различия между аутентификацией и авторизацией с точки зрения реализации, кодов состояния HTTP и архитектуры системы, чтобы избежать распространённых проблем безопасности.

1335 слов

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

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

Аутентификация: проверка идентичности

Аутентификация — это этап, на котором система подтверждает, что пользователь действительно является тем, за кого себя выдает. Она отвечает на один вопрос: подтверждается ли эта идентичность?

Это однократное событие, обычно происходящее при входе, и в результате формируется какой-либо тип учетных данных — сессия, токен или куки — который позволяет остальной части приложения доверять вызывающему без необходимости повторной проверки пароля при каждом запросе.

Типичные способы реализации аутентификации включают:

  • Аутентификация на основе пароля — предоставленные учетные данные сравниваются с хешированным значением, сохраненным в базе данных
  • Аутентификация на основе токена (JWT) — подписанный токен генерируется после успешного входа, затем проверяется при каждом последующем запросе
  • OAuth 2.0 / OpenID Connect — проверка личности передается внешнему поставщику, такому как Google или GitHub
  • Ключи API — фиксированная секретная строка, идентифицирующая конкретного клиента или сервис
  • Многократная аутентификация (MFA) — дополнительный шаг проверки (одноразовый код, приложение-аутентификатор), добавляемый к паролю
  • Вот как выглядит стандартный процесс аутентификации:

    POST /api/login
    Content-Type: application/json
    
    {
      "email": "user@example.com",
      "password": "hashed_and_verified_serverside"
    }
    
    Response: 200 OK
    {
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    }
    

    Сервер проверяет предоставленные учетные данные, и если они подтверждаются, возвращает токен. С этого момента этот токен служит доказательством личности пользователя при каждом запросе:

    GET /api/profile
    Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
    

    Промежуточный компонент, отвечающий за проверку токена, обычно выглядит примерно так:

    function authenticate(req, res, next) {
      const token = req.headers.authorization?.split(' ')[1];
      if (!token) return res.status(401).json({ error: 'No token provided' });
    
      jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
        if (err) return res.status(401).json({ error: 'Invalid or expired token' });
        req.user = decoded;
        next();
      });
    }
    

    Обратите внимание на то, что этот мидлвэрь намеренно упускает. Он ни в коем случае не определяет, что разрешено пользователю делать. Он просто проверяет, является ли токен действительным, и присоединяет расшифрованный пейлоад к объекту запроса. Больше ничего не входит в обязанности механизма аутентификации.

    Авторизация: проверка прав

    Авторизация вступает в силу только после успешной аутентификации. Она отвечает на совершенно другой вопрос: разрешено ли этому проверенному пользователю выполнять конкретное действие?

    Некоторые распространенные подходы к организации авторизации:

    • Контроль доступа на основе ролей (RBAC) — права привязываются к таким ролям, как admin, editor или viewer
  • Контроль доступа на основе атрибутов (ABAC) — решения о доступе принимаются на основе таких атрибутов, как отдел, регион, владелец ресурса или даже время суток
  • Списки контроля доступа (ACL) — разрешения определяются явно для каждого пользователя и каждого ресурса
  • Области действия (OAuth) — сам токен содержит информацию о конкретных операциях, разрешённых к выполнению, таких как read:messages или write:files
  • Типичная проверка авторизации выполняется после того, как запрос уже прошёл аутентификацию:

    function authorize(requiredRole) {
      return (req, res, next) => {
        if (req.user.role !== requiredRole) {
          return res.status(403).json({ error: 'Insufficient permissions' });
        }
        next();
      };
    }
    
    app.get('/api/admin/dashboard', authenticate, authorize('admin'), (req, res) => {
      res.json({ data: 'sensitive admin data' });
    });
    

    Это две разные функции промежуточного программирования, обрабатывающие две разные задачи и выполняемые в фиксированном порядке: authenticate определяет, кто является заявителем, а authorize решает, разрешено ли этому заявителю продолжить. Запрос может пройти первую проверку, но всё равно быть отклонённым второй — причём отказ здесь не связан с идентификацией.

    Коды состояния, фактически отражающие эту разницу

    Именно здесь теоретическое различие превращается во что-то, что можно буквально проверить в коде.

    401 Unauthorized — название вводит в заблуждение, но этот код на самом деле указывает на проблему с аутентификацией. Это означает, что сервер вообще не получил учетных данных или полученные им данные (включая токен) недействительны или истекли. На этом этапе сервер не имеет никакой уверенности в том, кто отправляет запрос.

    403 Forbidden — этот код указывает на проблему с авторизацией. Сервер уже определил, кто является заявителем; он просто не позволяет этому пользователю выполнить запрошенную операцию или получить доступ к требуемому ресурсу.

    // Authentication failure — identity unknown or invalid
    res.status(401).json({ error: 'Invalid credentials' });
    
    // Authorization failure — identity known, permission denied
    res.status(403).json({ error: 'You do not have access to this resource' });
    

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

    Где это встречается в реальных архитектурах

    В заголовках JWT часто смешиваются эти аспекты без ведома авторов. Токен предназначен для хранения данных идентификации — таких как sub, email, iat и exp. Необходимо ли также включать в него данные о правах, например role или scope, — это дизайнерское решение, зависящее от конкретной системы. Хранение ролей непосредственно в токене удобно и позволяет избежать дополнительных поисков, но это имеет свою цену: любые изменения в правах пользователя вступят в силу только после пересоздания токена. Это настоящий компромисс, а не упрощение без негативных последствий.

    {
      "sub": "user_12345",
      "email": "user@example.com",
      "role": "editor",
      "iat": 1710000000,
      "exp": 1710003600
    }
    

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

    В архитектурах на основе микросервисов авторизация обычно централизуется — она обрабатывается один раз гейтвеем или поставщиком идентификации, таким как Auth0, Keycloak или AWS Cognito, который выдает токен. Однако авторизация обычно оставляется на усмотрение отдельных сервисов, которые читают информацию из этого токена и применяют собственные правила. В результате один и тот же этап авторизации может интерпретироваться по-разному десятью отдельными сервисами, каждый из которых самостоятельно определяет, что разрешено данному пользователю делать.

    Правило, предотвращающее большинство проблем с безопасностью

    Аутентификация должна выполняться до авторизации, и эти два процесса никогда не должны сливаться в одну общую проверку.

    // Correct: sequential, separate concerns
    app.post('/api/posts/:id/delete', authenticate, authorize('editor'), deleteHandler);
    
    // Wrong: conflating identity with permission in one step
    app.post('/api/posts/:id/delete', (req, res) => {
      if (req.headers.authorization === 'valid-token-and-also-admin') {
        // fragile, unscalable, and impossible to audit correctly
      }
    });
    

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

    Оба слоя должны быть надежными, применяться в правильном порядке и рассматриваться как отдельные компоненты. В этом и заключается вся разница — и как только это будет должным образом отражено в кодах состояния, цепочке промежуточных компонентов и способе создания токенов, целый класс ошибок безопасности просто исчезает.

    Связанные материалы

  • Переосмысление состояния React: где на самом деле должны храниться ваши данные — В этой статье объясняется, как уменьшить количество ошибок в React путем размещения состояния в URL, DOM или вычисляемых значениях вместо чрезмерного использования useState.