ASP.NET Core | Авторизация по ролям

Asp.net core | jwt-токены

Последнее обновление: 27.12.2022

Общие подходы к авторизации и аутентификации в ASP.NET Core Web API несколько отличаются от того, что мы имеем в MVC. В частности, в Web API механизм авторизации полагается преимущественно
на JWT-токены. Что такое JWT-токен?

JWT (или JSON Web Token) представляет собой веб-стандарт, который определяет способ передачи данных о пользователе в формате JSON в зашифрованном виде.

JWT-токен состоит из трех частей:

Для использования JWT-токенов создадим новый проект ASP.NET Core по типу Empty.

JWT Token in ASP.NET Core Web API

Далее добавим в проект папку Models, в которой определим новый класс Person:

public class Person
{
    public string Login { get; set; }
    public string Password { get; set; }
    public string Role { get; set; }
}

Этот класс будет описывать учетные записи пользователей в приложении.

Для работы с JWT-токенами установим через Nuget пакет Microsoft.AspNetCore.Authentication.JwtBearer.

Также добавим в проект специальный класс AuthOptions, который будет описывать ряд свойств, нужных для генерации токена:

using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace TokenApp
{
    public class AuthOptions
    {
        public const string ISSUER = "MyAuthServer"; // издатель токена
        public const string AUDIENCE = "MyAuthClient"; // потребитель токена
        const string KEY = "mysupersecret_secretkey!123";   // ключ для шифрации
        public const int LIFETIME = 1; // время жизни токена - 1 минута
        public static SymmetricSecurityKey GetSymmetricSecurityKey()
        {
            return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KEY));
        }
    }
}

Константа ISSUER представляет издателя токена. Здесь можно определить любое название. AUDIENCE представляет потребителя токена – опять же может быть любая строка,
но в данном случае указан адрес текущего приложения.

Константа KEY хранит ключ, который будет применяться для создания токена.

Для встраивания функциональности JWT-токенов в конвейер обработки запроса используется компонент JwtBearerAuthenticationMiddleware.
Так, изменим класс Startup следующим образом:

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

namespace TokenApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                    .AddJwtBearer(options =>
                    {
                        options.RequireHttpsMetadata = false;
                        options.TokenValidationParameters = new TokenValidationParameters
                        {
                            // укзывает, будет ли валидироваться издатель при валидации токена
                            ValidateIssuer = true,
                            // строка, представляющая издателя
                            ValidIssuer = AuthOptions.ISSUER,

                            // будет ли валидироваться потребитель токена
                            ValidateAudience = true,
                            // установка потребителя токена
                            ValidAudience = AuthOptions.AUDIENCE,
                            // будет ли валидироваться время существования
                            ValidateLifetime = true,

                            // установка ключа безопасности
                            IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(),
                            // валидация ключа безопасности
                            ValidateIssuerSigningKey = true,
                        };
                    });
            services.AddControllersWithViews();
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseDeveloperExceptionPage();

            app.UseDefaultFiles();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapDefaultControllerRoute();
            });
        }
    }
}

Для установки аутентификации с помощью токенов в методе ConfigureServices в вызов services.AddAuthentication передается значение JwtBearerDefaults.AuthenticationScheme. Далее с помощью метода AddJwtBearer() добавляется конфигурация токена.

Для конфигурации токена применяется объект JwtBearerOptions, который позволяет с помощью свойств настроить работу с токеном. В данном случае использованы следующие свойства:

  • RequireHttpsMetadata: если равно false, то SSL при отправке токена не используется. Однако данный вариант установлен
    только дя тестирования. В реальном приложении все же лучше использовать передачу данных по протоколу https.

  • TokenValidationParameters: параметры валидации токена – сложный объект, определяющий, как токен будет валидироваться. Этот объект
    в свою очередь имеет множество свойств, которые позволяют настроить различные аспекты валидации токена. Но наиболее важные свойства:
    IssuerSigningKey – ключ безопасности, которым подписывается токен, и ValidateIssuerSigningKey – надо ли валидировать ключ безопасности.
    Ну и кроме того, можно установить ряд других свойств, таких как нужно ли валидировать издателя и потребителя токена, срок жизни токена, можно установить
    название claims для ролей и логинов пользователя и т.д.

Теперь мы можем использовать авторизацию на основе токенов. Однако в прокте пока не предусмотрена генерация токенов. По умолчанию в ASP.NET Core отсутствуют встроенные
возможности для создания токена. И в данном случае мы можем либо воспользоваться сторонними решениями (например,
IdentityServer или
OpenIdDict), либо же создать свой механизм. Выберем второй способ.

