自訂權限加驗證

介紹

此節將會說明如何做到以下權限驗證

  • 自訂的基本驗證(例如:會員資料表的帳密)
  • Token
    • 授權
    • 保持Session的資料
    • 延長Session的使用時間

Authentication(驗證)

主要是驗證正在存取服務的使用者是否有足夠的權限。

此節將會透過Html Form所傳進來的「帳號」、「密碼」去示範「基本驗證」的實作。

Authorization(授權)

驗證使用者的權限後第二個步驟就是授權了,在此步驟會依照使用者之前在驗證階段所傳的「帳號」、「密碼」去取得「權限」,並授予使用者它的「權限」, 。

此節會透過Token做為授權的主要方式。

維持Session

RESTful的服務是無狀態的(例如:Http),但若是要使用Web Api來實作登入及登出就必須有Session,那麼該怎麼做呢?可以透過在授權階段取得的Token來「保持Session的狀態」和「延長Session的使用時間」。

Basic Authentication(基本驗證)

Basic Authentication是一個機制,使用者可以將「帳號」、「密碼」夾帶於 Header中傳到Sever去做驗證,並回傳相對應的Http Code:

  • 200 (驗證成功)
  • 400,401 (驗證失敗)

http code 並無限制,這只是常見的做法

優點:

  • 實作簡單
  • 大部份的瀏覽器都能運作

缺點:

  • 每次跟Server溝通都需要傳送驗證的資訊(帳、密),很容易會有CSRF的問題
    • 作者建議使用SSL來避免

Token Based Authorization(Token驗證)

在驗證後的授權階段回傳記錄使用者權限的加密Token,而Token的加密方式無限制,只要讓Sever能夠解晰就行了。

若要讓Token可重複使用大多的方式都會記錄在DB或外部檔案,若有必要還可以加入有效時間,避免Token被濫用。

實作basic Authentication

Business Services

建立User的Service

IUserServices.cs

定義驗證User資料需要的Service

namespace BusinessServices
{
    public interface IUserServices
    {
        int Authenticate(string userName, string password);
    }
}
UserServices.cs

實作驗證User資料需要的Service

需在介面:IUnitofWork中加入UserRepository的定義

using DataModel.UnitOfWork;

namespace BusinessServices
{
    /// <summary>
    /// Offers services for user specific operations
    /// </summary>
    public class UserServices : IUserServices
    {
        private readonly IUnitOfWork _unitOfWork;

        /// <summary>
        /// Public constructor.
        /// </summary>
        public UserServices(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }

        /// <summary>
        /// Public method to authenticate user by user name and password.
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="password"></param>
        /// <returns></returns>
        public int Authenticate(string userName, string password)
        {
            var user = _unitOfWork.UserRepository.Get(u => u.UserName == userName && u.Password == password);
            if (user != null && user.UserId > 0)
            {
                return user.UserId;
            }
            return 0;
        }
    }
}
IUnitOfWork.cs

加入UserRepository的定義

namespace DataModel.UnitOfWork
{
    public interface IUnitOfWork
    {
        GenericRepository<Products> ProductRepository { get; }
        GenericRepository<User> UserRepository { get;  }

        /// <summary>
        /// Save method.
        /// </summary>
        void Save();
    }
}
DependencyResolver.cs

加入UserService的DI設定

using DataModel.UnitOfWork;
using Resolver;
using System.ComponentModel.Composition;

namespace BusinessServices
{
    [Export(typeof(IComponent))]
    public class DependencyResolver : IComponent
    {
        public void SetUp(IRegisterComponent registerComponent)
        {
            registerComponent.RegisterType<IProductServices, ProductServices>();
            registerComponent.RegisterType<IUnitOfWork, UnitOfWork>();

            //新加入的
            registerComponent.RegisterType<IUserServices, UserServices>();
        }
    }
}

在主專案建立Filter

加入資料夾:Filters,並建立授權用的Filter讓Web Api使用

