0918-33597796
当前位置:主页»解决方案»

微服务架构如何设计API署理网关和OAuth2授权认证框架

文章出处:开云app官网入口 人气:发表时间:2023-09-25 00:50
本文摘要:1,授权认证与微服务架构1.1,由差别团队互助引发的授权认证问题去年的时候,公司开发一款新产物,但人手不够,将B/S系统的Web开发外包,外包团队使用Vue.js框架,挪用我们的WebAPI,可是这些WebAPI并不在一台服务器上,甚至可能是第三方提供的WebAPI。

开云app官网入口

1,授权认证与微服务架构1.1,由差别团队互助引发的授权认证问题去年的时候,公司开发一款新产物,但人手不够,将B/S系统的Web开发外包,外包团队使用Vue.js框架,挪用我们的WebAPI,可是这些WebAPI并不在一台服务器上,甚至可能是第三方提供的WebAPI。同时处于系统宁静的架构设计,后端WebAPI是不能直接袒露在外面的;另一方面,我们这个新产物另有一个C/S系统,C端登录的时候,要求统一到B/S端登录,可以从C端无障碍的会见任意B/S端的页面,也可以挪用B/S系统的一些API,所以又增加了一个API网关署理。

整个系统的架构示意图如下:注:上图另有一个iMSF,这是一个实时消息服务框架,这里用来做文件服务,参见《消息服务框架使用案例之--大文件上传(断点续传)功效》。在Web端会读取这些上传的文件。

1.2,微服务--漫衍式“最彻底”的分1.2.1,为什么需要漫衍式大部门情况下,如果你的系统不是很庞大,API和授权认证服务,文件服务都可以放到一台服务器:Web Port 服务器上,但要把它们离开部署到差别的站点,或者差别的服务器,主要是出于以下思量:1,职责单一:每一个服务都只做一类事情,好比某某业务WebAPI,授权服务,用户身份认证服务,文件服务等;职责单一使得开发、部署和维护变得容易,好比很容易知道当前是授权服务的问题,而不是业务API问题。2,系统宁静:接纳内外网隔离的方案,一些功效需要直接袒露在公网,这需要支付分外的成本,好比带宽租用和宁静设施;另外一些功效部署在内网,这样能够提供更大的宁静保证。3,易于维护:每一个服务职责都比力单一,所以每一个服务都足够小,那么开发维护就更容易,好比要更新一个功效,只需要更新一个服务而不用所有服务器都暂停;另一方面也越发容易监控服务器的负载,如果发现某一个服务器负载太大可以增加服务器来疏散负载。4,第三方接入:现在系统越来越庞大,内部的系统很可能需要跟第三方的系统对接,一起协同事情;或者整个系统一部门是 .NET开发的,一部门又是Java平台开发的,两个平台部署的情况有很大差异,没法部署在一起;或者虽然同是ASP.NET MVC,可是一个是MVC3,一个是MVC5,所以需要划分独立部署。

以上就是各个服务需要离开部署的原因,而这样做的效果就是我们常说的漫衍式盘算了,这是自然需求的效果,不是为了分而拆分。1.2.2,依赖于中间层而不直接依赖于服务客户端直接会见后端服务,对后端的服务会形成比力强的依赖。

有架构履历的朋侪都知道,解决依赖的常见手段就是添加一其中间层,客户端依赖于这其中间层而不是直接依赖于服务层。这样做有几个很大的利益:当服务负载过大的时候可以在中间层做负载平衡;或者后端某个服务泛起问题可以切换主备服务;或者替换后端某个服务的版本做灰度公布。另一方面,当后端服务部署为多个独立的历程/服务器后,客户端直接会见这些服务,将是一个越发较庞大的问题,负载平衡,主备切换,灰度公布等运维功效更难操作,除此之外,另有下面两个比力重要的问题:客户端直接会见后端多个服务,将袒露过多的后端服务器地址,从而增加宁静隐患;后端服务太多,需要在客户端维护这些服务会见关系,增加开发调试的庞大性;B/S页面的AJax跨域问题,WebAPI地址跟主站地址纷歧样,要解决跨域问题比力庞大而且也会增加宁静隐患。

所以,为相识决客户端对后端服务层的依赖,而且解决后端服务太多以后引起的问题,我们需要在客户端和后端服务层之间添加一其中间层,这其中间层就是我们的服务署理层,也就是我们后面说的服务网关署理(WebAPI Gateway Proxy),它作为我们所有Web会见的入口站点,这就是上图所示的 Web Port。有了网关署理,后台所有的WebAPI都可以通过这个统一的入口提供对外服务的功效,而对于后端差别服务地址的路由,由网关署理的路由功效来实现,所以这个署理功效很像Nginx这样的反向署理,只不外,这里仅仅署理WebAPI,而不是其它Web资源。

现在,网关已经成为许多漫衍式系统的标配,好比TX的这个架构:注:上图泉源于网络,侵删!另外,这个读写分散署理,如果使用SOD框架,可以在AdoHelper工具直接设置读写差别的毗连字符串简朴到达效果。1.2.3,微服务架构经由上面的设计,我们发现这个架构有几个特点:每个服务足够小,职责单一;每个服务运行在自己的历程或者独立的服务器中,独立公布部署和开发维护;服务对外提供会见或者服务之间举行通信,都是使用轻量级的HTTP API;每个服务有自己独立的存储,相互之间举行数据交互都通过接口举行;有一个API署理网关统一提供服务的对外会见。

这些特点是很是切合现在盛行的微服务思想的,好比在《什么是微服务》这篇文章中,像下面说的这样:微服务最早由Martin Fowler与James Lewis于2014年配合提出,微服务架构气势派头是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的历程中,并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用差别的编程语言实现,以及差别数据存储技术,并保持最低限度的集中式治理。所以我们这个架构是基本切合微服务思想的,它的降生配景也是要解决其它传统单体软件项目现在遇到的问题一样的,是在比力庞大的实际需求情况下自然而然的一种需求,不外幸亏它没有过多的“技术债务”,所以设计实施起来比力容易。

下面我们来详细看看这个架构是如何落地的。2,“授权认证资源”独立服务的OAuth2.0架构2.1,为什么需要OAuth2.0 ?OAuth 2.0已经是一个“用户验证和授权”的工业级尺度。OAuth(开放授权)是一个开放尺度,1.0版本于2006年建立,它允许用户让第三方应用会见该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供应第三方应用。

OAuth 2.0关注客户端开发者的浅易性,同时为Web应用,桌面应用和手机,和起居室设备提供专门的认证流程。2012年10月,OAuth 2.0协议正式公布为RFC 6749。

以上内容详见OAuth 2.0官网。现在百度开放平台,腾讯开放平台等大部门的开放平台都是使用的OAuth 2.0协议作为支撑,海内越来越多的企业都开始支持OAuth2.0协议。

现在,我们的产物设计目的是要能够和第三方系统对接,那么在对接历程中的授权问题就是无法回避的问题。在我们原来的产物中,有用户授权验证的模块,但并没有拆分出独立的服务,用它与第三方系统对接会导致比力大的耦合性;另一方面,与第三方系统对接互助纷歧定每次都是以我们为主导,也有可能要用第三方的授权认证系统。

