RESTful日#5:Web api中的安全性——使用操作过滤器的Web api中的基本身份验证和基于令牌的自定义授权
- 下载Database Script - 1 KB
- 下载Day 4 source - 4.6 MB
- 下载source - 5 MB
- 下载source - 5 MB
- 下载Pdf_Article.zip - 3 MB
表的内容 目录介绍WebAPI中的路线图安全性 身份验证授权维护会话 基本身份验证 基本身份验证的优缺点 基于令牌的授权webapi,具有基本身份验证和基于令牌的授权 创建用户服务 解决UserService的依赖关系: 实现基本身份验证 第1步:创建通用身份验证过滤器第2步:创建基本身份验证身份第3步:创建自定义身份验证过滤器第4步:控制器上的基本身份验证 运行应用程序设计差异,实现基于令牌的授权 设置数据库设置业务服务设置WebAPI/控制器身份验证econtroller设置授权操作过滤器标记控制器与授权过滤器 使用运行应用程序的令牌维护会话 测试验证测试授权 参考其他系列 介绍 安全性一直是我们在讨论企业级应用程序时所关注的主要问题,特别是在讨论通过服务公开业务时。在本系列的前几篇文章中,我已经解释了很多关于WebAPI的内容。我解释了如何创建WebAPI,如何解决依赖关系使其成为松散耦合的设计,定义自定义路由,使用属性路由。我的文章将解释如何在WebAPI中实现安全性。本文将解释如何使用基本身份验证和基于令牌的授权来确保WebAPI的安全性。我还将解释如何利用WebAPI中基于令牌的授权和基本身份验证在WebAPI中维护会话。在WebAPI中没有实现安全性的标准方法。我们可以设计自己的安全技术和结构,最适合我们的应用。 路线图 下面是我为逐步学习WebAPI设置的路线图, RESTful日#1:企业级应用程序架构,使用实体框架、通用存储库模式和工作单元的Web api。RESTful日#2:使用Unity容器和引导程序在Web api中使用依赖注入实现控制反转。RESTful日#3:使用Unity容器和可管理扩展框架(MEF)在Asp.net Web api中使用控制反转和依赖注入来解决依赖关系的依赖关系。RESTful日#4:使用MVC 4 Web api中的属性路由自定义URL重写/路由。RESTful日#5:使用操作过滤器的Web api中基于基本身份验证和令牌的自定义授权。RESTful日#6:使用操作过滤器、异常过滤器和NLog在Web api中进行请求日志记录和异常处理/日志记录。RESTful日#7:使用NUnit和Moq框架在WebAPI中进行单元测试和集成测试(第一部分)。使用NUnit和Moq框架在WebAPI中进行单元测试和集成测试(第二部分)。净Web api。RESTful日#10:创建自托管的ASP。NET WebAPI与CRUD操作在Visual Studio 2010 我有意使用Visual Studio 2010和。net Framework 4.0,因为在。net Framework 4.0中很少有很难找到的实现,但我将通过演示如何实现来简化它。 之前的安全 安全性本身是一个非常复杂和棘手的话题。我将尝试解释如何用我自己的方式在WebAPI中实现它。 当我们计划创建企业级应用程序时,我们尤其需要关注身份验证和授权。如果使用得当,这两种技术将使我们的应用程序更加安全,在我们的例子中,将使我们的WebAPI更加安全。 图片来源:https://pixabay.com/static/uploads/photo/2014/12/25/10/56/road - 579554 __180.jpg迹象 身份验证 身份验证完全是关于最终用户的身份。它是关于验证正在访问我们系统的用户的身份,确认他是否通过了足够的身份验证来使用我们的资源。最终用户是否有有效的凭据来登录我们的系统?凭证可以是用户名和密码的形式。我们将使用基本的身份验证技术来理解如何在WebAPI中实现身份验证。 授权 授权应视为身份验证之后的第二步,以实现安全性。授权是指经过身份验证的用户访问web资源的所有权限。是否允许访问/执行该资源上的操作?这可以通过为经过身份验证的最终用户设置角色和权限来实现,也可以通过提供安全令牌来实现,最终用户可以使用该令牌访问其他服务或资源。 维护会话 RESTful服务在无状态协议(如HTTP)上工作。通过基于令牌的授权技术,可以实现在Web API中对会话的维护。通过身份验证的用户将被允许在一段特定的时间内访问资源,并且可以重新实例化请求增加会话时间增量以访问其他资源或相同资源。使用webapi作为RESTful服务的网站可能需要为用户实现登录/注销,为用户维护会话,为用户提供角色和权限,所有这些功能都可以通过使用基本的身份验证和基于令牌的授权实现。我将一步一步地解释这一点。 基本身份验证 基本身份验证是一种机制,最终用户通过我们的服务(即RESTful服务)通过普通凭证(如用户名和密码)进行身份验证。终端用户使用在请求头中嵌入的用户名和密码向服务发出身份验证请求。服务接收请求,检查凭据是否有效,并相应返回响应,如果无效的凭据,服务响应401错误代码,即未经授权。don所要比较的实际凭据可能存在于数据库、任何配置文件中,如web。配置或代码本身。 基本身份验证的优缺点 基本身份验证有它自己的优点和缺点。它在实现方面很有优势,它非常容易实现,几乎被所有现代浏览器支持,并且已经成为RESTful / Web api中的身份验证标准。它的缺点是用纯文本发送用户证书,在请求头中发送用户证书,容易被黑客攻击。每次调用服务时都必须发送凭据。不维护会话,并且用户通过基本身份验证登录后不能注销。很容易出现跨站点请求伪造。 基于令牌的身份验证 授权部分是在身份验证之后进行的,一旦经过身份验证,服务就可以向最终用户发送令牌,用户可以通过该令牌访问其他资源。令牌可以是任何加密的密钥,只有服务器/服务能够理解,当它从最终用户发出的请求中获取令牌时,它会验证令牌并将用户授权到系统中。生成的令牌可以存储在数据库或外部文件中,也就是说,我们需要持久化令牌以备将来引用。令牌可以有自己的生存期,并可能相应地过期。在这种情况下,用户必须再次通过身份验证进入系统。 具有基本身份验证和基于令牌的授权的WebAPI 创建用户服务 只要打开你的WebAPI项目或者我们在学习WebAPI的最后一部分中讨论的WebAPI项目。 我们还有BusinessEntities, BusinessServices, DataModel, DependencyResolver和WebApi项目。 我们已经在数据库中有了一个用户表,或者您可以创建自己的数据库,使用一个像User table这样的表,如下所示, 我使用WebAPI数据库,脚本我已附加下载。 userservice 转到BusinessServices项目并添加一个新接口IUserService和一个名为UserServices的实现该接口的服务, 只需在接口中定义一个名为Authenticate的方法。 隐藏,复制Code
namespace BusinessServices { public interface IUserServices { int Authenticate(string userName, string password); } }
该方法将用户名和密码作为参数,如果用户通过身份验证成功,则返回特定的userId。 只需在UserServices.cs类中实现这个方法,就像我们在本系列的前面创建服务一样, 隐藏,收缩,复制Code
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> /// <paramname="userName"></param> /// <paramname="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 project otself中打开DependencyResolver类并添加它的依赖类型,这样我们就可以在运行时解析UserServices依赖,因此添加 隐藏,复制Code
registerComponent.RegisterType<IUserServices, UserServices>();
行到设置方法。我们班就 隐藏,复制Code
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下的一个类。 我创建的通用身份验证过滤器是这样的, 隐藏,收缩,复制Code
[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> /// <paramname="isActive"></param> public GenericAuthenticationFilter(bool isActive) { _isActive = isActive; } /// <summary> /// Checks basic authentication request /// </summary> /// <paramname="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> /// <paramname="user"></param> /// <paramname="pass"></param> /// <paramname="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> /// <paramname="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> /// <paramname="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检查方案,如果它是“基本”,然后存储凭证,即用户名和密码在一个对象的类BasicAuthenticationIdentity,因此创建一个身份的有效凭证。 隐藏,复制Code
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,并将其分配给当前线程主体, 隐藏,复制Code
var genericPrincipal = new GenericPrincipal(identity, null); Thread.CurrentPrincipal = genericPrincipal; if (!OnAuthorizeUser(identity.Name, identity.Password, filterContext)) { ChallengeAuthRequest(filterContext); return; } base.OnAuthorization(filterContext);
完成之后,对该请求添加一个挑战,我们添加响应并告诉基本领域, 隐藏,复制Code
filterContext.Response.Headers.Add("WWW-Authenticate", string.Format("Basic realm=\"{0}\"", dnsHost));
在ChallengeAuthRequest方法。 如果请求中没有提供凭据,则此通用身份验证过滤器将通用身份验证主体设置为当前线程主体。 由于我们知道在基本身份验证凭据中以明文传递的缺点,所以如果我们的服务使用SSL进行通信或消息传递将是很好的。 我们还有一个被覆盖的构造函数,它允许通过传入一个参数来停止过滤器的默认行为,即true或false。 隐藏,复制Code
public GenericAuthenticationFilter(bool isActive) { _isActive = isActive; }
我们可以使用OnAuthorizeUser进行自定义授权。 步骤2:创建基本身份验证标识 在我们继续之前,我们还需要BasicIdentity类,它持有凭证并将其分配给通用主体。因此只需再添加一个类BasicAuthenticationIdentity,它来自于GenericIdentity。 这个类包含三个属性,即用户名、密码和用户id。我特意添加了UserId,因为我们将来会用到它。所以我们的班级会说, 隐藏,收缩,复制Code
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> /// <paramname="userName"></param> /// <paramname="password"></param> public BasicAuthenticationIdentity(string userName, string password) : base(userName, "Basic") { Password = password; UserName = userName; } } }
步骤3:创建自定义身份验证过滤器 现在可以使用自己的自定义身份验证过滤器了。只要在Filters项目下再添加一个类并将其称为ApiAuthenticationFilter,这个类将派生自我们在第一步中创建的GenericAuthenticationFilter。这个类覆盖了OnAuthorizeUser方法来添加用于验证请求的自定义逻辑。它使用我们之前创建的UserService来检查用户, 隐藏,复制Code
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; }
完整的类 隐藏,收缩,复制Code
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> /// <paramname="isActive"></param> public ApiAuthenticationFilter(bool isActive) : base(isActive) { } /// <summary> /// Protected overriden method for authorizing user /// </summary> /// <paramname="username"></param> /// <paramname="password"></param> /// <paramname="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:控制器上的基本身份验证 因为我们已经有了产品控制器, 隐藏,收缩,复制Code
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请求,可以在控制器的顶部添加这个过滤器, 隐藏,复制Code
[ApiAuthenticationFilter] [RoutePrefix("v1/Products/Product")] public class ProductController : ApiController
你也可以全局地将它添加到Web API配置文件中,这样过滤器就会应用到所有控制器和与它相关的所有动作, 隐藏,复制Code
GlobalConfiguration.Configuration.Filters.Add(new ApiAuthenticationFilter());
你也可以根据自己的意愿将它应用到动作级别, 隐藏,复制Code
// 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) { …………………….. }
运行应用程序 我们已经实现了基本的身份验证,只需尝试运行应用程序来测试它是否工作 运行这个应用程序,我们得到, 我们已经添加了我们的测试客户端,但是对于新的读者,只要去管理Nuget包,通过右击WebAPI项目和在搜索框中输入WebAPITestClient在线包, 您将获得“一个用于ASP的简单测试客户端”。NET Web API”,只要添加它。你将在以下区域得到一个帮助控制器->如下图所示, 我在上一篇文章中已经提供了数据库脚本和数据,您可以使用相同的脚本和数据。 在应用程序url中添加“/help”,您将获得测试客户端, 得到: 职位: 把: 删除: 您可以通过单击每个服务来测试它。单击服务链接后,您将被重定向到测试该特定服务的服务页面。在这个页面的右下角有一个按钮测试API,只要按下那个按钮就可以测试你的服务, 为Get所有产品提供服务, 当您单击Send request时,将出现一个弹出窗口询问身份验证要求。只需取消那个弹出窗口,让request在没有凭据的情况下运行。你会收到未经授权的回复,也就是401 这意味着我们的身份验证机制正在工作。 为了确保这一点,现在让我们发送带有凭据的请求。只需在请求中添加标题。标题应该是, 授权:Basic YWtoaWw6YWtoaWw= 这里“YWtoaWw6YWtoaWw=”是我的Base64编码的用户名和密码,即akhil:akhil 点击发送,我们会得到想要的响应, 同样,您可以测试所有服务端点。 这意味着我们的服务使用基本身份验证。 设计的差异 在SSL上运行时,这种设计非常适合实现基本身份验证。但是在一些场景中,除了基本身份验证之外,我还希望利用授权,甚至不是授权,而是会话。当我们谈到创建企业应用程序时,它并不局限于仅通过身份验证来保护端点。 在这种设计中,每次我都必须发送用户名和密码与每个请求。假设我想要创建这样的应用程序,身份验证只发生一次,因为我的登录已经完成,在身份验证成功后,即登录,我必须能够使用其他该申请的服务,即我现在被授权使用这些服务。我们的应用程序应该非常健壮,即使是经过身份验证的用户,也应该在未获得授权之前限制其使用其他服务。是的,我说的是基于令牌的授权。 我将只公开一个端点来进行身份验证,这将是我的登录服务,因此客户端只知道需要凭证才能登录到系统的登录服务。 在客户成功登录后,我将发送一个令牌,它可以是GUID或任何xyz算法加密的密钥,当用户在登录后对任何其他服务发出请求时,我想要的令牌应该与请求一起提供这个令牌。 为了维护会话,我们的令牌也会有一个有效期,它将持续15分钟,可以在web的帮助下进行配置。配置文件。在会话过期后,用户将注销,并将再次使用登录服务与凭证获得一个新的令牌。我觉得很兴奋,让我们实现这个:) 实现基于令牌的授权 为了克服上述场景,让我们开始开发并为应用程序提供一个厚客户机企业架构。 设置数据库 让我们从设置数据库开始。当我们看到在本系列的第一部分中已经创建的数据库时,我们有一个令牌表。我们需要这个令牌表来实现令牌持久性。我们的令牌将在数据库中保存一个过期时间。如果您使用自己的数据库,您可以将令牌表创建为, 设置业务服务 只需导航到BusinessServices并为基于令牌的操作创建另一个名为ITokenServices的接口, 隐藏,收缩,复制Code
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> /// <paramname="userId"></param> /// <returns></returns> TokenEntity GenerateToken(int userId); /// <summary> /// Function to validate token againt expiry and existance in database. /// </summary> /// <paramname="tokenId"></param> /// <returns></returns> bool ValidateToken(string tokenId); /// <summary> /// Method to kill the provided token id. /// </summary> /// <paramname="tokenId"></param> bool Kill(string tokenId); /// <summary> /// Delete tokens for the specific deleted user /// </summary> /// <paramname="userId"></param> /// <returns></returns> bool DeleteByUserId(int userId); #endregion } }
在这个接口中定义了四个方法。让我们创建TokenServices类,它实现ITokenServices并理解每种方法。 GenerateToken方法将userId作为参数并生成令牌,将该令牌封装在带有令牌到期时间的令牌实体中,并将其返回给调用者。 隐藏,收缩,复制Code
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; }
在生成令牌时,它将一个数据库条目命名为令牌表。 ValidateToken方法只验证与请求关联的令牌是否有效,也就是说,它在其到期时间限制内存在于数据库中。 隐藏,复制Code
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。 杀死令牌只是杀死令牌,即从数据库中删除令牌。 隐藏,复制Code
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方法从数据库w.r中删除所有令牌项。与这些令牌关联的特定用户id。 隐藏,复制Code
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和构造函数我们的类就变成了, 隐藏,收缩,复制Code
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> /// <paramname="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> /// <paramname="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> /// <paramname="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> /// <paramname="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类中解析此令牌服务的依赖关系。添加registerComponent.RegisterType< ITokenServices TokenServices> ();到BusinessServices项目中DependencyResolver类的设置方法。 隐藏,复制Code
[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端点需要身份验证或登录请求,利用令牌服务生成令牌和响应客户端/调用者与一个令牌,坚持数据库到期的细节。 在WebAPI的Controllers文件夹下添加一个名称验证的新控制器, AuthenticateController 隐藏,收缩,复制Code
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> /// <paramname="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; } } }
控制器用我们的身份验证过滤器装饰, 隐藏,复制Code
[ApiAuthenticationFilter] public class AuthenticateController : ApiController
因此,每个通过这个控制器的请求都必须通过这个身份验证过滤器,它检查BasicAuthentication头和凭证。身份验证过滤器将当前线程主体设置为经过身份验证的标识。 这个控制器中只有一个身份验证方法/操作。您可以使用多个端点来装饰它,就像我们在本系列的第4部分中讨论的那样, 隐藏,复制Code
[POST("login")] [POST("authenticate")] [POST("get/token")]
方法首先检查CurrentThreadPrincipal和用户是否经过身份验证i。通过认证过滤器完成的工作, 隐藏,复制Code
if (System.Threading.Thread.CurrentPrincipal!=null && System.Threading.Thread.CurrentPrincipal.Identity.IsAuthenticated)
当它发现用户已通过身份验证时,它会在TokenServices的帮助下生成一个认证令牌,并向用户返回令牌及其到期时间, 隐藏,复制Code
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方法中做的, 隐藏,复制Code
var basicAuthenticationIdentity = System.Threading.Thread.CurrentPrincipal.Identity as BasicAuthenticationIdentity; if (basicAuthenticationIdentity != null) { var userId = basicAuthenticationIdentity.UserId; return GetAuthToken(userId); }
现在,当您运行这个应用程序时,您也会看到Authenticate api,只要使用Baisc身份验证和用户凭证调用这个api,您就会得到过期的令牌,让我们一步一步来做这件事。 运行应用程序。 单击第一个api链接,即POST authenticate。您将获得用于测试api的页面, 按TestAPI b尤顿在右下角。在测试控制台中,以Base64格式提供具有授权的头信息和用户凭证,就像我们前面做的那样。点击发送。 现在,既然我们已经提供了有效的凭据,我们将从Authenticate控制器获得一个令牌,以及它的到期时间, 在数据库中, 这里我们得到的响应是200,即我们的用户通过身份验证并登录到系统中。在900分钟内失效,也就是15分钟。注意,IssuedOn和ExpiresOn之间的时间差是15分钟,这是我们在TokenServices类方法GenerateToken中完成的,您可以根据需要设置时间。令牌是604653 d8 - eb21 - 495 c - 8 -使用efd da50ef4e56d3。现在,在15分钟内,我们可以使用这个令牌来调用其他服务。但在此之前,我们应该标记其他服务,以理解此令牌并相应地响应。保存生成的令牌,以便我们可以在调用我将要解释的其他服务时进一步使用它。因此,让我们在其他服务上设置授权。 设置授权操作过滤器 我们已经准备好了身份验证过滤器,我们不想将其用于授权目的。因此,我们必须为授权创建一个新的操作过滤器。这个操作过滤器将只识别请求中的令牌。它假设,请求已经通过我们的登录通道进行了身份验证,现在用户被授权/未被授权使用其他服务,比如我们这里的产品,可能还有n个其他服务,它们可以使用这个授权操作过滤器。对于获得授权的请求,我们不需要通过用户凭证。只有令牌(在验证成功后从Authenticate controller接收)需要通过请求传递。 在WebAPI项目中添加一个名为ActionFilters的文件夹。并添加一个名为AuthorizationRequiredAttribute的类 从ActionFilterAttribute推导, 重写ActionFilterAttribute的onactionexecute方法,这是我们在API项目中定义动作过滤器的方法。 隐藏,收缩,复制Code
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”属性,如果Token存在,它从TokenServices调用ValidateToken方法来检查数据库中是否存在Token。如果令牌是有效的,我们的请求将导航到实际的控制器和我们请求的操作,否则您将得到一个错误消息,说未经授权。 使用授权过滤器标记控制器 我们已经准备好了动作过滤器。现在让我们用这个属性标记控制器ProductController。打开Product controller类在顶部用这个ActionFilter属性装饰这个类, 隐藏,复制Code
[AuthorizationRequired] [RoutePrefix("v1/Products/Product")] public class ProductController : ApiController {
我们已经用我们创建的动作过滤器标记了我们的控制器,现在每个请求到这个控制器的动作将必须通过这个ActionFilter,它检查请求中的令牌。 您也可以用相同的属性标记其他控制器,或者您也可以在操作级进行标记。如果您希望某些操作对所有用户都可用,而不考虑他们的授权,那么您就可以只标记那些需要授权的操作,而将其他操作保留为我在实现基本身份验证的步骤4中所解释的那样。 使用令牌维护会话 我们当然也可以使用这些令牌来维护会话。令牌的问题持续900秒,也就是15分钟。现在我们希望,如果用户正在为我们的应用程序使用其他服务,那么他应该继续使用这个令牌。或者假设有这样一种情况,我们只希望用户在15分钟内或在他发出新请求之前在他的会话时间内完成他在站点上的工作。因此,在TokenServices中验证令牌时,我所做的是,当一个有效请求带有一个有效令牌时,将令牌的时间增加900秒, 隐藏,复制Code
/// <summary> /// Method to validate token against expiry and existence in database. /// </summary> /// <paramname="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; }
在上面的令牌验证代码中,首先我们检查请求的令牌是否存在于数据库中并且没有过期。我们通过与当前日期时间进行比较来检查到期时间。如果它是有效的令牌,我们就用增加900秒的新的ExpiresOn时间将令牌更新到数据库中。 隐藏,复制Code
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处理实时应用程序时可以应用许多这样的条件。 运行应用程序 我们的工作快完成了。 我们只需要运行应用程序并测试它是否正常工作。如果在测试身份验证时保存了之前生成的令牌,那么可以使用它来测试授权。我只是再次运行整个周期来测试应用程序。 测试验证 重复前面的测试以获得认证令牌。只需使用vali调用验证控制器d凭证和基本授权标头。我得到了, 没有提供授权头信息作为基本凭证, 我只是保存了第一次请求时得到的令牌。 现在尝试调用ProductController动作。 测试授权 运行应用程序来调用产品控制器操作。尝试在不提供任何令牌的情况下调用它们, 调用列表中的第一个服务, 点击发送, 这里是未经授权的,例如,因为我们的ProductController被标记为检查令牌的授权属性。所以这里我们的请求无效。现在尝试通过提供我们保存的令牌来调用这个操作, 点击发送,我们得到, 这意味着我们得到了响应并且我们的令牌是有效的。现在我们看到了身份验证和授权,这两个功能都运行良好。您可以自己测试会话。 同样,您可以测试所有操作。您可以创建其他控制器并测试安全性,并使用不同的排列和组合。 结论 我们学习了很多知识。在本文中,我试图解释如何构建具有基本身份验证和授权的API应用程序。可以对这个概念进行建模,以达到所需的安全性级别。比如令牌生成机制可以根据自己的需求定制。当每个服务都需要身份验证和授权时,可以实现两级安全性。还可以基于角色实现对操作的授权。 图片来源:http://www.greencountryfordofparsons.com/images/secure_application.jpg 我已经说过,没有实现安全性的特定方法,您对这个概念理解得越多,系统就越安全。如果您将本文中使用的技术或本文中实现的设计与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: http://www.codeproject.com/Articles/620195/Learning-MVC-Part-Introduction-to-MVC-Architectu OOP: http://www.codeproject.com/Articles/771455/Diving-in-OOP-Day-Polymorphism-and-Inheritance-Ear 本文转载于:http://www.diyabc.com/frontweb/news409.html