авторизация в asp.net core mvc / хабр

Что делать дальше?

И вот мы подбираемся к самому интересному: а где хранить пароли и в каком виде? Давайте сначала подумаем, где их хранить.

Что такое сессии?

Попробую рассказать в паре абзацев.

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

На самом деле сессии хранятся не обязательно в файлах. Например, cms Modx хранит их базе данных.
Но сейчас нам это не важно, главное, что сессии предоставляют удобный способ работать с данными, уникальными для пользователя.
Для нас сессия выглядит как обычный ассоциативный массив под названием $_SESSION, в разные поля которого можно записать хоть число, хоть строку, хоть сериализованный объект.

Auth-сервис

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

Пользователи будут храниться в БД. В идеале это должна быть отдельная БД, но в обучающих целях будем использовать ту же, что использовали в первой части. Опишем DAO пользователя:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "clients")
public class ClientEntity {
    @Id
    @Column(name = "client_id")
    private String clientId;
    private String hash;
}

Репозиторий будет выглядеть совсем просто:

public interface ClientRepository
        extends CrudRepository<ClientEntity, String> {
}

Т.к. мы будем реализовывать Client Credentials Flow в терминах протокола OAuth, то здесь client — это и есть пользователь. Соответственно clientId, clientSecret — аутентификационные данные пользователя. В открытом виде пароль пользователя хранить нельзя, поэтому будем хранить некий хеш, о котором будет написано ниже.

Похожее:  Ответы на часто встречающиеся вопросы - Портал услуг Федеральной службы государственной регистрации, кадастра и картографии

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

public interface ClientService {
    void register(String clientId, String clientSecret);
    void checkCredentials(String clientId, String clientSecret);
}

Для правильного хранения паролей в БД будем использовать Bcrypt. Это криптографическая хеш-функция, основанная на шифре Blowfish. Воспользуемся реализацией в библиотеке jBCrypt, добавим зависимость в проект:

dependencies {
    ...
    implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.4'
}

Реализуем интерфейс ClientService:

Claims

Принципы авторизации и аутентификации в ASP.NET Core MVC не изменились по сравнению с предыдущей версией фреймворка, отличаясь лишь в деталях. Одним из относительно новых понятий является claim-based авторизация, с нее мы и начнем наше путешествие. Что же такое claim?

Это пара строк “ключ-значение”, в качестве ключа может выступать “FirstName”, “EmailAddress” и т.п. Таким образом, claim можно трактовать как свойство пользователя, как строку с данными, или даже как некоторое утверждение вида “у пользователя есть что-то”.

Знакомая многим разработчикам одномерная role-based модель органично содержится в многомерной claim-based модели: роль (утверждение вида “у пользователя есть роль X”) представляет собой один из claim и содержится в списке преопределенных System.Security.Claims.ClaimTypes. Не возбраняется создавать и свои claim.

Следующее важное понятие — identity. Это единое утверждение, содержащее набор claim. Так, identity можно трактовать как цельный документ (паспорт, водительские права и др.), в этом случае claim — строка в паспорте (дата рождения, фамилия…). В Core MVC используется класс System.Security.Claims.ClaimsIdentity.

Jwt (json web tokens)

JWT — это стандарт, основанный на JSON, для создания токенов доступа. Он состоит из трех частей: заголовка, тела и подписи:

В заголовке указывается тип токена и алгоритм подписи:

{
  "typ": "JWT",
  "alg": "HS256"
}

В тело записывается необходимая пользовательская информация (payload). Для аутентификации и авторизации это может быть id пользователя, его роль, время действия токена:

Permission-based авторизация. свой фильтр авторизации

Декларативное перечисление всех запрашиваемых операций (в первую очередь из числа CRUD) при авторизации пользователя, такое как:

Resource-based авторизация

Как уже говорилось ранее, policy-based авторизация выполняется Core MVC в filter pipeline, т.е. ДО вызова защищаемого action. Успех авторизации при этом зависит только от пользователя — либо он обладает нужными claim, либо нет. А что, если необходимо учесть также защищаемый ресурс и его свойства, получить какие данные из внешних источников?

Пример из жизни: защищаем action вида GET /Orders/{id}, считывающий по id строку с заказом из БД. Пусть наличие у пользователя прав на конкретный заказ мы сможем определить только после получения этого заказа из БД. Это автоматически делает непригодными рассмотренные ранее аспектно-ориентированные сценарии на основе фильтров MVC, выполняемых перед тем, как пользовательский код получает управление. К счастью, в Core MVC есть способы провести авторизацию вручную.

Для этого, в контроллере нам потребуется реализацияIAuthorizationService. Получим ее, как обычно, через внедрение зависимости в конструктор:

public class ResourceController : Controller
{
    IAuthorizationService _authorizationService;
    public ResourceController(IAuthorizationService authorizationService)
    {
        _authorizationService = authorizationService;
    }
}

Затем создадим новую политику и handler:

Авторизация

Нам осталось добавить авторизацию в написанное приложение Bookstore. Откроем проект из предыдущей части и добавим в зависимости библиотеку для работы с JWT (как было показано выше). Также добавим две новых настройки в application.properties:

auth.enabled=true
auth.jwt.secret=30faa058f27f690c7e9a098d54ebcfb3d8725bcb85ee7907a2d84c69622229e2

Одна из них вам уже знакома, а вторую будем использовать для включение/отключения авторизации.

Для проверки полученного токена опишем интерфейс TokenService и его реализацию:

public interface TokenService {
    boolean checkToken(String token);
}

@Service
@Slf4j
public class DefaultTokenService implements TokenService {
    @Value("${auth.jwt.secret}")
    private String secretKey;

    @Override
    public boolean checkToken(String token) {
        Algorithm algorithm = Algorithm.HMAC256(secretKey);
        JWTVerifier verifier = JWT.require(algorithm).build();

        try {
            DecodedJWT decodedJWT = verifier.verify(token);
            if (!decodedJWT.getIssuer().equals("auth-service")) {
                log.error("Issuer is incorrect");
                return false;
            }

            if (!decodedJWT.getAudience().contains("bookstore")) {
                log.error("Audience is incorrect");
                return false;
            }
        } catch (JWTVerificationException e) {
            log.error("Token is invalid: "   e.getMessage());
            return false;
        }

        return true;
    }
}

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

Закроем все эндпоинты и разрешим доступ только при наличии авторизационного токена. Значение авторизационного токена необходимо положить в заголовок следующим образом:

Authorization: Bearer <значение токена>

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

Выбираем правильное хэширование

Идею хранения паролей нашли, то есть хранения не паролей, а их хэшей. А вот какой алгоритм хэширования выбрать?


Давайте посмотрим на то, что пробовали выше – простая функция md5. Алгоритма его расшифровки нет, но тем не менее md5 не рекомендуется для использования. Почему?

Помимо сложности самого алгоритма, есть и другой момент. Да, расшифровать пароль по хэшу нельзя, но его можно подобрать. Простым брутфорсом.
К тому же существуют многочисленные базы паролей md5, где всевозможные варианты хранятся тупо списком.
И подбор вашего пароля по известному хэшу займет столько времени, сколько понадобится для выполнения sql-запроса

    select password from passwords where hash='e10adc3949ba59abbe56e057f20f883e';

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

Помимо md5 есть множество алгоритмов хэширования, sha256, sha512 и еще целая толпа. Их сложность выше, но это не отменяет опять-таки существования таблиц с готовыми паролями.
Нужно что-то хитрее.

Заглушка для авторизации

