這篇我們主要來介紹我們如何在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 Passwordif (!string.Equals(nowUser.Password, hashedPassword)){throw new ApiException("Wrong Password", "Account_WrongPassword");}#endregionif (!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 sessionvar 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中,但是無法實際測試接口。在接下來的代碼中我會完善這部分.
文章轉載出自 :http://www.cnblogs.com/Flyear/p/4875066.html