这就泛起了选择哪一方的授权认证方案的问题。之前我曾经履历过一个项目,因为其中的授权认证问题导致系统迟迟不能集成。所以,选择一个开放尺度的授权认证方案,才是最佳的解决方案,而OAuth 2.0正是这样的方案。

2.2,OAuth的名词解释和规范(1)Third-party application:第三方应用法式,本文中又称”客户端”(client),即上一节例子中的“Web Port”或者C/S客户端应用法式。(2)HTTP service:HTTP服务提供商,即上一节例子中提供软件产物的我们公司或者第三方公司。

(3)Resource Owner:资源所有者,本文中又称“用户”(user)。(4)User Agent:用户署理,本文中就是指浏览器或者C/S客户端应用法式。

(5)Authorization server:授权服务器,即服务提供商专门用来处置惩罚认证的服务器。(6)Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器,即上一节例子中的内部API服务器、第三方外部API服务器和文件服务器等。它与认证服务器,可以是同一台服务器,也可以是差别的服务器。

以上名词是OAuth规范内必须明白的一些名词,然后我们才气利便的讨论OAuth2.0是如何授权的。有关OAuth的思路、运行流程和详细的四种授权模式,请参考阮一峰老师的《明白OAuth 2.0》。2.3,OAuth2.0的授权模式为了表述利便,先简朴说说这4种授权模式:授权码模式(authorization code)--是功效最完整、流程最严密的授权模式。

它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器举行互动。简化模式(implicit)--不通过第三方应用法式的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对会见者是可见的,且客户端不需要认证。

密码模式(resource owner password credentials)--用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。在这种模式中,用户必须把自己的密码给客户端,可是客户端不得储存密码。

客户端模式(client credentials)--指客户端以自己的名义,而不是以用户的名义,向"服务提供商"举行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。

在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。在我们的需求中,用户不仅仅通过B/S系统的浏览器举行操作,还会通过C/S法式的客户端举行操作,B/S,C/S系统主要都是我们提供和集成的,客户购置了我们这个产物要使用它就意味着客户信任我们的产物。授权码模式虽然是最完整的授权模式,可是授权码模式授权完成后需要浏览器的跳转,显然浏览器无法直接跳转到我们的C/S客户端,虽然从技术上可以模拟,但实现起来成本还是比力高;简化模式也有这个问题。所以我们最终决议接纳OAuth2.0的密码模式。

2.4,OAuth2.0密码模式授权流程 简朴来说,密码模式的步骤如下: 用户向客户端提供用户名和密码。客户端将用户名和密码发给认证服务器,向后者请求令牌。认证服务器确认无误后,向客户端提供会见令牌。上面这个步骤只是说明晰令牌的获取历程,也就是我们常说用户登陆乐成的历程。

当用户登陆乐成之后,客户端获得了一个会见令牌,然后再使用这个令牌去会见资源服务器,详细说来另有如下后续历程:4,客户端携带此会见令牌,会见资源服务器;5,资源服务器去授权服务器验证客户端的会见令牌是否有效;6,如果会见令牌有效,授权服务器给资源服务器发送用户标识信息;7,资源服务器凭据用户标识信息,处置惩罚业务请求,最后发送响应效果给客户端。下面是流程图:注意:这个流程适用于资源服务器、授权服务器相分散的情况,否则,流程中的第5,6步不是必须的,甚至第4,7步都是显而易见的事情而不必说明。现在大部门有关OAuth2.0的先容文章都没有4,5,6,7步骤的说明,可能为了表述利便,默认都是将授权服务器跟资源服务器合在一起部署的。

2.5,授权、认证与资源服务的分散什么情况下授权服务器跟资源服务器必须离开呢?如果一个系统有多个资源服务器而且这些资源服务器的框架版本不兼容,运行情况有差异,代码平台差别(好比一个是.NET,一个是Java),或者一个是内部系统,一个是外部的第三方系统,必须离开部署。在这些情况下,授权服务器跟任意一个资源服务器部署在一起都倒霉于另一些资源服务器的使用,导致系统集成成本增加。这个时候,授权服务器必须跟资源服务器离开部署,我们在详细实现OAuth2.0系统的时候,需要做更多的事情。

什么情况下授权服务器跟认证服务器必须离开呢? 授权(authorization)和认证(authentication)有相似之处,但也是两个差别的观点:授权(authorization):授权,批准;批准(或授权)的证书;认证(authentication):认证;身份验证;证明,判定;密押。仅仅从这两个词的名词界说可能不太容易分辨,我们用实际的例子来说明他们的区别:有一个治理系统,包罗成熟的人员治理,角色治理,权限治理,系统登录的时候,用户输入的用户名和密码到系统的人员信息表中查询,通事后取得该用户的角色权限。在这个场景中,用户登录系统实际上分为了3个步骤:用户在登录界面,输入用户名和密码,提交登录请求;【认证】系统校验用户输入的用户名和密码是否在人员信息表中;【授权】给当前用户授予相应的角色权限。

现在,该治理系统需要和第三方系统对接,凭据前面的分析,这种情况下最好将授权功效独立出来,接纳OAuth这种开放授权方案,而认证问题,原有治理系统坚持用户信息是敏感信息,不能随意泄露给第三方,要求在原来治理系统完成认证。这样一来,授权和认证,只好划分作为两个服务,独立部署实现了。本文的重点就是讲述如何在授权服务器和资源服务器相分散,甚至授权和认证服务器相分散的情况下,如何设计实现OAuth2.0的问题。

3,PWMIS OAuth2.0 方案PWMIS OAuth2.0 方案就是一个切合上面要求的授权与认证相分散,授权与资源服务相分散的架构设计方案,该方案已经乐成支撑了我们产物的应用。下面划分来说说该方案是如何设计和落地的。3.1,使用Owin中间件搭建OAuth2.0认证授权服务器这里主要总结下本人在这个产物中搭建OAuth2.0服务器事情的履历。

至于为何需要OAuth2.0、为何是Owin、什么是Owin等问题,不再赘述。我假定读者是使用Asp.Net,并需要搭建OAuth2.0服务器,对于涉及的Asp.Net Identity(Claims Based Authentication)、Owin、OAuth2.0等知识点已有基本相识。若不相识,请先参考以下文章:MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN下一代Asp.net开发规范OWIN(1)—— OWIN发生的配景以及简朴先容OWIN OAuth 2.0 Authorization Server我们的事情,可以从研究《OWIN OAuth 2.0 Authorization Server》这个DEMO开始,不外为了更好的联合本文的主题,实现授权与认证相分散的微服务架构,推荐大家直接从我的DEMO开始:https://github.com/bluedoctor/PWMIS.OAuth2.0 PS:大家以为好,先点个赞支持下,谢谢!克隆我这个DEMO到当地,下面开始我们OAuth2.0如何落地的正式解说。3.2,PWMIS.OAuth2.0解决方案先容首先看到解决方案视图,先逐个做下简朴说明:3.2.1,运行解决方案将解决方案的项目,除了PWMIS.OAuth2.Tools,全部设置为启动项目,启动之后,在 http://localhost:62424/ 站点,输入下面的地址:http://localhost:62424/Home然后就可以看到下面的界面:点击登录页面,为了利便演示,不真正验证用户名和密码,所以随意输入,提交后效果如下图:点击确定,进入了业务操作页面,如下图:如果能够看到这个页面,我们的OAuth2.0演示法式就乐成了。

