[ASP.net MVC] ASP.net MVC整合FormsAuthentication表單驗證登入 - 簡易範例程式碼
前言
以往寫網站的登入機制,最常使用傳統的Session寫法
不過最近手頭上某案子被要求使用.net內建的FormsAuthentication表單驗證機制
所以寫個簡單的範例程式碼(很夠用),供未來的自己方便Copy Paste
※使用FormsAuthentication表單驗證機制不一定要照官方教學用那個很複雜的Membership資料庫,用自己專案資料庫的使用者資料表&角色群組資料表也可以整合
實作
1.先啟用Client端驗證※此步驟跟登入沒關係可略過
確認Web.config裡appSettings區段中已有下面兩個設定
<add key="ClientValidationEnabled" value="true" />
<add key="UnobtrusiveJavaScriptEnabled" value="true" />
管理Nuget套件
加入以下幾個套件
登入頁稍後再引用以下.js檔案
<script src="~/Scripts/jquery-2.1.0.js"></script>
<script src="~/Scripts/jquery.validate.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.js"></script>
頁面上再宣告@model使用ViewModel,即可啟用Client欄位驗證
※上面步驟跟登入沒關係可略過,以下開始才是重點...
2.在Web.config的<system.web>區段裡定義
※2018.3.12追加,不知道從什麼時候開始,一定要給name屬性,否則無法登入
至於slidingExpiration、cookieless、requireSSL="true"是為了資安白箱弱掃而加上去
<authentication mode="Forms">
<!--defaultUrl:登入後到哪一頁,loginUrl:使用者未登入的話,要導至哪一頁-->
<forms name="Demo_Site"
requireSSL="true"
defaultUrl="~/Home/Index/" loginUrl="~/Home/Login/"
slidingExpiration="false" cookieless="UseCookies" protection="All"
timeout="120" />
</authentication>
3. 定義Login登入頁面會用到的ViewModel
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Web;
namespace FormLoginSample.ViewModels
{
public class LoginVM : IValidatableObject
{
[DisplayFormat(ConvertEmptyStringToNull = false)]
public string ReturnUrl { get; set; }
/// <summary>
/// 帳號
/// </summary>
[DisplayFormat(ConvertEmptyStringToNull = false)]
[Required]
public string Account { get; set; }
/// <summary>
/// 密碼
/// </summary>
[DisplayFormat(ConvertEmptyStringToNull = false)]
[Required]
public string Password { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
//將使用者輸入的字串轉成Base64String
string base64Password = Convert.ToBase64String(Encoding.UTF8.GetBytes(Password));
//todo到DB抓使用者資料
//假如抓不到系統使用者資料
//※為了Demo用這種寫法,實際請換成判斷DB的資料存不存在
if (!(Account == "shadow" && base64Password == "c29ueQ=="))
{
yield return new ValidationResult("無此帳號或密碼", new string[] { "Account" });
}
}
}
}
4. Controller一律加上[Authorize] Attribute 代表該整個Controller必須要先進行過表單驗證登入後才可以進入
5. 為Login登入的Action加上 [AllowAnonymous] Attribute 代表該Action就算沒進行過表單驗證登入也可以執行
6.Controller完整的的登入和登出程式碼
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using FormLoginSample.ViewModels;
namespace FormLoginSample.Controllers
{
/// <summary>
/// 後台首頁、登入頁...
/// </summary>
[Authorize]
public class HomeController : Controller
{
/// <summary>
/// 呈現後台使用者登入頁
/// </summary>
/// <param name="ReturnUrl">使用者原本Request的Url</param>
/// <returns></returns>
[AllowAnonymous]
public ActionResult Login(string ReturnUrl)
{
//ReturnUrl字串是使用者在未登入情況下要求的的Url
LoginVM vm = new LoginVM() { ReturnUrl = ReturnUrl };
return View(vm);
}
/// <summary>
/// 後台使用者進行登入
/// </summary>
/// <param name="vm"></param>
/// <param name="u">使用者原本Request的Url</param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost]
public ActionResult Login(LoginVM vm)
{
//沒通過Model驗證(必填欄位沒填,DB無此帳密)
if (!ModelState.IsValid)
{
return View(vm);
}
//都成功...
//進行表單登入 ※之後使用User.Identity.Name的值就是vm.Account帳號的值
FormsAuthentication.SetAuthCookie(vm.Account, false);
//保哥文章使用 FormsAuthentication.RedirectFromLoginPage(帳號, false); 來登入也是可以
//但要留意↑會做Redirect,如果後續有使用到Request.UrlReferrer.AbsoluteUri的話,值會改變
//導向預設Url(Web.config裡的defaultUrl值)或
//使用者原先Request的Url(登入頁網址的QueryString:ReturnUrl,此QueryString在表單驗證機制下對於未登入的使用者會自動產生)
return Redirect(FormsAuthentication.GetRedirectUrl(vm.Account, false));
}
/// <summary>
/// 後台使用者登出動作
/// </summary>
/// <returns></returns>
[AllowAnonymous]
public ActionResult Logout()
{
//清除Session中的資料
Session.Abandon();
//登出表單驗證
FormsAuthentication.SignOut();
//導至登入頁
return RedirectToAction("Login", "Home");
}
/// <summary>
/// 後台首頁
/// </summary>
/// <returns></returns>
public ActionResult Index()
{
//取得目前登入者的帳號
string Account = User.Identity.Name;
//可以依帳號到DB抓登入者的資料...
return View();
}
/// <summary>
/// 測試用的
/// </summary>
/// <returns></returns>
public ActionResult Index2()
{
//如果要判斷此登入者有沒有登入的寫法
//if (User.Identity.IsAuthenticated)
//{//目前已登入
//}
return View();
}
}
}
7. Login登入頁的View
@using FormLoginSample.ViewModels
@model LoginVM
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<script src="~/Scripts/jquery-2.1.0.js"></script>
<script src="~/Scripts/jquery.validate.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.js"></script>
<title>Login</title>
</head>
<body>
<div>
@*如果↓表單的ReturnUrl沒給的話,就沒辦法導向使用者原先要求的頁面*@
@using (Html.BeginForm("Login", "Home", new { ReturnUrl = Model.ReturnUrl}, FormMethod.Post, new { autocomplete = "off" }))
{
@Html.TextBoxFor(m => m.Account ) @Html.ValidationMessageFor(m=>m.Account)
<br />
@Html.PasswordFor(m => m.Password ) @Html.ValidationMessageFor(m=>m.Password)
<br />
<input type="submit" value="登入" />
}
</div>
</body>
</html>
其他不是本文重點的另兩個頁面View(Index和Index2)
<!DOCTYPE html>
<html>
<head>
<title>Index</title>
</head>
<body>
<div>
<h1>Index</h1>
<div><a href="@Url.Action("Logout","Home")">登出</a></div>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>Index2</title>
</head>
<body>
<div>
<h2>Index2</h2>
<div><a href="@Url.Action("Logout","Home")">登出</a></div>
</div>
</body>
</html>
執行結果
1.假設一開始使用者沒登入狀態下進到/Home/Index2
使用者會被自動導至登入頁(瀏覽器後面會帶ReturnUrl)
若這時候使用者輸入正確的帳密再按一次登入
↑程式就會自動把使用者導至原先要求的Url(/Home/Index2)
當然若使用者一開始老老實實地在/Home/Login/輸入帳密登入成功,就直接進到/Home/Index頁囉
結語
使用傳統的Session寫法或採用.net內建的FormsAuthentication表單驗證機制各有好壞
Session不易維護(Session的Key值定義哪些,開發者通常要到各Action程式碼查找)、開發時期,網站一經過編譯,Session就會遺失等缺點
FormsAuthentication表單驗證機制的話,則無上述困擾(因為把資訊存到Cookie),但缺點是一個Web專案只能使用一個FormsAuthentication表單驗證機制,Session寫法則無此限制
而且以本範例程式碼而言,採用FormsAuthentication表單驗證,每次要抓登入者資訊的話,就只能用User.Identity.Name(帳號)從DB重新再撈資料,會有拖累效能問題
雖然解法可以在登入時把該登入者其他資訊存入Cookie(請見:簡介 ASP.NET 表單驗證 (FormsAuthentication) 的運作方式),不過各瀏覽器Cookie又有長度限制問題
而使用Session寫法的話,一般在登入後把我們會把登入者物件塞到Session裡,之後其他地方要讀取登入者資訊的話,就由Session中去取得就沒有上述表單驗證的缺點,但相對的
若登入者修改了自己的使用者資訊後,Session寫法通常要再更新Session物件,FormsAuthentication表單驗證機制則不用(以本範例程式碼寫法來說)
所以依自己的考量使用吧,混用也是可以XD
例如:同一個Web專案裡,前台用Session寫法寫登入機制,後台用FormsAuthentication表單驗證機制
2018.4.6追記,發現把登入驗證邏輯寫在ViewModel,萬一登入檢核很複雜,例如:登入失敗N次鎖帳號、登入失敗寄通知信、登入成功寫Log...什麼的
由於最近碰到登入邏輯很複雜的系統,所以以下再提供另一範例程式碼,不靠ViewModel版本
Controller完整代碼↓ ※提示:避免把密碼變數命名為Password或pwd這種敏感字眼,會被資安弱掃掃出問題,所以使用PD當做輸入密碼的變數
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
/*引用這個*/
using System.Web.Security;
namespace WebApplication1Login.Controllers
{
public class HomeController : Controller
{
/// <summary>
/// 登入畫面
/// </summary>
/// <returns></returns>
[AllowAnonymous]
public ActionResult Login()
{
return View();
}
[AllowAnonymous]
[HttpPost]
public ActionResult Login(string Account,string PD,string ReturnUrl)
{
//檢查必填欄位
if (string.IsNullOrEmpty(Account) || string.IsNullOrEmpty(PD))
{
ModelState.AddModelError("Account", "請輸入必填欄位");
ModelState.AddModelError("PD", "請輸入必填欄位");
return View();
}
//把輸入的密碼加密
string base64PD = Convert.ToBase64String(Encoding.UTF8.GetBytes(PD));
//todo 和DB裡的資料做比對
//登入成功
//進行表單登入 之後User.Identity.Name的值就是Account帳號的值
FormsAuthentication.RedirectFromLoginPage(Account, false);
//↓這行不會執行到,亂回傳XD
return Content("Login success");
}
/// <summary>
/// 登出
/// </summary>
/// <returns></returns>
[AllowAnonymous]
public ActionResult Logout()
{
FormsAuthentication.SignOut();//登出
return RedirectToAction("Login","Home");
}
/// <summary>
/// 登入後預設進入的畫面
/// </summary>
/// <returns></returns>
public ActionResult Index()
{
//登入帳號
ViewData["Login_Account"] = User.Identity.Name;
//是否登入(boolean值)
ViewData["isLogin"] = User.Identity.IsAuthenticated;
return View();
}
//有登入過才能進入此Action,請參考下述文章改寫成Ajax支援版
[Authorize]
public ActionResult LoginedPage()
{
return Content("is Login");
}
}
}
Login的View完整代碼↓
@using (Html.BeginForm("Login", "Home",new { ReturnUrl=Request.QueryString["ReturnUrl"] }, FormMethod.Post, new { name = "LoginForm" }))
{
@Html.TextBox("Account", "", new { required = "required" }) @Html.ValidationMessage("Account")<br />
@Html.Password("PD", "", new { required = "required", autocomplete = "new-password" }) @Html.ValidationMessage("PD")<br />
<input type="submit" value="登入" />
}
2014.5.4追記
有使用到Ajax的請參考下一篇: [ASP.MVC] 當jQuery Ajax呼叫遇上Login Timeout的處理
2018.8.12
有些網站,例如個人網銀,為了避免使用者登出後,仍可以透過瀏覽器的「上一頁」查看已登入的畫面(其實看到的是快取畫面)
這種資安漏洞Cross Site History Manipulation,要防止的話
可以為Controller加上Attribute
[OutputCache(Duration =1)]//如果Controller底下裡的Action方法,其中有給View使用@Html.Action()呼叫的話,就用這個
//這種寫法也可以
[OutputCache(Location = System.Web.UI.OutputCacheLocation.None, NoStore = true)]
參考文章: