Подробное объяснение механизма проверки плагинов Flask-JWT

Python

предисловие

jwt (веб-токены JSON) в настоящее время является самым популярным решением для междоменной аутентификации. По сравнению с сеансом он не имеет состояния, поэтому очень подходит для API формата json. В flask есть такой плагин, который специализируется на проверке jwt.

1. Структура исходного кода

Исходный код flask-jwt невелик, модуль всего один, сначала рассмотрим элементы его конфигурации.

элемент конфигурации

current_identity = LocalProxy(lambda: getattr(_request_ctx_stack.top, 'current_identity', None))

_jwt = LocalProxy(lambda: current_app.extensions['jwt'])

CONFIG_DEFAULTS = {
    'JWT_DEFAULT_REALM': 'Login Required',
    'JWT_AUTH_URL_RULE': '/auth',
    'JWT_AUTH_ENDPOINT': 'jwt',
    'JWT_AUTH_USERNAME_KEY': 'username',
    'JWT_AUTH_PASSWORD_KEY': 'password',
    'JWT_ALGORITHM': 'HS256',
    'JWT_LEEWAY': timedelta(seconds=10),
    'JWT_AUTH_HEADER_PREFIX': 'JWT',
    'JWT_EXPIRATION_DELTA': timedelta(seconds=300),
    'JWT_NOT_BEFORE_DELTA': timedelta(seconds=0),
    'JWT_VERIFY_CLAIMS': ['signature', 'exp', 'nbf', 'iat'],
    'JWT_REQUIRED_CLAIMS': ['exp', 'iat', 'nbf']
}

Первый взглядcurrent_identityа также_jwtЭти два объекта, в первую очередь это не обычный объект, а прокси-объектLocalProxy. Что такое прокси-объект, если вы поняли механизм flask, вы должны это знать очень хорошо, но не беда, если вы этого не знаете, вы можете просто понимать его как копию исходного объекта, но это не совсем то же самое. Давайте посмотрим, узнав этоLocalProxy, который принимает функцию без аргументов и возвращает объект. После прохождения прокси мы можем использовать все функции этого объекта. в_jwtпрокси основного объекта для подключаемого модуля JWT, аcurrent_identityЧто именно представляет собой этот объект, как следует из названия, это прокси объекта пользователя текущего потока, конкретного объекта, будет объяснено в следующем содержании.

основной объект

class JWT(object):

    def __init__(self, app=None, authentication_handler=None, identity_handler=None):
        self.authentication_callback = authentication_handler
        self.identity_callback = identity_handler

        self.auth_response_callback = _default_auth_response_handler
        self.auth_request_callback = _default_auth_request_handler
        self.jwt_encode_callback = _default_jwt_encode_handler
        self.jwt_decode_callback = _default_jwt_decode_handler
        self.jwt_headers_callback = _default_jwt_headers_handler
        self.jwt_payload_callback = _default_jwt_payload_handler
        self.jwt_error_callback = _default_jwt_error_handler
        self.request_callback = _default_request_handler

        if app is not None:
            self.init_app(app)
            
        ...

Из конструктора объекта видно, что помимоauthentication_handlerа такжеidentity_handlerДругие имеют реализации по умолчанию. Для каждого объекта обратного вызова существуют соответствующие декораторы для реализации настройки этих функций.

основной валидатор

def jwt_required(realm=None):
    """View decorator that requires a valid JWT token to be present in the request

    :param realm: an optional realm
    """
    def wrapper(fn):
        @wraps(fn)
        def decorator(*args, **kwargs):
            _jwt_required(realm or current_app.config['JWT_DEFAULT_REALM'])
            return fn(*args, **kwargs)
        return decorator
    return wrapper

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

2. Анализ исходного кода

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

Авторизоваться

API-аутентификация

Поймите процесс, анализ исходного кода прост.

Анализ исходного кода входа

Во-первых, войти в систему. Давайте сначала рассмотрим основную функцию, вызываемую при входе в систему._default_auth_request_handler

def _default_auth_request_handler():
    data = request.get_json()
    username = data.get(current_app.config.get('JWT_AUTH_USERNAME_KEY'), None)
    password = data.get(current_app.config.get('JWT_AUTH_PASSWORD_KEY'), None)
    criterion = [username, password, len(data) == 2]

    if not all(criterion):
        raise JWTError('Bad Request', 'Invalid credentials')

    identity = _jwt.authentication_callback(username, password)

    if identity:
        access_token = _jwt.jwt_encode_callback(identity)
        return _jwt.auth_response_callback(access_token, identity)
    else:
        raise JWTError('Bad Request', 'Invalid credentials')

Здесь упоминается, что интерфейс входа в систему flask-jwt не требует от разработчика написания соответствующей функции попытки, поскольку он уже зарегистрировал значение init_app.JWT_AUTH_ENDPOINT(Можно настроить в конфигурации, по умолчанию используется '/auth') маршрут в качестве интерфейса аутентификации.

