很多的業務系統都會有自己原有的用戶體系,甚至當你自己寫一套應用程序時,也會自定義自己的用戶體系(asp.net內置的那套根本就不夠用啊,而且擴展也不方便,各種修改和繼承)。
那么,我們究竟有沒有更簡單的方式來實現用戶登錄認證呢?不需要哪種各種修改的。身為直男的我們,直來直去習慣了,所以這段時間的搜索總是不免各種被繞碰壁,唯一慶幸的是,我們依舊還是那么直啊。為了堅持這個直下去個性,我們就去尋找一條捷徑吧。
在這里,首先要感謝以下博文鏈接的作者,這個寫得非常精煉,那么,我就在這個大大的基礎上進行記錄講解:
https://www.cnblogs.com/chenwolong/p/Authorize.html
做過三層架構的童鞋都知道,如果要驗證/授權一個用戶登錄,我們一般采取如下方法:
1、用戶輸入用戶名、密碼進行登錄
2、賬戶/密碼正確,保存用戶信息至Cookies或者Session,跳轉至主頁面
3、在主頁面繼承BasePage類,並在Page_Load的時候,判斷Cookies或者Session的值,如果Cookies或者Session中存放的值在數據庫驗證成功,則可以觀看主頁面信息,如果驗證不成功,或者Session/Cookies值為NULL時,直接執行語句:Response.Redirect(../Login.aspx);
上述方法很簡單,功能上也實現了登錄的認證與授權,相信很多人寫的項目都會采取此方法進行判斷,殊不知:這種方法安全系數較低,而且違背了NET的登錄授權與驗證原則。那么NET中的授權與驗證又是什么呢?
.net中的認證(authentication)與授權(authorization)
認證(authentication) 就是 :判斷用戶有沒有登錄,用戶輸入的賬戶密碼等信息是否通過數據庫比對
授權(authorization) 就是:用戶登錄后的身份/角色識別,用戶通過認證后<登陸后>我們記錄用戶的信息並授權
.net中與"認證"對應的是IIdentity接口,而與"授權"對應的則是IPrincipal接口,這二個接口的定義均在命名空間System.Security.Principal中,詳情如下:
用戶認證接口:
namespace System.Security.Principal { /// <summary>定義標識對象的基本功能。</summary> [ComVisible(true)] [__DynamicallyInvokable] public interface IIdentity { /// <summary>獲取當前用戶的名稱。</summary> /// <returns>用戶名,代碼當前即以該用戶的名義運行。</returns> [__DynamicallyInvokable] string Name { [__DynamicallyInvokable] get; } /// <summary>獲取所使用的身份驗證的類型。</summary> /// <returns>用於標識用戶的身份驗證的類型。</returns> [__DynamicallyInvokable] string AuthenticationType { [__DynamicallyInvokable] get; } /// <summary>獲取一個值,該值指示是否驗證了用戶。</summary> /// <returns>如果用戶已經過驗證,則為 <see langword="true" />;否則為 <see langword="false" />。</returns> [__DynamicallyInvokable] bool IsAuthenticated { [__DynamicallyInvokable] get; } } }
用戶授權接口:
namespace System.Security.Principal { /// <summary>定義主體對象的基本功能。</summary> [ComVisible(true)] [__DynamicallyInvokable] public interface IPrincipal { /// <summary>獲取當前用戶的標識。</summary> /// <returns>與當前用戶關聯的 <see cref="T:System.Security.Principal.IIdentity" /> 對象。</returns> [__DynamicallyInvokable] IIdentity Identity { [__DynamicallyInvokable] get; } /// <summary>確定當前用戶是否屬於指定的角色。</summary> /// <param name="role">要檢查其成員資格的角色的名稱。</param> /// <returns>如果當前用戶是指定角色的成員,則為 <see langword="true" />;否則為 <see langword="false" />。</returns> [__DynamicallyInvokable] bool IsInRole(string role); } }
應該注意到:IPrincipal接口中包含着一個只讀的IIdentity
1、IIdentity 接口中屬性的含義:
AuthenticationType 驗證方式:NET中有Windows、Forms、Passport三種驗證方式,其中以Forms驗證用的最多,本節講解的MVC驗證與授權就是基於Form。
IsAuthenticated 是否通過驗證,布爾值
Name 用戶登錄的賬戶名稱
2、IPrincipal 接口中屬性的含義:
IIdentity 上述IIdentity接口的實例
IsInRole 布爾值,驗證用戶權限
下面我們對HttpContext封閉類刨根問底
/// <summary>清除當前 HTTP 請求的所有錯誤。</summary> public void ClearError() { if (this._tempError != null) this._tempError = (Exception) null; else this._errorCleared = true; if (!this._isIntegratedPipeline || this._notificationContext == null) return; this._notificationContext.Error = (Exception) null; } /// <summary>獲取或設置當前 HTTP 請求的安全信息。</summary> /// <returns>當前 HTTP 請求的安全信息。</returns> public IPrincipal User { get { return this._principalContainer.Principal; } [SecurityPermission(SecurityAction.Demand, ControlPrincipal = true)] set { this.SetPrincipalNoDemand(value); } } IPrincipal IPrincipalContainer.Principal { get; set; }
HttpContext.User本身就是一個IPrincipal接口的實例。
有了上面的預備知識,我們在程序中處理認證問題時,只需對HttpContext.User賦值即可。
下面是小弟模擬的一個授權方法
/// <summary> /// 模擬授權 此處不經過驗證 直接授權訪問 UserCenter() /// </summary> /// <returns></returns> public ActionResult Login() { GenericIdentity _identity = new GenericIdentity("User", "Forms"); GenericPrincipal _principal = new GenericPrincipal(_identity, new string[] { "admins", "vips" }); HttpContext.User = _principal; return RedirectToAction("UserCenter"); } [Authorize(Roles = "admins")] public ActionResult UserCenter() { return View(); }
上面的方法中,一旦運行登陸頁,不需要填賬戶密碼,直接會訪問UserCenter(),即:直接會跳轉到個人中心
好啦,如果上邊的例子大家看懂了,那么下面的登錄其實就很簡單了,和上述所講一樣:由於 HttpContext.User 本身就是一個IPrincipal接口的實例!因此:我們只需給這個實例賦值即可,上面的例子就證實了這一點。
咱們回到開篇的描述,一般我們都是通過Session或者cookies就行Forms驗證登錄,那么在MVC中我們應當怎么做呢?
1.MVC的Forms驗證
Forms驗證在內部的機制為把用戶數據加密后保存在一個基於cookie的票據FormsAuthenticationTicket中,因為是經過特殊加密的,所以應該來說是比較安全的。而.net除了用這個票據存放自己的信息外,還留了一個地給用戶自由支配,這就是現在要說的UserData。
UserData可以用來存儲string類型的信息,並且也享受Forms驗證提供的加密保護,當我們需要這些信息時,也可以通過簡單的get方法得到,兼顧了安全性和易用性,用來保存一些必須的敏感信息還是很有用的。
在此,本案例中UserData用於存儲用戶角色值,admins/vips/superadmins等,這些值大家可根據系統需求自行定義。本案例中是寫死的,當然,在實際項目中應當根據登錄賬戶信息結合數據庫進行賦值。
2、FormsAuthenticationTicket基於forms的驗證
構建基於forms的驗證機制過程如下:
1,設置IIS為可匿名訪問和asp.net web.config中設置為form驗證,配置webConfig,如下:
<!--用戶沒有授權時 自動退回登陸界面--> <authentication mode="Forms"> <forms loginUrl="~/Home/Login" timeout="2880" path="/" /> </authentication>
2,檢索數據存儲驗證用戶,並檢索角色(其實就是登錄驗證)
3,使用FormsAuthenticationTicket創建一個Cookie並回發到客戶端,並存儲
關於2、3兩步,我以代碼示例:
我創建的登錄Model如下:
public class UserLogin { private readonly object LOCK = new object(); Maticsoft.BLL.YX_UserInfo Bll = new Maticsoft.BLL.YX_UserInfo(); Maticsoft.Model.YX_UserInfo Mol = new Maticsoft.Model.YX_UserInfo(); // [Required(ErrorMessage = "請輸入賬戶號碼/手機號")] [RegularExpression(@"^1[3458][0-9]{9}$", ErrorMessage = "手機號格式不正確")] public string UserName { get; set; } [Required(ErrorMessage="請輸入賬戶密碼")] [DataType(DataType.Password,ErrorMessage="密碼格式不正確")] [MinLength(6, ErrorMessage = "密碼長度介於6~15位之間")] [MaxLength(15, ErrorMessage = "密碼長度介於6~15位之間")] public string UserPwd { get; set; } public bool LoginAction() { lock (LOCK) { DataTable dt = Bll.GetList(" Uname='" + CommonMethod.CheckParamThrow(UserName) + "' and Upwd='" + CommonMethod.Md532(UserPwd) + "' and flat1=1").Tables[0]; if (dt.Rows.Count > 0) { FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket( 1, UserName, DateTime.Now, DateTime.Now.AddMinutes(30), false, "admins,vip", "/" ); string encryptedTicket = FormsAuthentication.Encrypt(authTicket); System.Web.HttpCookie authCookie = new System.Web.HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket); System.Web.HttpContext.Current.Response.Cookies.Add(authCookie); return true; } else { return false; } } } }
數據庫驗證成功,我們將用戶信息存入Cookies,
然后我們在Global中解析這個Cookies,然后把解析的結果賦值給:HttpContext.User
代碼如下:
/// <summary> /// 登錄驗證、s授權 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> protected void Application_AuthenticateRequest(Object sender, EventArgs e) { string cookieName = FormsAuthentication.FormsCookieName; HttpCookie authCookie = Context.Request.Cookies[cookieName]; FormsAuthenticationTicket authTicket = null; try { authTicket = FormsAuthentication.Decrypt(authCookie.Value); } catch (Exception ex) { return; } string[] roles = authTicket.UserData.Split(','); FormsIdentity id = new FormsIdentity(authTicket); GenericPrincipal principal = new GenericPrincipal(id, roles); Context.User = principal;//存到HttpContext.User中 }
Context.User一旦被賦值成功,我們就可以訪問那些有權限限制的方法。代碼如下:
[Authorize(Roles = "admins")] public ActionResult UserCenter() { return View(); }
SO,搞懂了本質,一切都可以解決。
補充:本人登錄代碼如下:
前端 Login.cshtml
@{ ViewBag.Title = "登錄"; } @section css { <link href="~/Content/login.css" rel="stylesheet" /> } <form id="form1"> <div class="wrap loginBox"> <div class="logo"><img src="/images/logo.png"/></div> <div class="head"> <ul> <li><input type="text" id="UserName" name="UserName" class="text n2" placeholder="手機號碼" /></li> <li><input type="password" id="UserPwd" name="UserPwd" class="text n3" placeholder="密碼" /></li> </ul> </div> <div class="body"> <a href="JavaScript:void(0)" class="btn btn-blue" onclick="Login()">立即登錄</a> @*<a href="JavaScript:void(0)" class="btn btn-yell">逗包賬號登錄</a>*@ </div> <div class="foot"> <a href="JavaScript:void(0)">忘記密碼?</a><a href="JavaScript:void(0)">賬號注冊</a> </div> </div> </form> <script type="text/javascript"> function Login() { var UserName = $("#UserName").val(); var UserPwd = $("#UserPwd").val(); $(document).ready(function (data) { $.ajax({ url: "/home/Login", type: "post", contentType: "application/json", dataType: "text", data: JSON.stringify({ UserName: UserName, UserPwd: UserPwd }), success: function (result, status) { if (result == "200") { //登錄成功 跳轉 location.href = "http://www.baidu.com"; } else { alert(result);//彈出錯誤碼 } }, error: function (error) { alert(error); } }); }); } </script>
后端 HomeController.cs:
public class HomeController : Controller { public ActionResult Index() { return RedirectToAction("Login"); } public ActionResult Login() { return View(); } [HttpPost] public object Login(UserLogin LoginMol) { if (NetworkHelper.Ping())//判斷當前是否有網絡 { if (ModelState.IsValid)//是否通過Model驗證 { if (LoginMol.LoginAction()) { return 200; } else { return "賬戶或密碼有誤"; } } else { //讀取錯誤信息 StringBuilder sb = new StringBuilder(""); var errors = ModelState.Values; foreach (var item in errors) { foreach (var item2 in item.Errors) { if (!string.IsNullOrEmpty(item2.ErrorMessage)) { if (string.IsNullOrEmpty(sb.ToString())) { sb.AppendLine(item2.ErrorMessage);//讀取一個錯誤信息 並返回 } } } } return sb.ToString(); } } else { return "當前無網絡,請稍后重試"; } } }
怕博文丟失,所以全部記錄下來了。
那么看完上面的解釋,我們就嘗試自己做一個吧,很簡單,我分別標注下代碼邏輯:
1. 實現用戶Data Object
public class UserVo { public string User { get; set; } public string Password { get; set; } }
2. 實現登錄控制器
public class AccountController : Controller { [AllowAnonymous] // GET: Account/Login public ActionResult Login() { return View(); } // POST: Account/Login [HttpPost] public string Login(UserVo LoginUser) { try { if (ModelState.IsValid && LoginUser != null) { using (WfContext db = new WfContext()) { //賬號密碼是否正確 LoginVo login = SqlDml.Select<LoginVo>(db, $"N'{LoginUser.User}',N'{LoginUser.Password}'").FirstOrDefault(); if (login != null && login.PasswordMatch) { //擁有哪些權限 ServerUser su = SqlDml.Select<ServerUser>(db, "").FirstOrDefault(); //權限寫入Ticket FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, LoginUser.User, DateTime.Now, DateTime.Now.AddMinutes(30), false, (su==null?"": Newtonsoft.Json.JsonConvert.SerializeObject(su)), "/"); //Ticket加密 string encryptedTicket = FormsAuthentication.Encrypt(authTicket); System.Web.HttpCookie authCookie = new System.Web.HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket); //寫Cookie System.Web.HttpContext.Current.Response.Cookies.Add(authCookie); return $"登錄成功"; } } } } catch (Exception ex) { AlertBox.Show(ex.Message); return "Error"; } return $"{LoginUser?.User} 登錄失敗"; } } public class HomeController : Controller { [Authorize(Roles = "admins")] // GET: Home/Index public string Index() { return "當前賬號已經授權訪問"; } }
3. 實現登錄視圖
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>登錄</title> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr") </head> <body> @using (Html.BeginForm("Login", "Account", FormMethod.Post,new {@style="margin-left: 20px"})) { <div class="form-group"> <label for="User" stype="display:inline;">賬戶:</label> <input type="text" class="form-control" id="User" name="User" style="display: inline; width: 200px;" autocomplete="off" /> </div> <div class="form-group"> <label for="Password" style="display: inline;">密碼:</label> <input type="text" class="form-control" id="Password" name="Password" style="display: inline; width: 200px;" autocomplete="off" /> </div> <button type="submit" class="btn btn-primary" style="margin-left: 80px; margin-top: 10px;width: 100px">登錄</button> } @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/bootstrap") </body> </html>
4. 實現Global解析Cookie
public class MvcApplication : System.Web.HttpApplication { /// <summary> /// 登錄驗證、s授權 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> protected void Application_AuthenticateRequest(Object sender, EventArgs e) { string cookieName = FormsAuthentication.FormsCookieName; HttpCookie authCookie = Context.Request.Cookies[cookieName]; if (authCookie!=null) { try { FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value); List<string> roles = new List<string>(); if (!string.IsNullOrEmpty(authTicket?.UserData)) { try { var su= Newtonsoft.Json.JsonConvert.DeserializeObject<Models.ServerUser>(authTicket.UserData); if (su.Admin) { roles.Add("Admin"); } if (su.Export) { roles.Add("Export"); } if (su.Impersonate) { roles.Add("Impersonate"); } } catch { roles.Clear(); } } FormsIdentity id = new FormsIdentity(authTicket); GenericPrincipal principal = new GenericPrincipal(id, roles.ToArray()); Context.User = principal;//存到HttpContext.User中 } catch (Exception ex) { return; } } } }
5. 實現授權特性
6. 實現外部認證
private void BtnLogin_Click(object sender, EventArgs e) { if (Common.Common.NotNull(txtUser.Text,txtPwd.Text)) { System.Net.Http.HttpClient client = new System.Net.Http.HttpClient(); UserVo user = new UserVo() { User = txtUser.Text, Password = txtPwd.Text }; var s = client.PostAsJsonAsync("http://localhost:50249/Account/Login", user); var cookies = s.Result.Headers.GetValues("Set-Cookie"); foreach (string cookie in cookies) { System.Diagnostics.Debug.Print(cookie); Application.Cookies.Add(FormsAuthentication.FormsCookieName, cookie.Replace($"{FormsAuthentication.FormsCookieName}=","")); } if (s.Result!=null) { if (Application.IsAuthenticated) { Parent.Controls.Add(new ctlNavig() { Dock = DockStyle.Fill }); this.Dispose(); } else { AlertBox.Show($"{txtUser.Text} 用戶未經過認證", MessageBoxIcon.Error, null, ContentAlignment.BottomRight, 3000); } } } }