建立共用的授權Filter

GenericAuthenticationFilter.cs

Basic驗證可參考以下文章

實作AuthorizationFilterAttribute讓它有授權Filter的功能

  • 建構式加入IsActive來指定是否啟用Fitler(預設:啟用)
  • 繼承AuthorizationFilter
  • 覆寫OnAuthorization方法
    • 呼叫自訂的驗證方法(FetchAuthHeader)
    • 若帳號及密碼符合驗證,則針對目前連線使用者做授權
    • 透過ChallengeAuthRequest方法將授權結果夾帶於Response中
  • FetchAuthHeader
    • 驗證Request中的Header是否有包含"Basic"關鍵字
    • 驗證Request中的Header所傳送的「帳號」及「密碼」
    • 預期字串會是Base64的格式
  • ChallengeAuthRequest
    • 回傳401當作授權失敗
    • 在Header中的「WWW-Authenticate」建立路「Basic realm」並給了目前的網域名稱
using System;
using System.Net;
using System.Net.Http;
using System.Security.Principal;
using System.Text;
using System.Threading;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace WebApi.Filters
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
    public class GenericAuthenticationFilter : AuthorizationFilterAttribute
    {

        /// <summary>
        /// Public default Constructor
        /// </summary>
        public GenericAuthenticationFilter()
        {
        }

        private readonly bool _isActive = true;

        /// <summary>
        /// parameter isActive explicitly enables/disables this filetr.
        /// </summary>
        /// <param name="isActive"></param>
        public GenericAuthenticationFilter(bool isActive)
        {
            _isActive = isActive;
        }

        /// <summary>
        /// Checks basic authentication request
        /// </summary>
        /// <param name="filterContext"></param>
        public override void OnAuthorization(HttpActionContext filterContext)
        {
            if (!_isActive) return;
            var identity = FetchAuthHeader(filterContext);
            if (identity == null)
            {
                ChallengeAuthRequest(filterContext);
                return;
            }
            var genericPrincipal = new GenericPrincipal(identity, null);
            //針對目前連線的使用者做授權 
            Thread.CurrentPrincipal = genericPrincipal;
            if (!OnAuthorizeUser(identity.Name, identity.Password, filterContext))
            {
                ChallengeAuthRequest(filterContext);
                return;
            }
            base.OnAuthorization(filterContext);
        }

        /// <summary>
        /// Virtual method.Can be overriden with the custom Authorization.
        /// </summary>
        /// <param name="user"></param>
        /// <param name="pass"></param>
        /// <param name="filterContext"></param>
        /// <returns></returns>
        protected virtual bool OnAuthorizeUser(string user, string pass, HttpActionContext filterContext)
        {
            if (string.IsNullOrEmpty(user) || string.IsNullOrEmpty(pass))
                return false;
            return true;
        }

        /// <summary>
        /// Checks for autrhorization header in the request and parses it, creates user credentials and returns as BasicAuthenticationIdentity
        /// </summary>
        /// <param name="filterContext"></param>
        protected virtual BasicAuthenticationIdentity FetchAuthHeader(HttpActionContext filterContext)
        {
            string authHeaderValue = null;
            var authRequest = filterContext.Request.Headers.Authorization;
            if (authRequest != null && !String.IsNullOrEmpty(authRequest.Scheme) && authRequest.Scheme == "Basic")
                authHeaderValue = authRequest.Parameter;
            if (string.IsNullOrEmpty(authHeaderValue))
                return null;
            authHeaderValue = Encoding.Default.GetString(Convert.FromBase64String(authHeaderValue));
            var credentials = authHeaderValue.Split(':');
            return credentials.Length < 2 ? null : new BasicAuthenticationIdentity(credentials[0], credentials[1]);
        }


        /// <summary>
        /// Send the Authentication Challenge request
        /// </summary>
        /// <param name="filterContext"></param>
        private static void ChallengeAuthRequest(HttpActionContext filterContext)
        {
            var dnsHost = filterContext.Request.RequestUri.DnsSafeHost;
            filterContext.Response = filterContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
            filterContext.Response.Headers.Add("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", dnsHost));
        }
    }
}