Функции, связанные с авторизацией, будут лежать в отдельном файле и своем пространстве имен. Создадим файл auth.php в api/v1/common – там, где уже лежит helpers.php.
Если вы разбирали уроки админки, особенно третий, про серверную часть, то эти пути вам будут знакомы. Если у вас свой проект, то кладите auth.php куда удобно.
Главное, потом правильно указать пути.

Содержимое auth.php

Как обойтись без шифрования. хэширование

Фокус в том, что не нужно хранить пароли в открытом виде, но и не нужно шифровать их с возможностью расшифровки. Пароли нужно хэшировать и в базе хранить не пароль, а его хэш.
Хитрым образом закодированную строку, которую нельзя расшифровать. Например, не password, а 5f4dcc3b5aa765d61d8327deb882cf99


Вы можете спросить, что это за хрень и как же сравнить пароль, введенный пользователем, с паролем, лежащим в базе. А нам и не нужно сравнивать пароли – достаточно сравнить их хэши.

Например, есть простая функция хэширования md5. Вот так она работает

Настройки политик доступа

Создание политик доступа через рассмотренный выше fluent-интерфейс не дает той гибкости, которая требуется в реальных приложениях. Конечно, можно явно указать допустимые значения claim через вызов RequireClaim(“x”, params values), можно скомбинировать через логическое И несколько условий, вызвав RequireClaim(“x”).

RequireClaim(“y”). Наконец, можно навесить на controller и action разные политики, что, впрочем, приведет к той же комбинации условий через логическое И. Очевидно, что необходим более гибкий механизм создания политик, и он у нас есть: requirements и handlers.

services.AddAuthorization(options =>
{
    options.AddPolicy("age-policy", 
        policy => policy.Requirements.Add(new AgeRequirement(42), new FooRequirement()));
});

Подготовительные работы

Здесь и далее на протяжении статьи, мы будем настраивать доступ для различных страниц веб-сайта. Для запуска представленного кода, достаточно создать в Visual Studio 2022 новое приложение типа “ASP.NET Core Web Application”, задать шаблон Web Application и тип аутентификации “No Authentication”.

Проверяем работоспособность vue

Вроде бы, чего там может сломаться? Нам же практически не пришлось лезть в код vue, за исключением добавления кнопки Выйти в шапке админки.

Но попробуем пересобрать админку, чтобы убедиться, что все хорошо. Сначала в production режиме

    npm run build

Создание форм регистрации и авторизации на php

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

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

Ниже приведен HTML-код необходимый для создания формы регистрации. Сохраните его вфайле register.php.

Несмотря на простоту данной формы, для проведения простейшей валидации данных в ней используется HTML5. К примеру, использование type=”email” обеспечит уведомление пользователя о том, что он неправильно ввел адрес электронной почты. Кроме того, применение pattern позволит провести проверку имени пользователя – логин может состоять только из латинских букв и цифр.

Наиболее продвинутый способ валидации данных подразумевает использование jQuery – в этом случае разработчик получает полный контроль над показом, расположением и внешним видом сообщений об ошибках ввода. Подробнее о валидации на стороне клиента с использованием jQuery рассказывается в этой статье.

HTML-код страницы входа в систему приведен ниже. Сохраните его в файле login.php.

Для улучшения внешнего вида форм примените к ним следующие CSS-стили:

В коде, приведенном выше, предусмотрено оформление заголовков и сообщений об ошибках валидации. Фрагменты HTML и CSS кода, рассмотренные выше, могут использоваться в качестве основы, поскольку ваш собственный проект может нуждаться в другом стиле оформления, а также в дополнительных полях ввода.

Следующий шаг – создание таблицы базы данных, содержащей учетные данные пользователей. В нашем случае таблица состоит всего из четырех столбцов:

  1. Порядковый номер ID, который для каждого нового пользователя увеличивается автоматически.
  2. Уникальное имя пользователя.
  3. Адрес электронной почты.
  4. Пароль.

Для быстрого создания таблицы базы данных можно использовать следующий SQL-запрос:

Теперь создайте файл config.php и сохраните в нем приведенный далее код для подключения к базе данных:

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

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

Сохраните приведенный далее код в начале файла registration.php:

<?php
    session_start();
    include('config.php');
    if (isset($_POST['register'])) {
        $username = $_POST['username'];
        $email = $_POST['email'];
        $password = $_POST['password'];
        $password_hash = password_hash($password, PASSWORD_BCRYPT);
        $query = $connection->prepare("SELECT * FROM users WHERE email=:email");
        $query->bindParam("email", $email, PDO::PARAM_STR);
        $query->execute();
        if ($query->rowCount() > 0) {
            echo '<p class="error">Этот адрес уже зарегистрирован!</p>';
        }
        if ($query->rowCount() == 0) {
            $query = $connection->prepare("INSERT INTO users(username,password,email) VALUES (:username,:password_hash,:email)");
            $query->bindParam("username", $username, PDO::PARAM_STR);
            $query->bindParam("password_hash", $password_hash, PDO::PARAM_STR);
            $query->bindParam("email", $email, PDO::PARAM_STR);
            $result = $query->execute();
            if ($result) {
                echo '<p class="success">Регистрация прошла успешно!</p>';
            } else {
                echo '<p class="error">Неверные данные!</p>';
            }
        }
    }
?>

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

Далее, с помощью $_POST[‘register’] мы проверяем, нажал ли пользователь кнопку «Регистрация». Следует помнить, что пароли нельзя сохранять в виде незашифрованного текста. Поэтому наш код использует функцию password_hash() и сохраняет пароль в хэшированном виде. Эта функция записывает пароль в базу данных в виде хэш-строки, состоящей из 60 случайных символов.

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

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

Приведенный далее код должен располагаться в начале файла login.php:

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

Как только мы получаем подтверждение правильности пароля, мы назначаем переменную $_SESSION[‘user_id’] для ID пользователя из базы данных. При необходимости на этом этапе передаются и значения для других переменных.

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

Все, что нужно сделать для ограничения или предоставления доступа – это использовать в начале приведенного выше скрипта строку session_start().

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

Чаще всего ошибки в работе скрипта связаны с неверными именами переменных – как правило, с использованием букв в неправильном регистре. Именно поэтому крайне важно придерживаться одного и того же шаблона при выборе имен. К примеру, ключи в функции $_POST основаны на значениях, полученных из полей ввода в формах. Это означает, что $_POST[‘USERNAME’] и $_POST[‘username’] получат разные значения.

Некоторые функции, например session_start() и header(), изменяют HTTP-заголовки. Поскольку PHP сбрасывает все заголовки перед выводом любых данных, важно вызывать все подобные функции до того, как вы начнете что-либо выводить – включая фрагменты сырого HTML или случайные пробелы перед открывающим тегом <?php.

Вы можете использовать переменные сессии только в том случае, если на странице осуществлен вызов функции session_start(). Если значения суперглобальной переменной $_SESSION вам не доступны, причина этого заключается в том, что вы забыли вызвать session_start(). Помните о том, что функцию надо вызывать перед выводом чего-либо на страницу сайта. В противном случае вы получите ошибку «Заголовки уже отправлены», рассмотренную выше.

Пожалуйста, опубликуйте ваши комментарии по текущей теме статьи. За комментарии, подписки, дизлайки, отклики, лайки огромное вам спасибо!

Пожалуйста, опубликуйте свои мнения по текущей теме статьи. За комментарии, подписки, дизлайки, отклики, лайки низкий вам поклон!

Вместо заключения

Проблемы с dev режимом во vue – это неприятный момент. Мы хотим и апишечку подергать, и все удобства vue-cli использовать. И на елку влезть, и ничего не ободрать.
Возможно, есть более изящный способ обойти эти проблемы в dev режиме, но я их пока не нашел. Поэтому приходится подпирать код лишними условиями.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *