Для начала расскажу, что приложение, которое я разрабатывал, долго существовало на небольшом «подстольном» сервере в виде прототипа, которым в работе пользовалось небольшое число сотрудников. По прошествии некоторого времени, руководство приняло решение тиражировать это приложение в пром – с переносом на пром-сервер и организацией доступов к нему сотрудникам всего структурного подразделения.

Естественно, как это всегда бывает, сопровождение выдало нам список требований, которым должны соответствовать приложения, размещаемые на пром-серверах. Одним из таких требований было реализация авторизации по учетной записи Windows, а старую авторизацию по логину/паролю использовать было нельзя. О том, с какими подводными камнями мы столкнулись в ходе реализации такой, казалось бы, простой фичи, и как мы их решили, и пойдет речь в этом посте. Как я и упомянул ранее, в начальной точке этой истории у нас было классическое MVC-приложение. Информация о пользователях, их ролях (Admin, Common) и доступах к определенным действиям и процедурам хранилась в БД MS SQL. Упрощенно структуру этого сегмента БД можно представить вот так:

По названию таблиц можно догадаться, что в самом приложении эта связка таблиц захватывалась Entity Framework 6, а после использовалась подсистемой ASP.NET Identity. В начале сессии пользователю выводилась форма для входа, в которую он вводил свои учетные данные, после чего происходил редирект на домашнюю страницу приложения. Далее, исходя из того, какие доступы у данного пользователя прописаны в БД, и какими привилегиями он обладает, система подстраивала UI под эти данные.
Авторизация была реализована с помощью HTML-форм путём применения стандартного хелпера Html.BeginForm, отсылающего введенные данные по нажатию кнопки Submit. Вот как это выглядело с точки зрения кода:
@using (Html.BeginForm("Login", "Auth", FormMethod.Post, new { @class = "form-signin" }))
{
@Html.AntiForgeryToken()
<div class="form-group form-ie">
<span class="oi oi-person"></span>
@Html.TextBoxFor(x => x.Login, new { @class = "form-control", @placeholder = "Логин", @id = "username" })
@Html.ValidationMessageFor(x => x.Login)
</div>
<div class="form-group form-ie">
<span class="oi oi-lock-locked"></span>
@Html.PasswordFor(x => x.Password, new { @class = "form-control", @placeholder = "Пароль", @id = "inputPassword" })
@Html.ValidationMessageFor(x => x.Password)
</div>
<input type="submit" class="btn btn-mybtn-lg btn-my btn-block text-uppercase" value="Войти" />
}Далее логин с паролем передавались в контроллер авторизации AuthController, который в себе хранил UserManager, SignInManager и AppDbContext (пронаследованный от IdentityDBContext) из ASP.NET Identity. Вот как выглядел код этого контроллера.
[AllowAnonymous]
[RoutePrefix("Auth")]
public class AuthController : Controller
{
private AppDbContext _dbContext;
private ApplicationSignInManager _signInManager;
private ApplicationUserManager _userManager;
public ApplicationSignInManager SignInManager
{
get
{
return _signInManager ?? HttpContext.GetOwinContext().Get<ApplicationSignInManager>();
}
private set
{
_signInManager = value;
}
}
public ApplicationUserManager UserManager
{
get
{
return _userManager ?? HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
}
private set
{
_userManager = value;
}
}
public AppDbContext DbContext
{
get
{
return _dbContext ?? HttpContext.GetOwinContext().Get<AppDbContext>();
}
private set
{
_dbContext = value;
}
}
public AuthController()
{
}
[HttpGet]
public ActionResult Index()
{
return View(new AuthViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(AuthViewModel model)
{
var result = await SignInManager.PasswordSignInAsync(model.Login, model.Password, false, false);
if (result == SignInStatus.Success)
{
return RedirectToAction("Index", "Home");
}
Log.Warning("Ошибка авторизации: Неправильный логин или пароль");
ModelState.AddModelError("Password", "Неправильный логин или пароль");
return View("Index", model);
}
private IAuthenticationManager AuthenticationManager
{
get
{
return HttpContext.GetOwinContext().Authentication;
}
}
[HttpGet]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
return RedirectToAction("Index", "Auth");
}
}Сам факт авторизации в системе в других контроллерах проверялся посредством применения фильтра-нотации [Authorize], а принадлежность к роли – посредством применения [Authorize(Roles = “role1”)].
[Authorize]
public class HomeController : Controller
{
private AppDbContext _dbContext;
public AppDbContext DbContext
{
get
{
return _dbContext ?? HttpContext.GetOwinContext().Get<AppDbContext>();
}
private set
{
_dbContext = value;
}
}
public HomeController()
{
}
[Authorize(Roles = "Common, Admin")]
public ActionResult Index()
{
///something is happening
return View();
}
}Как заметит знакомый с вышеописанным стеком человек, не происходит вообще ничего необычного – это базовые элементы, знакомые каждому ASP.NET-разработчику.
Итак, после получения требования об изменении порядка авторизации, мы стали менять его. Для тех, кто с этим не знаком — в ASP.NET существуют следующие типы авторизации, которые можно поставить как с конфига, так и с помощью шаблона Visual Studio при создании проекта:
Без авторизации;
Авторизация на основе отдельных учётных записей (логин+пароль, классика)
Авторизация с помощью Active Directory, Microsoft Azure или Office 365.
Авторизация с помощью учётной записи Windows.
Так как у нас нет возможности использовать Active Directory ввиду требований сопровождения, остаётся один вариант – авторизация с помощью УЗ Windows.
Поигравшись немного со сменой способа авторизации в пустых приложениях и убедившись, что в них всё работает, я сделал то же самое с нашим приложением, заменив authentication mode на «Windows» в web.config.
Итак, настало время прогона. Изначально я предполагал, что после изменения авторизации можно будет подгонять логин пользователя в SignInManager, после чего проводить авторизацию по-старому (только без пароля) – т.е., что SignInManager будет маппить логин с таблицей AspNetUsers и вносить в контекст текущей пользовательской сессии соответствующий AspNetIdentity. Для чистоты эксперимента я удалил себя из таблицы с пользователями. Иии…я все равно спокойно авторизовался. Покопавшись в переменных, я понял, что при смене authentication mode на «Windows» используется другой вид Identity: не AspNetIdentity, а WindowsIdentity. При использовании WindowsIdentity любой пользователь, который вошёл в Windows – априори авторизован, причем автономно – никакой связи с БД и EF не наблюдалось. Это означало, что если ничего не исправить, то…

Ну вы поняли
