Web使用者的身份驗證及WebApi許可權驗證流程的設計和實現
前言:Web 使用者的身份驗證,及頁面操作許可權驗證是B/S系統的基礎功能,一個功能複雜的業務應用系統,通過角色授權來控制使用者訪問,本文通過Form認證,Mvc的Controller基類及Action的許可權驗證來實現Web系統登入,Mvc前端許可權校驗以及WebApi服務端的訪問校驗功能。
1. Web Form認證介紹
Web應用的訪問方式因為是基於瀏覽器的Http地址請求,所以需要驗證使用者身份的合法性。目前常見的方式是Form認證,其處理邏輯描述如下:
1. 使用者首先要在登入頁面輸入使用者名稱和密碼,然後登入系統,獲取合法身份的票據,再執行後續業務處理操作;
2. 使用者在沒有登入的情況下提交Http頁面訪問請求,如果該頁面不允許匿名訪問,則直接跳轉到登入頁面;
3. 對於允許匿名訪問的頁面請求,系統不做許可權驗證,直接處理業務資料,並返回給前端;
4. 對於不同許可權要求的頁面Action操作,系統需要校驗使用者角色,計算許可權列表,如果請求操作在許可權列表中,則正常訪問,如果不在許可權列表中,則提示“未授權的訪問操作”到異常處理頁面。
2. WebApi 服務端Basic 方式驗證
WebApi服務端接收訪問請求,需要做安全驗證處理,驗證處理步驟如下:
1. 如果是合法的Http請求,在Http請求頭中會有使用者身份的票據資訊,服務端會讀取票據資訊,並校驗票據資訊是否完整有效,如果滿足校驗要求,則進行業務資料的處理,並返回給請求發起方;
2. 如果沒有票據資訊,或者票據資訊不是合法的,則返回“未授權的訪問”異常訊息給前端,由前端處理此異常。
3. 登入及許可權驗證流程
流程處理步驟說明:
1. 使用者開啟瀏覽器,並在位址列中輸入頁面請求地址,提交;
2. 瀏覽器解析Http請求,傳送到Web伺服器;Web伺服器驗證使用者請求,首先判斷是否有登入的票據資訊;
3. 使用者沒有登入票據資訊,則跳轉到登入頁面;
4. 使用者輸入使用者名稱和密碼資訊;
5. 瀏覽器提交登入表單資料給Web伺服器;
6. Web服務需要驗證使用者名稱和密碼是否匹配,傳送api請求給api伺服器;
7. api使用者賬戶服務根據使用者名稱,讀取儲存在資料庫中的使用者資料,判斷密碼是否匹配;
1)如果使用者名稱和密碼不匹配,則提示密碼錯誤等資訊,然該使用者重新填寫登入資料;
2)如果驗證通過,則儲存使用者票據資訊;
8. 接第3步,如果使用者有登入票據資訊,則跳轉到使用者請求的頁面;
9. 驗證使用者對當前要操作的頁面或頁面元素是否有許可權操作,首先需要發起api服務請求,獲取使用者的許可權資料;
10. api使用者許可權服務根據使用者名稱,查詢該使用者的角色資訊,並計算使用者許可權列表,封裝為Json資料並返回;
11. 當用戶有許可權操作頁面或頁面元素時,跳轉到頁面,並由頁面Controller提交業務資料處理請求到api伺服器;
如果使用者沒有許可權訪問該頁面或頁面元素時,則顯示“未授權的訪問操作”,跳轉到系統異常處理頁面。
12. api業務服務處理業務邏輯,並將結果以Json 資料返回;
13. 返回渲染後的頁面給瀏覽器前端,並呈現業務資料到頁面;
14. 使用者填寫業務資料,或者查詢業務資料;
15. 當填寫或查詢完業務資料後,使用者提交表單資料;
16. 瀏覽器指令碼提交get,post等請求給web伺服器,由web伺服器再次解析請求操作,重複步驟2的後續流程;
17. 當api伺服器驗證使用者身份是,沒有可信使用者票據,系統提示“未授權的訪問操作”,跳轉到系統異常處理頁面。
4. Mvc前端程式碼示例
4.1 使用者登入AccountController
public class AccountController : Controller
{
//
// GET: /Logon/
public ActionResult Login(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpPost]
public ActionResult Logon(LoginUser loginUser, string returnUrl)
{
string strUserName = loginUser.UserName;
string strPassword = loginUser.Password;
var accountModel = new AccountModel();
//驗證使用者是否是系統註冊使用者
if (accountModel.ValidateUserLogin(strUserName, strPassword))
{
if (Url.IsLocalUrl(returnUrl))
{
//建立使用者ticket資訊
accountModel.CreateLoginUserTicket(strUserName, strPassword);
//讀取使用者許可權資料
accountModel.GetUserAuthorities(strUserName);
return new RedirectResult(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
throw new ApplicationException("無效登入使用者!");
}
}
/// <summary>
/// 使用者登出,登出之前,清除使用者ticket
/// </summary>
/// <returns></returns>
[HttpPost]
public ActionResult Logout()
{
var accountModel = new AccountModel();
accountModel.Logout();
return RedirectToAction("Login", "Account");
}
}
4.2 使用者模型AccountModel
public class AccountModel
{
/// <summary>
/// 建立登入使用者的票據資訊
/// </summary>
/// <param name="strUserName"></param>
internal void CreateLoginUserTicket(string strUserName, string strPassword)
{
//構造Form驗證的票據資訊
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, strUserName, DateTime.Now, DateTime.Now.AddMinutes(90),
true, string.Format("{0}:{1}", strUserName, strPassword), FormsAuthentication.FormsCookiePath);
string ticString = FormsAuthentication.Encrypt(ticket);
//把票據資訊寫入Cookie和Session
//SetAuthCookie方法用於標識使用者的Identity狀態為true
HttpContext.Current.Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, ticString));
FormsAuthentication.SetAuthCookie(strUserName, true);
HttpContext.Current.Session["USER_LOGON_TICKET"] = ticString;
//重寫HttpContext中的使用者身份,可以封裝自定義角色資料;
//判斷是否合法使用者,可以檢查:HttpContext.User.Identity.IsAuthenticated的屬性值
string[] roles = ticket.UserData.Split(',');
IIdentity identity = new FormsIdentity(ticket);
IPrincipal principal = new GenericPrincipal(identity, roles);
HttpContext.Current.User = principal;
}
/// <summary>
/// 獲取使用者許可權列表資料
/// </summary>
/// <param name="userName"></param>
/// <returns></returns>
internal string GetUserAuthorities(string userName)
{
//從WebApi 訪問使用者許可權資料,然後寫入Session
string jsonAuth = "[{\"Controller\": \"SampleController\", \"Actions\":\"Apply,Process,Complete\"}, {\"Controller\": \"Product\", \"Actions\": \"List,Get,Detail\"}]";
var userAuthList = ServiceStack.Text.JsonSerializer.DeserializeFromString(jsonAuth, typeof(UserAuthModel[]));
HttpContext.Current.Session["USER_AUTHORITIES"] = userAuthList;
return jsonAuth;
}
/// <summary>
/// 讀取資料庫使用者表資料,判斷使用者密碼是否匹配
/// </summary>
/// <param name="name"></param>
/// <param name="password"></param>
/// <returns></returns>
internal bool ValidateUserLogin(string name, string password)
{
//bool isValid = password == passwordInDatabase;
return true;
}
/// <summary>
/// 使用者登出執行的操作
/// </summary>
internal void Logout()
{
FormsAuthentication.SignOut();
}
}
4.3 控制器基類WebControllerBase
/// <summary>
/// 前端Mvc控制器基類
/// </summary>
[Authorize]
public abstract class WebControllerBase : Controller
{
/// <summary>
/// 對應api的Url
/// </summary>
public string ApiUrl
{
get;
protected set;
}
/// <summary>
/// 使用者許可權列表
/// </summary>
public UserAuthModel[] UserAuthList
{
get
{
return AuthorizedUser.Current.UserAuthList;
}
}
/// <summary>
/// 登入使用者票據
/// </summary>
public string UserLoginTicket
{
get
{
return AuthorizedUser.Current.UserLoginTicket;
}
}
}
4.4 許可權屬性RequireAuthorizationAttribute
/// <summary>
/// 許可權驗證屬性類
/// </summary>
public class RequireAuthorizeAttribute : AuthorizeAttribute
{
/// <summary>
/// 使用者許可權列表
/// </summary>
public UserAuthModel[] UserAuthList
{
get
{
return AuthorizedUser.Current.UserAuthList;
}
}
/// <summary>
/// 登入使用者票據
/// </summary>
public string UserLoginTicket
{
get
{
return AuthorizedUser.Current.UserLoginTicket;
}
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
////驗證是否是登入使用者
var identity = filterContext.HttpContext.User.Identity;
if (identity.IsAuthenticated)
{
var actionName = filterContext.ActionDescriptor.ActionName;
var controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
//驗證使用者操作是否在許可權列表中
if (HasActionQulification(actionName, controllerName, identity.Name))
if (!string.IsNullOrEmpty(UserLoginTicket))
//有效登入使用者,有許可權訪問此Action,則寫入Cookie資訊
filterContext.HttpContext.Response.Cookies[FormsAuthentication.FormsCookieName].Value = UserLoginTicket;
else
//使用者的Session, Cookie都過期,需要重新登入
filterContext.HttpContext.Response.Redirect("~/Account/Login", false);
else
//雖然是登入使用者,但沒有該Action的許可權,跳轉到“未授權訪問”頁面
filterContext.HttpContext.Response.Redirect("~/Home/UnAuthorized", true);
}
else
{
//未登入使用者,則判斷是否是匿名訪問
var attr = filterContext.ActionDescriptor.GetCustomAttributes(true).OfType<AllowAnonymousAttribute>();
bool isAnonymous = attr.Any(a => a is AllowAnonymousAttribute);
if (!isAnonymous)
//未驗證(登入)的使用者, 而且是非匿名訪問,則轉向登入頁面
filterContext.HttpContext.Response.Redirect("~/Account/Login", true);
}
}
/// <summary>
/// 從許可權列表驗證使用者是否有權訪問Action
/// </summary>
/// <param name="actionName"></param>
/// <param name="controllerName"></param>
/// <returns></returns>
private bool HasActionQulification(string actionName, string controllerName, string userName)
{
//從該使用者的許可權資料列表中查詢是否有當前Controller和Action的item
var auth = UserAuthList.FirstOrDefault(a =>
{
bool rightAction = false;
bool rightController = a.Controller == controllerName;
if (rightController)
{
string[] actions = a.Actions.Split(',');
rightAction = actions.Contains(actionName);
}
return rightAction;
});
//此處可以校驗使用者的其它許可權條件
//var notAllowed = HasOtherLimition(userName);
//var result = (auth != null) && notAllowed;
//return result;
return (auth != null);
}
}
4.5 業務Controller示例
public class ProductController : WebControllerBase
{
[AllowAnonymous]
public ActionResult Query()
{
return View("ProductQuery");
}
[HttpGet]
//[AllowAnonymous]
[RequireAuthorize]
public ActionResult Detail(string id)
{
var cookie = HttpContext.Request.Cookies;
string url = base.ApiUrl + "/Get/" + id;
HttpClient httpClient = HttpClientHelper.Create(url, base.UserLoginTicket);
string result = httpClient.GetString();
var model = JsonSerializer.DeserializeFromString<Product>(result);
ViewData["PRODUCT_ADD_OR_EDIT"] = "E";
return View("ProductForm", model);
}
}
5. WebApi 服務端程式碼示例
5.1 控制器基類ApiControllerBase
/// <summary>
/// Controller的基類,用於實現適合業務場景的基礎功能
/// </summary>
/// <typeparam name="T"></typeparam>
[BasicAuthentication]
public abstract class ApiControllerBase : ApiController
{
}
5.2 許可權屬性BaseAuthenticationAttribute
/// <summary>
/// 基本驗證Attribtue,用以Action的許可權處理
/// </summary>
public class BasicAuthenticationAttribute : ActionFilterAttribute
{
/// <summary>
/// 檢查使用者是否有該Action執行的操作許可權
/// </summary>
/// <param name="actionContext"></param>
public override void OnActionExecuting(HttpActionContext actionContext)
{
//檢驗使用者ticket資訊,使用者ticket資訊來自呼叫發起方
if (actionContext.Request.Headers.Authorization != null)
{
//解密使用者ticket,並校驗使用者名稱密碼是否匹配
var encryptTicket = actionContext.Request.Headers.Authorization.Parameter;
if (ValidateUserTicket(encryptTicket))
base.OnActionExecuting(actionContext);
else
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
else
{
//檢查web.config配置是否要求許可權校驗
bool isRquired = (WebConfigurationManager.AppSettings["WebApiAuthenticatedFlag"].ToString() == "true");
if (isRquired)
{
//如果請求Header不包含ticket,則判斷是否是匿名呼叫
var attr = actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().OfType<AllowAnonymousAttribute>();
bool isAnonymous = attr.Any(a => a is AllowAnonymousAttribute);
//是匿名使用者,則繼續執行;非匿名使用者,丟擲“未授權訪問”資訊
if (isAnonymous)
base.OnActionExecuting(actionContext);
else
actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
else
{
base.OnActionExecuting(actionContext);
}
}
}
/// <summary>
/// 校驗使用者ticket資訊
/// </summary>
/// <param name="encryptTicket"></param>
/// <returns></returns>
private bool ValidateUserTicket(string encryptTicket)
{
var userTicket = FormsAuthentication.Decrypt(encryptTicket);
var userTicketData = userTicket.UserData;
string userName = userTicketData.Substring(0, userTicketData.IndexOf(":"));
string password = userTicketData.Substring(userTicketData.IndexOf(":") + 1);
//檢查使用者名稱、密碼是否正確,驗證是合法使用者
//var isQuilified = CheckUser(userName, password);
return true;
}
}
5.3 api服務Controller例項
public class ProductController : ApiControllerBase
{
[HttpGet]
public object Find(string id)
{
return ProductServiceInstance.Find(2);
}
// GET api/product/5
[HttpGet]
[AllowAnonymous]
public Product Get(string id)
{
var headers = Request.Headers;
var p = ProductServiceInstance.GetById<Product, EPProduct>(long.Parse(id));
if (p == null)
{
throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.BadRequest)
Content = new StringContent("id3 not found"), ReasonPhrase = "product id not exist." });
}
return p;
}
}
6. 其它配置說明
6.1 Mvc前端Web.Config 配置
<system.web>
<compilation debug="true" targetFramework="4.5">
<assemblies>
<add assembly="System.Web.Http.Data.Helpers, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</assemblies>
</compilation>
<httpRuntime targetFramework="4.5" />
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" defaultUrl="~/Home/Index" protection="All" timeout="90" name=".AuthCookie"></forms>
</authentication>
<machineKey validationKey="3FFA12388DDF585BA5D35E7BC87E3F0AB47FBBEBD12240DD3BEA2BEAEC4ABA213F22AD27E8FAD77DCFEE306219691434908D193A17C1FC8DCE51B71A4AE54920" decryptionKey="ECB6A3AF9ABBF3F16E80685ED68DC74B0B13CCEE538EBBA97D0B893139683B3B" validation="SHA1" decryption="AES" />
</system.web>
machineKey節點配置,是應用於對使用者ticket資料加密和解密。
6.2 WebApi服務端Web.Config配置
<system.web>
<machineKey validationKey="3FF112388DDF585BA5D35E7BC87E3F0AB47FBBEBD12240DD3BEA2BEAEC4ABA213F22AD27E8FAD77DCFEE306219691434908D193A17C1FC8DCE51B71A4AE54920" decryptionKey="ECB6A3AF9ABBF3F16E80685ED68DC74B0B13CCEE538EBBA97D0B893139683B3B" validation="SHA1" decryption="AES" />
</system.web>
machineKey節點配置,是應用於對使用者ticket資料加密和解密。
7. 總結
Web系統的使用者登入及頁面操作許可權驗證在處理邏輯上比較複雜,需要考慮到Form認證、匿名訪問,Session和Cookie儲存,以及Session和Cookie的過期處理,本文實現了整個許可權驗證框架的基本功能,供系統架構設計人員以及Web開發人員參考。
Demo專案程式碼地址:
https://github.com/besley/DemoUserAuthorization/
8. 春風吹又起--後記
由於專案的需要,徹底打造了登入及許可權驗證的一個新開源專案SlickSafe.NET,歡迎大家挪步。
GitHub:
http://github.com/besley/slicksafe
部落格文章
http://www.cnblogs.com/slickflow/p/6478887.html
線上DEMO地址:
http://demo.slickflow.com//ssweb/
(使用者名稱密碼:admin/123456, jack/123456)
---------------------
原文:https://blog.csdn.net/besley/article/details/8516894