Автор оригинала: Adam McQuistan.
Аутентификация JWT
Добро пожаловать в шестую часть этой многоступенчатой серии учебников по полнотекстовой веб-разработке с использованием Vue.js и Фляжка. В этом посте я продемонстрирую способ использования аутентификации JSON Web Token (JWT).
Код для этого поста можно найти на моем аккаунте GitHub под веткой Шестой пост .
Содержание серии
- Настройка и знакомство с VueJS
- Навигация по маршрутизатору Vue
- Государственное управление с помощью Vuex
- RESTful API с колбой
- Интеграция AJAX с REST API
- Аутентификация JWT (вы здесь)
- Развертывание на виртуальном частном сервере
Базовое введение в аутентификацию JWT
Подобно некоторым другим постам в этой серии, я не буду вдаваться в существенные детали теории того, как ЭТО работает. Вместо этого я буду придерживаться прагматичного подхода и демонстрировать специфику его реализации, используя интересующие меня технологии в рамках Flask и Vue.js. Если вы заинтересованы в более глубоком понимании Jwt , я отсылаю вас к превосходному посту Скотта Робинсона здесь, на StackAbuse , где он объясняет низкоуровневые детали этой техники.
В базовом смысле JWT-это закодированный объект JSON, используемый для передачи информации между двумя системами, который состоит из заголовка, полезной нагрузки и сигнатуры в виде [HEADER].[ПОЛЕЗНАЯ НАГРУЗКА].[ПОДПИСЬ]
все, что содержится в заголовке HTTP как “Authorization: Bearer [HEADER].[ПОЛЕЗНАЯ НАГРУЗКА].[ПОДПИСЬ]”. Процесс начинается с того, что клиент (запрашивающая система) аутентифицируется на сервере (сервис с требуемым ресурсом), который генерирует JWT, действительный только в течение определенного периода времени. Затем сервер возвращает его в виде подписанного и закодированного токена для хранения клиентом и использования для проверки в последующих коммуникациях.
Аутентификация JWT довольно хорошо работает для SPA-приложений, подобных тому, что строится в этой серии, и приобрела значительную популярность среди разработчиков, реализующих их.
Реализация аутентификации JWT в Flask RESTful API
Что касается колбы, то я буду использовать пакет Python PyJWT для обработки некоторых деталей, связанных с созданием, анализом и проверкой JWTS.
(venv) $ pip install PyJWT
С установленным пакетом PyJWT я могу перейти к реализации частей, необходимых для аутентификации и верификации в приложении Flask. Для начала я дам приложению возможность создавать новых зарегистрированных пользователей, которые будут представлены классом User
. Как и все другие классы в этом приложении, класс User
будет находиться в models.py модуль.
Первое, что нужно сделать, это импортировать пару функций, generate_password_hash
и check_password_hash
из модуля werkzeug пакета security
, который я буду использовать для генерации и проверки хэшированных паролей. Нет необходимости устанавливать этот пакет, так как он поставляется с колбой автоматически.
""" models.py - Data classes for the surveyapi application """ from datetime import datetime from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash db = SQLAlchemy()
Непосредственно под приведенным выше кодом я определяю класс User
, который наследуется от класса SQLAlchemy Model
, аналогичного другим, определенным в предыдущих сообщениях. Этот класс User
должен содержать автоматически сгенерированное целое поле класса первичного ключа с именем id
, а затем два строковых поля с именем email
и password
с адресом электронной почты, настроенным на уникальность. Я также даю этому классу поле relationship
для связывания любых опросов, которые может создать пользователь. С другой стороны этого уравнения я добавил creator_id
внешний ключ к классу Survey
, чтобы связать пользователей с опросами, которые они создают.
Я переопределяю метод __init__ (...)
, чтобы я мог хэшировать пароль при создании экземпляра нового объекта User
. После этого я даю ему метод класса authenticate
, чтобы запросить пользователя по электронной почте и проверить , что предоставленный хэш пароля совпадает с тем, который хранится в базе данных. Если они совпадают, я возвращаю аутентифицированного пользователя. И последнее, но не менее важное: я использовал метод to_dict ()
, чтобы помочь с сериализацией пользовательских объектов.
""" models.py - Data classes for the surveyapi application """ # # omitting imports and what not # class User(db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) email = db.Column(db.String(120), unique=True, nullable=False) password = db.Column(db.String(255), nullable=False) surveys = db.relationship('Survey', backref="creator", lazy=False) def __init__(self, email, password): self.email = email self.password = generate_password_hash(password, method='sha256') @classmethod def authenticate(cls, **kwargs): email = kwargs.get('email') password = kwargs.get('password') if not email or not password: return None user = cls.query.filter_by(email=email).first() if not user or not check_password_hash(user.password, password): return None return user def to_dict(self): return dict(id=self.id, email=self.email) class Survey(db.Model): __tablename__ = 'surveys' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Text) created_at = db.Column(db.DateTime, default=datetime.utcnow) questions = db.relationship('Question', backref="survey", lazy=False) creator_id = db.Column(db.Integer, db.ForeignKey('users.id')) def to_dict(self): return dict(id=self.id, name=self.name, created_at=self.created_at.strftime('%Y-%m-%d %H:%M:%S'), questions=[question.to_dict() for question in self.questions])
Далее необходимо сгенерировать новую миграцию и обновить базу данных с ее помощью, чтобы связать класс Python User
с таблицей базы данных users sqlite . Для этого я запускаю следующие команды в том же каталоге, что и мой manage.py модуль.
(venv) $ python manage.py db migrate (venv) $ python manage.py db upgrade
Ладно, пора прыгать к api.py модуль и реализовать функциональность для регистрации и аутентификации пользователей наряду с функциональностью проверки для защиты создания новых опросов. В конце концов, я не хочу, чтобы какие-то гнусные веб-боты или другие плохие актеры загрязняли мое потрясающее приложение для опроса.
Для начала я добавляю класс User
в список импорта из models.py модуль по направлению к верхней части api.py модуль. Пока я там, я пойду дальше и добавлю пару других импортных товаров, которые буду использовать позже.
""" api.py - provides the API endpoints for consuming and producing REST requests and responses """ from functools import wraps from datetime import datetime, timedelta from flask import Blueprint, jsonify, request, current_app import jwt from .models import db, Survey, Question, Choice, User
Теперь, когда у меня есть все необходимые инструменты, я могу реализовать набор функций просмотра регистрации и входа в систему в api.py модуль.
Я начну с функции register()
view, которая ожидает, что электронная почта и пароль будут отправлены в JSON в теле запроса POST. Пользователь просто создается с тем, что дается для электронной почты и пароля, и я весело возвращаю ответ JSON (что не обязательно является лучшим подходом, но на данный момент он будет работать).
""" api.py - provides the API endpoints for consuming and producing REST requests and responses """ # # omitting inputs and other view functions # @api.route('/register/', methods=('POST',)) def register(): data = request.get_json() user = User(**data) db.session.add(user) db.session.commit() return jsonify(user.to_dict()), 201
Круто. Бэкэнд способен создавать новых пользователей, стремящихся создать множество опросов, поэтому я лучше добавлю некоторые функции для их аутентификации и позволю им продолжать создавать свои опросы.
Функция login использует метод User.authenticate(...)
class для поиска и аутентификации пользователя. Если пользователь, соответствующий заданному адресу электронной почты и паролю, найден, то функция входа переходит к созданию токена JWT, в противном случае возвращается None
, в результате чего функция входа возвращает сообщение “сбой аутентификации” с соответствующим кодом состояния HTTP 401.
Я создаю токен JWT с помощью PyJWT (как jwt), кодируя словарь, содержащий следующее:
- sub – тема jwt, которая в данном случае является электронной почтой пользователя
- в – время, когда jwt был выдан в
- exp – это момент истечения срока действия jwt, который в данном случае составляет 30 минут после выдачи
""" api.py - provides the API endpoints for consuming and producing REST requests and responses """ # # omitting inputs and other view functions # @api.route('/login/', methods=('POST',)) def login(): data = request.get_json() user = User.authenticate(**data) if not user: return jsonify({ 'message': 'Invalid credentials', 'authenticated': False }), 401 token = jwt.encode({ 'sub': user.email, 'iat':datetime.utcnow(), 'exp': datetime.utcnow() + timedelta(minutes=30)}, current_app.config['SECRET_KEY']) return jsonify({ 'token': token.decode('UTF-8') })
Процесс кодирования использует значение свойства Base Config
class SECRET_KEY
, определенного в config.py и удерживается в свойстве config current_app
после создания приложения Flask.
Далее я хотел бы разбить функциональность GET и POST, которая в настоящее время находится в плохо названной функции представления с именем fetch_survey (...)
, показанной ниже в ее исходном состоянии. Вместо этого я позволю fetch_surveys(...)
отвечать исключительно за извлечение всех опросов при запросе “/api/surveys/” с помощью запроса GET. С другой стороны, создание опроса, которое происходит, когда тот же URL-адрес попадает в POST-запрос, теперь будет находиться в новой функции с именем create_survey(...)
.
Так вот…
""" api.py - provides the API endpoints for consuming and producing REST requests and responses """ # # omitting inputs and other view functions # @api.route('/surveys/', methods=('GET', 'POST')) def fetch_surveys(): if request.method == 'GET': surveys = Survey.query.all() return jsonify([s.to_dict() for s in surveys]) elif request.method == 'POST': data = request.get_json() survey = Survey(name=data['name']) questions = [] for q in data['questions']: question = Question(text=q['question']) question.choices = [Choice(text=c) for c in q['choices']] questions.append(question) survey.questions = questions db.session.add(survey) db.session.commit() return jsonify(survey.to_dict()), 201
становится этим…
""" api.py - provides the API endpoints for consuming and producing REST requests and responses """ # # omitting inputs and other view functions # @api.route('/surveys/', methods=('POST',)) def create_survey(current_user): data = request.get_json() survey = Survey(name=data['name']) questions = [] for q in data['questions']: question = Question(text=q['question']) question.choices = [Choice(text=c) for c in q['choices']] questions.append(question) survey.questions = questions survey.creator = current_user db.session.add(survey) db.session.commit() return jsonify(survey.to_dict()), 201 @api.route('/surveys/', methods=('GET',)) def fetch_surveys(): surveys = Survey.query.all() return jsonify([s.to_dict() for s in surveys])
Реальный ключ теперь заключается в защите функции create_survey(...)
view, чтобы только аутентифицированные пользователи могли создавать новые опросы. Другими словами, если POST-запрос сделан против “/api/surveys”, приложение должно проверить, что он выполняется действительным и аутентифицированным пользователем.
В комплекте идет удобный Python decorator! Я буду использовать декоратор, чтобы обернуть функцию create_survey(...)
view, которая проверит, что запросчик содержит допустимый токен JWT в своем заголовке, и отклонит любые запросы, которые этого не делают. Я вызову этот декоратор token_required
и реализую его выше всех других функций представления в api.py вот так:
""" api.py - provides the API endpoints for consuming and producing REST requests and responses """ # # omitting inputs and other view functions # def token_required(f): @wraps(f) def _verify(*args, **kwargs): auth_headers = request.headers.get('Authorization', '').split() invalid_msg = { 'message': 'Invalid token. Registeration and / or authentication required', 'authenticated': False } expired_msg = { 'message': 'Expired token. Reauthentication required.', 'authenticated': False } if len(auth_headers) != 2: return jsonify(invalid_msg), 401 try: token = auth_headers[1] data = jwt.decode(token, current_app.config['SECRET_KEY']) user = User.query.filter_by(email=data['sub']).first() if not user: raise RuntimeError('User not found') return f(user, *args, **kwargs) except jwt.ExpiredSignatureError: return jsonify(expired_msg), 401 # 401 is Unauthorized HTTP status code except (jwt.InvalidTokenError, Exception) as e: print(e) return jsonify(invalid_msg), 401 return _verify
Основная логика этого декоратора заключается в том, чтобы:
- Убедитесь, что он содержит заголовок “Authorization” со строкой, которая выглядит как токен JWT
- Проверьте, что срок действия JWT не истек, о чем PyJWT позаботится за меня, выдав ошибку подписи
Expired
, если она больше не действительна - Проверьте, что JWT является допустимым токеном, о котором PyJWT также заботится, бросая
Недопустимая ошибка токена
если она недопустима - Если все допустимо, то связанный пользователь запрашивается из базы данных и возвращается в функцию, которую оборачивает декоратор
Теперь все, что осталось, это добавить декоратор в метод create_survey(...)
вот так:
""" api.py - provides the API endpoints for consuming and producing REST requests and responses """ # # omitting inputs and other functions # @api.route('/surveys/', methods=('POST',)) @token_required def create_survey(current_user): data = request.get_json() survey = Survey(name=data['name']) questions = [] for q in data['questions']: question = Question(text=q['question']) question.choices = [Choice(text=c) for c in q['choices']] questions.append(question) survey.questions = questions survey.creator = current_user db.session.add(survey) db.session.commit() return jsonify(survey.to_dict()), 201
Реализация аутентификации JWT в Vue.js СПА
С бэкендом уравнение аутентификации завершено Теперь мне нужно застегнуть клиентскую сторону, реализовав аутентификацию JWT в Vue.js. Я начинаю с создания нового модуля в приложении под названием “utils” в каталоге src и размещения index.js файл внутри папки utils. Этот модуль будет содержать две вещи:
- Шина событий, которую я могу использовать для отправки сообщений по всему приложению, когда происходят определенные вещи, такие как неудачная аутентификация в случае истечения срока действия JWT
- Функция для проверки JWT, чтобы увидеть, если он все еще действителен или нет
Эти две вещи реализуются следующим образом:
// utils/index.js import Vue from 'vue' export const EventBus = new Vue() export function isValidJwt (jwt) { if (!jwt || jwt.split('.').length < 3) { return false } const data = JSON.parse(atob(jwt.split('.')[1])) const exp = new Date(data.exp * 1000) // JS deals with dates in milliseconds since epoch const now = new Date() return now < exp }
Переменная EventBus
– это всего лишь экземпляр объекта Vue. Я могу использовать тот факт, что объект Vue имеет как $emit
, так и пару методов $on
/| $off , которые используются для генерации событий, а также для регистрации и отмены регистрации событий.
Функция invalid(jwt)
– это то, что я буду использовать, чтобы определить, является ли пользователь аутентифицированным на основе информации в JWT. Напомним из более раннего базового объяснения JWTs, что стандартный набор свойств находится в закодированном объекте JSON вида “[HEADER].[ПОЛЕЗНАЯ НАГРУЗКА].[ПОДПИСЬ]”. Например, скажем, у меня есть следующий JWT ‘eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJleGFtcGxlQG1haWwuY29tIiwiaWF0IjoxNTIyMzI2NzMyLCJleHAiOjE1MjIzMjg1MzJ9.1n9fx0vL9GumDGatwm2vfUqQl3yZ7Kl4t5NWMvW-pgw’. Я могу декодировать средний раздел тела, чтобы проверить его содержимое, используя следующий JavaScript:
const token = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJleGFtcGxlQG1haWwuY29tIiwiaWF0IjoxNTIyMzI2NzMyLCJleHAiOjE1MjIzMjg1MzJ9.1n9fx0vL9GumDGatwm2vfUqQl3yZ7Kl4t5NWMvW-pgw' const tokenParts = token.split('.') const body = JSON.parse(atob(tokenParts[1])) console.log(body) // {sub: "[email protected]", iat: 1522326732, exp: 1522328532}
Здесь содержимым тела токена являются sub
, представляющий электронную почту подписчика, iat
, который выдается с отметкой времени в секундах, и exp
, который является временем истечения срока действия токена в секундах от эпохи (количество секунд, прошедших с 1 января 1970 года (полночь UTC/GMT), не считая високосных секунд (в ISO 8601: 1970-01-01T00:00:00Z)). Как вы можете видеть, я использую значение exp
в функции is Valid Jwt(jwt)
, чтобы определить, истек ли срок действия JWT или нет.
Далее нужно добавить пару новых функций AJAX для выполнения вызовов REST API Flask для регистрации новых пользователей и входа в систему существующих, плюс мне нужно будет изменить функцию postNewSurvey (...)
, чтобы включить заголовок, содержащий JWT.
// api/index.js // // omitting stuff ... skipping to the bottom of the file // export function postNewSurvey (survey, jwt) { return axios.post(`${API_URL}/surveys/`, survey, { headers: { Authorization: `Bearer: ${jwt}` } }) } export function authenticate (userData) { return axios.post(`${API_URL}/login/`, userData) } export function register (userData) { return axios.post(`${API_URL}/register/`, userData) }
Хорошо, теперь я могу использовать эти вещи в магазине для управления состоянием, необходимым для обеспечения надлежащей функциональности аутентификации. Для начала я импортирую EventBus
и допустимую функцию Jwt(...)
из модуля utils, а также две новые функции AJAX из модуля api. Затем добавьте определение объекта user
и строку токена jwt
в объект состояния хранилища следующим образом:
// store/index.js import Vue from 'vue' import Vuex from 'vuex' // imports of AJAX functions will go here import { fetchSurveys, fetchSurvey, saveSurveyResponse, postNewSurvey, authenticate, register } from '@/api' import { isValidJwt, EventBus } from '@/utils' Vue.use(Vuex) const state = { // single source of data surveys: [], currentSurvey: {}, user: {}, jwt: '' } // // omitting all the other stuff below //
Далее мне нужно добавить пару методов действий, которые будут вызывать либо register (...)
, либо authenticate(...)
AJAX-функции, которые мы только что определили. Я называю того , кто отвечает за аутентификацию пользователя login (...)
, который вызывает функцию authenticate(...)
AJAX, и когда он возвращает успешный ответ, содержащий новый JWT , он фиксирует мутацию, которую я назову setJwtToken
, которая должна быть добавлена к объекту мутаций. В случае неудачного запроса аутентификации я связываю метод catch
с цепочкой обещаний, чтобы поймать ошибку, и использую EventBus
для выдачи события, уведомляющего всех подписчиков о том, что аутентификация не удалась.
Метод действия register(...)
очень похож на login(...)
, на самом деле он фактически использует login(...)
. Я также показываю небольшую модификацию метода submit New Survey(...)
action, который передает токен JWT в качестве дополнительного параметра вызову postNewSurvey(...)
AJAX.
const actions = { // asynchronous operations // // omitting the other action methods... // login (context, userData) { context.commit('setUserData', { userData }) return authenticate(userData) .then(response => context.commit('setJwtToken', { jwt: response.data })) .catch(error => { console.log('Error Authenticating: ', error) EventBus.$emit('failedAuthentication', error) }) }, register (context, userData) { context.commit('setUserData', { userData }) return register(userData) .then(context.dispatch('login', userData)) .catch(error => { console.log('Error Registering: ', error) EventBus.$emit('failedRegistering: ', error) }) }, submitNewSurvey (context, survey) { return postNewSurvey(survey, context.state.jwt.token) } }
Как уже упоминалось ранее, мне нужно добавить новую мутацию, которая явно устанавливает JWT и пользовательские данные.
const mutations = { // isolated data mutations // // omitting the other mutation methods... // setUserData (state, payload) { console.log('setUserData payload = ', payload) state.userData = payload.userData }, setJwtToken (state, payload) { console.log('setJwtToken payload = ', payload) localStorage.token = payload.jwt.token state.jwt = payload.jwt } }
Последнее, что я хотел бы сделать в магазине, – это добавить метод getter, который будет вызван в нескольких других местах приложения, который будет указывать, аутентифицирован ли текущий пользователь или нет. Я достигаю этого, вызывая функцию is Valid Jwt(jwt)
из модуля utils в геттере следующим образом:
const getters = { // reusable data accessors isAuthenticated (state) { return isValidJwt(state.jwt.token) } }
Ладно, я уже близко. Мне нужно добавить новый Vue.js компонент для страницы входа/регистрации в приложении. Я создаю файл Login.vue в каталоге components. В разделе шаблона я даю ему два поля ввода, одно для электронной почты, которая будет служить именем пользователя, а другое для пароля. Под ними находятся две кнопки: одна для входа в систему, если вы уже являетесь зарегистрированным пользователем, и другая для регистрации.
Очевидно, что этот компонент будет нуждаться в некотором локальном состоянии, связанном с пользователем, как указано моим использованием v-model
в полях ввода, поэтому я добавляю это в свойство data компонента далее. Я также добавляю свойство error Msg
data, которое будет содержать любые сообщения, испускаемые EventBus
в случае неудачной регистрации или аутентификации. Чтобы использовать EventBus
Я подписываюсь на события “неудачная регистрация” и “неудачная аутентификация” в mounted
Vue.js этап жизненного цикла компонентов и отмените их регистрацию на этапе beforeDestroy
. Еще одна вещь, которую следует отметить, – это использование обработчиков событий @click
, вызываемых при нажатии кнопок Входа и регистрации. Они должны быть реализованы как компонентные методы, authenticate()
и register()
.
Хорошо, теперь мне просто нужно, чтобы остальная часть приложения знала, что компонент входа существует. Я делаю это, импортируя его в модуль маршрутизатора и определяя его маршрут. Пока я нахожусь в модуле маршрутизатора, мне нужно внести дополнительные изменения в маршрут компонента New Survey
, чтобы защитить его доступ только для аутентифицированных пользователей, как показано ниже:
// router/index.js import Vue from 'vue' import Router from 'vue-router' import Home from '@/components/Home' import Survey from '@/components/Survey' import NewSurvey from '@/components/NewSurvey' import Login from '@/components/Login' import store from '@/store' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'Home', component: Home }, { path: '/surveys/:id', name: 'Survey', component: Survey }, { path: '/surveys', name: 'NewSurvey', component: NewSurvey, beforeEnter (to, from, next) { if (!store.getters.isAuthenticated) { next('/login') } else { next() } } }, { path: '/login', name: 'Login', component: Login } ] })
Здесь стоит упомянуть, что я использую router guard vue-router перед входом
, чтобы проверить, аутентифицирован ли текущий пользователь через IsAuthenticated
getter из магазина. Если IsAuthenticated
возвращает false, то я перенаправляю приложение на страницу входа.
Закодировав компонент входа в систему и определив его маршрут, я могу обеспечить доступ к нему через компонент router-link в компоненте заголовка в components/Header.vue. Я условно показываю либо ссылку на компонент New Survey
, либо компонент Login
, используя геттер IsAuthenticated
store еще раз в вычисляемом свойстве в компоненте Header
, на который ссылаются директивы v-if
, например:
Отлично! Теперь я наконец могу запустить серверы dev для приложения Flask и Vue.js приложение и тест, чтобы увидеть, могу ли я зарегистрировать и войти в систему пользователя.
Сначала я запускаю сервер разработки Flask.
(venv) $ python appserver.py
Затем сервер webpack dev для компиляции и обслуживания Vue.js приложение.
$ npm run dev
В моем браузере я посещаю http://localhost:8080
(или любой другой порт, указанный сервером webpack dev) и убедитесь, что на навигационной панели теперь отображается “Login/Register” вместо “Create Survey”, как показано ниже:
Затем я нажимаю на ссылку “Войти/Зарегистрироваться” и заполняю входные данные для электронной почты и пароля, затем нажимаю “Зарегистрироваться”, чтобы убедиться, что он работает должным образом, и я перенаправляюсь обратно на домашнюю страницу и вижу ссылку “Создать опрос”, отображаемую вместо ссылки “Войти/Зарегистрироваться”, которая была там до регистрации.
Ладно, моя работа в основном закончена. Единственное, что осталось сделать, это добавить небольшую обработку ошибок в submit Survey(...)
Vue.js метод компонента New Survey
для обработки события, когда токен истекает, когда пользователь создает новый опрос, например:
Ресурсы
Хотите узнать больше о различных фреймворках, используемых в этой статье? Попробуйте проверить некоторые из следующих ресурсов для более глубокого погружения в использование Vue.js или создание внутренних API-интерфейсов на Python:
Вывод
В этом посте я продемонстрировал, как реализовать аутентификацию JWT в приложении опроса с помощью Vue.js и Фляжка. JWT-это популярный и надежный метод обеспечения аутентификации в SPA-приложениях, и я надеюсь, что после прочтения этого поста вы почувствуете себя комфортно, используя эти технологии для защиты ваших приложений. Тем не менее, я рекомендую посетить статью Скотта StackAbuse для более глубокого понимания того, как и почему работает JWT.
Как всегда, спасибо за чтение и не стесняйтесь комментировать или критиковать ниже.