上一篇: 【WEB API項目實戰乾貨系列】- 接口文檔與在線測試(二)html
這篇咱們主要來介紹咱們如何在API項目中完成API的登陸及身份認證. 因此這篇會分爲兩部分, 登陸API, API身份驗證.web
這一篇的主要原理是: API會提供一個單獨的登陸API, 經過用戶名,密碼來產生一個SessionKey, SessionKey具備過時時間的特色, 系統會記錄這個SessionKey, 在後續的每次的API返回的時候,客戶端需帶上這個Sessionkey, API端會驗證這個SessionKey.數據庫
咱們先來看一下登陸API的方法簽名api
SessionObject是登陸以後,給客戶端傳回的對象, 裏面包含了SessionKey及當前登陸的用戶的信息安全
這裏每次的API調用,都須要傳SessionKey過去, SessionKey表明了用戶的身份信息,及登陸過時信息。session
登陸階段生成的SessionKey咱們須要作保存,存儲到一個叫作UserDevice的對象裏面, 從語意上能夠知道用戶經過不一樣的設備登陸會產生不一樣的UserDevice對象. ide
最終的登陸代碼以下: 測試
[RoutePrefix("api/accounts")] public class AccountController : ApiController { private readonly IAuthenticationService _authenticationService = null; public AccountController() { //this._authenticationService = IocManager.Intance.Reslove<IAuthenticationService>(); } [HttpGet] public void AccountsAPI() { } /// <summary> /// 登陸API /// </summary> /// <param name="loginIdorEmail">登陸賬號(郵箱或者其餘LoginID)</param> /// <param name="hashedPassword">加密後的密碼,這裏避免明文,客戶端加密後傳到API端</param> /// <param name="deviceType">客戶端的設備類型</param> /// <param name="clientId">客戶端識別號, 通常在APP上會有一個客戶端識別號</param> /// <remarks>其餘的登陸位置啥的,是要客戶端能傳的東西,均可以在這裏擴展進來</remarks> /// <returns></returns> [Route("account/login")] public SessionObject Login(string loginIdorEmail, string hashedPassword, int deviceType = 0, string clientId = "") { if (string.IsNullOrEmpty(loginIdorEmail)) throw new ApiException("username can't be empty.", "RequireParameter_username"); if (string.IsNullOrEmpty(hashedPassword)) throw new ApiException("hashedPassword can't be empty.", "RequireParameter_hashedPassword"); int timeout = 60; var nowUser = _authenticationService.GetUserByLoginId(loginIdorEmail); if (nowUser == null) throw new ApiException("Account Not Exists", "Account_NotExits"); #region Verify Password if (!string.Equals(nowUser.Password, hashedPassword)) { throw new ApiException("Wrong Password", "Account_WrongPassword"); } #endregion if (!nowUser.IsActive) throw new ApiException("The user is inactive.", "InactiveUser"); UserDevice existsDevice = _authenticationService.GetUserDevice(nowUser.UserId, deviceType);// Session.QueryOver<UserDevice>().Where(x => x.AccountId == nowAccount.Id && x.DeviceType == deviceType).SingleOrDefault(); if (existsDevice == null) { string passkey = MD5CryptoProvider.GetMD5Hash(nowUser.UserId + nowUser.LoginName + DateTime.UtcNow.ToString() + Guid.NewGuid().ToString()); existsDevice = new UserDevice() { UserId = nowUser.UserId, CreateTime = DateTime.UtcNow, ActiveTime = DateTime.UtcNow, ExpiredTime = DateTime.UtcNow.AddMinutes(timeout), DeviceType = deviceType, SessionKey = passkey }; _authenticationService.AddUserDevice(existsDevice); } else { existsDevice.ActiveTime = DateTime.UtcNow; existsDevice.ExpiredTime = DateTime.UtcNow.AddMinutes(timeout); _authenticationService.UpdateUserDevice(existsDevice); } nowUser.Password = ""; return new SessionObject() { SessionKey = existsDevice.SessionKey, LogonUser = nowUser }; } }
身份信息的認證是經過Web API 的 ActionFilter來實現的, 每各須要身份驗證的API請求都會要求客戶端傳一個SessionKey在URL裏面丟過來。ui
在這裏咱們經過一個自定義的SessionValidateAttribute來作客戶端的身份驗證, 其繼承自 System.Web.Http.Filters.ActionFilterAttribute, 把這個Attribute加在每一個須要作身份驗證的ApiControler上面,這樣該 Controller下面的全部Action都將擁有身份驗證的功能, 這裏會存在若是有少許的API不須要身份驗證,那該如何處理,這個會作一些排除,爲了保持文章的思路清晰,這會在後續的章節再說明.this
public class SessionValidateAttribute : System.Web.Http.Filters.ActionFilterAttribute { public const string SessionKeyName = "SessionKey"; public const string LogonUserName = "LogonUser"; public override void OnActionExecuting(HttpActionContext filterContext) { var qs = HttpUtility.ParseQueryString(filterContext.Request.RequestUri.Query); string sessionKey = qs[SessionKeyName]; if (string.IsNullOrEmpty(sessionKey)) { throw new ApiException("Invalid Session.", "InvalidSession"); } IAuthenticationService authenticationService = IocManager.Intance.Reslove<IAuthenticationService>(); //validate user session var userSession = authenticationService.GetUserDevice(sessionKey); if (userSession == null) { throw new ApiException("sessionKey not found", "RequireParameter_sessionKey"); } else { //todo: 加Session是否過時的判斷 if (userSession.ExpiredTime < DateTime.UtcNow) throw new ApiException("session expired", "SessionTimeOut"); var logonUser = authenticationService.GetUser(userSession.UserId); if (logonUser == null) { throw new ApiException("User not found", "Invalid_User"); } else { filterContext.ControllerContext.RouteData.Values[LogonUserName] = logonUser; SetPrincipal(new UserPrincipal<int>(logonUser)); } userSession.ActiveTime = DateTime.UtcNow; userSession.ExpiredTime = DateTime.UtcNow.AddMinutes(60); authenticationService.UpdateUserDevice(userSession); } } private void SetPrincipal(IPrincipal principal) { Thread.CurrentPrincipal = principal; if (HttpContext.Current != null) { HttpContext.Current.User = principal; } } }
OnActionExcuting方法:
這個是在進入某個Action以前作檢查, 這個時候咱們恰好能夠同RequestQueryString中拿出SessionKey到UserDevice表中去作查詢,來驗證Sessionkey的真僞, 以達到身份驗證的目的。
用戶的過時時間:
在每一個API訪問的時候,會自動更新Session(也就是UserDevice)的過時時間, 以保證SessionKey不會過時,若是長時間未更新,則下次訪問會過時,須要從新登陸作處理。
Request.IsAuthented:
上面代碼的最後一段SetPrincipal就是來設置咱們線程上下文及HttpContext上下文中的用戶身份信息, 在這裏咱們實現了咱們本身的用戶身份類型
public class UserIdentity<TKey> : IIdentity { public UserIdentity(IUser<TKey> user) { if (user != null) { IsAuthenticated = true; UserId = user.UserId; Name = user.LoginName.ToString(); DisplayName = user.DisplayName; } } public string AuthenticationType { get { return "CustomAuthentication"; } } public TKey UserId { get; private set; } public bool IsAuthenticated { get; private set; } public string Name { get; private set; } public string DisplayName { get; private set; } } public class UserPrincipal<TKey> : IPrincipal { public UserPrincipal(UserIdentity<TKey> identity) { Identity = identity; } public UserPrincipal(IUser<TKey> user) : this(new UserIdentity<TKey>(user)) { } /// <summary> /// /// </summary> public UserIdentity<TKey> Identity { get; private set; } IIdentity IPrincipal.Identity { get { return Identity; } } bool IPrincipal.IsInRole(string role) { throw new NotImplementedException(); } } public interface IUser<T> { T UserId { get; set; } string LoginName { get; set; } string DisplayName { get; set; } }
這樣能夠保證咱們在系統的任何地方,經過HttpContext.User 或者 System.Threading.Thread.CurrentPrincipal能夠拿到當前線程上下文的用戶信息, 方便各處使用
加入身份認證以後的Product相關API以下:
[RoutePrefix("api/products"), SessionValidate] public class ProductController : ApiController { [HttpGet] public void ProductsAPI() { } /// <summary> /// 產品分頁數據獲取 /// </summary> /// <returns></returns> [HttpGet, Route("product/getList")] public Page<Product> GetProductList(string sessionKey) { return new Page<Product>(); } /// <summary> /// 獲取單個產品 /// </summary> /// <param name="productId"></param> /// <returns></returns> [HttpGet, Route("product/get")] public Product GetProduct(string sessionKey, Guid productId) { return new Product() { ProductId = productId }; } /// <summary> /// 添加產品 /// </summary> /// <param name="product"></param> /// <returns></returns> [HttpPost, Route("product/add")] public Guid AddProduct(string sessionKey, Product product) { return Guid.NewGuid(); } /// <summary> /// 更新產品 /// </summary> /// <param name="productId"></param> /// <param name="product"></param> [HttpPost, Route("product/update")] public void UpdateProduct(string sessionKey, Guid productId, Product product) { } /// <summary> /// 刪除產品 /// </summary> /// <param name="productId"></param> [HttpDelete, Route("product/delete")] public void DeleteProduct(string sessionKey, Guid productId) { }
能夠看到咱們的ProductController上面加了SessionValidateAttribute, 每一個Action參數的第一個位置,加了一個string sessionKey的佔位, 這個主要是爲了讓Swagger.Net能在UI上生成測試窗口
這篇並無使用OAuth等受權機制,只是簡單的實現了登陸受權,這種方式適合小項目使用.
這裏也只是實現了系統的登陸,API訪問安全,並不能保證 API系統的絕對安全,咱們能夠透過 路由的上的HTTP消息攔截, 攔截到咱們的API請求,截獲密碼等登陸信息, 所以咱們還須要給咱們的API增長SSL證書,實現 HTTPS加密傳輸。
另外在前幾天的有看到結合客戶端IP地址等後混合生成 Sessionkey來作安全的,可是也具備必定的侷限性, 那種方案合適,仍是要根據本身的實際項目狀況來肯定.
因爲時間緣由, 本篇只是從原理方面介紹了API用戶登陸與訪問身份認證,由於這部分真實的測試設計到數據庫交互, Ioc等基礎設施的支撐,因此這篇的代碼只能出如今SwaggerUI中,可是沒法實際測試接口。在接下來的代碼中我會完善這部分.