建立Basic Authentication Identity

BasicAuthenticationIdentity.cs
  • 建構式
    • 指定為驗證類型:Basic
using System.Security.Principal;

namespace WebApi.Filters
{
    public class BasicAuthenticationIdentity : GenericIdentity
    {
        /// <summary>
        /// Get/Set for password
        /// </summary>
        public string Password { get; set; }
        /// <summary>
        /// Get/Set for UserName
        /// </summary>
        public string UserName { get; set; }
        /// <summary>
        /// Get/Set for UserId
        /// </summary>
        public int UserId { get; set; }

        /// <summary>
        /// Basic Authentication Identity Constructor
        /// </summary>
        /// <param name="userName"></param>
        /// <param name="password"></param>
        public BasicAuthenticationIdentity(string userName, string password)
            : base(userName, "Basic")
        {
            Password = password;
            UserName = userName;
        }
    }
}

建立自訂的授權Filter

ApiAuthenticationFilter.cs
using BusinessServices;
using System.Threading;
using System.Web.Http.Controllers;

namespace WebApi.Filters
{
    /// <summary>
    /// Custom Authentication Filter Extending basic Authentication
    /// </summary>
    public class ApiAuthenticationFilter : GenericAuthenticationFilter
    {
        /// <summary>
        /// Default Authentication Constructor
        /// </summary>
        public ApiAuthenticationFilter()
        {
        }

        /// <summary>
        /// AuthenticationFilter constructor with isActive parameter
        /// </summary>
        /// <param name="isActive"></param>
        public ApiAuthenticationFilter(bool isActive)
            : base(isActive)
        {
        }

        /// <summary>
        /// Protected overriden method for authorizing user
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <param name="actionContext"></param>
        /// <returns></returns>
        protected override bool OnAuthorizeUser(string username, string password, HttpActionContext actionContext)
        {
            var provider = actionContext.ControllerContext.Configuration
                               .DependencyResolver.GetService(typeof(IUserServices)) as IUserServices;
            if (provider != null)
            {
                var userId = provider.Authenticate(username, password);
                if (userId > 0)
                {
                    var basicAuthenticationIdentity = Thread.CurrentPrincipal.Identity as BasicAuthenticationIdentity;
                    if (basicAuthenticationIdentity != null)
                        basicAuthenticationIdentity.UserId = userId;
                    return true;
                }
            }
            return false;
        }
    }
}

使用Filter

在ProductController裡加入授權的ActionFilter

namespace WebApi.Controllers
{
    ...省略其它Attribute
    [ApiAuthenticationFilter]
    public class ProductController : ApiController
    {
      ....以下省略
    }
}

加入Filter的全域設定

若是要直接套用到全部的Controller,可透過AppStrt資料夾下的設定檔,加入Filter的全域設定檔(非必要)

WebApiConfig.cs
using System.Web.Http;
using WebApi.Filters;

namespace WebApi
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 設定和服務
            GlobalConfiguration.Configuration.Filters.Add(new ApiAuthenticationFilter());

            ...以下省略
        }
    }
}

驗證身份

在Header上加入下列資訊去取得授權後的Token

Key Value
Authorization Basic YWtoaWw6YWtoaWw=

Authorization中所傳入的Base64編碼(YWtoaWw6YWtoaWw=)解密後為 akhil:akhil,正好是資料庫內User資料表的其中一筆資料

設計差異

basic Authentication 與 token Authentication

在SSL中Base Authentication(基本驗證)是很安全的,但每次都需要傳送「帳號」及「密碼」。

相較於Base Authentication,使用token Authentication的好處在於取得授權結果後就可以直接去使用其它的相關服務,不需要在每次的要求都先做一次驗證。

使用者若要登入,都會透過同樣的endpoint,所以客戶端就只需要知道如何透過憑證登入至系統。

使用者登入後就會取得guid或任何加密方式的一個token,後續在每個request時都需要送出token,會用它來做身份驗證。

同時token也可以達到「保持Session資料」的效用,預設會給token的有效時間為15分鐘(會寫在web.config裡),當Session過期時,使用者將會被登出,當再次登入時會再取得新的token。

改為token Authentication

後續會將token放置於db中的Tokens資料表,它的用途:

  • 記錄過期時間

Business Services

建立Token用的Service

ITokenServices.cs
using BusinessEntities;

namespace BusinessServices
{
    public interface ITokenServices
    {
        #region Interface member methods.
        /// <summary>
        ///  Function to generate unique token with expiry against the provided userId.
        ///  Also add a record in database for generated token.
        /// </summary>
        /// <param name="userId"></param>
        /// <returns></returns>
        TokenEntity GenerateToken(int userId);

        /// <summary>
        /// Function to validate token againt expiry and existance in database.
        /// </summary>
        /// <param name="tokenId"></param>
        /// <returns></returns>
        bool ValidateToken(string tokenId);

        /// <summary>
        /// Method to kill the provided token id.
        /// </summary>
        /// <param name="tokenId"></param>
        bool Kill(string tokenId);

        /// <summary>
        /// Delete tokens for the specific deleted user
        /// </summary>
        /// <param name="userId"></param>
        /// <returns></returns>
        bool DeleteByUserId(int userId);
        #endregion
    }
}
TokenServices.cs
  • GenerateToken
    • 建立一個有效期限的Token
    • 新增至DB的Token資料表做記錄
  • ValidateToken
    • 驗證Token是否有效及是期過期
    • 若Token有效則累加有效時間(剩於:5分鐘 + 有效時間:15分鐘 = 20分鐘),並更新回Db的Token資料表
  • Kill
    • 由資料庫內刪除token的記錄
  • DeleteByUserId
    • 刪除特定UserID的Token資料
using BusinessEntities;
using DataModel;
using DataModel.UnitOfWork;
using System;
using System.Configuration;
using System.Linq;

namespace BusinessServices
{
    public class TokenServices : ITokenServices
    {
        #region Private member variables.
        private readonly IUnitOfWork _unitOfWork;
        #endregion

        #region Public constructor.
        /// <summary>
        /// Public constructor.
        /// </summary>
        public TokenServices(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }
        #endregion


        #region Public member methods.

        /// <summary>
        ///  Function to generate unique token with expiry against the provided userId.
        ///  Also add a record in database for generated token.
        /// </summary>
        /// <param name="userId"></param>
        /// <returns></returns>
        public TokenEntity GenerateToken(int userId)
        {
            string token = Guid.NewGuid().ToString();
            DateTime issuedOn = DateTime.Now;
            //由web.config取得token的過期時間
            DateTime expiredOn = DateTime.Now.AddSeconds(
            Convert.ToDouble(ConfigurationManager.AppSettings["AuthTokenExpiry"]));
            var tokendomain = new Tokens
            {
                UserId = userId,
                AuthToken = token,
                IssuedOn = issuedOn,
                ExpiresOn = expiredOn
            };

            _unitOfWork.TokenRepository.Insert(tokendomain);
            _unitOfWork.Save();
            var tokenModel = new TokenEntity()
            {
                UserId = userId,
                IssuedOn = issuedOn,
                ExpiresOn = expiredOn,
                AuthToken = token
            };

            return tokenModel;
        }

        /// <summary>
        /// Method to validate token against expiry and existence in database.
        /// </summary>
        /// <param name="tokenId"></param>
        /// <returns></returns>
        public bool ValidateToken(string tokenId)
        {
            var token = _unitOfWork.TokenRepository.Get(t => t.AuthToken == tokenId && t.ExpiresOn > DateTime.Now);
            if (token != null && !(DateTime.Now > token.ExpiresOn))
            {
                token.ExpiresOn = token.ExpiresOn.AddSeconds(
                Convert.ToDouble(ConfigurationManager.AppSettings["AuthTokenExpiry"]));
                _unitOfWork.TokenRepository.Update(token);
                _unitOfWork.Save();
                return true;
            }
            return false;
        }

        /// <summary>
        /// Method to kill the provided token id.
        /// </summary>
        /// <param name="tokenId">true for successful delete</param>
        public bool Kill(string tokenId)
        {
            _unitOfWork.TokenRepository.Delete(x => x.AuthToken == tokenId);
            _unitOfWork.Save();
            var isNotDeleted = _unitOfWork.TokenRepository.GetMany(x => x.AuthToken == tokenId).Any();
            if (isNotDeleted) { return false; }
            return true;
        }

        /// <summary>
        /// Delete tokens for the specific deleted user
        /// </summary>
        /// <param name="userId"></param>
        /// <returns>true for successful delete</returns>
        public bool DeleteByUserId(int userId)
        {
            _unitOfWork.TokenRepository.Delete(x => x.UserId == userId);
            _unitOfWork.Save();

            var isNotDeleted = _unitOfWork.TokenRepository.GetMany(x => x.UserId == userId).Any();
            return !isNotDeleted;
        }

        #endregion
    }
}
IUnitOfWork.cs

加入TokenRepository的定義

namespace DataModel.UnitOfWork
{
    public interface IUnitOfWork
    {
        ....省略

        GenericRepository<Tokens> TokenRepository { get;  }
    }
}
DependencyResolver.cs

加入ITokenService的設定

using DataModel.UnitOfWork;
using Resolver;
using System.ComponentModel.Composition;

namespace BusinessServices
{
    [Export(typeof(IComponent))]
    public class DependencyResolver : IComponent
    {
        public void SetUp(IRegisterComponent registerComponent)
        {
            registerComponent.RegisterType<IProductServices, ProductServices>();
            registerComponent.RegisterType<IUnitOfWork, UnitOfWork>();
  registerComponent.RegisterType<IUserServices, UserServices>();

            //新加入的
            registerComponent.RegisterType<ITokenServices, TokenServices>();
        }
    }
}

建立Token驗證的Controller

回到主專案並建立會下列檔案

AuthenticateController.cs
  • 在Controler上加入授權的Attriubte
  • Authenticate方法
    • 透過CurrentThread驗證目前的登入者是否通過授權
    • 透過Attriubute Route讓使用者可以用多種路徑存取
      • login
      • authenticate
      • get/token
    • 存取時必須為HttpPost
  • GetAuthToken方法
    • 建立Http Header
      • Token : 有授權的token值
      • TokenExpiry : token過期時間
      • Access-Control-Expose-Headers : 允許傳送的Header白名單
      • HTTP access control (CORS)
using BusinessServices;
using System.Configuration;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using WebApi.Filters;

namespace WebApi.Controllers
{
    [ApiAuthenticationFilter]
    public class AuthenticateController : ApiController
    {
        #region Private variable.

        private readonly ITokenServices _tokenServices;

        #endregion

        #region Public Constructor

        /// <summary>
        /// Public constructor to initialize product service instance
        /// </summary>
        public AuthenticateController(ITokenServices tokenServices)
        {
            _tokenServices = tokenServices;
        }

        #endregion

        /// <summary>
        /// Authenticates user and returns token with expiry.
        /// </summary>
        /// <returns></returns>
        [Route("login")]
        [Route("authenticate")]
        [Route("get/token")]
        [HttpPost]
        public HttpResponseMessage Authenticate()
        {
            if (System.Threading.Thread.CurrentPrincipal != null && System.Threading.Thread.CurrentPrincipal.Identity.IsAuthenticated)
            {
                var basicAuthenticationIdentity = System.Threading.Thread.CurrentPrincipal.Identity as BasicAuthenticationIdentity;
                if (basicAuthenticationIdentity != null)
                {
                    var userId = basicAuthenticationIdentity.UserId;
                    return GetAuthToken(userId);
                }
            }
            return null;
        }

