RESTful 日常 #5:Web API 中的安全性 - 基本身份验证和使用 Action Filters 在 Web API 中实现基于 Token 的自定义授权






4.96/5 (168投票s)
本文将解释如何使用基本身份验证和基于令牌的授权来保护 WebAPI 的安全。
目录
引言
在企业级应用程序中,安全性始终是主要关注点,尤其是在我们通过服务公开业务时。我已经在我的系列早期文章中对 WebAPI 进行了很多解释。我解释了如何创建 WebAPI,如何解决依赖关系以使其成为松耦合设计,定义自定义路由,以及如何使用属性路由。我的文章将解释如何在 WebAPI 中实现安全性。本文将解释如何使用基本身份验证和基于令牌的授权来保护 WebAPI 的安全。我还将解释如何利用 WebAPI 中基于令牌的授权和基本身份验证来维护 WebAPI 中的会话。WebAPI 中没有标准的实现安全性方法。我们可以设计自己的安全技术和结构,以最适合我们的应用程序。
路线图
以下是我逐步学习 WebAPI 的路线图:
- RESTful 第 1 天:使用 Entity Framework、通用仓库模式和工作单元的 Web API 企业级应用程序架构。
- RESTful 第 2 天:使用 Unity 容器和引导程序在 Web API 中使用依赖注入实现控制反转。
- RESTful 第 3 天:使用 Unity 容器和托管可扩展性框架 (MEF) 在 Asp.net Web API 中使用控制反转和依赖注入解决依赖关系的依赖关系。
- RESTful 第 4 天:使用 MVC 4 Web API 中的属性路由进行自定义 URL 重写/路由。
- RESTful 第 5 天:使用 Action Filter 在 Web API 中实现基本身份验证和基于令牌的自定义授权。
- RESTful 第 6 天:使用 Action Filter、异常过滤器和 NLog 在 Web API 中进行请求日志记录和异常处理/日志记录。
- RESTful 第 7 天:使用 NUnit 和 Moq 框架在 WebAPI 中进行单元测试和集成测试(第 1 部分)。
- RESTful 第 8 天:使用 NUnit 和 Moq 框架在 WebAPI 中进行单元测试和集成测试(第 2 部分)。
- RESTful Day #9:在ASP.NET Web API中扩展OData支持.
- RESTful 第 10 天:在 Visual Studio 2010 中创建带有 CRUD 操作的自托管 ASP.NET WebAPI
我将特意使用 Visual Studio 2010 和 .NET Framework 4.0,因为在 .NET Framework 4.0 中,有些实现很难找到,但我会通过展示如何做到这一点来使其变得容易。
WebAPI 中的安全性
安全本身就是一个非常复杂和棘手的话题。我将尝试以我自己的方式解释如何在 WebAPI 中实现它。
当我们计划创建企业级应用程序时,我们尤其需要注意身份验证和授权。如果使用得当,这两种技术可以使我们的应用程序更加安全,在我们的案例中,可以使我们的 WebAPI 更加安全。
身份验证
身份验证是关于最终用户的身份。它是关于验证访问我们系统的用户身份,他是否被充分授权使用我们的资源。该最终用户是否拥有有效的凭据登录我们的系统?凭据可以是用户名和密码的形式。我们将使用基本身份验证技术来了解如何在 WebAPI 中实现身份验证。
Authorization
授权应被视为实现安全性的身份验证之后的第二步。授权意味着经过身份验证的用户拥有访问 Web 资源的所有权限。是否允许访问/对该资源执行操作?这可以通过为经过身份验证的最终用户设置角色和权限来实现,或者通过提供安全令牌来实现,最终用户可以使用该令牌访问其他服务或资源。
维护会话
RESTful 服务基于无状态协议(即 HTTP)工作。我们可以通过基于令牌的授权技术在 Web API 中实现会话维护。经过身份验证的用户将被允许在特定时间段内访问资源,并且可以重新实例化请求,增加会话时间增量以访问其他资源或相同资源。使用 WebAPI 作为 RESTful 服务的网站可能需要为用户实现登录/注销,以维护用户会话,为用户提供角色和权限,所有这些功能都可以通过基本身份验证和基于令牌的授权来实现。我将逐步解释这一点。
基本身份验证
基本身份验证是一种机制,最终用户通过我们的服务(即 RESTful 服务)借助用户名和密码等明文凭据进行身份验证。最终用户向服务发出身份验证请求,请求头中嵌入了用户名和密码。服务接收请求并检查凭据是否有效,并相应地返回响应,如果凭据无效,服务将返回 401 错误代码,即未经授权。进行比较的实际凭据可能位于数据库、任何配置文件(如 web.config)或代码本身中。
基本身份验证的优缺点
基本身份验证有其自身的优缺点。它在实现方面具有优势,易于实现,几乎所有现代浏览器都支持它,并且已成为 RESTful / Web API 中的身份验证标准。它也有缺点,例如以明文形式发送用户凭据,在请求头中发送用户凭据,即容易受到攻击。每次调用服务时都必须发送凭据。没有维护会话,用户一旦通过基本身份验证登录就无法注销。它非常容易受到 CSRF(跨站请求伪造)的攻击。
基于令牌的授权
授权部分紧随身份验证之后,一旦身份验证成功,服务就可以向最终用户发送一个令牌,用户可以通过该令牌访问其他资源。该令牌可以是任何加密密钥,只有服务器/服务才能理解,当它从最终用户发出的请求中获取令牌时,它会验证令牌并授权用户进入系统。生成的令牌可以存储在数据库或外部文件中,即我们需要持久化令牌以供将来参考。令牌可以有自己的生命周期,并且可能相应地过期。在这种情况下,用户将不得不再次在系统中进行身份验证。
使用基本身份验证和基于令牌授权的 WebAPI
创建用户服务
只需打开您的 WebAPI 项目或我们上一篇学习 WebAPI 文章中讨论的 WebAPI 项目。
我们有 BusinessEntities、BusinessServices、DataModel、DependencyResolver 和一个 WebApi 项目。
我们数据库中已经有一个用户表,或者您可以创建自己的数据库,其中包含一个类似用户表的表,如下所示:
我正在使用 WebAPI 数据库,我已经附上了脚本供下载。
用户服务
转到 BusinessServices 项目,添加一个名为 IUserService 的新接口和一个实现该接口的服务 UserServices,
只需在接口中定义一个名为 Authenticate
的方法。
namespace BusinessServices
{
public interface IUserServices
{
int Authenticate(string userName, string password);
}
}
此方法以用户名和密码作为参数,如果用户成功通过身份验证,则返回特定的 userId。
只需在 UserServices.cs 类中实现此方法,就像我们之前在该系列中创建服务一样,
using DataModel.UnitOfWork;
namespace BusinessServices
{
/// <summary>
/// Offers services for user specific operations
/// </summary>
public class UserServices : IUserServices
{
private readonly UnitOfWork _unitOfWork;
/// <summary>
/// Public constructor.
/// </summary>
public UserServices(UnitOfWork 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;
}
}
}
您可以清楚地看到 Authenticate
方法只是从 UserRepository
中检查用户凭据并相应地返回值。代码非常一目了然。
解决 UserService 的依赖关系
只需打开 BusinessServices 项目中的 DependencyResolver
类,并添加其依赖类型,以便在运行时解析 UserServices 的依赖关系,因此添加
registerComponent.RegisterType<IUserServices, UserServices>();
行到 SetUP
方法。我们的类变为:
using System.ComponentModel.Composition;
using DataModel;
using DataModel.UnitOfWork;
using Resolver;
namespace BusinessServices
{
[Export(typeof(IComponent))]
public class DependencyResolver : IComponent
{
public void SetUp(IRegisterComponent registerComponent)
{
registerComponent.RegisterType<IProductServices, ProductServices>();
registerComponent.RegisterType<IUserServices, UserServices>();
}
}
}
实现基本身份验证
步骤 1:创建通用身份验证过滤器
在 WebAPI 项目中添加一个名为 Filters 的文件夹,并在该文件夹下添加一个名为 GenericAuthenticationFilter 的类。
该类派生自 AuthorizationFilterAttribute
,这是一个位于 System.Web.Http.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));
}
}
由于这是一个 AuthorizationFilter
派生类,我们需要重写其方法以添加我们的自定义逻辑。在这里,"OnAuthorization
" 方法被重写以添加自定义逻辑。每当我们在 OnAuthorization
上获得 ActionContext
时,我们都会检查其请求头,因为我们正在推动我们的服务遵循 BasicAuthentication
,请求头应该包含此信息。我使用了 FetchAuthHeader
来检查方案,如果它为“Basic”,然后将凭据(即用户名和密码)以 BasicAuthenticationIdentity
类的对象形式存储,从而从有效凭据中创建身份。
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]);
}
我期望值使用 Base64 字符串加密;您也可以使用自己的加密机制。
稍后在 OnAuthorization
方法中,我们使用创建的身份创建一个 genericPrincipal
并将其分配给当前线程主体,
var genericPrincipal = new GenericPrincipal(identity, null);
Thread.CurrentPrincipal = genericPrincipal;
if (!OnAuthorizeUser(identity.Name, identity.Password, filterContext))
{
ChallengeAuthRequest(filterContext);
return;
}
base.OnAuthorization(filterContext);
完成后,将向该请求添加一个挑战,我们在其中添加响应并告知基本领域,
filterContext.Response.Headers.Add("WWW-Authenticate", string.Format("Basic
realm=\"{0}\"", dnsHost));
在 ChallengeAuthRequest
方法中。
如果请求中未提供凭据,则此通用身份验证过滤器会将通用身份验证主体设置为当前线程主体。
由于我们知道基本身份验证中凭据以明文形式传递的缺点,因此如果我们的服务使用 SSL 进行通信或消息传递,那将是很好的。
我们也有一个重载的构造函数,它允许通过传递一个参数(即 true 或 false)来停止过滤器的默认行为。
public GenericAuthenticationFilter(bool isActive)
{
_isActive = isActive;
}
我们可以使用 OnAuthorizeUser
进行自定义授权目的。
步骤 2:创建基本身份验证标识
在继续之前,我们还需要 BasicIdentity 类,它负责凭据并将其分配给 Generic Principal。因此,只需添加一个名为 BasicAuthenticationIdentity
的类,它派生自 GenericIdentity
。
这个类包含三个属性,即 UserName
、Password
和 UserId
。我特意添加了 UserId
,因为我们将来会需要它。所以我们的类将是:
using System.Security.Principal;
namespace WebApi.Filters
{
/// <summary>
/// Basic Authentication identity
/// </summary>
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;
}
}
}
步骤 3:创建自定义身份验证过滤器
现在您已准备好使用您自己的自定义身份验证过滤器。只需在该 Filters 项目下添加一个名为 ApiAuthenticationFilter
的类,该类将派生自我们在第一步中创建的 GenericAuthenticationFilter
。该类重写 OnAuthorizeUser
方法以添加自定义逻辑来验证请求,它使用我们之前创建的 UserService 来检查用户,
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;
}
完整类
using System.Threading;
using System.Web.Http.Controllers;
using BusinessServices;
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;
}
}
}
步骤 4:控制器上的基本身份验证
因为我们已经有了产品控制器,
public class ProductController : ApiController
{
#region Private variable.
private readonly IProductServices _productServices;
#endregion
#region Public Constructor
/// <summary>
/// Public constructor to initialize product service instance
/// </summary>
public ProductController(IProductServices productServices)
{
_productServices = productServices;
}
#endregion
// GET api/product
[GET("allproducts")]
[GET("all")]
public HttpResponseMessage Get()
{
var products = _productServices.GetAllProducts();
var productEntities = products as List<ProductEntity> ?? products.ToList();
if (productEntities.Any())
return Request.CreateResponse(HttpStatusCode.OK, productEntities);
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");
}
// GET api/product/5
[GET("productid/{id?}")]
[GET("particularproduct/{id?}")]
[GET("myproduct/{id:range(1, 3)}")]
public HttpResponseMessage Get(int id)
{
var product = _productServices.GetProductById(id);
if (product != null)
return Request.CreateResponse(HttpStatusCode.OK, product);
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");
}
// POST api/product
[POST("Create")]
[POST("Register")]
public int Post([FromBody] ProductEntity productEntity)
{
return _productServices.CreateProduct(productEntity);
}
// PUT api/product/5
[PUT("Update/productid/{id}")]
[PUT("Modify/productid/{id}")]
public bool Put(int id, [FromBody] ProductEntity productEntity)
{
if (id > 0)
{
return _productServices.UpdateProduct(id, productEntity);
}
return false;
}
// DELETE api/product/5
[DELETE("remove/productid/{id}")]
[DELETE("clear/productid/{id}")]
[PUT("delete/productid/{id}")]
public bool Delete(int id)
{
if (id > 0)
return _productServices.DeleteProduct(id);
return false;
}
}
您可以通过三种方式使用此身份验证过滤器。
只需将此过滤器应用于 ProductController
。您可以将此过滤器添加到控制器的顶部,以验证所有 API 请求,
[ApiAuthenticationFilter]
[RoutePrefix("v1/Products/Product")]
public class ProductController : ApiController
您也可以将其全局添加到 Web API 配置文件中,以便过滤器应用于所有控制器及其关联的所有操作,
GlobalConfiguration.Configuration.Filters.Add(new ApiAuthenticationFilter());
您也可以根据自己的意愿将其应用于 Action 级别,以决定是否将身份验证应用于该 Action,
// GET api/product
[ApiAuthenticationFilter(true)]
[GET("allproducts")]
[GET("all")]
public HttpResponseMessage Get()
{
…………………
}
// GET api/product/5
[ApiAuthenticationFilter(false)]
[GET("productid/{id?}")]
[GET("particularproduct/{id?}")]
[GET("myproduct/{id:range(1, 3)}")]
public HttpResponseMessage Get(int id)
{
……………………..
}
运行应用程序
我们已经实现了基本身份验证,现在尝试运行应用程序以测试它是否正常工作
运行应用程序,我们得到:
我们已经添加了测试客户端,但对于新读者,只需右键单击 WebAPI 项目,转到管理 Nuget 包,然后在在线包的搜索框中键入 WebAPITestClient,
您将得到“ASP.NET Web API 的简单测试客户端”,只需添加它。您将在 Areas-> HelpPage 中得到一个帮助控制器,如下所示:
我已经在之前的文章中提供了数据库脚本和数据,您可以使用相同的。
在应用程序 URL 中追加 "/help",您将获得测试客户端,
GET
POST
PUT
删除
您可以单击每个服务进行测试。单击服务链接后,您将被重定向到该特定服务的测试服务页面。在该页面右下角有一个“测试 API”按钮,只需按下该按钮即可测试您的服务,
获取所有产品的服务,
当您点击发送请求时,会弹出一个窗口要求进行身份验证。只需取消该弹窗,让请求在没有凭据的情况下发送。您将收到一个未授权的响应,即 401,
这意味着我们的身份验证机制正在工作。
为了确保万无一失,现在让我们使用凭据发送请求。只需在请求中添加一个头部。头部应如下所示:
授权:基本 YWtoaWw6YWtoaWw=
这里“YWtoaWw6YWtoaWw=”是我的 Base64 编码的用户名和密码,即 akhil:akhil
点击发送,我们得到了预期的响应,
同样,您可以测试所有服务终结点。
这意味着我们的服务正在使用基本身份验证。
设计差异
这种设计在 SSL 上运行时对于实现基本身份验证非常有用。但在某些场景中,除了基本身份验证之外,我还想利用授权,甚至不仅仅是授权,还有会话。当我们谈论创建企业应用程序时,它不仅仅局限于仅通过身份验证来保护我们的端点。
在这个设计中,我每次都必须随请求发送用户名和密码。假设我想创建一个这样的应用程序,其中身份验证只在我的登录完成后发生一次,并且在成功身份验证(即登录)后,我必须能够使用该应用程序的其他服务,即我现在被授权使用这些服务。我们的应用程序应该足够健壮,即使是经过身份验证的用户,在未授权的情况下也无法使用其他服务。是的,我正在谈论基于令牌的授权。
我将只公开一个用于身份验证的端点,这将是我的登录服务。因此,客户端只知道需要凭据才能登录系统的登录服务。
客户端成功登录后,我将发送一个令牌,它可能是一个 GUID 或任何我想要的 XYZ 算法加密密钥,当用户在登录后请求任何其他服务时,应该随请求提供此令牌。
为了维护会话,我们的令牌也会有过期时间,持续 15 分钟,可以通过 web.config 文件进行配置。会话过期后,用户将退出登录,并需要再次使用凭据通过登录服务获取新令牌。这对我来说似乎很令人兴奋,让我们来实现它 :)
实现基于令牌的授权
为了克服上述场景,让我们开始开发并赋予我们的应用程序一个厚客户端企业架构的形状。
设置数据库
让我们从设置数据库开始。当我们查看我们在该系列第一部分中设置的已创建数据库时,我们有一个令牌表。我们需要这个令牌表来实现令牌持久化。我们的令牌将以过期时间持久化在数据库中。如果您使用自己的数据库,可以创建令牌表,如下所示:
设置业务服务
只需导航到 BusinessServices
并创建另一个名为 ITokenServices
的接口,用于基于令牌的操作,
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
}
}
此接口中定义了四个方法。让我们创建实现 ITokenServices
的 TokenServices
类并理解每个方法。
GenerateToken
方法接收 userId 作为参数并生成一个令牌,将该令牌封装在一个带有令牌过期时间的令牌实体中,并将其返回给调用者。
public TokenEntity GenerateToken(int userId)
{
string token = Guid.NewGuid().ToString();
DateTime issuedOn = DateTime.Now;
DateTime expiredOn = DateTime.Now.AddSeconds(
Convert.ToDouble(ConfigurationManager.AppSettings["AuthTokenExpiry"]));
var tokendomain = new Token
{
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;
}
在生成令牌时,它会在 Token 表中创建一个数据库条目。
ValidateToken
方法只是验证与请求关联的令牌是否有效,即它是否存在于数据库中并在其过期时间限制内。
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;
}
它只获取请求中提供的令牌 ID。
Kill Token 只是杀死令牌,即从数据库中删除令牌。
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;
}
DeleteByUserId
方法删除数据库中与特定 userId 相关联的所有令牌条目。
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;
}
因此,通过 _unitOfWork
以及 Constructor
,我们的类变为:
using System;
using System.Configuration;
using System.Linq;
using BusinessEntities;
using DataModel;
using DataModel.UnitOfWork;
namespace BusinessServices
{
public class TokenServices:ITokenServices
{
#region Private member variables.
private readonly UnitOfWork _unitOfWork;
#endregion
#region Public constructor.
/// <summary>
/// Public constructor.
/// </summary>
public TokenServices(UnitOfWork 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;
DateTime expiredOn = DateTime.Now.AddSeconds(
Convert.ToDouble(ConfigurationManager.AppSettings["AuthTokenExpiry"]));
var tokendomain = new Token
{
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
}
}
Do not forget to resolve the dependency of this Token service in DependencyResolver class. Add registerComponent.RegisterType<ITokenServices, TokenServices>(); to the SetUp method of DependencyResolver class in BusinessServices project.
[Export(typeof(IComponent))]
public class DependencyResolver : IComponent
{
public void SetUp(IRegisterComponent registerComponent)
{
registerComponent.RegisterType<IProductServices, ProductServices>();
registerComponent.RegisterType<IUserServices, UserServices>();
registerComponent.RegisterType<ITokenServices, TokenServices>();
}
}
不要忘记在 DependencyResolver
类中解析此 Token 服务的依赖关系。将 registerComponent.RegisterType<ITokenServices, TokenServices>();
添加到 BusinessServices 项目中 DependencyResolver
类的 SetUp
方法中。
[Export(typeof(IComponent))]
public class DependencyResolver : IComponent
{
public void SetUp(IRegisterComponent registerComponent)
{
registerComponent.RegisterType<IProductServices, ProductServices>();
registerComponent.RegisterType<IUserServices, UserServices>();
registerComponent.RegisterType<ITokenServices, TokenServices>();
}
}
设置 WebAPI/控制器
现在既然我们决定,我们不想将身份验证应用于每个暴露的 API,我将创建一个单个控制器/API 端点,它接收身份验证或登录请求,并使用 Token Service 生成令牌并响应客户端/调用者一个带有过期详细信息并持久化在数据库中的令牌。
在 WebAPI 的 Controllers 文件夹下添加一个名为 Authenticate 的新控制器,
认证控制器
using System.Configuration;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using AttributeRouting.Web.Http;
using BusinessServices;
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>
[POST("login")]
[POST("authenticate")]
[POST("get/token")]
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;
}
}
}
控制器装饰有我们的身份验证过滤器,
[ApiAuthenticationFilter]
public class AuthenticateController : ApiController
因此,通过此控制器发出的每个请求都必须通过此身份验证过滤器,该过滤器检查 BasicAuthentication
头部和 credentials.Authentication
过滤器会将 CurrentThread
主体设置为已验证的身份。
此控制器中只有一个 Authenticate 方法/操作。您可以像我们在系列第四部分中讨论的那样,用多个端点装饰它:
[POST("login")]
[POST("authenticate")]
[POST("get/token")]
Authenticate
方法首先检查 CurrentThreadPrincipal
以及用户是否已通过身份验证(即由身份验证过滤器完成的工作),
if (System.Threading.Thread.CurrentPrincipal!=null &&
System.Threading.Thread.CurrentPrincipal.Identity.IsAuthenticated)
当它发现用户已通过身份验证时,它会借助 TokenServices
生成一个身份验证令牌,并向用户返回令牌及其过期时间,
response.Headers.Add("Token", token.AuthToken);
response.Headers.Add("TokenExpiry", ConfigurationManager.AppSettings["AuthTokenExpiry"]);
response.Headers.Add("Access-Control-Expose-Headers", "Token,TokenExpiry" );
return response;
在我们的 BasicAuthenticationIdentity
类中,我特意使用了 userId
属性,以便在尝试生成令牌时可以使用此属性,我们正在此控制器的 Authenticate
方法中执行此操作,
var basicAuthenticationIdentity = System.Threading.Thread.CurrentPrincipal.Identity as BasicAuthenticationIdentity;
if (basicAuthenticationIdentity != null)
{
var userId = basicAuthenticationIdentity.UserId;
return GetAuthToken(userId);
}
现在,当您运行此应用程序时,您还会看到 Authenticate API,只需使用基本身份验证和用户凭据调用此 API,您将获得带有过期时间的令牌,让我们一步一步地进行。
- 运行应用程序。
- 单击第一个 API 链接,即 POST authenticate。您将进入测试 API 的页面,
- 按右下角的 TestAPI 按钮。在测试控制台中,提供头部信息,其中 Authorization 为 Basic,用户凭据为 Base64 格式,就像我们之前做的那样。点击发送。
- 现在既然我们提供了有效的凭据,我们将从 Authenticate 控制器获得一个令牌,以及它的过期时间,
在数据库中,
这里我们得到响应 200,即我们的用户已通过身份验证并登录系统。TokenExpiry 为 900,即 15 分钟。请注意,IssuedOn 和 ExpiresOn 之间的时间差为 15 分钟,这是我们在 TokenServices 类方法 GenerateToken 中完成的,您可以根据需要设置时间。令牌为 604653d8-eb21-495c-8efd-da50ef4e56d3。现在,在 15 分钟内,我们可以使用此令牌调用我们的其他服务。但在此之前,我们应该标记我们的其他服务以理解此令牌并做出相应响应。请保存生成的令牌,以便我们可以在调用我将要解释的其他服务时进一步使用它。因此,让我们在其他服务上设置授权。
设置授权 Action Filter
我们已经有了身份验证过滤器,我们不想将其用于授权目的。所以我们必须创建一个新的 Action Filter 用于授权。这个 Action Filter 将只识别请求中传入的令牌。它假设请求已经通过我们的登录渠道进行了身份验证,现在用户被授权/未授权使用我们案例中的其他服务,例如 Products,也可以有许多其他服务可以使用此授权 Action Filter。要使请求获得授权,现在我们不必传递用户凭据。只需通过请求传递令牌(从 Authenticate 控制器成功验证后收到)。
在 WebAPI 项目中添加一个名为 ActionFilters 的文件夹。并添加一个名为 AuthorizationRequiredAttribute 的类
派生自 ActionFilterAttribute
,
重写 ActionFilterAttribute
的 OnActionExecuting
方法,这是我们在 API 项目中定义 Action Filter 的方式。
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
using BusinessServices;
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);
}
}
}
被重写的方法会检查每个请求头中的“Token
”属性,如果存在令牌,它会调用 TokenServices
的 ValidateToken
方法来检查令牌是否存在于数据库中。如果令牌有效,我们的请求将导航到我们请求的实际控制器和操作,否则您将收到一条未授权的错误消息。
使用授权过滤器标记控制器
我们的动作过滤器已准备就绪。现在让我们用这个属性标记我们的控制器 ProductController
。只需打开 Product 控制器类,并在顶部用这个 ActionFilter
属性装饰该类,
[AuthorizationRequired]
[RoutePrefix("v1/Products/Product")]
public class ProductController : ApiController
{
我们已经用我们创建的动作过滤器标记了控制器,现在发送到此控制器动作的每个请求都必须通过此 ActionFilter
,它会检查请求中的令牌。
您也可以用相同的属性标记其他控制器,或者您也可以在动作级别进行标记。假设您希望某些动作对所有用户都可用,无论他们是否授权,那么您只需标记那些需要授权的动作,而其他动作保持原样,就像我在实现基本身份验证的第 4 步中解释的那样。
使用令牌维护会话
我们当然也可以使用这些令牌来维护会话。令牌的有效期为 900 秒,即 15 分钟。现在我们希望用户在使用我们应用程序的其他服务时也能继续使用此令牌。或者假设存在一种情况,我们只希望用户在 15 分钟内或在他的会话时间内完成网站上的工作,然后才能发出新请求。因此,在 TokenServices 中验证令牌时,我所做的是,每当收到带有有效令牌的有效请求时,将令牌的时间增加 900 秒,
/// <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;
}
在上述令牌验证代码中,我们首先检查请求的令牌是否存在于数据库中且未过期。我们通过将其与当前日期时间进行比较来检查过期时间。如果它是有效令牌,我们只需更新数据库中的令牌,并使用一个新的 ExpiresOn
时间,即增加 900 秒。
if (token != null && !(DateTime.Now > token.ExpiresOn))
{
token.ExpiresOn = token.ExpiresOn.AddSeconds(
Convert.ToDouble(ConfigurationManager.AppSettings["AuthTokenExpiry"]));
_unitOfWork.TokenRepository.Update(token);
_unitOfWork.Save();
通过这样做,我们可以允许最终用户或客户端维护会话,并以 15 分钟的会话超时时间继续使用我们的服务/应用程序。这种方法也可以通过多种方式利用,例如创建具有不同会话超时的不同服务,或者在实际使用 API 的应用程序中应用许多此类条件。
运行应用程序
我们的工作几乎完成了。
我们只需要运行应用程序并测试它是否正常工作。如果您在测试身份验证时保存了之前生成的令牌,您可以使用相同的令牌来测试授权。我只是再次运行整个循环来测试应用程序。
测试身份验证
重复我们之前进行的测试以获取身份验证令牌。只需使用有效凭据和基本授权头调用 Authenticate 控制器。我得到:
在不提供授权头(作为基本凭据)的情况下,我得到:
我只是保存了我在第一个请求中获得的令牌。
现在尝试调用 ProductController 的动作。
测试授权
运行应用程序以调用 Product Controller 的动作。尝试在不提供任何令牌的情况下调用它们,
调用列表中的第一个服务,
点击发送,
这里我们得到未经授权,这是因为我们的 ProductController 被标记为检查令牌的授权属性。所以这里我们的请求是无效的。现在尝试通过提供我们保存的令牌来调用此操作,
点击发送,我们得到:
这意味着我们收到了响应,并且我们的令牌是有效的。现在我们看到我们的身份验证和授权,两个功能都运行良好。您可以自行测试会话。
同样地,您可以测试所有动作。您可以创建其他控制器并测试安全性,并尝试不同的排列组合。
结论
我们涵盖并学习了很多。在这篇文章中,我试图解释如何使用基本身份验证和授权构建一个 API 应用程序。人们可以根据需要调整这个概念以达到所需的安全性级别。例如,令牌生成机制可以根据个人需求进行定制。可以实现两级安全性,其中每个服务都需要身份验证和授权。还可以根据角色在操作上实现授权。
我已经说过,实现安全性没有特定的方法,您对概念理解得越深入,您就能构建出越安全的系统。本文中使用的技术或实现的设计,如果您将其与 SSL (Secure Socket Layer) 结合使用,在 HTTPS 上运行 REST API,将能发挥很好的作用。在我的下一篇文章中,我将尝试解释更多精美的实现和概念。在此之前,祝您编码愉快 :)
您也可以从 Github 下载包含所有包的完整源代码。
参考文献
https://msdn.microsoft.com/en-us/magazine/dn781361.aspx
http://weblog.west-wind.com/posts/2013/Apr/18/A-WebAPI-Basic-Authentication-Authorization-Filter
其他系列
我的其他系列文章
MVC: https://codeproject.org.cn/Articles/620195/Learning-MVC-Part-Introduction-to-MVC-Architectu
OOP: https://codeproject.org.cn/Articles/771455/Diving-in-OOP-Day-Polymorphism-and-Inheritance-Ear