自訂權限加驗證
介紹
此節將會說明如何做到以下權限驗證
- 自訂的基本驗證(例如:會員資料表的帳密)
- 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驗證可參考以下文章
- OAuth 2.0 筆記 (6) Bearer Token 的使用方法
- HTTP使用BASIC认证的原理及实现方法
- 自定义http Authorization Header
- Token Authorization
實作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
- 繼承GenericAuthenticationFilter
- OnAuthorizeUser方法
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)
- 建立Http Header
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
{
...省略
}
}