Declaration of VAR

and some other stuff

Forms Authentication в ASP.NET MVC

2015-05-31 12:26:07 +0300

2015-05-31 12:26:07 +0300 | Comments

В ASP.NET MVC 4 к текущему моменту есть масса реализаций аутентификации, но мне была нужна Forms Authentication. Как обычно, полностью готового решения при поиске в интернетах мне не встретилось, потому спешу поделиться своим. Оказалось, я пропустил пару нюансов, и всё давно уже расписано. Но не выкидывать же теперь статью.

Идея такова, что на все действия вешается требование авторизации (хотя нам достаточно лишь аутентифицировать пользователя), и всякий незалогиненный пользователь будет перенаправляться на страницу входа. После ввода действительного логина и пароля создаётся Authentication cookie, благодаря которому пользователь считается аутентифицированным.

Первым делом смотрим в свойствах у проекта, какой тип аутентификации выставлен (эти значения задаются при создании проекта). Я-то думал, что все настройки хранятся в web.config и свойствах проекта, которые открываются по правой кнопке мыши на имени проекта (не всего решения), но оказалось, что есть ещё одни свойства (из файла ВАШПРОЕКТ.csproj):

Вот такие значения должны быть. Соответственно, когда будете хостить сайт в IIS (а не запускать из Студии), там тоже надо изменить эти параметры.

В /web.config заносим следующее:

<authentication mode="Forms">
  <forms loginUrl="/Account/Login" timeout="2880" />
</authentication>

И в фильтр /App_Start/FilterConfig.cs вот это:

using System.Web;
using System.Web.Mvc;

namespace aspmvc
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            // вот тут и вешается атрибут на действия
            filters.Add(new AuthorizeAttribute());
        }
    }
}

После этого в /Global.asax:

protected void Application_Start()
{
    logger.Info("Application Start");

    AreaRegistration.RegisterAllAreas();

    WebApiConfig.Register(GlobalConfiguration.Configuration);
    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
}

Допустим, у вас где-то есть база данных с таблицей логинов и паролей. Создаём модель для этой таблицы /Models/Authoritah.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity;
using System.Linq;
using System.Web;

namespace aspmvc.Models
{
    [Table("authoritah", Schema = "dbo")]
    public class Authoritah
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public long id_auth { get; set; }
        
        [Required]
        [Display(Name = "Логин")]
        public string login { get; set; }
        
        [Required]
        [Display(Name = "Пароль")]
        public string password { get; set; }
    }

    public class AuthoritahContext : DbContext
    {
        public AuthoritahContext(string connString) : base(connString) { }

        public DbSet<Authoritah> Authoritahs { get; set; }
    }
}

Теперь создаём контроллер /Controllers/AccountController.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;

namespace aspmvc.Controllers
{
    public class AccountController : Controller
    {
        [AllowAnonymous]
        public ActionResult Login()
        {
            return View();
        }

        [AllowAnonymous]
        [HttpPost]
        public ActionResult Login(Models.Authoritah model, string returnUrl)
        {
            // проверка модели
            if (ModelState.IsValid)
            {
                using (Models.AuthoritahContext db = new Models.AuthoritahContext("ТУТВАШАСТРОКАПОДКЛЮЧЕНИЯИЗWEB.CONFIG"))
                {
                    string login = model.login;
                    string password = model.password;

                    bool userValid = db.Authoritahs.Any(user => user.login == login && user.password == password);

                    // есть такой пользователь в таблице?
                    if (userValid)
                    {
                        // создаём Authentication cookie
                        FormsAuthentication.SetAuthCookie(login, false);                       

                        // и пишем логин в сессию, на всякий
                        Session.Add("login", login);

                        // если есть куда возвращаться, то туда, иначе на главную страницу
                        if (string.IsNullOrEmpty(returnUrl))
                        { return RedirectToAction("Index", "Home"); }
                        else
                        { return Redirect(returnUrl); }
                    }
                    else
                    {
                        ModelState.AddModelError("Ошибка входа", "Вы указали неправильный логин или пароль.");
                    }
                }
            }

            return View(model);
        }

        public ActionResult LogOff()
        {
            FormsAuthentication.SignOut();
            Session.Abandon();

            return RedirectToAction("Login", "Account");
        }
    }
}

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

Осталось представление /Views/Account/Login.cshtml. Я использую стили Bootstrap, если что:

@model aspmvc.Models.Authoritah

@{
    ViewBag.Title = Страница входа";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@{
    //if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
    if (Request.IsAuthenticated)
    {
        <div class="alert alert-success">
            Вы уже выполнили вход в систему. @Html.ActionLink("Перейти на главную страницу", "Index", "Home").
        </div>
    }
    else
    {
        <div class="alert alert-warning">
            Для работы с системой вам необходимо осуществить вход, указав ваш логин и пароль.
        </div>

        using (Html.BeginForm())
        {
            <fieldset>
                <div class="input-group" style="margin-bottom:5px;">
                    @Html.LabelFor(model => model.login, new { Class = "input-group-addon", Style = "min-width: 80px;" })
                    @Html.TextBoxFor(model => model.login, new { Class = "form-control", Style = "width: 300px;" })
                </div>

                <div class="input-group" style="margin-bottom:15px;">
                    @Html.LabelFor(model => model.password, new { Class = "input-group-addon", Style = "min-width: 80px;" })
                    @Html.PasswordFor(model => model.password, new { Class = "form-control", Style = "width: 300px;" })
                </div>

                <p>
                    <input type="submit" value="Вход" class="btn btn-default btn-lg" />
                </p>
            </fieldset>
        }
    }
}

Обратите внимание на выделенные строки. В разных местах для проверки, залогинен пользователь на сайте или ещё нет, предлагали различные способы. Например, Request.IsAuthenticated или User.Identity.IsAuthenticated, но они у меня всегда возвращали true, даже сразу после старта приложения, когда ещё не загрузилась сама страница логина, а в User.Identity.Name лежал мой логин от Windows.

Причиной оказалось то, что как раз таки в свойствах проекта стояла аутентификация Windows (посмотрите первый скриншот и описание к нему). И пока я это не нашёл, приходилось извращаться проверкой наличия Authentication cookie.

Для оживления статьи вот картинка, как выглядит страница входа:

Ну всё, теперь в шаблоне для представлений /Views/Shared/_Layout.cshtml для полноты картины где-то надо добавить кнопку выхода:

@Html.ActionLink("Выход", "LogOff", "Account")