
Я часто путаю понятия авторизации и аутентификации между собой, поэтому решил создать материал, который закрепил бы эти понятия через какой-то практический опыт.
Термины аутентификации и авторизации для меня достаточно расплывчаты, если представить их в контексте практической бэкенд разработки. Я не совсем понимал логику работы аутентификации в фреймворках. В добавок эти концепции можно легко перепутать между собой. При общении с коллегами это было достаточно часто.
В данном случае под бэкенд разработкой я подразумеваю создание разного рода API с использованием протокола HTTP. Мне, как разработчику, часто приходится писать очередной REST API для мобильных приложений или веб страниц. Например, берем Flask и быстро пишем микросервис с API для конкретной части приложения заказчика. Или берем django и django-rest-framework и пишем REST API для стартапа. Во всех таких проектах когда-нибудь, да приходится иметь дело с аутентификацей. Аутентификация через JWT, Oauth. И всегда реализация этой части сводилась к гуглению статей, копированию кода и подходов.
Я же хочу создать статью, которая поэтапно раскроет базовые концепции аутентификации и авторизации на практике, после чего вы сможете применять эти знания, чтобы не путать понятия и строить свои системы. Надеюсь, вам будет легче решать связанные с этим задачи. Я не собираюсь просто в лоб написать, что означают эти термины. Статья написана в таком стиле, будто бы вы изначально находитесь у истоков создания аутентификации и придумываете свой велосипед.
Сервис для создания заметок
Давайте представим, что вы делаете веб-сервис API для заметок. Новый стартап кремниевой долины. В качестве фреймворка вы выбрали Flask, БД – sqlite, для доступа к БД через ORM – SQLAlchemy.
Сервис должен уметь сохранять заметки с заголовком и текстом. Начальная модель данных достаточно простая – заголовок(varchar 256) и содержание(text).

# models.py class Note(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(256)) body = db.Column(db.Text) def __repr__(self): return '<Note %r>' % self.title
Вы написали эндпоинт, который принимает данные для заметки, вызывает сервисную функцию create_note и возвращает HTTP ответ с кодом 201 CREATED и json, дублирующий информацию о заметке. Почему такой статус? Потому что это стандартный ответ для методов создания сущностей в REST API.
Этот эндпоинт может быть использован любой персоной, у кого есть доступ к сервису. Например, наш сервис тестирует ваш друг Вован. Вован садится за компьютер, включает свой любимый HTTP клиент и шлет запросы. Заметка Вована сохраняется на сервере.

# main.py from flask import request, jsonify import services as notes_service from app import app @app.route("/notes", methods=['POST']) def create_note(): note = notes_service.create_note(request=request) response = make_response( jsonify( { 'title': note.title, 'body': note.body, } ), 201 ) response.headers["Content-Type"] = "application/json" return response
Вован решил позвать еще одного друга – Димона. Димон садится за компьютер, включает свой любимый HTTP клиент и тоже шлет запросы. Затем вы вместе любуетесь своими заметками в базе данных.
После тестирования сервиса вы с Вованом и Диманом обсуждаете, что можно
добавить в ваш новый стартап. Вован говорит, что сейчас заметки кладутся в
общую кучу и невозможно, понять, кто какую заметку написал. Новые юзеры точно
так же будут класть заметки в общую кучу. Разобраться, кто ее написал, будет
очень сложно.

Идентификация
Вы обсуждаете, что делать дальше:
— Как понять, кто написал заметку?
— Нужно узнать человека по какому-то признаку.
— Например, по имени?
— Что-то вроде того, но имя должно быть уникальным
— Можно использовать псевдоним. Пусть Димон будет сообщать, что он dimon.
Чтобы понять, кто есть кто в вашей системе, люди должны пометить себя какой-то уникальной меткой. Эти люди должны быть нам известны, то есть известны нашей базе денных. Для этого мы создадим отдельную таблицу/модель пользователя. Новая модель пользователя будет иметь псевдоним, который является набором символом, причем уникальным.

Таким образом, username позволит нам уникально обозначить пользователя.
from db import db class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(120), unique=True, nullable=False) def __repr__(self): return '<User %r>' % self.username class Note(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(256)) body = db.Column(db.Text) def __repr__(self): return '<Note %r>' % self.title
Теперь вы создаете пару записей в базе данных и заносите туда 2 пользователя – dimon_01 и vovan. Наша база данных теперь знает об этих пользователях.
Теперь вам нужно дать возможно связывать заметки и пользователей. Для этого вы добавляет новое поле в модель заметки, которое указывает на автора заметки. Такая связь называет один ко многим — один пользователь может иметь много заметок.

# models.py from db import db class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(120), unique=True, nullable=False) def __repr__(self): return '<User %r>' % self.username class Note(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(256)) body = db.Column(db.Text) user_id = db.Column( db.Integer, db.ForeignKey('user.id'), nullable=False ) user = db.relationship( 'User', backref=db.backref('notes', lazy=True) ) def __repr__(self): return '<Note %r>' % self.title
После того, как мы создали записи пользователей в БД, мы хотим как-то использовать свои учетные данные(username) для создания заметки. Для этого в json помимо добавления данных о заметке(ее название и тело) мы будем передавать еще и учетные данные в поле credentials. В коде эндпоинта мы можем добавить вызов функции, которая считывает учетные данные и ищет пользователя в БД по его псевдониму. Если пользователь найден – возвращаем экземпляр класса модели юзера и передаем в сервисную функцию create_note, в которой создаем заметку с указанием автора.
Функция, считывающая данные пользователя и находящая его в БД, это есть идентификация пользователя.

# main.py from typing import Optional from flask import request, jsonify import services as notes_service from app import app from models import User def identify_user(credentials: Optional[dict]) -> Optional[User]: if credentials is None: return None username = credentials.get('username') user = User.query.filter_by(username=username).first() return user @app.route("/notes", methods=['POST']) def create_note_api(): credentials = request.json().get('credentials') user = identify_user(credentials=credentials) note = notes_service.create_note(request=request, user=user) response = make_response( jsonify( { 'title': note.title, 'body': note.body, } ), 201 ) response.headers["Content-Type"] = "application/json" return response
# services.py from flask import Request from db import db from models import Note, User def create_note(request: Request, user: User) -> Note: note = Note( title=request.json['title'], body=request.json['body'], user=user, ) db.session.add(note) db.session.commit() return note
Более формальное определение идентификации можно получить из википедии и викисловаря
Викисловарь:
комп. процесс определения какой-либо сущности (устройства, объекта или данных) путём присвоения уникального идентификатора либо сравнения идентификатора с перечнем присвоенных идентификаторов
Википедия:
Идентифика́ция в информационных системах — процедура, в результате выполнения которой для субъекта идентификации выявляется его идентификатор, однозначно идентифицирующий этого субъекта в информационной системе. Для выполнения процедуры идентификации в информационной системе субъекту предварительно должен быть назначен соответствующий идентификатор (то есть проведена регистрация субъекта в информационной системе).
В нашем случае мы присвоили уникальные идентификаторы пользователям в виде их псевдонимов(username), а затем использовали их, чтобы найти пользователей в БД. Поиск в БД это и есть сравнение идентификатора пользователя с перечнем присвоенных идентификаторов(определение из викисловаря)
Аутентификация
В нашем решении есть одна проблема. Что будет, если кто-то другой представит себя пользователем, которым он не является? Например, какой-нибудь злоумышленник в credentials отошлет тот же самый username, который использует Вован.

Ответ прост – злоумышленник будет действовать от имени Вована. Это дыра в безопасности. Чтобы защититься от этого пользователю нужно доказать, что он является Вованом(prove user’s identity).
Вы собираете собрание и размышляете, как Вован может доказать, что он и есть Вован.
Сказать, что Вован это Вован, уже недостаточно. Он должен предоставить только то, что есть только у него. Что может охарактеризовать и выделить Вована от остальных людей и что знает только он? Набор атомов? Уникальный ДНК?
— Вована отличают от остальных его темные волосы. Пусть вместе со своим псевдонимом сообщит и то, что у него темные волосы.
— А еще у него есть собака по имени Тузик.
— Еще его любимый цвет – красный.
— А ведь по сути это все факты о нем. Его характеристика
— Пусть Вован сам выбирает, какой факт о нем можно сообщить, чтобы система поверила ему. Нам хватит и одного факта.
— Это, наверное, будет секретная фраза? Один фактор для аутентификации
Вы пришли к выводу, что к модели юзера нужно добавить поле, которое будет отражать какой-то факт, характеристику Вована. Вы вспоминаете, что в фильмах люди используют такие фразы и называют паролями. В добавок вы узнали, что пароли нужно хэшироваать, но вдаваться в детали тут не будем.

from db import db class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(120), unique=True, nullable=False) password = db.Column(db.String(256)) def __repr__(self): return '<User %r>' % self.username class Note(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(256)) body = db.Column(db.Text) user_id = db.Column( db.Integer, db.ForeignKey('user.id'), nullable=False ) user = db.relationship( 'User', backref=db.backref('notes', lazy=True) ) def __repr__(self): return '<Note %r>' % self.title
Таким образом, мы дополняем БД паролями, которые знают только админ. Админ затем передает лично пароли Вовану и Димон. Те, в свою очередь, используют их при взаимодействии с эндпоинтом для создания заметок.
Внутри эндпоинта при успешном сценарии происходит следующее:
1. Мы идентифицируем пользователя по псевдониму(username);
2. Сравниваете пароли. Этот процесс называют аутентификацией.
3. Сервисная функция выполняется и создается новая заметка, привязанная к пользователю
После идентификации пользователя вы сравниваете пароли. Если они совпадают, то пользователь подтверждает свою личность(prove user’s identity), на профессиональном термине это называется аутентификацией.

# main.py from typing import Optional from flask import request, jsonify, make_response import services as notes_service from app import app from models import User from utils import hash_password def authenticate_user(user: Optional[User], password: str) -> bool: if user is None: return False return user.password == hash_password(password) def identify_user(credentials: Optional[dict]) -> Optional[User]: if credentials is None: return None username = credentials.get('username') user = User.query.filter_by(username=username).first() return user @app.route("/notes", methods=['POST']) def create_note_api(): credentials = request.json().get('credentials') user = identify_user(credentials=credentials) password = credentials.get('password') if not authenticate_user(user=user, password=password): # Can't authenticate user, permission denied response = make_response( jsonify( { 'msg': "Not authenticated" } ), 403 ) response.headers["Content-Type"] = "application/json" return response note = notes_service.create_note(request=request, user=user) response = make_response( jsonify( { 'title': note.title, 'body': note.body, } ), 201 ) response.headers["Content-Type"] = "application/json" return response
Более формальное определение аутентификации можно получить из википедии:
Аутентифика́ция (англ. authentication < греч. αὐθεντικός [authentikos] «реальный, подлинный» < αὐτός [autos] «сам; он самый») — процедура проверки подлинности, например:
— проверка подлинности пользователя путём сравнения введённого им пароля (для указанного логина) с паролем, сохранённым в базе данных пользовательских логинов;
— подтверждение подлинности электронного письма путём проверки цифровой подписи письма по открытому ключу отправителя;
— проверка контрольной суммы файла на соответствие сумме, заявленной автором этого файла.
Резюмируя:
· Аутентификация это процесс доказательства личности пользователя;
· Мы аутентифицируем пользователя с помощью одного факта/характеристики пользователя – его пароля. Однофакторная аутентификация;
· Перед аутентификацией мы можем идентифицировать юзера;
· Пример аутентификации – предоставление никнейма/имейла и сверка пароля(пароль доказывает, что этот человек тот, кем себя выдает).
· Аутентификация может происходить при процедуре логина, а так же при каждом запросе к ресурсу.
Авторизация
Что будет, если пользователь предоставил учетные данные, которых нет в базе данных? Или может пароль от пользователя не совпадает с тем, что есть в БД?