Вернемся к самой функции, для запроса интерфейса проверки, упомянутого выше, необходимо передать объект json, содержащий пароль учетной записи в теле, в котором имя ключа пароля учетной записи может быть передано в файле конфигурации.JWT_AUTH_USERNAME_KEYа такжеJWT_AUTH_PASSWORD_KEYчтобы указать, по умолчанию используется имя пользователя и пароль. После получения пароля учетной записи от тела нам необходимо его настроитьauthentication_callbackЧтобы проверить правильность информации, эту функцию можно передать в качестве параметра при инициализации объекта JWT или через@authentication_handlerдекоратор для прохождения. Он должен принимать имя пользователя, пароль, два параметра и возвращатьПользовательский объект. Из кода видно, что после успешной проверки будет сгенерирован токен и передан вauth_response_callbackОн используется в функции для генерации объекта json и возврата его во внешний интерфейс.Обратите внимание, что токен генерируется функцией кодирования, давайте посмотрим на ее реализацию.

def _default_jwt_encode_handler(identity):
    secret = current_app.config['JWT_SECRET_KEY']
    algorithm = current_app.config['JWT_ALGORITHM']
    required_claims = current_app.config['JWT_REQUIRED_CLAIMS']

    payload = _jwt.jwt_payload_callback(identity)
    missing_claims = list(set(required_claims) - set(payload.keys()))

    if missing_claims:
        raise RuntimeError('Payload is missing required claims: %s' % ', '.join(missing_claims))

    headers = _jwt.jwt_headers_callback(identity)

    return jwt.encode(payload, secret, algorithm=algorithm, headers=headers)

Он внутренне вызывает алгоритм кодирования JWT, который поставляется с python, и выводит декодируемый код.Закодированная информация здесь представляет собой просто словарь со временем выдачи, сроком действия и информацией об учетной записи пользователя. Для кодирования и декодирования требуется один и тот же ключ, который является секретным и находится в файле конфигурации по умолчанию.SECRET_KEY. Код здесь — это токен, выводимый во внешний интерфейс на предыдущем шаге.

На этом весь процесс входа в систему завершен.

Проверить анализ исходного кода

Чтобы проверить эту часть, пожалуйста, спросите только что упомянутогоjwt_required. На самом деле это просто декоратор, настоящая функция проверки_jwt_required, давайте заглянем внутрь него.

def _jwt_required(realm):
    """Does the actual work of verifying the JWT data in the current request.
    This is done automatically for you by `jwt_required()` but you could call it manually.
    Doing so would be useful in the context of optional JWT access in your APIs.

    :param realm: an optional realm
    """
    token = _jwt.request_callback()

    if token is None:
        raise JWTError('Authorization Required', 'Request does not contain an access token',
                       headers={'WWW-Authenticate': 'JWT realm="%s"' % realm})

    try:
        payload = _jwt.jwt_decode_callback(token)
    except jwt.InvalidTokenError as e:
        raise JWTError('Invalid token', str(e))

    _request_ctx_stack.top.current_identity = identity = _jwt.identity_callback(payload)

    if identity is None:
        raise JWTError('Invalid JWT', 'User does not exist')

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

_request_ctx_stack.top.current_identity = identity = _jwt.identity_callback(payload)

Что делает этот код, в первую очередь он проходит вidentity_callbackизвлек наш пользовательский объект и поместил его в_request_ctx_stackВ этом стеке друзья, знакомые с flask, знают, что это стек с изолированным потоком. Для каждого запроса от пользователя создается поток, а стек находится в каждом независимом потоке, поэтому он потокобезопасен. flask-jwt помещает пользовательские объекты в этот стек, поэтому этот поток несет информацию об идентификаторе пользователя. Так как же нам получить этот пользовательский объект из стека? На этот раз давайте перейдем к тому, что мы сказали в началеcurrent_identityобъект. Объект, который он проксирует, — это пользовательский объект, помещенный сюда. Таким образом, мы можем вызвать функцию представления фляги, вызвавcurrent_identityчтобы получить информацию о пользователе, который в настоящее время делает запрос.

На данный момент проанализирован весь процесс проверки.

3. Резюме

Механизм jwt реализует аутентификацию посредством кодирования без сохранения состояния, что облегчает разделение внешнего и внутреннего интерфейса. Однако в нем скрыты определенные проблемы с безопасностью, например, в случае утечки ключа, утечка ключа и идентификатора пользователя могут быть использованы для самостоятельной выдачи токена в обход системы проверки. Поэтому в реальном процессе разработки необходимо настроить словарь, содержащий информацию (полезную нагрузку в исходном коде), чтобы злоумышленник не мог знать формат зашифрованной информации, чтобы злоумышленник не мог выдать токен путем себя; регулярное обновление ключа также является эффективной профилактической мерой.

Автор: теорема Джетто Чжана

кв: 1045569270

утверждение:Эта статья не может быть воспроизведена без разрешения автора