Создадим в проекте новую папку Controllers и добавим в нее новый контроллер AccountController:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using TokenApp.Models; // класс Person

namespace TokenApp.Controllers
{
    public class AccountController : Controller
    {
        // тестовые данные вместо использования базы данных
        private List<Person> people = new List<Person>
        {
            new Person {Login="[email protected]", Password="12345", Role = "admin" },
            new Person { Login="[email protected]", Password="55555", Role = "user" }
        };

        [HttpPost("/token")]
        public IActionResult Token(string username, string password)
        {
            var identity = GetIdentity(username, password);
            if (identity == null)
            {
                return BadRequest(new { errorText = "Invalid username or password." });
            }

            var now = DateTime.UtcNow;
            // создаем JWT-токен
            var jwt = new JwtSecurityToken(
                    issuer: AuthOptions.ISSUER,
                    audience: AuthOptions.AUDIENCE,
                    notBefore: now,
                    claims: identity.Claims,
                    expires: now.Add(TimeSpan.FromMinutes(AuthOptions.LIFETIME)),
                    signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256));
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

            var response = new
            {
                access_token = encodedJwt,
                username = identity.Name
            };

            return Json(response);
        }

        private ClaimsIdentity GetIdentity(string username, string password)
        {
            Person person = people.FirstOrDefault(x => x.Login == username && x.Password == password);
            if (person != null)
            {
                var claims = new List<Claim>
                {
                    new Claim(ClaimsIdentity.DefaultNameClaimType, person.Login),
                    new Claim(ClaimsIdentity.DefaultRoleClaimType, person.Role)
                };
                ClaimsIdentity claimsIdentity =
                new ClaimsIdentity(claims, "Token", ClaimsIdentity.DefaultNameClaimType,
                    ClaimsIdentity.DefaultRoleClaimType);
                return claimsIdentity;
            }

            // если пользователя не найдено
            return null;
        }
    }
}

Для упрощения ситуации данные пользователей определены в виде простого списка. Для поиска пользователя в этом списке по логину и паролю определен метод
GetIdentity(), который возвращает объект ClaimsIdentity.

Принцип создания ClaimsIdentity здесь тот же, что и при аутентификации с помощью кук: создается набор объектов Claim, которые включают различные данные о пользователе,
например, логин, роль и т.д.

Для обработки запроса в контроллере создан метод Token, который сопоставлен с маршрутом “/token”. Этот метод обрабатывает запросы POST и через
параметры принимает логин и пароль пользователя.

Сам токен представляет объект JwtSecurityToken, для инициализации которого применяются все те же константы и ключ безопасности,
которые определены в классе AuthOptions и которые использовались в классе Startup для настройки JwtBearerAuthenticationMiddleware. Важно, чтобы эти значения совпадали.

С помощью параметра claims: identity.Claims в токен добавляются набор объектов Claim, которые содержат информацию о логине и роли пользователя.

Далее посредством метода JwtSecurityTokenHandler().WriteToken(jwt) создается Json-представление токена. И в конце он сериализуется и отправляет клиенту с помощью метода Json().

Таким образом генерируется токен.

Для тестирования токена создадим простенький контроллер ValuesController:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;

namespace TokenApp.Controllers
{
	[ApiController]
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        [Authorize]
        [Route("getlogin")]
        public IActionResult GetLogin()
        {
            return Ok($"Ваш логин: {User.Identity.Name}");
        }
		
        [Authorize(Roles = "admin")]
        [Route("getrole")]
        public IActionResult GetRole()
        {
            return Ok("Ваша роль: администратор");
        }
    }
}

И в конце добавим в проект для статических файлов папку wwwroot, а в нее – новый файл index.html:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>JWT в ASP.NET Core Web API</title>
</head>
<body>
    <div id="userInfo" style="display:none;">
        <p>Вы вошли как: <span id="userName"></span></p>
        <input type="button" value="Выйти" id="logOut" />
    </div>
    <div id="loginForm">
        <h3>Вход на сайт</h3>
        <label>Введите email</label><br />
        <input type="email" id="emailLogin" /> <br /><br />
        <label>Введите пароль</label><br />
        <input type="password" id="passwordLogin" /><br /><br />
        <input type="submit" id="submitLogin" value="Логин" />
    </div>
    <div>
        <input type="submit" id="getDataByLogin" value="Данные по логину" />
    </div>
    <div>
        <input type="submit" id="getDataByRole" value="Данные по роли" />
    </div>

    <script>
        var tokenKey = "accessToken";

        // отпавка запроса к контроллеру AccountController для получения токена
        async function getTokenAsync() {

            // получаем данные формы и фомируем объект для отправки
            const formData = new FormData();
            formData.append("grant_type", "password");
            formData.append("username", document.getElementById("emailLogin").value);
            formData.append("password", document.getElementById("passwordLogin").value);

            // отправляет запрос и получаем ответ
            const response = await fetch("/token", {
                method: "POST",
                headers: {"Accept": "application/json"},
                body: formData
            });
            // получаем данные 
            const data = await response.json();

            // если запрос прошел нормально
            if (response.ok === true) {

                // изменяем содержимое и видимость блоков на странице
                document.getElementById("userName").innerText = data.username;
                document.getElementById("userInfo").style.display = "block";
                document.getElementById("loginForm").style.display = "none";
                // сохраняем в хранилище sessionStorage токен доступа
                sessionStorage.setItem(tokenKey, data.access_token);
                console.log(data.access_token);
             }
            else {
                // если произошла ошибка, из errorText получаем текст ошибки
                console.log("Error: ", response.status, data.errorText);
            }
        };
        // отправка запроса к контроллеру ValuesController
        async function getData(url) {
            const token = sessionStorage.getItem(tokenKey);

            const response = await fetch(url, {
                method: "GET",
                headers: {
                    "Accept": "application/json",
                    "Authorization": "Bearer "   token  // передача токена в заголовке
                }
            });
            if (response.ok === true) {
                
                const data = await response.json();
                alert(data)
            }
            else
                console.log("Status: ", response.status);
        };

        // получаем токен
        document.getElementById("submitLogin").addEventListener("click", e => {

            e.preventDefault();
            getTokenAsync();
        });

        // условный выход - просто удаляем токен и меняем видимость блоков
        document.getElementById("logOut").addEventListener("click", e => {

            e.preventDefault();
            document.getElementById("userName").innerText = "";
            document.getElementById("userInfo").style.display = "none";
            document.getElementById("loginForm").style.display = "block";
            sessionStorage.removeItem(tokenKey);
        });


        // кнопка получения имя пользователя  - /api/values/getlogin
        document.getElementById("getDataByLogin").addEventListener("click", e => {

            e.preventDefault();
            getData("/api/values/getlogin");
        });

        // кнопка получения роли  - /api/values/getrole
        document.getElementById("getDataByRole").addEventListener("click", e => {

            e.preventDefault();
            getData("/api/values/getrole");
        });
    </script>
</body>
</html>

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

После нажатия кнопки на форме логина запрос будет отправляться методом POST на адрес “/token”. Поскольку за обработку запросов по этому маршруту
отвечает метод Token контроллера AccountController, по результатам работы которого будет формироваться токен.

Ответом сервера в случае удачной аутентификации будет примерно следующий объект:

{
	access_token : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93
					cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoicXdlcnR5IiwiaHR0cDovL3NjaGVtYXMub
					Wljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoidXNlciIsIm5iZi
					I6MTQ4MTYzOTMxMSwiZXhwIjoxNDgxNjM5MzcxLCJpc3MiOiJNeUF1dGhTZXJ2ZXIiLCJhdWQiOiJ
					odHRwOi8vbG9jYWxob3N0OjUxODg0LyJ9.dQJF6pALUZW3wGBANy_tCwk5_NR0TVBwgnxRbblp5Ho",
	username: "[email protected]"
}

Параметр access_token как раз и будет представлять токен доступа. Также в объекте передается дополнительная информация о нике
пользователя.

Для того, чтобы в коде js данный токен в дальнейшем был доступен, то он сохраняется в хранилище sessionStorage.

Последние два блока предназначены для отправки запросов к методам ValuesController. Чтобы отправить токен в запросе, нам нужно настроить в запросе заголовок Authorization:

headers: {
	"Accept": "application/json",
	"Authorization": "Bearer "   token  // передача токена в заголовке
}

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

JWT в ASP.NET Core Web API

По ранее сохраненному ключу получаем из хранилища sessionStorage токен и формируем заголовок.

Теперь после получения токена мы можем осуществить запрос к методам контроллера ValuesController:

Роли в токене JWT в ASP.NET Core Web API

В то же время если мы попробуем обратиться к тем же методам без токена или с токеном с истекшим сроком, то получим ошибку 401 (Unauthorized).

НазадСодержаниеВперед

Похожее:  Форма регистрации на сайте - 60 бесплатных HTML5 и CSS3 форм авторизации для сайта

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

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