Вы подумали, что заметки могут оставлять только пользователи, которых мы можем идентифицировать и которые предоставили правильный пароль. Если процедуры идентификации и сравнения не пройдены, то пользователь не имеет доступа к функционалу, то есть он не авторизован выполнять такие действия.
Какой HTTP код мы можем вернуть пользователю в таком случае? Порыскав в интернете список HTTP кодов, вы обнаружили говорящий за себя ответ 401 Unauthroized. То есть говорим пользователю, что он не авторизован.

from typing import Optional from flask import request, jsonify, make_response import services as notes_service from app import app from models import User from utils import hash_password def authenticate_user(user: Optional[User], password: str) -> bool: if user is None: return False return user.password == hash_password(password) def identify_user(credentials: Optional[dict]) -> Optional[User]: if credentials is None: return None username = credentials.get('username') user = User.query.filter_by(username=username).first() return user def is_authorized(user: Optional[User]) -> bool: data = request.get_json() credentials = data.get('credentials') password = credentials.get('password') if not authenticate_user(user=user, password=password): # Can't authenticate user, permission denied return False return True @app.route("/notes", methods=['POST']) def create_note(): data = request.get_json() credentials = data.get('credentials') user = identify_user(credentials=credentials) if not is_authorized(user=user): response = make_response( jsonify( { 'msg': 'Credentials not valid', } ), 401 ) response.headers["Content-Type"] = "application/json" return response note = notes_service.create_note(request=request, user=user) response = make_response( jsonify( { 'title': note.title, 'body': note.body, } ), 201 ) response.headers["Content-Type"] = "application/json" return response
Вы всей командой вновь тестируете сервис. Каждый из вас обнаруживает, что неудобно как-то писать каждый раз свои учетные данные в json. Вован вспоминает, что в протоколе HTTP сообщения могут содержать заголовки, куда можно будет спрятать данные для аутентификации и последующей авторизации.

Какой заголовок нам нужен? Если данные для аутентификации неправильные, то мы не авторизуем юзера и возвращаем код ответа 401 Unathorized. Пусть пользователь введет правильные данные и повторит запрос с заголовком “Authorization”. Мы не можем его авторизовать, потому что у него неправильные данные для аутентификации. Если они станут валидными, то мы его авторизуем. Пользователь посылает заголовок “Authorization” и говорит как бы контекст «я с повторной авторизцией”.

Таким образом, мы пришли к следующему алгоритму:
1. Пользователь посылает данные для создания заметки, но без каких-либо данных для аутентификации, либо с неверными данными аутентификации
2. Наш эндпоинт видит, что данных для аутентификации нет, либо они неправильные и говорит “ты не авторизован”.
3. HTTP клиент пользователя просит пользователя ввести правильные данные для аутентификации.
4. Пользователь посылает данные для создания заметки с заголовком Autthorization, содержащим данные для аутентификации перед авторизацией
5. Пользователь получает успешный статус

from typing import Optional from flask import request, jsonify, make_response import services as notes_service from app import app from models import User from utils import hash_password def authenticate_user(user: Optional[User], password: str) -> bool: if user is None: return False return user.password == hash_password(password) def identify_user() -> Optional[User]: credentials = request.headers.get('Authorization') if not credentials: return None username, _ = credentials.split(':') user = User.query.filter_by(username=username).first() return user def is_authorized(user: Optional[User]) -> bool: credentials = request.headers.get('Authorization') username, password = credentials.split(':') if not authenticate_user(user=user, password=password): # Can't authenticate user, permission denied return False return True @app.route("/notes", methods=['POST']) def create_note(): user = identify_user() if not is_authorized(user=user): response = make_response( jsonify( { 'msg': 'Credentials not valid', } ), 401 ) response.headers["Content-Type"] = "application/json" return response note = notes_service.create_note(request=request, user=user) response = make_response( jsonify( { 'title': note.title, 'body': note.body, } ), 201 ) response.headers["Content-Type"] = "application/json" return response
Масштабирование алгоритма аутентификации
Димон всегда думал наперед и предложил подумать о том, как поступать, если мы придумаем дополнительный способ аутентификации. Тот, который мы сейчас придумали, давайте назовем базовый(basic). Когда пользователь предоставит данные для другого способа аутентификации(например, будет использовать какой-то одноразовый код), мы сообщим ему, что в данном контексте нужно использовать базовую аутентификацию, а не одноразовый код.
— еще я не хочу, чтобы учетные данные передавались в виде китайских или кириллических символов. Придется возиться с кодировками, это боль. Пусть лучше будут закодированы с помощью base64. Эта кодировка переводит все в ASCII символы без возни с другими символами. То, что нужно.
— Вы еще забыли, что если мы добавим в сервис заметок сервис секретных файлов, для аутентификации в котором нужен будет другой хост, то их стоило бы разграничить, чтобы не перепутали окружения. Давайте назовем это realm.
В итоге вы пришли к следующей схеме, учитывающей масштабирование способов аутентификации:
1. Пользователь посылает данные для создания заметки, но без каких-либо данных для аутентификации, либо с неверными данными аутентификации
2. Наш эндпоинт видит, что данных для аутентификации нет, либо они неправильные и говорит “ты не авторизован, предоставь данные для базовой схемы аутентификации”. В добавок мы добавляем заголовок WWW-Authenticate со значением нужной схемы аутентификации и указанием окружения(realm) API заметок.
3. HTTP клиент пользователя смотрит, что нужно взять учетные даанные для базовой аутентификации и просит пользователя ввести псевдоним и пароль. Смотрит, что нужно использовать тот же самый сервис notes_api.
4. Пользователь посылает закодированные в base64 данные для создания заметки с заголовком Authorization, содержащим данные для базовой аутентификации перед авторизацией
5. Пользователь получает успешный статус

# main.py from typing import Optional from flask import request, jsonify, make_response import base64 import services as notes_service from app import app from models import User from utils import hash_password def authenticate_user(user: Optional[User], password: str) -> bool: if user is None: return False return user.password == hash_password(password) def identify_user() -> Optional[User]: scheme, credentials = request.headers.get('Authorization').split() if not credentials: return None decoded_credentials = base64.b64decode(credentials).decode() username, _ = decoded_credentials.split(':') user = User.query.filter_by(username=username).first() return user def is_authorized(user: Optional[User]) -> bool: scheme, credentials = request.headers.get('Authorization').split() decoded_credentials = base64.b64decode(credentials).decode() username, password = decoded_credentials.split(':') if not authenticate_user(user=user, password=password): # Can't authenticate user, permission denied return False return True @app.route("/notes", methods=['POST']) def create_note(): user = identify_user() if not is_authorized(user=user): response = make_response( jsonify( { 'msg': 'Credentials not valid', } ), 401 ) response.headers["Content-Type"] = "application/json" response.headers["WWW-Authenticate"] = "Basic realm=notes_api" return response note = notes_service.create_note(request=request, user=user) response = make_response( jsonify( { 'title': note.title, 'body': note.body, } ), 201 ) response.headers["Content-Type"] = "application/json" return response
Middleware
Чтобы не проводить аутентификацию прямо в коде эндпоинта, мы можем переместить этот код в место, которое аутентифицирует юзера до попадания запроса в эндпоинт. Обычно для этого в различных фреймворках используют Middleware. Этот термин обозначает ряд функций или классов, которые по цепочке вызываются друг за другом перед попаданием запроса в эндпоинт. Они могут менять какие-то данные, что-то проверять и т.д Чтобы избежать дублирования кода, мы можем написать свой кастомный authentication middleware и использовать его повсюду в приложении, где нужна аутентификация. Во Flask же система middleware неудобная в использовании. Можно заметить много библиотек для аутентификации, которые используют просто декораторы. Декораторы оборачивают какую-то функцию и выполняют какой-то код до обработки самим эндпоинтом. Это и будет своего рода middleware

# main.py import base64 from functools import wraps from typing import Optional from flask import request, jsonify, make_response import services as notes_service from app import app from models import User from utils import hash_password def authenticate_user(user: Optional[User], password: str) -> bool: if user is None: return False return user.password == hash_password(password) def identify_user() -> Optional[User]: scheme, credentials = request.headers.get('Authorization').split() if not credentials: return None decoded_credentials = base64.b64decode(credentials).decode() username, _ = decoded_credentials.split(':') user = User.query.filter_by(username=username).first() return user def is_authorized(user: Optional[User]) -> bool: scheme, credentials = request.headers.get('Authorization').split() decoded_credentials = base64.b64decode(credentials).decode() username, password = decoded_credentials.split(':') if not authenticate_user(user=user, password=password): # Can't authenticate user, permission denied return False return True def authenticate(f): @wraps(f) def wrapper(*args, **kwargs): user = identify_user() if not is_authorized(user=user): response = make_response( jsonify( { 'msg': 'Credentials not valid', } ), 401 ) response.headers["Content-Type"] = "application/json" response.headers["WWW-Authenticate"] = "Basic realm=notes_api" return response request.user = user return f(*args, **kwargs) return wrapper @app.route("/notes", methods=['POST']) @authenticate def create_note(): note = notes_service.create_note(request=request, user=request.user) response = make_response( jsonify( { 'title': note.title, 'body': note.body, } ), 201 ) response.headers["Content-Type"] = "application/json" return response
Выводы и зачем это все
Что мы узнали из этой статьи:
-
идентификация это процесс выявления какой-то сущности с идентификатором путем сравнения с уже присвоенными идентификаторами. Мы хотим узнать, кто есть кто в системе, поэтому ввели новую сущность юзера, которая имеет уникальный идентификатор username. Он и позволяет нам идентифицировать юзера. Например, под идентификацией можно понимать поиск в БД пользователя с указанным username/email.

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

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


