上一篇: 【WEB API專案實戰乾貨系列】- 介面文件與線上測試(二)
這篇我們主要來介紹我們如何在API專案中完成API的登入及身份認證. 所以這篇會分為兩部分, 登入API, API身份驗證.
這一篇的主要原理是: API會提供一個單獨的登入API, 通過使用者名稱,密碼來產生一個SessionKey, SessionKey具有過期時間的特點, 系統會記錄這個SessionKey, 在後續的每次的API返回的時候,客戶端需帶上這個Sessionkey, API端會驗證這個SessionKey.
登入API
我們先來看一下登入API的方法簽名
SessionObject是登入之後,給客戶端傳回的物件, 裡面包含了SessionKey及當前登入的使用者的資訊
這裡每次的API呼叫,都需要傳SessionKey過去, SessionKey代表了使用者的身份資訊,及登入過期資訊。
登入階段生成的SessionKey我們需要做儲存,儲存到一個叫做UserDevice的物件裡面, 從語意上可以知道使用者通過不同的裝置登入會產生不同的UserDevice物件.
最終的登入程式碼如下:
[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 }; } }
API身份驗證
身份資訊的認證是通過Web API 的 ActionFilter來實現的, 每各需要身份驗證的API請求都會要求客戶端傳一個SessionKey在URL裡面丟過來。
在這裡我們通過一個自定義的SessionValidateAttribute來做客戶端的身份驗證, 其繼承自 System.Web.Http.Filters.ActionFilterAttribute, 把這個Attribute加在每個需要做身份驗證的ApiControler上面,這樣該 Controller下面的所有Action都將擁有身份驗證的功能, 這裡會存在如果有少量的API不需要身份驗證,那該如何處理,這個會做一些排除,為了保持文章的思路清晰,這會在後續的章節再說明.
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中,但是無法實際測試介面。在接下來的程式碼中我會完善這部分.