Коротко: ошибка 401 Unauthorized, то есть код HTTP 401 («не авторизован»), значит, что сервер хочет, чтобы вы прошли аутентификацию, а в запросе не было действительных учётных данных: их не было совсем, они не того вида, истекли или были отвергнуты. Заголовок ответа WWW-Authenticate называет схему, которую ждёт сервер, например Basic или Bearer. Войдите заново или отправьте свежий токен в этом формате. Если вы уже аутентифицированы и всё равно получаете отказ, перед вами 403, а это другая проблема.
curl -v https://example.com/admin/ 2>&1 | grep -iE '^> authorization|^< HTTP|^< www-authenticate'
# < HTTP/1.1 401 Unauthorized
# < WWW-Authenticate: Basic realm="Staff only"
#
# В выводе нет строки "> Authorization:"? Значит, учётные данные вообще не ушли с вашей машины.Чем 401 отличается от 403?
Несмотря на название, 401 касается аутентификации (кто вы), а 403 авторизации (что вам можно). RFC 9110 требует, чтобы каждый ответ 401 содержал заголовок WWW-Authenticate хотя бы с одним вызовом (challenge), а если в запросе были учётные данные, 401 значит, что их отвергли. 403 значит, что сервер понял запрос и отказывает в нём; повтор с теми же учётными данными ответа не изменит.
| Вопрос | 401 Unauthorized | 403 Forbidden |
|---|---|---|
| Что говорит сервер | Кто вы? Действительных учётных данных нет | Я знаю, кто вы, или это неважно: нет |
| Обязательный заголовок | WWW-Authenticate с ожидаемой схемой | Нет |
| Поможет ли войти заново? | Да, с действительными учётными данными | Нет: нужны другие права, другая учётная запись или изменение на сервере |
| Типичная причина в nginx | auth_basic без правильных пользователя и пароля | Правила deny, права на файлы, нет индексного файла |
| API с OAuth Bearer (RFC 6750) | invalid_token: токен истёк, отозван или искажён | insufficient_scope: токен действителен, но прав не хватает |
407 Proxy Authentication Required означает то же самое для прокси между вами и сервером, с заголовком Proxy-Authenticate. Если у вас 403, эту сторону разбирает статья о 403 Forbidden в nginx.
Что говорит заголовок WWW-Authenticate?
WWW-Authenticate: Basic realm="Staff only"
WWW-Authenticate: Bearer realm="api"
WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="The access token expired"
WWW-Authenticate: Negotiate- Basic: имя пользователя и пароль в base64, в
Authorization: Basic …. Именно эта схема заставляет браузер показать окно входа. - Bearer без ошибки: вы вообще не отправили токен. RFC 6750 велит серверам в этом случае не сообщать подробностей.
- Bearer с
error="invalid_token": токен истёк, отозван или искажён. Получите новый. Ошибкаinsufficient_scopeотносится к 403, аinvalid_requestк 400. - Negotiate или NTLM: встроенная аутентификация Windows, как у IIS в домене. Первая 401, за которой следует повтор с токеном, — это нормальное рукопожатие, а не сбой.
- Заголовка нет вовсе: сервер нарушает спецификацию, как делают многие API. Прочитайте тело ответа: обычно там написано, что не так.
Почему возникает ошибка 401? Причины от частых к редким
- 1Пригодные учётные данные не дошли до сервера
В выводе
curl -vнайдите строку> Authorization:. Если её нет или она выглядит неправильно, виноват запрос, а не сервер:учётные данные в curl echo "${#TOKEN}" # 0 значит, что $TOKEN в этой оболочке пуст: уходит "Bearer " и больше ничего curl -v -H "Authorization: Bearer $TOKEN" https://api.example.com/me curl -v --oauth2-bearer "$TOKEN" https://api.example.com/me # тот же заголовок curl -v -u 'alice:pa$$word' https://example.com/admin/ # одинарные кавычки оставляют $ как естьОбычные промахи: не то слово схемы (
Bearer,TokenиBotне взаимозаменяемы; токены ботов Discord идут вAuthorization: Bot …), вставленные в токен кавычки или перенос строки, а также пароль с$внутри двойных кавычек, который раскрывает оболочка. Собирать заголовок Basic черезechoтоже нельзя: перевод строки становится частью пароля:заголовок Basic вручную echo 'alice:secret' | base64 # неверно: кодирует перевод строки в конце, 401 printf '%s' 'alice:secret' | base64 # верно curl -u 'alice:secret' https://example.com/admin/ # проще всего: пусть curl соберёт его сам - 2Токен истёк, отозван или выдан для другого места
У ключей API и токенов есть срок жизни, после утечки их меняют, и часто они привязаны к одному окружению: тестовый ключ, отправленный в боевой API, или ключ одного региона либо аккаунта, предъявленный другому, получает 401. JWT несёт собственный срок действия, и прочитать его можно локально. Полезная нагрузка лишь закодирована в base64url, а не зашифрована, так что вставлять боевой токен на какой-либо сайт незачем:
Debian · Ubuntu python3 -c 'import sys,json,base64; p=sys.argv[1].split(".")[1]; p+="="*(-len(p)%4); print(json.dumps(json.loads(base64.urlsafe_b64decode(p)), indent=2))' "$TOKEN" date -u -d @1799999999 # перевести число из "exp" или "nbf" в дату timedatectl # ищите "System clock synchronized: yes"Часы важны с обеих сторон. Сервер, у которого время сбито на несколько минут, считает свежие токены ещё не действительными (
nbf) или уже истёкшими (exp), поэтому 401, которую получает только одна машина, часто означает, что на ней сломана синхронизация времени. - 3Учётные данные потерялись по дороге
- Их отбросил редирект. curl отправляет
-u,--oauth2-bearerи вручную заданный заголовокAuthorizationтолько на тот origin, к которому вы обратились; редирект, который меняет хост, схему или порт, их отбрасывает. Поэтомуcurl -L -u alice:… http://example.com/api, перенаправленный наhttps://, приходит без учётных данных и получает 401.fetch()в браузерах тоже отбрасываетAuthorizationпри редиректах на другой origin. Обращайтесь сразу к конечному адресуhttps://, а--location-trustedиспользуйте, только если доверяете цели редиректа. - nginx отбросил заголовок с подчёркиванием. Нестандартный заголовок вроде
X_API_KEYпо умолчанию выбрасывается (underscores_in_headers off), так что приложение его не видит и отвечает 401. Переименуйте его вX-API-Key. - Apache не передал его в PHP. С PHP-FPM или CGI Apache не показывает скриптам заголовок
Authorization, пока вы не зададитеCGIPassAuth On(Apache 2.4.13 и новее) в блоке<Directory>или в.htaccess. - Два слоя претендуют на один и тот же заголовок. В запросе только один заголовок
Authorization. Если перед приложением, которое ждёт собственный Bearer-токен, стоит nginx сauth_basic, этот заголовок удовлетворит кого-то одного, но никогда обоих. Защитите приложение списком разрешённых IP или перенесите одни из учётных данных в другой заголовок.
Кроме того, nginx по умолчанию передаёт учётные данные Basic бэкенду. Если приложение не должно видеть пароль сотрудников, очистите заголовок в этом location:
nginx · не передавать учётные данные Basic location /internal/ { auth_basic "Staff only"; auth_basic_user_file /etc/nginx/.htpasswd; proxy_set_header Authorization ""; # пустое значение удаляет заголовок proxy_pass http://127.0.0.1:3000; }proxy_set_headerв location заменяет все такие директивы, которые иначе унаследовались бы из блока server, поэтому повторите там свои строкиHostиX-Forwarded-*. - Их отбросил редирект. curl отправляет
- 4nginx auth_basic отвергает пользователя
Лог ошибок называет причину каждого отказа во входе:
/var/log/nginx/error.log user "alice": password mismatch user "carol" was not found in "/etc/nginx/.htpasswd" no user/password was provided for basic authentication (уровень info)Последняя строка соответствует первой 401, которую получает любой браузер перед тем, как показать окно входа, так что это нормально. Файл паролей создаётся так:
Debian · Ubuntu sudo apt install -y apache2-utils sudo htpasswd -c -B /etc/nginx/.htpasswd alice # -c только для ПЕРВОГО пользователя: он опустошает существующий файл sudo htpasswd -B /etc/nginx/.htpasswd bob # каждый следующий пользователь, без -c sudo chown root:www-data /etc/nginx/.htpasswd sudo chmod 640 /etc/nginx/.htpasswd # без apache2-utils: хеш SHA-512 crypt, его nginx тоже принимает sudo -v printf 'carol:%s\n' "$(openssl passwd -6)" | sudo tee -a /etc/nginx/.htpasswd-Bсохраняет хеш bcrypt. nginx проверяет такие хеши системной функциейcrypt(), а в Debian 12 она понимает и bcrypt, и SHA-512. Мы проверили это на Debian 12 (libcrypt1 4.4.33), вызвав ту же функцию из Python 3.11.2; сам nginx при этом не запускали. Если ваша система bcrypt не умеет, уберите-B, и htpasswd запишет свой стандартный хеш на основе MD5. Затем закройте сайт паролем и откройте без пароля пути, до которых машины должны добираться сами:в вашем блоке server auth_basic "Staff only"; auth_basic_user_file /etc/nginx/.htpasswd; location /.well-known/acme-challenge/ { auth_basic off; # проверка certbot через webroot не умеет вводить пароль root /var/www/html; }применяем sudo nginx -t && sudo systemctl reload nginxБраузеры кешируют учётные данные Basic на время сессии и после смены пароля продолжают отправлять старые, поэтому проверяйте в приватном окне. Basic-аутентификация лишь кодирует данные в base64, так что используйте её только поверх HTTPS; как это настроить, показывает статья об обратном прокси и SSL (англ.).
- 5Браузер сообщает об ошибке CORS, а 401 получил OPTIONS
Перед кросс-доменным запросом с заголовком
Authorizationбраузер отправляет предварительный запросOPTIONS(preflight). Preflight никогда не несёт учётных данных и должен получить ответ 2xx. Если ваш сервер требует аутентификацию и дляOPTIONS, preflight получает 401, браузер блокирует настоящий запрос, а в консоли видна только ошибка CORS (англ.). ПропускайтеOPTIONSбез аутентификации:nginx · аутентификация для всех методов, кроме OPTIONS location /api/ { limit_except OPTIONS { auth_basic "API"; auth_basic_user_file /etc/nginx/.htpasswd; } proxy_pass http://127.0.0.1:3000; # на preflight приложение отвечает своими заголовками CORS }В приложении правило то же: middleware CORS должен срабатывать раньше middleware аутентификации.
Что значит 401 от Discord или Telegram?
Для бота 401 почти всегда значит, что токен неверный или был сброшен. На неверный токен бота Telegram отвечает так:
curl -s https://api.telegram.org/bot<TOKEN>/getMe
# {"ok":false,"error_code":401,"description":"Unauthorized"}Discord ждёт Authorization: Bot <token>, и библиотеки выставляют его за вас. Не давайте боту с мёртвым токеном перезапускаться без паузы, раз за разом: Discord засчитывает ответы 401 в свой лимит в 10 000 недопустимых запросов за 10 минут и затем на время блокирует IP, и один плохой токен превращается в проблему 429 Too Many Requests. Задайте юниту systemd RestartSec=10 или больше и исправьте токен в его файле окружения. Тарифы, на которых бот работает такой службой, перечислены на странице хостинга Discord-ботов.
Как сделать, чтобы ошибки 401 не возвращались?
- Обращайтесь сразу к конечному адресу
https://; никогда не рассчитывайте, что учётные данные донесёт редирект. - Храните токены в файле окружения, который может читать только пользователь службы (
chmod 600), а не в коде или репозитории. - Обновляйте токены до истечения срока и держите часы синхронизированными на каждой машине.
- В именах собственных заголовков используйте дефисы, никогда не подчёркивания.
- В скриптах считайте 401 сигналом остановиться и исправить. Повтор с теми же учётными данными лишь добавляет неудачных попыток.
- Блокируйте адреса, которые подбирают пароли, стандартным jail
nginx-http-authиз fail2ban: он читает те же строки лога ошибок, что показаны выше.
Чаще всего auth_basic ставят паролем перед инструментом администрирования, например перед phpMyAdmin (англ.), и все команды из этой статьи без изменений работают на Debian VPS или Ubuntu VPS.
Частые вопросы
Что значит ошибка 401 Unauthorized?
Код HTTP 401 значит, что сервер требует аутентификацию, а запрос пришёл без действительных учётных данных: их не было совсем, они не того вида, истекли или были отвергнуты. В заголовке ответа WWW-Authenticate сервер называет схему, которую ждёт, например Basic или Bearer. Войдите заново или отправьте свежий токен в этом формате.
Чем ошибка 401 отличается от 403?
401 значит, что сервер не знает, кто вы: учётных данных нет, они истекли или неверны, и в заголовке WWW-Authenticate он обязан сказать, как пройти аутентификацию. 403 значит, что сервер понял запрос и отказывает в нём, поэтому повторный вход с той же учётной записью не поможет: нужны другие права или изменение на сервере.
Как узнать, почему запрос получает 401?
Выполните запрос с curl -v. Строки, которые начинаются с «>», покажут, ушёл ли заголовок Authorization на самом деле, а строка «< WWW-Authenticate» покажет, какую схему ждёт сервер, и для токенов Bearer часто ошибку вроде invalid_token. На сервере nginx с auth_basic лог ошибок называет пользователя и пишет «password mismatch» или «was not found».