Создание ограничений для политики авторизации

Данное руководство устарело. Актуальное руководство: Руководство по ASP.NET Core 7

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

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

public class User
{
    public int Id { get; set; }
    public string Email { get; set; }
    public string Password { get; set; }
	public string City { get; set; }
    public string Company { get; set; }
	
	// год рождения пользователя
    public int Year { get; set; }
}

Что если мы хотим ограничить доступ в зависимости от возраста пользователя. Для этого нам надо создать собственное ограничение.

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

using Microsoft.AspNetCore.Authorization;

public class AgeRequirement : IAuthorizationRequirement
{
    protected internal int Age { get; set; }
	
    public AgeRequirement(int age)
    {
        Age = age;
    }
}

Класс ограничения должен реализовать интерфейс IAuthorizationRequirement. С помощью свойства Age устанавливается минимально допустимый возраст.

Сам класс ограничения только устанавливает некоторые лимиты, больше он ничего не делает. Чтобы его использовать при обработке запроса, нам надо добавить специальный класс - обработчик. Итак, добавим в проект новый класс AgeHandler:

using System;
using System.Threading.Tasks;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;

public class AgeHandler : AuthorizationHandler<AgeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, 
        AgeRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == ClaimTypes.DateOfBirth))
        {
            var year = 0;
            if(Int32.TryParse(context.User.FindFirst(c => c.Type == ClaimTypes.DateOfBirth).Value, out year))
            {
                if ((DateTime.Now.Year - year) >= requirement.Age)
                {
                    context.Succeed(requirement);
                }
            }
        }
        return Task.CompletedTask;
    }
}

Класс обработчика должен наследоваться от класса AuthorizationHandler<T>, где параметр T представляет тип ограничения. Вся обработка производится в методе HandleRequirementAsync(). Этот метод вызывается системой авторизации при доступе к ресурсу, к которому применяется ограничение, используемое обработчиком.

В качестве параметов метод HandleRequirementAsync() получает объект применяемого ограничения и контекст авторизации AuthorizationHandlerContext, который содержит информацию о запросе. В частности, через свойство User он возвращает объект ClaimPrincipal, представляющий текущего пользователя.

А методы класса AuthorizationHandlerContext позволяют управлять авторизацией. Так, метод Succeed(requirement) вызывается, если запрос соответствует ограничению requirement.

И наоброт, метод Fail(), если запрос не соответствует ограничению.

В данном случае мы получаем для текущего пользователя claim с типом ClaimTypes.DateOfBirth. Предполагается, что этот claim содержит год рождения пользователя. И далее по этому году получаем возраст пользователя относительно текущей даты. И если возраст оказался больше минимально допустимого, то вызываем метод context.Succeed(requirement). Вызов этого метода будет означать, что работа обработчика завершилась успешно. Если этот метод не вызывается, то считается, что авторизация прошла неудачно.

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

Restrictions for Claims Policy Authorization in ASP.NET Core MVC

Далее изменим класс Startup, чтобы применить эти классы:

using ClaimsApp.Infrastructure;
using ClaimsApp.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace ClaimsApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            string connection = "Server=(localdb)\mssqllocaldb;Database=claimsstoredb;Trusted_Connection=True;";
            services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(connection));

            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(options =>
                {
                    options.LoginPath = new Microsoft.AspNetCore.Http.PathString("/Account/Register");
                });
            // встраиваем сервис AgeHandler
            services.AddTransient<IAuthorizationHandler, AgeHandler>();

            services.AddAuthorization(opts => {
                // устанавливаем ограничение по возрасту
                opts.AddPolicy("AgeLimit",
                    policy => policy.Requirements.Add(new AgeRequirement(18)));
            });

            services.AddControllersWithViews();
        }

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

            app.UseStaticFiles();

            app.UseRouting();

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

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

Здесь надо отметить два момента. Во-первых, в методе ConfigureServices() происходит установка зависимости для IAuthorizationHandler:

services.AddTransient<IAuthorizationHandler, AgeHandler>();

Во-вторых, в коллекцию Requirements добавляется кастомное ограничение:

opts.AddPolicy("AgeLimit", policy => policy.Requirements.Add(new AgeRequirement(18)));

Теперь нам остается установить нужный объект Claim. Для этого можно изменить в контроллере AccountController метод Authenticate, который вызывается при регистрации:

private async Task Authenticate(User user)
{
	var claims = new List<Claim>
	{
		new Claim(ClaimsIdentity.DefaultNameClaimType, user.Email),
		new Claim(ClaimTypes.Locality, user.City),
		new Claim("company", user.Company),
		new Claim(ClaimTypes.DateOfBirth, user.Year.ToString())
	};
	ClaimsIdentity id = new ClaimsIdentity(claims, "ApplicationCookie", ClaimsIdentity.DefaultNameClaimType,
                ClaimsIdentity.DefaultRoleClaimType);
	await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(id));
}

Полный код контроллера AccountController в итоге должен выглядеть так:

using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ClaimsApp.Models;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;

namespace ClaimsApp.Controllers
{
    public class AccountController : Controller
    {
        private ApplicationContext _context;
        public AccountController(ApplicationContext context)
        {
            _context = context;
        }

        [HttpGet]
        public IActionResult Register()
        {
            return View();
        }
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Register(RegisterModel model)
        {
            if (ModelState.IsValid)
            {
                User user = await _context.Users.FirstOrDefaultAsync(u => u.Email == model.Email);
                if (user == null)
                {
                    user = new User
                    {
                        Email = model.Email,
                        Password = model.Password,
                        Year = model.Year,
                        City = model.City,
                        Company = model.Company
                    };
                    _context.Users.Add(user);
                    await _context.SaveChangesAsync();

                    await Authenticate(user);

                    return RedirectToAction("Index", "Home");
                }
                else
                    ModelState.AddModelError("", "Некорректные логин и(или) пароль");
            }
            return View(model);
        }
        private async Task Authenticate(User user)
        {
            var claims = new List<Claim>
            {
                new Claim(ClaimsIdentity.DefaultNameClaimType, user.Email),
                new Claim(ClaimTypes.Locality, user.City),
                new Claim("company", user.Company),
                new Claim(ClaimTypes.DateOfBirth, user.Year.ToString())
            };
            ClaimsIdentity id = new ClaimsIdentity(claims, "ApplicationCookie", ClaimsIdentity.DefaultNameClaimType,
                        ClaimsIdentity.DefaultRoleClaimType);
            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(id));
        }
    }
}

И далее мы можем использовать созданную политику для ограничения доступа:

public class HomeController : Controller
{
    [Authorize(Policy = "AgeLimit")]
    public IActionResult Index()
    {
        return View();
	}
	public IActionResult About()
	{
            return Content("For all ages");
	}
}

Теперь к методу Index смогут обратиться только те, кто удовлетворяет ограничению AgeLimit (в данном случае кому исполнилось 18 лет), в то время как к методу About смогут обратиться все желающие.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850