还可以运行解决方案内里的WinForm测试法式,先登录,然后运行性能测试,如下图:更多信息,请参考下文的【3.8集成C/S客户端会见】下面我们来看看各个法式集项目的构建历程。3.3,项目 PWMIS.OAuth2.AuthorizationCenter首先添加一个MVC5项目PWMIS.OAuth2.AuthorizationCenter,然后添加如下包引用:Microsoft.AspNet.MvcMicrosoft.Owin.Host.SystemWebMicrosoft.Owin.Security.OAuthMicrosoft.Owin.Security.Cookies然后在项目根目录下添加一个OWin的启动类 Startup:using Microsoft.Owin;using Microsoft.Owin.Security;using Microsoft.Owin.Security.OAuth;using Owin;using System;using System.Collections.Generic;using System.Diagnostics;using System.Web.Http;namespace PWMIS.OAuth2.AuthorizationCenter{ public partial class Startup { public void ConfigureAuth(IAppBuilder app) { var OAuthOptions = new OAuthAuthorizationServerOptions { AllowInsecureHttp = true, AuthenticationMode = AuthenticationMode.Active, TokenEndpointPath = new PathString("/api/token"), //获取 access_token 授权服务请求地址 AuthorizeEndpointPath = new PathString("/authorize"), //获取 authorization_code 授权服务请求地址 AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(60), //access_token 逾期时间,默认10秒太短 Provider = new OpenAuthorizationServerProvider(), //access_token 相关授权服务 AuthorizationCodeProvider = new OpenAuthorizationCodeProvider(), //authorization_code 授权服务 RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授权服务 }; app.UseOAuthBearerTokens(OAuthOptions); //表现 token_type 使用 bearer 方式 } public void Configuration(IAppBuilder app) { // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888 ConfigureAuth(app); var configuration = new HttpConfiguration(); WebApiConfig.Register(configuration); app.UseWebApi(configuration); } }}上面的代码中,界说了access_token 授权服务请求地址和access_token 逾期时间,这里设置60秒后逾期。

由于本篇着重讲述OAuth2.0的密码授权模式,我们直接看到类 OpenAuthorizationServerProvider的界说: OpenAuthorizationServerProvider token逾期时间不宜太长,好比一天,这样不宁静,但也不能太短,好比10秒,这样当API会见量比力大的时候会增大刷新token的肩负,所以这里设置成60秒。3.3.1,验证客户端信息在本类的第一个方法 ValidateClientAuthentication 验证客户端的信息,这里的客户端可能是C/S法式的客户端,也可能是会见授权服务器的网关署理服务器,OAuth2.0会验证需要生成会见令牌的客户端,只有正当的客户端才可以提供后续的生成令牌服务。

客户端信息有2个部门,一个是clientId,一个是clientSecret,前者是客户端的唯一标识,后者是授权服务器发表给客户端的秘钥,这个秘钥可以设定有效期或者设定授权规模。为轻便起见,我们的演示法式仅仅到数据库去检查下通报的这两个参数是否有对应的数据记载,使用下面一行代码: var identityRepository = IdentityRepositoryFactory.CreateInstance();这里会用到一个验证客户端的接口,包罗验证用户名和密码的方法一起界说了: /// <summary> /// 身份认证持久化接口 /// </summary> public interface IIdentityRepository { /// <summary> /// 客户ID是否存在 /// </summary> /// <param name="clientId"></param> /// <returns></returns> Task<bool> ExistsClientId(string clientId); /// <summary> /// 校验客户标识 /// </summary> /// <param name="clientId">客户ID</param> /// <param name="clientSecret">客户秘钥</param> /// <returns></returns> Task<bool> ValidateClient(string clientId, string clientSecret); /// <summary> /// 校验用户名密码 /// </summary> /// <param name="userName"></param> /// <param name="password"></param> /// <returns></returns> Task<bool> ValidatedUserPassword(string userName, string password); }这样我们就可以通过反射或者简朴 IOC框架将客户端验证的详细实现类注入到法式中,本例实现了一个简朴的客户端和用户认证类,接纳的是SOD框架会见数据库:namespace PWMIS.OAuth2.AuthorizationCenter.Repository{ public class SimpleIdentityRepository : IIdentityRepository { private static System.Collections.Concurrent.ConcurrentDictionary<string, string> dictClient = new System.Collections.Concurrent.ConcurrentDictionary<string, string>(); public async Task<bool> ExistsClientId(string clientId) { return await Task.Run<bool>(() => { AuthClientInfoEntity entity = new AuthClientInfoEntity(); entity.ClientId = clientId; OQL q = OQL.From(entity) .Select(entity.ClientId) .Where(entity.ClientId) .END; AuthDbContext context = new AuthDbContext(); AuthClientInfoEntity dbEntity = context.QueryObject<AuthClientInfoEntity>(q); return dbEntity != null; }); } public async Task<bool> ValidateClient(string clientId, string clientSecret) { string dict_clientSecret; if (dictClient.TryGetValue(clientId, out dict_clientSecret) && dict_clientSecret== clientSecret) { return true; } else { return await Task.Run<bool>(() => { AuthClientInfoEntity entity = new AuthClientInfoEntity(); entity.ClientId = clientId; entity.ClientSecret = clientSecret; OQL q = OQL.From(entity) .Select(entity.ClientId) .Where(entity.ClientId, entity.ClientSecret) .END; AuthDbContext context = new AuthDbContext(); AuthClientInfoEntity dbEntity = context.QueryObject<AuthClientInfoEntity>(q); if (dbEntity != null) { dictClient.TryAdd(clientId, clientSecret); return true; } else return false; }); } } public async Task<bool> ValidatedUserPassword(string userName, string password) { return await Task.Run<bool>(() => { UserInfoEntity user = new UserInfoEntity(); user.UserName = userName; user.Password = password; OQL q = OQL.From(user) .Select() .Where(user.UserName, user.Password) .END; AuthDbContext context = new AuthDbContext(); AuthClientInfoEntity dbEntity = context.QueryObject<AuthClientInfoEntity>(q); return dbEntity != null; }); } }}AuthDbContext 类很是简朴,它会自动生成验证客户端所需要的表:namespace PWMIS.OAuth2.AuthorizationCenter.Repository{ public class AuthDbContext:DbContext { public AuthDbContext() : base("OAuth2") { } protected override bool CheckAllTableExists() { base.CheckTableExists<AuthClientInfoEntity>(); base.CheckTableExists<UserInfoEntity>(); return true; } }}3.3.2,认证用户,生成会见令牌生成会见令牌需要重写OWIN OAuthAuthorizationServerProvider类的 GrantResourceOwnerCredentials方法(方法的详细内容看前面【OpenAuthorizationServerProvider的界说】),方法内里使用到了IdentityService 工具,它有一个UserLogin 方法,用来实现或者挪用用户认证服务: IdentityServiceUserLogin方法提供了2种方式来认证用户身份,一种是直接会见用户数据库,一种是挪用第三方的用户认证接口,这也是当前演示法式默认设置的方式。当用户认证比力庞大的时候,推荐使用这种方式,好比认证的时候需要检检验证码。

需要在授权服务器的应用法式设置文件中设置使用何种用户身份验证方式以及验证地址: <appSettings> <add key="webpages:Version" value="3.0.0.0"/> <add key="webpages:Enabled" value="false"/> <add key="ClientValidationEnabled" value="true"/> <add key="UnobtrusiveJavaScriptEnabled" value="true"/> <!--IdentityLoginMode 认证登录模式,值为DataBase/WebAPI ,默认为WebAPI;设置为WebAPI将使用 IdentityWebAPI 设置的地址会见WebAPI来认证用户--> <add key="IdentityLoginMode" value=""/> <!--IdentityWebAPI 认证服务器身份认证接口--> <!--<add key="IdentityWebAPI" value="http://localhost:61001/api/Login"/>--> <add key="IdentityWebAPI" value="http://localhost:50697/Login"/> <!--DataBase 认证模式的持久化提供法式类和法式集信息 此提供法式继续自 PWMIS.OAuth2.Tools法式集的IIdentityRepository 接口。--> <add key="IdentityRepository" value="PWMIS.OAuth2.AuthorizationCenter.Repository.SimpleIdentityRepository,PWMIS.OAuth2.AuthorizationCenter"/> <add key="SessionCookieName" value="ASP.NET_SessionId"/> <add key="LogFile" value="~AuthError.txt"/> </appSettings>如果认证用户名和密码通过,在GrantResourceOwnerCredentials方法最后,挪用OWin的用户标识方式表现授权验证通过:3.4,项目 PWMIS.OAuth2.Tools项目 PWMIS.OAuth2.Tools 封装了OAuth2.0挪用相关的一些API函数,前面我们先容了基于OWIN实现的OAuth2.0服务端,下面我们来看看如何挪用它生成一个会见令牌。3.4.1,OAuthClient类--获取和刷新令牌看到 OAuthClient.cs 文件的 OAuthClient类的GetToken 方法 /// <summary> /// 获取会见令牌 /// </summary> /// <param name="grantType">授权模式</param> /// <param name="refreshToken">刷新的令牌</param> /// <param name="userName">用户名</param> /// <param name="password">用户密码</param> /// <param name="authorizationCode">授权码</param> /// <param name="scope">可选业务参数</param> /// <returns></returns> public async Task<TokenResponse> GetToken(string grantType, string refreshToken = null, string userName = null, string password = null, string authorizationCode = null,string scope=null) { var clientId = System.Configuration.ConfigurationManager.AppSettings["ClientID"]; var clientSecret = System.Configuration.ConfigurationManager.AppSettings["ClientSecret"]; this.ExceptionMessage = ""; var parameters = new Dictionary<string, string>(); parameters.Add("grant_type", grantType); if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(password)) { parameters.Add("username", userName); parameters.Add("password", password); parameters.Add("scope", scope); } if (!string.IsNullOrEmpty(authorizationCode)) { var redirect_uri = System.Configuration.ConfigurationManager.AppSettings["RedirectUri"]; parameters.Add("code", authorizationCode); parameters.Add("redirect_uri", redirect_uri); //和获取 authorization_code 的 redirect_uri 必须一致,否则会报错 } if (!string.IsNullOrEmpty(refreshToken)) { parameters.Add("refresh_token", refreshToken); } httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( "Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret))); string errCode = "00"; try { //PostAsync 在ASP.NET下面,必须加).ConfigureAwait(false);否则容易导致死锁 //详细内容,请参考 http://blog.csdn.net/ma_jiang/article/details/53887967 var cancelTokenSource = new CancellationTokenSource(50000); var response = await httpClient.PostAsync("/api/token", new FormUrlEncodedContent(parameters), cancelTokenSource.Token).ConfigureAwait(false); var responseValue = await response.Content.ReadAsStringAsync(); if (response.StatusCode != HttpStatusCode.OK) { try { var error = await response.Content.ReadAsAsync<HttpError>(); if (error.ExceptionMessage == null) { string errMsg = ""; foreach (var item in error) { errMsg += item.Key + ":"" + (item.Value == null ? "" : item.Value.ToString()) + "","; } this.ExceptionMessage = "HttpError:{" + errMsg.TrimEnd(',')+"}"; } else { this.ExceptionMessage = error.ExceptionMessage; } errCode = "1000"; } catch (AggregateException agex) { string errMsg = ""; foreach (var ex in agex.InnerExceptions) { errMsg += ex.Message; } errCode = "1001"; this.ExceptionMessage = errMsg; } catch (Exception ex) { this.ExceptionMessage = response.Content.ReadAsStringAsync().Result; errCode = "1002"; WriteErrorLog(errCode, ex.Message); } WriteErrorLog(errCode, "StatusCode:" + response.StatusCode + "rn" + this.ExceptionMessage); this.ExceptionMessage = "{ErrorCode:" + errCode + ",ErrorObject:{" + this.ExceptionMessage + "}}"; return null; } return await response.Content.ReadAsAsync<TokenResponse>(); } catch (AggregateException agex) { string errMsg = ""; foreach (var ex in agex.InnerExceptions) { errMsg += ex.Message+","; } errCode = "1003"; this.ExceptionMessage = errMsg; WriteErrorLog(errCode, errMsg); this.ExceptionMessage = "{ErrorCode:" + errCode + ",ErrorMessage:'" + this.ExceptionMessage + "'}"; return null; } catch (Exception ex) { this.ExceptionMessage = ex.Message; errCode = "1004"; WriteErrorLog(errCode, this.ExceptionMessage); this.ExceptionMessage = "{ErrorCode:" + errCode + ",ErrorMessage:'" + this.ExceptionMessage + "'}"; return null; } }方法首先要获取客户端的clientId 和clientSecret 信息,这个信息需要指定到本次请求的Authorization 头信息内里;然后在请求正文内里,指定授权类型,这里应该是"password",再在正文内里添加用户名和密码参数。接着,挪用HttpClient工具,会见授权服务器的 /api/token ,该地址正是前面先容的授权服务器项目内里指定的。

最后,对请求返回的响应效果做庞大的异常处置惩罚,获得正确的返回值或者异常效果。在本例中,获取的令牌有效期只有1分钟,凌驾时间就需要刷新令牌: /// <summary> /// 使用指定的令牌,直接刷新会见令牌 /// </summary> /// <param name="token"></param> /// <returns></returns> public TokenResponse RefreshToken(TokenResponse token) { this.CurrentToken = token; return GetToken("refresh_token", token.RefreshToken).Result; }3.4.2,TokenManager类--令牌的治理由于令牌逾期后需要刷新令牌获取新的会见令牌,否则应用使用逾期的令牌会见就会堕落,因此我们应该在令牌超期之前就检查令牌是否马上到期,在到期之前的前一秒我们就立刻刷新令牌,用新的令牌来会见资源服务器;可是刷新令牌可能导致之前一个线程使用的令牌失效,造成会见未授权的问题,究竟授权服务跟资源服务器分散之后,这个可能性是比力高的,因此我们需要对令牌的使用举行治理,降低发生问题的风险。

首先看到 PWMIS.OAuth2.Tools.TokenManager 文件的 CreateToken 生成令牌的方法: /// <summary> /// 使用密码模式,给当前用户建立一个会见令牌 /// </summary> /// <param name="password">用户登录密码</param> /// <param name="validationCode">验证码</param> /// <returns></returns> public async Task<TokenResponse> CreateToken(string password,string validationCode=null) { OAuthClient oc = new OAuthClient(); oc.SessionID = this.SessionID; var tokenRsp= await oc.GetTokenOfPasswardGrantType(this.UserName, password, validationCode); if (tokenRsp != null) { UserTokenInfo uti = new UserTokenInfo(this.UserName, tokenRsp); dictUserToken[this.UserName] = uti; } else { this.TokenExctionMessage = oc.ExceptionMessage; } return tokenRsp; }生成的令牌存储在一个字段中,通过登任命户名来获取对应的令牌。然后看TakeToken 方法,它首先实验获取一个当前用户的令牌,如果令牌快逾期,就实验刷新令牌: /// <summary> /// 取一个会见令牌 /// </summary> /// <returns>如果没有或者获取令牌失败,返回空</returns> public TokenResponse TakeToken() { if (dictUserToken.ContainsKey(this.UserName)) { UserTokenInfo uti = dictUserToken[this.UserName]; this.OldToken = uti.Token; //如果令牌超期,刷新令牌 if (DateTime.Now.Subtract(uti.FirstUseTime).TotalSeconds >= uti.Token.expires_in || NeedRefresh) { lock (uti.SyncObject) { //防止线程重入,再次判断 if (DateTime.Now.Subtract(uti.FirstUseTime).TotalSeconds >= uti.Token.expires_in || NeedRefresh) { //等候之前的用户使用完令牌再刷新 while (uti.UseCount > 0) { if (DateTime.Now.Subtract(uti.LastUseTime).TotalSeconds > 5) { //如果发出请求凌驾5秒使用计数还大于0,可以认为资源服务器响应缓慢,最终请求此资源可能会拒绝会见 this.TokenExctionMessage = "Resouce Server maybe Request TimeOut."; OAuthClient.WriteErrorLog("00", "**警告** "+DateTime.Now.ToString()+":用户"+this.UserName+" 最近一次使用当前令牌(" +uti.Token.AccessToken +")已经超时(10秒),使用次数:"+uti.UseCount+",线程ID:"+System.Threading.Thread.CurrentThread.ManagedThreadId+"。rn**下面将刷新令牌,但可能导致之前还未处置惩罚完的资源服务器会见被拒绝会见。

"); break; } System.Threading.Thread.Sleep(100); } //刷新令牌 try { OAuthClient oc = new OAuthClient(); var newToken = oc.RefreshToken(uti.Token); if (newToken == null) throw new Exception("Refresh Token Error:" + oc.ExceptionMessage); else if( string.IsNullOrEmpty( newToken.AccessToken)) throw new Exception("Refresh Token Error:Empty AccessToken. Other Message:" + oc.ExceptionMessage); uti.ResetToken(newToken); this.TokenExctionMessage = oc.ExceptionMessage; } catch (Exception ex) { this.TokenExctionMessage = ex.Message; return null; } NeedRefresh = false; } }//end lock } this.CurrentUserTokenInfo = uti; uti.BeginUse(); //this.CurrentTokenLock.Set(); return uti.Token; } else { //throw new Exception(this.UserName+" 还没有会见令牌。"); this.TokenExctionMessage = "UserNoToken"; return null; } }有了令牌治理功效,客户端生成和获取一个会见令牌就利便了,下面看看客户端如何来使用它。3.5,项目 Demo.OAuth2.Port项目 Demo.OAuth2.Port 在本解决方案内里有3个作用:提供静态资源的会见,好比挪用WebAPI的Vue.js 功效代码;提供后端API路由功效,作为前端所有API会见的网关署理;存储用户的登录票据,关联用户的会见令牌。

这里我们着重解说第3点功效,网关署理功效另外详细先容。在方案中,用户的会见令牌缓存在Port站点的历程中,每当用户登录乐成后,就生成一个用户会见令牌跟当前用户票据关联。

看到项目的控制器 LogonController 的用户登录Action: [HttpPost] [AsyncTimeout(60000)] public async Task<ActionResult> Index(LogonModel model) { LogonResultModel result = new LogonResultModel(); //首先,挪用授权服务器,以密码模式获取会见令牌 //授权服务器会携带用户名和密码到认证服务器去验证用户身份 //验证服务器验证通过,授权服务器生成会见令牌给当前站点法式 //当前站点标志此用户登录乐成,并将会见令牌存储在当前站点的用户会话中 //当前用户下次会见此外站点的WebAPI的时候,携带此会见令牌。TokenManager tm = new TokenManager(model.UserName, Session.SessionID); var tokenResponse = await tm.CreateToken(model.Password,model.ValidationCode); if (tokenResponse != null && !string.IsNullOrEmpty(tokenResponse.AccessToken)) { result.UserId = 123; result.UserName = model.UserName; result.LogonMessage = "OK"; /* OWin的方式 ClaimsIdentity identity = new ClaimsIdentity("Basic"); identity.AddClaim(new Claim(ClaimTypes.Name, model.UserName)); ClaimsPrincipal principal = new ClaimsPrincipal(identity); HttpContext.User = principal; */ FormsAuthentication.SetAuthCookie(model.UserName, false); } else { result.LogonMessage = tm.TokenExctionMessage; } return Json(result); }Port站点作为授权服务器的客户端,需要设置客户端信息,看到Web.config文件的设置: <appSettings> <add key="webpages:Version" value="3.0.0.0" /> <add key="webpages:Enabled" value="false" /> <add key="ClientValidationEnabled" value="true" /> <add key="UnobtrusiveJavaScriptEnabled" value="true" /> <!--向授权服务器挂号的客户端ID和秘钥--> <add key="ClientID" value="PWMIS.OAuth2.Port"/> <add key="ClientSecret" value="1234567890"/> <!--授权服务器地址--> <add key="Host_AuthorizationCenter" value="http://localhost:60186"/> <!--资源服务器地址--> <add key="Host_Webapi" value="http://localhost:62477"/> </appSettings>另外,再提供一个获取当前用户令牌的方法,固然前提是必须先登录乐成: [HttpGet] [Authorize] public ActionResult GetUserToken() { using (TokenManager tm = new TokenManager(User.Identity.Name, Session.SessionID)) { var token = tm.TakeToken(); return Content(token.AccessToken); } } 3.6,项目 Demo.OAuth2.WebApi项目 Demo.OAuth2.WebApi是本解决方案中的资源服务器。

由于资源服务器跟授权服务器并不是在同一台服务器,所以资源服务器必须检查每次客户端请求的会见令牌是否正当,检查的方法就是将客户端的令牌提取出来发送到授权服务器去验证,获得这个令牌对应的用户信息,包罗登任命户名和角色信息等。如果是ASP.NET MVC5,我们可以拦截API请求的 DelegatingHandler 处置惩罚器,我们界说一个 AuthenticationHandler 类继续它来处置惩罚:namespace PWMIS.OAuth2.Tools{ /// <summary> /// WebAPI 认证处置惩罚法式 /// </summary> /// <remarks> /// 需要在 WebApiApplication.Application_Start() 方法中,增加下面一行代码: /// GlobalConfiguration.Configuration.MessageHandlers.Add(new AuthenticationHandler()); /// </remarks> public class AuthenticationHandler : DelegatingHandler { /* * 【认证处置惩罚法式】处置惩罚历程: * 1,客户端使用之前从【授权服务器】申请的会见令牌,会见【资源服务器】; * 2,【资源服务器】加载【认证处置惩罚法式】 * 3,【认证处置惩罚法式】未来自客户端的会见令牌,拿到【授权服务器】举行验证; * 4,【授权服务器】验证客户端的会见令牌有效,【认证处置惩罚法式】写入身份验证票据; * 5,【资源服务器】的受限资源(API)验证通过会见,返回效果给客户端。*/ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { if (request.Headers.Authorization != null && request.Headers.Authorization.Parameter != null) { string token = request.Headers.Authorization.Parameter; string Host_AuthCenter = System.Configuration.ConfigurationManager.AppSettings["OAuth2Server"];// "http://localhost:60186"; HttpClient _httpClient = new HttpClient(); ; _httpClient.BaseAddress = new Uri(Host_AuthCenter); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await _httpClient.GetAsync("/api/AccessToken");//.Result; if (response.StatusCode == HttpStatusCode.OK) { string[] result = await response.Content.ReadAsAsync<string[]>();//.Result; ClaimsIdentity identity = new ClaimsIdentity(result[2]); identity.AddClaim(new Claim(ClaimTypes.Name, result[0])); ClaimsPrincipal principal = new ClaimsPrincipal(identity); HttpContext.Current.User = principal; //添加角色示例,更多信息,请参考 https://msdn.microsoft.com/zh-cn/library/5k850zwb(v=vs.80).aspx //string[] userRoles = ((RolePrincipal)User).GetRoles(); //Roles.AddUserToRole("JoeWorden", "manager"); } } return await base.SendAsync(request, cancellationToken); } }}最后,在WebApiApplication 的Application_Start 方法挪用此工具:namespace Demo.OAuth2.WebApi{ public class WebApiApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); GlobalConfiguration.Configure(WebApiConfig.Register); GlobalConfiguration.Configuration.MessageHandlers.Add(new AuthenticationHandler()); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } }} 这样,我们跟OAuth2.0相关的客户端,授权服务器与资源服务器的实现历程就先容完了。认证服务器的实现比力简朴,但它涉及到登录验证码问题的时候就比力庞大了,之后单独先容。

3.7,接入第三方OAuth2.0资源服务器前面的例子中,我们使用ASP.NET WebAPI作为OAuth2.0的资源服务器,它可以很利便的挪用我们的AuthenticationHandler 拦截器来处置惩罚API挪用,发现有会见令牌信息就将它发送到授权服务器验证。如果是单纯的ASP.NET WebForms, ASP.NET MVC3 ,甚至是Java等其它平台的资源服务器呢?没有关系,我们发现OAuth自己就是一个开放的授权协议,任何能够处置惩罚HTTP请求的服务器都能够集成OAuth,只要相应的请求响应切合规范即可。对于会见令牌,它存在HTTP请求头的Authorization 内里,剖析使用它即可。下面我们以某个比力老的治理系统来举例,它基于 ASP.NET MVC3定制开发,扩展了一些底层的工具,所以没法升级到兼容支持ASP.NET WebAPI MVC5。

DefaultRequestHeaders.Authorization上面代码有2个需要注意的地方,一个是提取出HTTP请求头中的Authorization,然后需要结构一个新的请求(请求授权服务器),添加AuthenticationHeaderValue,它的类型是“Bearer”,值是当前会见令牌;另一个是需要在站点设置文件中设置 “OAuth2Server”,值为授权服务器的地址。3.8,集成C/S客户端会见OAuth提供了多种授权方案,密码模式和客户端模式比力适合C/S客户端授权。

不外,为了跟B/S端统一,都使用密码模式,可以让客户端法式直接会见授权服务器。但这并不是最佳的方案,可以让B/S的Web Port作为会见署理,C/S客户端模拟浏览器提倡会见,这样就跟B/S端会见完全统一了。详细会见架构如前面的架构图所示。

集成C/S客户端会见,包罗登录功效和会见授权资源功效,我们在实际实现的时候,都以Web Port为会见署理。为了轻便起见,这里的客户端应用法式使用一个WinForm法式来模拟。请看到解决方案的项目 Demo.OAuth2.WinFormTest。

如下图所示的登录效果:接着使用浏览器打开一个API地址: http://localhost:62424/api/values接着模拟登录而且打开授权会见的资源地址,这个效果跟在法式内里使用授权后的会见令牌去会见需要授权会见的资源,效果是一样的,入下图:下面我们来简朴先容下以上的统一登录、打开浏览器会见授权会见的资源和应用法式直接会见授权资源是如何实现的,这些方法都封装在OAuthClient 类中。Demo.OAuth2.WinFormTest客户端法式会见资源服务器,授权服务器,可以通过网关署理举行的,可以划分设置。为了演示授权服务器的效果,这里客户端直接会见了授权服务器,所以需要设置它的客户端ID和秘钥,请看它的应用法式设置信息:<appSettings> <add key="ClientID" value="PWMIS OAuth2 Client1"/> <add key="ClientSecret" value="1234567890"/> <!--授权服务器地址--> <add key="Host_AuthorizationCenter" value="http://localhost:60186"/> <!--资源服务器地址--> <add key="Host_Webapi" value="http://localhost:62424"/> </appSettings> 4,PWMIS API Gateway前面的架构分析说明,要让多个资源服务独立部署,而且简化客户端对资源服务的会见,一个统一的会见入口必不行少,它就是API网关,实际上它是客户端会见后端API的一个署理,在署理模式上属于反向署理,我们这个方案中的PWMIS API Gateway 正是这样一个反向署理。网关法式与网站其它部门部署在一起,作为统一的Web会见入口--Web Port。

在本示例解决方案中,网关署理就在 Demo.OAuth2.Port 项目上。4.1,署理设置首先我们来看看署理的设置文件 ProxyServer.config:# ======PWMIS API Gateway Proxy,Ver 1.1 ==================# ======PWMIS API网关署理设置,版本 1.1 ==================## 注释说明:每行第一个非空缺字符串是#,表现这行是一个注释# 版本说明:# Ver 1.0:# * 实现API网关署理与OAuth2.0 的集成# * OAuth2.0 授权与认证服务实现相分散的架构# Ver 1.1:# * 为每一个目的主机使用相同的HttpClient工具,而且保持长毗连,优化网络会见效率# * 网关会见资源服务器,支持毗连会话保持功效,使得资源服务器可以使用自身的会话状态# * 资源服务器 由 /api/ ,/api2/ 增加到 /api3/# Ver 1.2:# * 在路由项目上支持会话毗连,整体上默认不启用会话毗连,优化网络会见效率## 全局设置:# EnableCache: 是否支持缓存,值为 false/true,但当前版本不支持# EnableRequestLog: 是否开启请求日志,值为 false,true# LogFilePath: 请求日志文件生存的目录# ServerName: 署理服务器名字# UnauthorizedRedir:目的API地址会见未授权,是否跳转,值为 false,true。

# 如果跳转,将跳转到OAuthRedirUrl 指定的页面,如果不跳转,会直接抛出 HTTP Statue Unauthorized# OAuthRedirUrl:未授权要跳转的地址,通常为网关的登录页# RouteMaps:路由项目设置清单## 路由项目设置: # Prefix:要匹配的API Url 前缀。注意,如果设置文件设置了多个路由项目,会根据配路由项目的顺序依次匹配,直到不能设置为止, # 所以理论上可以对一个Url举行多次匹配和替换,请注意路由项目的编排顺序 # Host: 匹配后,要会见的目的主机地址,好比 "localhost:62477" # Match: 匹配该路由项目后,要对Url 内容举行替换的要匹配的字符串 # Map: 匹配该路由项目后,要对Url Match的内容举行替换的目的字符串#{"EnableCache":false,"EnableRequestLog":true,"LogFilePath":"C:\WebApiProxyLog","ServerName":"PWMIS ASP.NET Proxy,Ver 1.2","UnauthorizedRedir":false,"OAuthRedirUrl":"http://localhost:62424/Logon","RouteMaps": [ { "Prefix":"/api/", "Host":"localhost:62477", "Match":"", "Map":null }, # 授权服务器设置 { "Prefix":"/api/token", "Host":"localhost:60186", "Match":"", "Map":null }, { "Prefix":"/api/AccessToken", "Host":"localhost:60186", "Match":"", "Map":null }, # 登录验证码设置 { "Prefix":"/api/Login/CreateValidate", "Host":"localhost:50697", "Match":"/api/", "Map":"/", "SessionRequired":true }, { "Prefix":"/api2/common/GetValidationCode", "Host":"localhost:8088", "Match":"/api2/", "Map":"/", "SessionRequired":true }, # 其它资源服务器设置 { "Prefix":"/api2/", "Host":"localhost:8088", "Match":"/api2/", "Map":"/" } ]}设置文件分为全局设置和路由项目设置,全局设置包罗署理会见的日志信息设置,以及资源未授权会见的跳转设置,路由信息设置包罗要匹配的URL前缀,路由的目的主机地址,要替换的内容和是否支持会话请求。

需要注意的是,路由项目的匹配不是匹配到该项目后就竣事,而是会实验匹配所有路由项目,举行多次匹配和替换,直到不能匹配为止,所以署理设置文件对于路由项目的顺序很重要,也不宜编写太多的路由设置项目。现在,支持的路由项目的API前缀地址,有 /api,/api2,api3/ 三大种,更多的匹配前缀需要修改署理服务的源码。4.2,API 署理请求拦截器首先界说一个拦截器 ProxyRequestHandler,它继续自 WebAPI的DelegatingHandler,可以在底层拦截对API挪用的消息,在重载的SendAsync 方法内实现会见请求的处置惩罚:public class ProxyRequestHandler : DelegatingHandler{ /// <summary> /// 拦截请求 /// </summary> /// <param name="request">请求</param> /// <param name="cancellationToken">用于发送取消操作信号</param> /// <returns></returns> protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { //实现暂略 }}首先,我们需要从request请求工具中拿出当前请求的URL地址,处置惩罚署理规则,举行路由项目匹配: bool matched = false; bool sessionRequired = false; string url = request.RequestUri.PathAndQuery; Uri baseAddress = null; //处置惩罚署理规则 foreach (var route in this.Config.RouteMaps) { if (url.StartsWith(route.Prefix)) { baseAddress = new Uri("http://" + route.Host + "/"); if (!string.IsNullOrEmpty(route.Match)) { if (route.Map == null) route.Map = ""; url = url.Replace(route.Match, route.Map); } matched = true; if (route.SessionRequired) sessionRequired = true; //break; //只要不替换前缀,还可以继续匹配而且替换剩余部门 } }如果未匹配到,说明是一个当地地址请求,直接返回当地请求的响应效果: if (!matched) { return await base.SendAsync(request, cancellationToken); }如果匹配到,那么进入GetNewResponseMessage 方法进一步处置惩罚请求: /// <summary> /// 请求目的服务器,获取响应效果 /// </summary> /// <param name="request"></param> /// <param name="url"></param> /// <param name="baseAddress"></param> /// <param name="sessionRequired">是否需要会话支持</param> /// <returns></returns> private async Task<HttpResponseMessage> GetNewResponseMessage(HttpRequestMessage request, string url, Uri baseAddress, bool sessionRequired) { HttpClient client = GetHttpClient(baseAddress, request, sessionRequired); var identity = HttpContext.Current.User.Identity; if (identity == null || identity.IsAuthenticated == false) { return await ProxyReuqest(request, url, client); } //如果当前请求上下文的用户标识工具存在而且已经认证过,那么获取它关联的会见令牌,添加到请求头部 using (TokenManager tm = new TokenManager(identity.Name, null)) { TokenResponse token = tm.TakeToken(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken); return await ProxyReuqest(request, url, client); } }这里的代码只是一个简化后的示意代码,实际处置惩罚的时候可能存在请求令牌失败,刷新令牌失败,或者获取到了令牌但等到会见资源服务器的时候令牌又被此外线程刷新导致资源会见未授权失败的情况,这些庞大的情况处置惩罚起来比力贫苦,现在遇到会见未授权的时候,接纳重试2次的计谋。

详细请看真是源码。最后,就是我们真正的署理请求会见的方法 ProxyReuqest 了: private async Task<HttpResponseMessage> ProxyReuqest(HttpRequestMessage request, string url, HttpClient client) { HttpResponseMessage result = null; if (request.Method == HttpMethod.Get) { result = await client.GetAsync(url); } else if (request.Method == HttpMethod.Post) { result = await client.PostAsync(url, request.Content); } else if (request.Method == HttpMethod.Put) { result = await client.PutAsync(url, request.Content); } else if (request.Method == HttpMethod.Delete) { result = await client.DeleteAsync(url); } else { result = SendError("PWMIS ASP.NET Proxy 不支持这种 Method:" + request.Method.ToString(), HttpStatusCode.BadRequest); } result.Headers.Add("Proxy-Server", this.Config.ServerName); return result; }4.3,注册署理拦截器和API路由前面界说了拦截器 ProxyRequestHandler,现在需要把它注册到API的请求管道内里去,看到项目的 WebApiConfig 文件:namespace Demo.OAuth2.Port.App_Start{ public class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API 设置和服务 config.MessageHandlers.Add(new ProxyRequestHandler()); // Web API 路由 config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); config.Routes.MapHttpRoute( name: "MyApi", routeTemplate: "api2/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); config.Routes.MapHttpRoute( name: "MyApi3", routeTemplate: "api3/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } }} 4.4 HttpClient工具的优化 HttpClient工具封装了许多HTTP请求有用的方法,特别是哪些异步方法,感受它跟ASP.NET MVC WebAPI就是标配。可是也经常听见有朋侪在讨论HttpClient的性能问题,主要原因就是它的毗连问题,如果每个请求一个HttpClient实例在高并发下会发生许多TCP毗连,进而降低请求响应的效率,解决措施就是复用HttpClient工具,而且设置长毗连。

有关这个问题的测试息争决方案,可以参考这篇文章《WebApi系列~HttpClient的性能隐患》。在本解决方案的署理服务器中,默认情况下会见每一个署理的目的主机,会使用同一个HttpClient工具。好比有站点A,B,会建立 httpClientA,httpClientB 两个工具。

这样,相当于署理服务器跟每一个被署理的目的主机(资源服务器)都建设了一个长毗连,从而提高网络会见效率。private HttpClient GetHttpClient(Uri baseAddress, HttpRequestMessage request, bool sessionRequired) { if (sessionRequired) { //注意:应该每个浏览器客户端一个HttpClient 实例,这样才可以保证各自的会话不冲突 var client = getSessionHttpClient(request, baseAddress.Host); setHttpClientHeader(client, baseAddress, request); return client; } else { string key = baseAddress.ToString(); if (dictHttpClient.ContainsKey(key)) { return dictHttpClient[key]; } else { lock (sync_obj) { if (dictHttpClient.ContainsKey(key)) { return dictHttpClient[key]; } else { var client = getNoneSessionHttpClient(request, baseAddress.Host); setHttpClientHeader(client, baseAddress, request); dictHttpClient.Add(key, client); return client; } } } } }上面的代码,凭据URL请求的基础地址(被署理会见的目的主机地址)为字典的键,获取或者添加一个HttpClient工具,建立新HttpClient工具使用下面这个方法: private HttpClient getNoneSessionHttpClient(HttpRequestMessage request, string host) { HttpClient client = new HttpClient(); client.DefaultRequestHeaders.Connection.Add("keep-alive"); return client; }这个方法主要作用是为新建立的HttpClient工具添加长毗连请求标头。另外,还需要解决DNS缓存问题,在ServicePointManager 类举行设定,每一分钟刷新一次。//定期清除DNS缓存 var sp = ServicePointManager.FindServicePoint(baseAddress); sp.ConnectionLeaseTimeout = 60 * 1000; // 1 分钟最后,修改默认的并发毗连数为512,如下: static ProxyRequestHandler() { ServicePointManager.DefaultConnectionLimit = 512; }有关这问题,可以进一步参考下面的文章:C#中HttpClient使用注意:预热与长毗连多线程情况下挪用 HttpWebRequest 并发毗连限制4.5,署理的会话支持 我们的入口网站(Web Port)一般都是支持会话的,有时候,需要在资源服务器或者认证服务器保持用户的会话状态,提供有状态的服务。

前面我们说明实现署理会见使用了HttpClient工具,默认情况下同一个HttpClient工具与服务器交互是可以保持会话状态的,在署理请求的时候,将原始请求的Cookie值附加到署理请求的HttpCliet的CookieContainer工具即可。然而为了优化HttpClient的会见效率,我们对同一个被署理会见的资源服务器使用了同一个HttpClient工具,而不是对同一个浏览器的请求使用同一个HttpClient工具。

实际上,并不需要这样做,只要确保当前HttpClient工具的Cookie能够发送到被署理的资源服务器即可,针对每个请求线程建立一个HttpClient工具实例是最宁静的做法。回到前面的 GetHttpClient 方法,看到下面代码: if (sessionRequired) { //注意:应该每个浏览器客户端一个HttpClient 实例,这样才可以保证各自的会话不冲突 var client = getSessionHttpClient(request, baseAddress.Host); setHttpClientHeader(client, baseAddress, request); return client; }在 getSessionHttpClient 方法中,将原始请求的Cookie值一一复制到新的请求上去。CookieContainer 内里的Cookie跟HttpRequestMessage 请求头内里的Cookie基础就不是一回事,需要一个个的转换: private HttpClient getSessionHttpClient(HttpRequestMessage request, string host) { CookieContainer cc = new CookieContainer(); HttpClientHandler handler = new HttpClientHandler(); handler.CookieContainer = cc; handler.UseCookies = true; HttpClient client = new HttpClient(handler); //复制Cookies var headerCookies = request.Headers.GetCookies(); foreach (var chv in headerCookies) { foreach (var item in chv.Cookies) { Cookie cookie = new Cookie(item.Name, item.Value); cookie.Domain = host; cc.Add(cookie); } } return client; }我们知道对于ASP.NET来说,服务器支持会话是因为服务器给客户端发送了一个 名字为 ASP.NET_SessionId 的Cookie,只要这个Cookie发送已往了,被署理的服务器就不会再为“客户端”生成这个会话ID,而且会使用这个会话ID,在当前服务器(资源服务器)维护自己的会话状态。注意:虽然Web Port跟被署理的服务器使用了一样的SessionID,但它们的会话状态并不相同,只不外看起来会见两个服务器的客户端(浏览器)是同一个而已。

这样,我们就间接的实现了资源服务器“会话状态”的署理。种开发架构,接纳前后端分散,后端提供API,那么直接将前端公布的静态资源文件和网关项目法式部署到IIS的一个站点即可,法式不用做任何修改。部署之后,仅仅需要做下Web.config的修改和设置下署理网关的设置文件ProxyServer.config ,这两个文件的设置前面已经详细做了说明。

小结如果你计划在你的软件项目中也使用OAuth2.0的密码认证方案,PWMIS.OAuth2.0可以作为一个样例解决方案,你可以直接使用,做好API的署理设置即可,岂论你的API是不是.NET开发的。


本文关键词:微,服务,架构,如何,设计,API,署理,网关,和,开云app官网入口

本文来源:开云app官网入口-www.ywjdcs.com

同类文章排行

最新资讯文章

Copyright © 2000-2023 www.ywjdcs.com. 开云app官网入口科技 版权所有  http://www.ywjdcs.com  XML地图  开云APP·官方入口(kaiyun)(中国)官方网站IOS/Android/手机app下载