【WEB API專案實戰乾貨系列】- API登入與身份驗證(三)

DukeCheng發表於2015-10-13

上一篇: 【WEB API專案實戰乾貨系列】- 介面文件與線上測試(二)

這篇我們主要來介紹我們如何在API專案中完成API的登入及身份認證. 所以這篇會分為兩部分, 登入API, API身份驗證.

這一篇的主要原理是: API會提供一個單獨的登入API, 通過使用者名稱,密碼來產生一個SessionKey, SessionKey具有過期時間的特點, 系統會記錄這個SessionKey, 在後續的每次的API返回的時候,客戶端需帶上這個Sessionkey, API端會驗證這個SessionKey.

登入API

我們先來看一下登入API的方法簽名

image

 

SessionObject是登入之後,給客戶端傳回的物件, 裡面包含了SessionKey及當前登入的使用者的資訊

image

這裡每次的API呼叫,都需要傳SessionKey過去, SessionKey代表了使用者的身份資訊,及登入過期資訊。

 

登入階段生成的SessionKey我們需要做儲存,儲存到一個叫做UserDevice的物件裡面, 從語意上可以知道使用者通過不同的裝置登入會產生不同的UserDevice物件.

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裡面丟過來。

在這裡我們通過一個自定義的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上生成測試視窗

image

這篇並沒有使用OAuth等授權機制,只是簡單的實現了登入授權,這種方式適合小專案使用.

這裡也只是實現了系統的登入,API訪問安全,並不能保證 API系統的絕對安全,我們可以透過 路由的上的HTTP訊息攔截, 攔截到我們的API請求,截獲密碼等登入資訊, 因此我們還需要給我們的API增加SSL證書,實現 HTTPS加密傳輸。

另外在前幾天的有看到結合客戶端IP地址等後混合生成 Sessionkey來做安全的,但是也具有一定的侷限性, 那種方案合適,還是要根據自己的實際專案情況來確定.

 

由於時間原因, 本篇只是從原理方面介紹了API使用者登入與訪問身份認證,因為這部分真實的測試設計到資料庫互動, Ioc等基礎設施的支撐,所以這篇的程式碼只能出現在SwaggerUI中,但是無法實際測試介面。在接下來的程式碼中我會完善這部分.

程式碼: 程式碼下載(程式碼託管在CSDN Code)

相關文章