        /// <summary>
        /// Returns auth token for the validated user.
        /// </summary>
        /// <param name="userId"></param>
        /// <returns></returns>
        private HttpResponseMessage GetAuthToken(int userId)
        {
            var token = _tokenServices.GenerateToken(userId);
            var response = Request.CreateResponse(HttpStatusCode.OK, "Authorized");
            response.Headers.Add("Token", token.AuthToken);
            response.Headers.Add("TokenExpiry", ConfigurationManager.AppSettings["AuthTokenExpiry"]);
            response.Headers.Add("Access-Control-Expose-Headers", "Token,TokenExpiry");
            return response;
        }
    }
}
web.config

設定token的有效時間(預設15分鐘)

<configuration>
  <appSettings>
    <!-- 單位為秒 900秒 = 15分鐘 -->
    <add key="AuthTokenExpiry" value="900"/>
  </appSettings>
</configuration>

驗證身份

跟之前的方式一樣,只是改為HttpPost至以下的路徑去做驗證

  • login
  • authenticate
  • get/token
回傳的http header
Access-Control-Expose-Headers →Token,TokenExpiry
Cache-Control →no-cache
Content-Length →12
Content-Type →application/json; charset=utf-8
Date →Thu, 14 Jul 2016 08:21:45 GMT
Expires →-1
Pragma →no-cache
Server →Microsoft-IIS/10.0
Token →46b8eb5c-2f2e-48f4-898a-53ad26030b82
TokenExpiry →15
X-AspNet-Version →4.0.30319
X-Powered-By →ASP.NET
X-SourceFiles →=?UTF-8?B?RDpcUHJvamVjdFxHaXRcTGVhcm5fUmVzdGZ1bFxXZWJBcGlcbG9naW4=?=

建立Action Filter

確定會回傳token後,回到主專案,建立下列檔案

AuthorizationRequiredAttribute.cs

需自行建立ActionFilters資料夾

我們之前有建立了一個ApiAuthenticationFilter用來做basic驗證(授權階段),現在我們將會改由ActionFilter在每一個Request中驗證Token是否有效。

--

在每個reqeust中會透過TokenServer的ValidateToken方法去確認Token是否存在於DB中:

  • 驗證通過:進入controller及Action
  • 驗證失敗:回傳Invalid Request的訊息
using BusinessServices;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace WebApi.ActionFilters
{
    public class AuthorizationRequiredAttribute : ActionFilterAttribute
    {
        private const string Token = "Token";

        public override void OnActionExecuting(HttpActionContext filterContext)
        {
            //  Get API key provider
            var provider = filterContext.ControllerContext.Configuration
            .DependencyResolver.GetService(typeof(ITokenServices)) as ITokenServices;

            if (filterContext.Request.Headers.Contains(Token))
            {
                var tokenValue = filterContext.Request.Headers.GetValues(Token).First();

                // Validate Token
                if (provider != null && !provider.ValidateToken(tokenValue))
                {
                    var responseMessage = new HttpResponseMessage(HttpStatusCode.Unauthorized) { ReasonPhrase = "Invalid Request" };
                    filterContext.Response = responseMessage;
                }
            }
            else
            {
                filterContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
            }

            base.OnActionExecuting(filterContext);

        }
    }
}

修改ProductController的驗證方式

加入token的驗證actionFilter至controller上面,讓每個Action都會套用到驗證

ProductController.cs
namespace WebApi.Controllers
{
    ....省略其它的Attribute
    [AuthorizationRequired]    
    //[ApiAuthenticationFilter]
    public class ProductController : ApiController
    {
        ...省略
    }
}

results matching ""

    No results matching ""