Почему эта статья может помочь? Система аутентификации в таких фреймворках, как django, django-rest-framework, flask, fastAPI использует похожие понятия и процессы, хотя детали реализации разные — где-то нужно наследовать специальный класс, где-то реализовать вызываемый объект и т.д.
Кусок внутреннего кода django-rest-framework:
class BasicAuthentication(BaseAuthentication): """ HTTP Basic authentication against username/password. """ www_authenticate_realm = 'api' def authenticate(self, request): """ Returns a `User` if a correct username and password have been supplied using HTTP Basic authentication. Otherwise returns `None`. """ auth = get_authorization_header(request).split() if not auth or auth[0].lower() != b'basic': return None if len(auth) == 1: msg = _('Invalid basic header. No credentials provided.') raise exceptions.AuthenticationFailed(msg) elif len(auth) > 2: msg = _('Invalid basic header. Credentials string should not contain spaces.') raise exceptions.AuthenticationFailed(msg) try: try: auth_decoded = base64.b64decode(auth[1]).decode('utf-8') except UnicodeDecodeError: auth_decoded = base64.b64decode(auth[1]).decode('latin-1') auth_parts = auth_decoded.partition(':') except (TypeError, UnicodeDecodeError, binascii.Error): msg = _('Invalid basic header. Credentials not correctly base64 encoded.') raise exceptions.AuthenticationFailed(msg) userid, password = auth_parts[0], auth_parts[2] return self.authenticate_credentials(userid, password, request) def authenticate_credentials(self, userid, password, request=None): """ Authenticate the userid and password against username and password with optional request for context. """ credentials = { get_user_model().USERNAME_FIELD: userid, 'password': password } user = authenticate(request=request, **credentials) if user is None: raise exceptions.AuthenticationFailed(_('Invalid username/password.')) if not user.is_active: raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) return (user, None) def authenticate_header(self, request): return 'Basic realm="%s"' % self.www_authenticate_realm
кусок кода FastAPI
class HTTPBasic(HTTPBase): def __init__( self, *, scheme_name: Optional[str] = None, realm: Optional[str] = None, description: Optional[str] = None, auto_error: bool = True, ): self.model = HTTPBaseModel(scheme="basic", description=description) self.scheme_name = scheme_name or self.__class__.__name__ self.realm = realm self.auto_error = auto_error async def __call__( # type: ignore self, request: Request ) -> Optional[HTTPBasicCredentials]: authorization: str = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if self.realm: unauthorized_headers = {"WWW-Authenticate": f'Basic realm="{self.realm}"'} else: unauthorized_headers = {"WWW-Authenticate": "Basic"} invalid_user_credentials_exc = HTTPException( status_code=HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials", headers=unauthorized_headers, ) if not authorization or scheme.lower() != "basic": if self.auto_error: raise HTTPException( status_code=HTTP_401_UNAUTHORIZED, detail="Not authenticated", headers=unauthorized_headers, ) else: return None try: data = b64decode(param).decode("ascii") except (ValueError, UnicodeDecodeError, binascii.Error): raise invalid_user_credentials_exc username, separator, password = data.partition(":") if not separator: raise invalid_user_credentials_exc return HTTPBasicCredentials(username=username, password=password)
кусок кода flask_httpauth
class HTTPBasicAuth(HTTPAuth): def __init__(self, scheme=None, realm=None): super(HTTPBasicAuth, self).__init__(scheme or 'Basic', realm) self.hash_password_callback = None self.verify_password_callback = None def hash_password(self, f): self.hash_password_callback = f return f def verify_password(self, f): self.verify_password_callback = f return f def get_auth(self): # this version of the Authorization header parser is more flexible # than Werkzeug's, as it also accepts other schemes besides "Basic" header = self.header or 'Authorization' if header not in request.headers: return None value = request.headers[header].encode('utf-8') try: scheme, credentials = value.split(b' ', 1) username, password = b64decode(credentials).split(b':', 1) except (ValueError, TypeError): return None try: username = username.decode('utf-8') password = password.decode('utf-8') except UnicodeDecodeError: username = None password = None return Authorization( scheme, {'username': username, 'password': password}) def authenticate(self, auth, stored_password): if auth: username = auth.username client_password = auth.password else: username = "" client_password = "" if self.verify_password_callback: return self.ensure_sync(self.verify_password_callback)( username, client_password) if not auth: return if self.hash_password_callback: try: client_password = self.ensure_sync( self.hash_password_callback)(client_password) except TypeError: client_password = self.ensure_sync( self.hash_password_callback)(username, client_password) return auth.username if client_password is not None and \ stored_password is not None and \ hmac.compare_digest(client_password, stored_password) else None
В кусках кода сверху можно заметить, что в деталях реализации они отличаются, но есть и одинаковые элементы. Например, краем глаза можно зацепиться за методы и переменные authenticate , заголовокAuthorization , заголовок WWW-Authenticate , scheme, credentials , HTTP_401_UNAUTHORIZED
Хотя идентификацию юзера тут явно не выделяют, но этот процесс присутствует.
Итого — вы можете примерно понять о чем речь. Поменять эту аутентификацию и реализовать свою. Например, для JWT аутентификации нужно будет точно так же читать заголовок Authorization , парсить scheme (Bearer) и credentials (сам токен), идентифицировать юзера(по айди в токене) и аутентифицировать(проверять валидность токена и существование юзера). И фреймворки как раз прячут это все где-то в части, связанной с middleware.
ссылка на оригинал статьи https://habr.com/ru/post/682170/
Добавить комментарий