【WEB API項目實戰乾貨系列】- API登陸與身份驗證(三)

上一篇: 【WEB API項目實戰乾貨系列】- 接口文檔與在線測試(二)html

這篇咱們主要來介紹咱們如何在API項目中完成API的登陸及身份認證. 因此這篇會分爲兩部分, 登陸API, API身份驗證.web

這一篇的主要原理是: API會提供一個單獨的登陸API, 經過用戶名,密碼來產生一個SessionKey, SessionKey具備過時時間的特色, 系統會記錄這個SessionKey, 在後續的每次的API返回的時候,客戶端需帶上這個Sessionkey, API端會驗證這個SessionKey.數據庫

登陸API

咱們先來看一下登陸API的方法簽名api

image

 

SessionObject是登陸以後,給客戶端傳回的對象, 裏面包含了SessionKey及當前登陸的用戶的信息安全

image

這裏每次的API調用,都須要傳SessionKey過去, SessionKey表明了用戶的身份信息,及登陸過時信息。session

 

登陸階段生成的SessionKey咱們須要作保存,存儲到一個叫作UserDevice的對象裏面, 從語意上能夠知道用戶經過不一樣的設備登陸會產生不一樣的UserDevice對象. ide

image

 

最終的登陸代碼以下: 測試

[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裏面丟過來。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上生成測試窗口

image

這篇並無使用OAuth等受權機制,只是簡單的實現了登陸受權,這種方式適合小項目使用.

這裏也只是實現了系統的登陸,API訪問安全,並不能保證 API系統的絕對安全,咱們能夠透過 路由的上的HTTP消息攔截, 攔截到咱們的API請求,截獲密碼等登陸信息, 所以咱們還須要給咱們的API增長SSL證書,實現 HTTPS加密傳輸。

另外在前幾天的有看到結合客戶端IP地址等後混合生成 Sessionkey來作安全的,可是也具備必定的侷限性, 那種方案合適,仍是要根據本身的實際項目狀況來肯定.

 

因爲時間緣由, 本篇只是從原理方面介紹了API用戶登陸與訪問身份認證,由於這部分真實的測試設計到數據庫交互, Ioc等基礎設施的支撐,因此這篇的代碼只能出如今SwaggerUI中,可是沒法實際測試接口。在接下來的代碼中我會完善這部分.

代碼: 代碼下載(代碼託管在CSDN Code)

相關文章
相關標籤/搜索