|
8 { ^/ X1 v! k3 {6 [7 O( K' f# J8 l
1,授权认证与微服务架构1.1,由不同团队合作引发的授权认证问题去年的时候,公司开发一款新产品,但人手不够,将B/S系统的Web开发外包,外包团队使用Vue.js框架,调用我们的WebAPI,但是这些WebAPI并不在一台服务器上,甚至可能是第三方提供的WebAPI。
9 e0 x8 w$ A$ L 同时处于系统安全的架构设计,后端WebAPI是不能直接暴露在外面的;另一方面,我们这个新产品还有一个C/S系统,C端登录的时候,要求统一到B/S端登录,可以从C端无障碍的访问任意B/S端的页面,也可以调用B/S系统的一些API,所以又增加了一个API网关代理。 9 J' B" e' s3 E
整个系统的架构示意图如下:
9 k( c4 ~2 T; e: g1 t5 c9 l 注:上图还有一个iMSF,这是一个实时消息服务框架,这里用来做文件服务,参见《消息服务框架使用案例之--大文件上传(断点续传)功能》在Web端会读取这些上传的文件1.2,微服务--分布式“最彻底”的分1.2.1,为什么需要分布式 6 K, Y0 k) {# I
大部分情况下,如果你的系统不是很复杂,API和授权认证服务,文件服务都可以放到一台服务器:Web Port 服务器上,但要把它们分开部署到不同的站点,或者不同的服务器,主要是出于以下考虑:1,职责单一:每一个服务都只做一类工作,比如某某业务WebAPI,授权服务,用户身份认证服务,文件服务等;职责单一使得开发、部署和维护变得容易,比如很容易知道当前是授权服务的问题,而不是业务API问题。 8 H7 j3 _0 P& P1 r- U6 |
2,系统安全:采用内外网隔离的方案,一些功能需要直接暴露在公网,这需要付出额外的成本,比如带宽租用和安全设施;另外一些功能部署在内网,这样能够提供更大的安全保证3,易于维护:每一个服务职责都比较单一,所以每一个服务都足够小,那么开发维护就更容易,比如要更新一个功能,只需要更新一个服务而不用所有服务器都暂停;另一方面也更加容易监控服务器的负载,如果发现某一个服务器负载太大可以增加服务器来分散负载。 ! |, C3 m$ N$ L! @& V
4,第三方接入:现在系统越来越复杂,内部的系统很可能需要跟第三方的系统对接,一起协同工作;或者整个系统一部分是 .NET开发的,一部分又是Java平台开发的,两个平台部署的环境有很大差异,没法部署在一起;或者虽然同是ASP.NET MVC,但是一个是MVC3,一个是MVC5,所以需要分别独立部署。
2 W# F C# e% c. R5 d- b2 C 以上就是各个服务需要分开部署的原因,而这样做的结果就是我们常说的分布式计算了,这是自然需求的结果,不是为了分而拆分1.2.2,依赖于中间层而不直接依赖于服务客户端直接访问后端服务,对后端的服务会形成比较强的依赖。 6 U2 b+ ?2 v6 L$ A* Z
有架构经验的朋友都知道,解决依赖的常见手段就是添加一个中间层,客户端依赖于这个中间层而不是直接依赖于服务层这样做有几个很大的好处:当服务负载过大的时候可以在中间层做负载均衡;或者后端某个服务出现问题可以切换主备服务;
3 e* T L% n# {, A/ b- U 或者替换后端某个服务的版本做灰度发布另一方面,当后端服务部署为多个独立的进程/服务器后,客户端直接访问这些服务,将是一个更加较复杂的问题,负载均衡,主备切换,灰度发布等运维功能更难操作,除此之外,还有下面两个比较重要的问题:。
6 X3 f# s4 Q% z+ w& \" d1 S( ^ 客户端直接访问后端多个服务,将暴露过多的后端服务器地址,从而增加安全隐患;后端服务太多,需要在客户端维护这些服务访问关系,增加开发调试的复杂性;B/S页面的AJax跨域问题,WebAPI地址跟主站地址不一样,要解决跨域问题比较复杂并且也会增加安全隐患。
- \3 s0 P" @! m" \ 所以,为了解决客户端对后端服务层的依赖,并且解决后端服务太多以后引起的问题,我们需要在客户端和后端服务层之间添加一个中间层,这个中间层就是我们的服务代理层,也就是我们后面说的服务网关代理(WebAPI Gateway Proxy),它作为我们所有Web访问的入口站点,这就是上图所示的 Web Port。 7 ^' Z: G3 L; u4 F/ ^: j# s& I
有了网关代理,后台所有的WebAPI都可以通过这个统一的入口提供对外服务的功能,而对于后端不同服务地址的路由,由网关代理的路由功能来实现,所以这个代理功能很像Nginx这样的反向代理,只不过,这里仅仅代理WebAPI,而不是其它Web资源。
1 V2 w( {0 i! W: P1 q 现在,网关已经成为很多分布式系统的标配,比如TX的这个架构: , S8 d2 [3 z$ ~* c# _' V5 E
注:上图来源于网络,侵删!另外,这个读写分离代理,如果使用SOD框架,可以在AdoHelper对象直接设置读写不同的连接字符串简单达到效果1.2.3,微服务架构经过上面的设计,我们发现这个架构有几个特点:。 + J9 F+ [; ~( z" G' E& \
每个服务足够小,职责单一;每个服务运行在自己的进程或者独立的服务器中,独立发布部署和开发维护;服务对外提供访问或者服务之间进行通信,都是使用轻量级的HTTP API;每个服务有自己独立的存储,彼此之间进行数据交互都通过接口进行; , y7 i4 D5 C$ `" f/ v" U' T
有一个API代理网关统一提供服务的对外访问这些特点是非常符合现在流行的微服务思想的,比如在《什么是微服务》这篇文章中,像下面说的这样:微服务最早由Martin Fowler与James Lewis于2014年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,
' T9 \7 `4 j9 m* i C' J* } 并使用轻量级机制通信,通常是HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,7 X. ^1 [9 F \* @
并保持最低限度的集中式管理。
: }) b+ N R. X( l: J M: H 所以我们这个架构是基本符合微服务思想的,它的诞生背景也是要解决其它传统单体软件项目现在遇到的问题一样的,是在比较复杂的实际需求环境下自然而然的一种需求,不过好在它没有过多的“技术债务”,所以设计实施起来比较容易。 # @3 ?- y8 n) e+ \
下面我们来详细看看这个架构是如何落地的2,“授权\认证\资源”独立服务的OAuth2.0架构2.1,为什么需要OAuth2.0 ?OAuth 2.0已经是一个“用户验证和授权”的工业级标准OAuth(开放授权)是一个开放标准,1.0版本于2006年创立,它允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。 , m4 W( k# }! r
OAuth 2.0关注客户端开发者的简易性,同时为Web应用,桌面应用和手机,和起居室设备提供专门的认证流程2012年10月,OAuth 2.0协议正式发布为RFC 6749以上内容详见OAuth 2.0官网。
( L4 {1 }6 b) V. H, }* Z 现在百度开放平台,腾讯开放平台等大部分的开放平台都是使用的OAuth 2.0协议作为支撑,国内越来越多的企业都开始支持OAuth2.0协议现在,我们的产品设计目标是要能够和第三方系统对接,那么在对接过程中的授权问题就是无法回避的问题。
/ ?! ~0 E- X; L" Q' x 在我们原来的产品中,有用户授权验证的模块,但并没有拆分出独立的服务,用它与第三方系统对接会导致比较大的耦合性;另一方面,与第三方系统对接合作不一定每次都是以我们为主导,也有可能要用第三方的授权认证系统这就出现了选择哪一方的授权认证方案的问题。 & i% w; F) P& m3 o i. i
之前我曾经经历过一个项目,因为其中的授权认证问题导致系统迟迟不能集成所以,选择一个开放标准的授权认证方案,才是最佳的解决方案,而OAuth 2.0正是这样的方案2.2,OAuth的名词解释和规范(1)Third-party application:第三方应用程序,本文中又称”客户端”(client),即上一节例子中的“Web Port”或者C/S客户端应用程序。 6 |4 U. `, i* F0 W: s% D: P
(2)HTTP service:HTTP服务提供商,即上一节例子中提供软件产品的我们公司或者第三方公司(3)Resource Owner:资源所有者,本文中又称“用户”(user)(4)User Agent:用户代理,本文中就是指浏览器或者C/S客户端应用程序。
2 ?; K! j( S b/ n8 w (5)Authorization server:授权服务器,即服务提供商专门用来处理认证的服务器(6)Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器,即上一节例子中的内部API服务器、第三方外部API服务器和文件服务器等。 4 u6 U- w3 U! ]- O$ H: g
它与认证服务器,可以是同一台服务器,也可以是不同的服务器以上名词是OAuth规范内必须理解的一些名词,然后我们才能方便的讨论OAuth2.0是如何授权的有关OAuth的思路、运行流程和详细的四种授权模式,请参考阮一峰老师的《理解OAuth 2.0》。 7 s6 {( \2 b7 X7 |* o
2.3,OAuth2.0的授权模式为了表述方便,先简单说说这4种授权模式:授权码模式(authorization code)--是功能最完整、流程最严密的授权模式它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。
* }: f$ Q' Y& I! [# `; b 简化模式(implicit)--不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证密码模式(resource owner password credentials)--用户向客户端提供自己的用户名和密码。
( W( o9 U# D2 N& }% x 客户端使用这些信息,向"服务商提供商"索要授权在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码客户端模式(client credentials)--指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。 ; _3 C" v; w& u# [8 H+ q& Y; h
严格地说,客户端模式并不属于OAuth框架所要解决的问题在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题在我们的需求中,用户不仅仅通过B/S系统的浏览器进行操作,还会通过C/S程序的客户端进行操作,B/S,C/S系统主要都是我们提供和集成的,客户购买了我们这个产品要使用它就意味着客户信任我们的产品。 6 F N/ J* H2 y, Q) Z3 Q N
授权码模式虽然是最完整的授权模式,但是授权码模式授权完成后需要浏览器的跳转,显然浏览器无法直接跳转到我们的C/S客户端,虽然从技术上可以模拟,但实现起来成本还是比较高;简化模式也有这个问题所以我们最终决定采用OAuth2.0的密码模式。 # E& @. g7 e# M7 {7 m: [+ i$ E K
2.4,OAuth2.0密码模式授权流程 简单来说,密码模式的步骤如下: 用户向客户端提供用户名和密码客户端将用户名和密码发给认证服务器,向后者请求令牌认证服务器确认无误后,向客户端提供访问令牌 上面这个步骤只是说明了令牌的获取过程,也就是我们常说用户登陆成功的过程。 * s+ U- c3 F1 W( W
当用户登陆成功之后,客户端得到了一个访问令牌,然后再使用这个令牌去访问资源服务器,具体说来还有如下后续过程:4,客户端携带此访问令牌,访问资源服务器;5,资源服务器去授权服务器验证客户端的访问令牌是否有效;
! O3 l7 e% j6 H4 o6 \& i( ] 6,如果访问令牌有效,授权服务器给资源服务器发送用户标识信息;7,资源服务器根据用户标识信息,处理业务请求,最后发送响应结果给客户端。下面是流程图:
( a( f5 x0 ^4 l ]& ? 注意:这个流程适用于资源服务器、授权服务器相分离的情况,否则,流程中的第5,6步不是必须的,甚至第4,7步都是显而易见的事情而不必说明现在大部分有关OAuth2.0的介绍文章都没有4,5,6,7步骤的说明,可能为了表述方便,默认都是将授权服务器跟资源服务器合在一起部署的。 : Q0 f3 {) x D- q
2.5,授权、认证与资源服务的分离什么情况下授权服务器跟资源服务器必须分开呢?如果一个系统有多个资源服务器并且这些资源服务器的框架版本不兼容,运行环境有差异,代码平台不同(比如一个是.NET,一个是Java),或者一个是内部系统,一个是外部的第三方系统,必须分开部署。
: }$ s& N% W2 |" E& K 在这些情况下,授权服务器跟任意一个资源服务器部署在一起都不利于另一些资源服务器的使用,导致系统集成成本增加这个时候,授权服务器必须跟资源服务器分开部署,我们在具体实现OAuth2.0系统的时候,需要做更多的事情。 + w' y. }' ~5 t0 Y! w! Y
什么情况下授权服务器跟认证服务器必须分开呢? 授权(authorization)和认证(authentication)有相似之处,但也是两个不同的概念:授权(authorization):授权,批准;批准(或授权)的证书;
. e$ B6 T1 e7 W# r* K 认证(authentication):认证;身份验证;证明,鉴定;密押仅仅从这两个词的名词定义可能不太容易分辨,我们用实际的例子来说明他们的区别:有一个管理系统,包括成熟的人员管理,角色管理,权限管理,系统登录的时候,用户输入的用户名和密码到系统的人员信息表中查询,通过后取得该用户的角色权限。
" A$ {: f0 @, W- z: G& u- H3 u 在这个场景中,用户登录系统实际上分为了3个步骤:用户在登录界面,输入用户名和密码,提交登录请求;【认证】系统校验用户输入的用户名和密码是否在人员信息表中;【授权】给当前用户授予相应的角色权限现在,该管理系统需要和第三方系统对接,根据前面的分析,这种情况下最好将授权功能独立出来,采用OAuth这种开放授权方案,而认证问题,原有管理系统坚持用户信息是敏感信息,不能随意泄露给第三方,要求在原来管理系统完成认证。 2 i9 Q4 R! J# h9 R7 O4 m- {0 ~
这样一来,授权和认证,只好分别作为两个服务,独立部署实现了本文的重点就是讲述如何在授权服务器和资源服务器相分离,甚至授权和认证服务器相分离的情况下,如何设计实现OAuth2.0的问题3,PWMIS OAuth2.0 方案。
1 m, E! ~0 T" j PWMIS OAuth2.0 方案就是一个符合上面要求的授权与认证相分离,授权与资源服务相分离的架构设计方案,该方案已经成功支撑了我们产品的应用下面分别来说说该方案是如何设计和落地的3.1,使用Owin中间件搭建OAuth2.0认证授权服务器。
9 R! a/ \& u# v. o; E$ @# P% ~/ ~; g 这里主要总结下本人在这个产品中搭建OAuth2.0服务器工作的经验至于为何需要OAuth2.0、为何是Owin、什么是Owin等问题,不再赘述我假定读者是使用Asp.Net,并需要搭建OAuth2.0服务器,对于涉及的Asp.Net Identity(Claims Based Authentication)、Owin、OAuth2.0等知识点已有基本了解。 - R2 I1 t2 m! }' Z3 N# d+ S
若不了解,请先参考以下文章:MVC5 - ASP.NET Identity登录原理 - Claims-based认证和OWIN下一代Asp.net开发规范OWIN(1)—— OWIN产生的背景以及简单介绍
/ O, e% R. l8 h4 c OWIN OAuth 2.0 Authorization Server我们的工作,可以从研究《OWIN OAuth 2.0 Authorization Server》这个DEMO开始,不过为了更好的结合本文的主题,实现授权与认证相分离的微服务架构,推荐大家直接从我的DEMO开始:https://github.com/bluedoctor/PWMIS.OAuth2.0 1 _" G, X. ^4 k5 A
PS:大家觉得好,先点个赞支持下,谢谢!克隆我这个DEMO到本地,下面开始我们OAuth2.0如何落地的正式讲解3.2,PWMIS.OAuth2.0解决方案介绍首先看到解决方案视图,先逐个做下简单说明:。 * B1 W* c# z% m
3.2.1,运行解决方案将解决方案的项目,除了PWMIS.OAuth2.Tools,全部设置为启动项目,启动之后,在 http://localhost:62424/ 站点,输入下面的地址:http://localhost:62424/Home
9 W& h3 O+ ]5 Q! b2 s 然后就可以看到下面的界面:
8 M" T& x' D! p& r& L" f 点击登录页面,为了方便演示,不真正验证用户名和密码,所以随意输入,提交后结果如下图: - p1 S3 b& P, e9 z# ~4 Z
点击确定,进入了业务操作页面,如下图:
6 g' U6 ~% y/ m% z 如果能够看到这个页面,我们的OAuth2.0演示程序就成功了。还可以运行解决方案里面的WinForm测试程序,先登录,然后运行性能测试,如下图: % V5 o' j( r `& z/ g
更多信息,请参考下文的【3.8集成C/S客户端访问】下面我们来看看各个程序集项目的构建过程3.3,项目 PWMIS.OAuth2.AuthorizationCenter首先添加一个MVC5项目PWMIS.OAuth2.AuthorizationCenter,然后添加如下包引用:。
5 T; q- b. @* M: h/ w; |. @ Microsoft.AspNet.MvcMicrosoft.Owin.Host.SystemWebMicrosoft.Owin.Security.OAuthMicrosoft.Owin.Security 4 ~6 F+ t- {- e* G
.Cookies然后在项目根目录下添加一个OWin的启动类 Startup:using Microsoft.Owin;
* n. L1 u: Y" R/ }+ a using Microsoft.Owin.Security;
0 R; B# @" F( h. o# D; {- @5 i using Microsoft.Owin.Security.OAuth;
2 c: ] K& n }' D0 E7 Z + w. r" h4 N8 n: j
using Owin;
4 t0 Z/ o3 m5 ?% W9 Z using System;
# w! d- b. W' A% N# D using System.Collections.Generic;
( }* ^8 ~3 Q/ F4 M2 T2 ?9 b using System.Diagnostics;' q* k' ~% p; d+ j" r4 u
using System.Web.Http;7 }4 S$ x, T4 r; T7 O) q m
4 U1 U, ^4 @- L) z) d4 |' | ) H/ Z$ S$ C9 X/ m
namespacePWMIS.OAuth2.AuthorizationCenter0 m7 i+ R' P* t
{* H. Y; b- a, P* ]; U; T4 J2 _
publicpartialclassStartup+ o/ Y4 k. h. W
{. E) U* `# h/ _7 |
publicvoidConfigureAuth
1 |9 q1 b9 t& E$ r: `* p, w1 H, F (IAppBuilder app)8 |$ ~5 r8 K8 |) w* I! G0 j
{0 L. |/ m' x1 P# E
var OAuthOptions = new OAuthAuthorizationServerOptions
0 g6 A3 C* Q0 L {# e/ F/ i: {; J6 p0 x
AllowInsecureHttp = 2 d1 @/ S& p d& g
true,0 b% N9 p# V- B/ v
AuthenticationMode = AuthenticationMode.Active,
1 ?* l+ F. X+ Z% | TokenEndpointPath = ) m- R8 T) C* f4 ?4 |) `6 Y# P
new PathString("/api/token"), //获取 access_token 授权服务请求地址, O& T1 ?% o* d; H6 m
AuthorizeEndpointPath = new PathString(
7 x. A: @* P* ^6 s% \7 V "/authorize"), //获取 authorization_code 授权服务请求地址' _7 J3 M1 O; I5 }( D
AccessTokenExpireTimeSpan = TimeSpan.FromSeconds( % ~. l2 I) e N4 g) H
60), //access_token 过期时间,默认10秒太短
: m6 z/ I! P" I! ?1 x& A" J; @( k: ~ P+ i
Provider = new OpenAuthorizationServerProvider(), //access_token 相关授权服务 3 g' `) N8 A8 }9 n$ I
) x- H- O* w/ g% }; T1 {" u* r
AuthorizationCodeProvider = new OpenAuthorizationCodeProvider(), //authorization_code 授权服务 # ?) i+ y8 m4 k' j
2 s& S% z) T4 W/ z) M% h# L5 F/ g
RefreshTokenProvider = new OpenRefreshTokenProvider() //refresh_token 授权服务! r( M# G% j; c) {/ h; |
};3 O8 f* u! k; _
app.UseOAuthBearerTokens(OAuthOptions); 9 T5 a, w5 R3 ?0 r3 ~! W
//表示 token_type 使用 bearer 方式
2 ?7 l! O: F9 a! L; | }
: L( V2 h5 X G' A% q, s
" Y9 |( @, m: h( e U! q. M, Q publicvoidConfiguration(IAppBuilder app)
9 H8 p0 O# O4 }6 w$ Y9 p {" k. q: H% _+ k/ a7 q
6 S- J+ a% ~- }9 z // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888
+ w; N) Y! E( s$ E9 A; L ; c6 g% Y H: D% ]3 H
ConfigureAuth(app);2 i: g4 Z M, h! y$ N
% R+ D$ U" ^* m! O+ b, }" a3 F0 G var configuration = new HttpConfiguration();
- \& W; p e+ X5 B5 V- Q. Y WebApiConfig.Register(configuration);0 I4 M% @2 j: J: H' O
app.UseWebApi(configuration);5 x5 E( Y. U8 X! v/ p6 g
) P1 J* v, D7 T6 L
}
! ~* f) Z n, ]9 S
! `( o, p0 z# M1 j( S% ~4 S8 V5 m }
1 w! B3 |) `- x7 G1 G }
S5 g8 t o) e2 w# l) R 上面的代码中,定义了access_token 授权服务请求地址和access_token 过期时间,这里设置60秒后过期由于本篇着重讲述OAuth2.0的密码授权模式,我们直接看到类 OpenAuthorizationServerProvider的定义:。 4 S1 j; P2 V5 [
OpenAuthorizationServerProvider token过期时间不宜太长,比如一天,这样不安全,但也不能太短,比如10秒,这样当API访问量比较大的时候会增大刷新token的负担,所以这里设置成60秒。
8 T4 |; F& R& u* {$ g2 R& X 3.3.1,验证客户端信息在本类的第一个方法 ValidateClientAuthentication 验证客户端的信息,这里的客户端可能是C/S程序的客户端,也可能是访问授权服务器的网关代理服务器,OAuth2.0会验证需要生成访问令牌的客户端,只有合法的客户端才可以提供后续的生成令牌服务。
3 M7 ^- C8 Q, U( M. y) y 客户端信息有2个部分,一个是clientId,一个是clientSecret,前者是客户端的唯一标识,后者是授权服务器颁发给客户端的秘钥,这个秘钥可以设定有效期或者设定授权范围为简便起见,我们的演示程序仅仅到数据库去检查下传递的这两个参数是否有对应的数据记录,使用下面一行代码:。
3 R4 W( B! e' M7 O9 ?4 Z var identityRepository = IdentityRepositoryFactory.CreateInstance();这里会用到一个验证客户端的接口,包括验证用户名和密码的方法一起定义了: 5 o! J6 w0 C3 ?: j, y6 q" m0 d' h
////// 身份认证持久化接口///publicinterfaceIIdentityRepository
9 O; U5 b+ @$ K5 y& o {8 E6 X9 H: ~& m: Q
//////
* l! p( t: r0 g, @' h 客户ID是否存在/////////Task ExistsClientId * I' n2 c$ l$ e, {; L$ ~( U3 I
(string clientId);1 w9 Z n! Y$ Z; f
////// 校验客户标识//////客户ID///
* n% G. Z% D7 q! }+ {4 m, a' z' D 客户秘钥///Task ValidateClient(string clientId, 6 ~: G( `5 Y6 g- m) w
string clientSecret);
- s a1 N! D# X4 F ////// 校验用户名密码///////// + y" k" s J% Q+ l& O
///Task ValidatedUserPassword(string userName,
; Z+ H8 @; J- k% b string password);& C3 {+ c) c4 }
}这样我们就可以通过反射或者简单 IOC框架将客户端验证的具体实现类注入到程序中,本例实现了一个简单的客户端和用户认证类,采用的是SOD框架访问数据库:namespace
3 \ c$ l' S$ m; k PWMIS.OAuth2.AuthorizationCenter.Repository
% ^( }5 x% P5 p P. a* C2 _) g9 k5 V {5 E3 e' Z3 O) q+ \8 `+ A
publicclass SimpleIdentityRepository : IIdentityRepository
5 ?, ]- z. B% i; Y$ i; D/ U# T {6 A" g$ K/ m- {
\7 l2 ?& X+ z: M% E. V, ` privatestatic System.Collections.Concurrent.ConcurrentDictionary dictClient = new System.Collections.Concurrent.ConcurrentDictionary<
, D! e% v& c8 I8 d- a7 c0 W0 I9 x string, string>();
3 _+ d5 X7 I% V' o; `) z publicasync Task ExistsClientId(string clientId); i4 o/ u3 T- K' E, |
{7 Z2 I# J* k/ A% w& k/ X. z$ o
" O5 V# p+ {$ q5 d6 p returnawait Task.Run(() =>, \ Y/ | m3 T0 L' `1 F2 F3 z
{
$ b6 U# v0 g8 d5 }) e AuthClientInfoEntity entity = new AuthClientInfoEntity();
5 p0 |* [3 k- S# o% r4 j) o0 G entity.ClientId = clientId;8 H8 k( Y3 ^% {5 h! R$ q
% A1 o* e5 r S2 S0 p OQL q = OQL.From(
1 E3 O) a* |- f3 ~ w entity) ^; a. a2 `9 v9 r
.Select(entity.ClientId)
5 v+ P5 y( [- ^& V) @3 B .Where(entity.ClientId)/ ~( C+ X! L: L. `3 Z2 p/ ]) h
.END;
+ O2 M3 P; k) H }1 i6 _ AuthDbContext context =
8 P4 L/ l- I, ?9 S new AuthDbContext();! T! R E& v3 X) Q
AuthClientInfoEntity dbEntity = context.QueryObject(
3 K9 h) J" n9 p( @1 D: R& e+ r$ m q);
6 `# f7 V! P* R return dbEntity != null;) _% N% z2 D0 c$ P/ S, ]) T
});
2 k" ]0 T. ]/ Z3 ~0 l: O ~9 x }
1 w' w/ T: P h5 y8 ~( J
) x7 d0 L4 P3 E1 w' j6 [; } publicasyncTask + v; r% a5 o Y H# ~; j! x
ValidateClient(string clientId, string clientSecret)9 c( ?( `9 P$ ]* N
{
7 f0 \- x2 \# B. x0 |2 u3 Z) b stringdict_clientSecret;
4 \ L- l1 c8 ~5 R& Z
. H! P$ B, Q. ~( `6 r$ h7 F" S/ s, S if (dictClient.TryGetValue(clientId, out dict_clientSecret) && dict_clientSecret== clientSecret)% {; J t5 ~, |; r
{: n' ~+ O- h( @/ {% d% k* d
3 X5 K" w* W/ z3 z, i% g returntrue;
4 {7 T! m9 s, Y' Y0 t5 y }
" g% S9 v" S5 [6 x else6 a, o- M- L1 i; O3 u
{) K9 i" i& `' S
returnawaitTask.Run(() => {& o, E# ?+ t+ V* x& h1 ^" n- B) `' C
AuthClientInfoEntity entity =
. P. S6 }- H$ L4 T" J) d new AuthClientInfoEntity();9 @6 n3 E0 E0 r
entity.ClientId = clientId;
4 W. P g0 T/ p% S8 ]; _. C entity.ClientSecret = clientSecret;1 M s: `) ]4 M2 r, e( u4 }: o
OQL q = OQL.From(
% x7 d$ r+ u1 A. p# g3 D; A3 p entity)
7 V8 W6 A# u6 U .Select(entity.ClientId)
' O& k% c& b6 u" K .Where(entity.ClientId, entity.ClientSecret 8 Q: F, Q, f9 _* G. f B6 ?
)
9 y4 X4 ?' U/ N! `8 B .END;1 Q# G; f! L$ T7 U
AuthDbContext context = new AuthDbContext();0 ? A" D1 c5 Q9 c5 P7 B: n" x
AuthClientInfoEntity dbEntity = context.QueryObject(
S' z1 C' Q3 O9 }. E q);7 Z6 _3 G6 b$ Y
if (dbEntity != null)
1 V& ]9 D( o7 y; | {; D+ o- X* O3 O. v* f1 D' e
dictClient.TryAdd( # ] |' B* j) R( c7 l$ j9 n1 o1 E
clientId, clientSecret);8 H. [6 c* {6 H" w
returntrue;$ @: x1 A' `. W9 E$ R! {# [0 _6 i! P
}& E& j5 l7 ?- ^
% J/ |8 g; L% n- W/ f4 R# G
elsereturnfalse;
8 R4 o: Z- q! f q& M });
0 p8 l, m9 q, U& S, R9 j8 i }
2 I" P! \! T: l
( t( z) c I1 X9 u- R) t }
' Z2 L. d- N( c# W; B
5 |" v9 q3 H5 `. o2 A publicasyncTask
1 j7 B9 Y) X D2 |5 A > ValidatedUserPassword(string userName, string password)) R! u1 E1 E+ N6 {4 Q- w
{4 `: j& F9 d, j6 R
returnawaitTask.Run 8 f) i. [; @5 v3 C1 G, X( T
>(() =>
# S+ x1 B+ W, @8 d9 \$ ~$ N {
5 t) T, F; x# N; k2 q/ | UserInfoEntity user = new UserInfoEntity();$ R! ]/ f, p( o1 r0 n
user.UserName = userName;
+ h! ?, l# l' y! i G2 X user.Password = password;
6 G9 [' U# O! S0 K7 K OQL q = OQL.From(
. _$ H$ P, \& s+ _( |5 s+ v user)
+ v3 c+ V0 y0 m! U7 ? .Select()( N! u, \+ \7 p1 D' s# r5 \8 Y
.Where(user.UserName, user.Password)6 o- [& _) Q1 ~3 ?* j% f: n
.END;4 S" |9 {# q0 P5 n6 U8 ^; d& T7 @4 S
AuthDbContext context = ! n' E% Z1 K1 j/ ?/ n: N# }* v
new AuthDbContext();
# }: i t5 M' ^ AuthClientInfoEntity dbEntity = context.QueryObject(
}+ c6 {% v, J0 w. J+ P5 E5 C' Z q);
% u7 Q* }; r$ A6 ?. m3 V( Y& p return dbEntity != null;
m2 \$ M% `* j* N });4 [- j- g& K9 x
}" v; l$ t# d4 j/ I0 Q* K0 D7 M
}1 k6 a! |) G; L5 f
}AuthDbContext 类非常简单,它会自动生成验证客户端所需要的表: " _* e+ Q6 R" U; e8 W
namespacePWMIS.OAuth2.AuthorizationCenter.Repository- r7 b7 g: [2 }* c) Y1 O# p3 Q
{& Y, e4 G. D9 s6 p. Y7 T" [
publicclassAuthDbContext bContext
' g& y4 c/ C6 o* T, B9 V {
+ @, y# U1 ?4 q 9 Z1 ~# k' T# [# I
publicAuthDbContext()
8 Y; _! _0 g0 L; M : base("OAuth2")
4 |9 t8 ~6 r4 h; S% w. o# c3 v$ i {
' T' @9 ^# Y, i# U6 H# d6 _, t' M% n3 @0 e4 _5 C
}0 N0 u6 o9 u# h2 s' N
' E) k# b0 x2 p4 i
( K. A0 g* J0 x" { b) B
7 L3 Y) K4 x! k0 M5 x5 M protectedoverrideboolCheckAllTableExists()2 ]6 s8 Y. m! h7 q! R' _
{
. C6 C# m' C5 f* j base.CheckTableExists();
# Y' x/ f1 ?6 ]& k
& b" D ?; Q) C* X& |7 Z" D2 S base.CheckTableExists();, C- P1 g3 C+ o) ^- I5 L
returntrue;
! N% M" h0 N! d) _6 M) M4 f7 ? }
; z0 T4 v$ S; S% t n# I }
% R8 q& R; k/ l& l# |" ^ }3.3.2,认证用户,生成访问令牌生成访问令牌需要重写OWIN OAuthAuthorizationServerProvider类的 GrantResourceOwnerCredentials方法(方法的详细内容看前面【OpenAuthorizationServerProvider的定义】),方法里面使用到了IdentityService 对象,它有一个UserLogin 方法,用来实现或者调用用户认证服务:
& w) k- m: G8 N- x0 q% w1 T/ T IdentityServiceUserLogin方法提供了2种方式来认证用户身份,一种是直接访问用户数据库,一种是调用第三方的用户认证接口,这也是当前演示程序默认配置的方式当用户认证比较复杂的时候,推荐使用这种方式,比如认证的时候需要检查验证码。
8 T' G7 n9 J* ?" @3 z3 x7 B 需要在授权服务器的应用程序配置文件中配置使用何种用户身份验证方式以及验证地址:
! u* ]9 x d6 I; B$ D+ O ="webpages:Enabled"value="false"/>
. [( e6 W( c+ e, Z+ { value="true"/> / ~% I: z! ]$ B- ?
/> ( K$ z" V, z$ W& r- `( {& v
>如果认证用户名和密码通过,在GrantResourceOwnerCredentials方法最后,调用OWin的用户标识方式表示授权验证通过:3.4,项目 PWMIS.OAuth2.Tools项目 PWMIS.OAuth2.Tools 封装了OAuth2.0调用相关的一些API函数,前面我们介绍了基于OWIN实现的OAuth2.0服务端,下面我们来看看如何调用它生成一个访问令牌。 3 l3 ?, J8 ?0 \5 } U/ w
3.4.1,OAuthClient类--获取和刷新令牌看到 OAuthClient.cs 文件的 OAuthClient类的GetToken 方法////// 获取访问令牌///
9 l( p1 G- W$ b1 G! A ///授权模式///刷新的令牌///
: S! P! p& t7 G" m5 Q* R' c4 r" V 用户名///用户密码///授权码///
/ ~. c3 D/ u* M9 v3 `4 _ 可选业务参数///publicasync Task GetToken(string grantType, string * v0 @1 ~* { d. G5 D
refreshToken = null, string userName = null, string password = null, string authorizationCode = null 4 G& _! c$ ?; ]# G" A% d; _5 q& @6 q
,string scope=null)
! Q( H g- k" w; u {6 L. w+ E3 Q' M4 Q% J7 V
var clientId = System.Configuration.ConfigurationManager.AppSettings[ 0 t6 |1 g. c2 _
"ClientID"];
' Q, E; B) H! L2 s) J& `* m var clientSecret = System.Configuration.ConfigurationManager.AppSettings["ClientSecret" 2 r# j! `2 v/ t
];
8 U4 t4 N! Q& Y' J this.ExceptionMessage = "";( d' `* ^/ w) k; g
var parameters = new Dictionary
" O: W. e( L @- F4 j& X >();1 T Q! j' l1 j- A
parameters.Add("grant_type", grantType);- h. y+ ]& x" o* U0 R- U+ f( e
5 t9 h/ k3 g$ y) w/ V if (!string.IsNullOrEmpty(userName) && ! & B4 `2 }# m0 U9 \* F
string.IsNullOrEmpty(password))
6 e+ i- ~1 @! v- t# A: L" G# ? {
$ y- U' X# c9 K6 D4 g- z% k parameters.Add("username", userName);. P4 ~$ n! `: V8 e o( M
parameters.Add( 5 Y' S4 O( R$ ^5 [) ^. k. ?" a
"password", password);
7 }( g; H. N) x1 V1 \ parameters.Add("scope", scope);
* Z& W& \& u2 k0 O' @8 {! P; A
5 h$ ~, i: b; t4 |7 W }* U- n8 E: M( s- R K3 ` J3 [
$ W7 }. k. Q! o) B if (!string.IsNullOrEmpty(authorizationCode))3 j! |- e; l/ b4 b" @* y
{
: ~) h) t+ Y/ [1 X3 I1 D var redirect_uri = System.Configuration.ConfigurationManager.AppSettings[ 4 `- R ?) }* x: t) m7 Q
"RedirectUri"];
) C: P$ x2 S3 S) W, a% z0 e. J parameters.Add("code", authorizationCode);
$ o, V9 m4 P6 R' r' c k parameters.Add(
3 {$ s% A& q0 [9 A "redirect_uri", redirect_uri); //和获取 authorization_code 的 redirect_uri 必须一致,不然会报错
" y7 I/ n4 g5 x$ J }
; k6 O$ I8 J, y" M
B# ~: K3 L- k! l( g5 m8 A if (!string.IsNullOrEmpty(refreshToken))2 S: e; U9 Y" m. w8 L, H* S2 j& e$ ^; W% w
{
M* E7 L4 L3 @- D5 F2 b parameters.Add("refresh_token"
+ |- R6 ]2 l* w C$ r0 \ , refreshToken);5 s& E. F1 ^ o1 x; L
}4 D8 v( s* v+ E: n
2 W1 m, @: V" h! r
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(" v# v, L5 k2 ~8 F! B( j+ Q2 K
{$ d0 V& E; y" {$ a
"Basic",
: `+ s* ~4 K1 e5 c9 F Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret)));
/ \3 m0 U$ `. y5 x: S- b0 i8 A
7 V2 \: m3 f$ d& d6 m string errCode = "00";8 p6 F, U! X9 S0 S% B1 N
try* A. D7 z, }3 p
{. `4 `$ }2 B* p3 G
//PostAsync 在ASP.NET下面,必须加).ConfigureAwait(false);否则容易导致死锁 , C& u1 o! g5 \6 |) @9 C
//详细内容,请参考 http://blog.csdn.net/ma_jiang/article/details/53887967var cancelTokenSource = new CancellationTokenSource(
0 k) b) N: q; c' A) i" L 50000);# Y$ z" c. X, D+ H% F
var response = await httpClient.PostAsync("/api/token", new FormUrlEncodedContent(parameters), cancelTokenSource.Token).ConfigureAwait(
8 Y; J- J- e+ u8 H; e7 c2 i false);
9 t% l1 x% ] u* q' k0 Z) H5 P$ n$ ] var responseValue = await response.Content.ReadAsStringAsync();& C8 j: E @& I1 ]1 R9 X
! T5 B J9 Y. {. l+ ?9 J$ L4 m9 o if (response.StatusCode != HttpStatusCode.OK)+ q" W- S- {8 _9 D1 h
{5 {: }; T% C$ w, E" h3 x
try2 x. q6 [* |. z
{
( x8 l9 H' j6 z+ d4 O$ z' k ' V4 E, D u h9 R1 b6 R
var error = await response.Content.ReadAsAsync();' a4 I; s9 ^. b5 x" [
if (error.ExceptionMessage ==
5 u2 [3 X( ]- C/ \7 w- b null). m, J ]4 q4 T8 E6 U3 [
{' ^, o9 G( G5 C) z/ W/ P
string errMsg = ""; w/ K! n% z6 L9 M( Q
5 r3 z2 B m2 u9 x6 x6 {1 y foreach (var item in error)
" ^4 F* L, E- u& Q {7 a! t0 d& @( E6 G! R
errMsg += item.Key + 8 ?" U, M" b1 p; x5 K e& i
":\"" + (item.Value == null ? "" : item.Value.ToString()) + "\",";
! K* B: F" \ [+ p& c }7 J. J' W9 G+ X m# D& R m. o
8 @& ] m0 s% J( L, b3 p( Y
this.ExceptionMessage = "HttpError:{" + errMsg.TrimEnd(,)+"}";
6 E' T6 |3 T. D4 d, |1 E( }) y! H" ]" `" f }
$ C% T3 k" H# v; Q8 W9 V
' ]; f& m/ x, c else
' D. r, h& d9 A/ @( w4 V { y+ t9 B# Y. M8 v0 {/ P
this.ExceptionMessage = error.ExceptionMessage;/ M+ k" m2 x. U2 F: G
}
. P/ F0 N' h; ~ f errCode =
3 [' V$ j3 L* e8 l- Y "1000";
( |% m( t* |. M7 ]- l6 ? }
- [2 B& ]5 D5 V; I6 O. F catch (AggregateException agex)
& k5 z3 C {; Z% g {
; w# K4 u2 d' Z+ a3 g
" C" d+ A9 W/ J: e c string errMsg = "";5 |/ J/ W# ?. F
foreach (var ex in agex.InnerExceptions)
2 f; e2 F9 s' c# J6 q {# R! Q, s2 u, U( S, V1 Q _6 b
errMsg += ex.Message;
$ z6 d6 i2 l$ ^; Q# a+ K }
+ }7 k! o( F' C2 Y% V/ d% K s5 E* K- U; C/ j9 n$ V# t0 A' ?4 I
errCode = : o" B: j. ]# k1 O4 W# a' ]
"1001";
4 _. h& F5 g" ~) |% P+ B% @ this.ExceptionMessage = errMsg;
2 r0 {# f! a% ]! M# y. [) [ }
/ H$ j+ p4 W, q: k8 F
6 F1 n2 O- N8 Z; S/ d catch (Exception ex)
7 n- a2 ?& H6 J& Z! V3 ]$ d { U3 A7 D2 s( E0 r7 F7 u
this.ExceptionMessage = response.Content.ReadAsStringAsync().Result;
; c: V( p1 O$ ~' J1 j+ L n errCode = - `0 E; M# o8 N. ]- @( K& Z# V4 \/ \
"1002";
. _8 c' {& F$ S WriteErrorLog(errCode, ex.Message);
1 y: F% @5 x! X }
* C. }* Q* L1 B+ u) B; U2 C) _
% F2 v1 Z# D8 { WriteErrorLog(errCode,
7 a; l6 A* U$ k$ B0 _8 S "StatusCode:" + response.StatusCode + "\r\n" + this.ExceptionMessage);
3 |) n U' ?- V1 ~3 i0 f this.ExceptionMessage =
: Q$ S* ~4 r2 a2 _ "{ErrorCode:" + errCode + ",ErrorObject:{" + this.ExceptionMessage + "}}";) [" a" i# _; i! ` U1 _9 W5 J1 r
return 3 R' r8 F% g( f3 `6 T% I
null;
A* c. t% H8 o+ y( ~ }+ T2 t0 \1 D2 B$ k. r1 d, y0 |
returnawait response.Content.ReadAsAsync();1 p1 i4 Y# j# X& s8 N8 h
}5 b) i( }3 I3 F
H1 \0 |/ S2 v# K
catch (AggregateException agex)! }; J0 U/ e% ^' B* I
{
# @1 M) R; n8 u2 P string errMsg = "";9 W6 A ^" K5 R( k0 I
foreach ' ~5 F# u" a4 X m
(var ex in agex.InnerExceptions)
% |. N1 }* b0 _" n0 \ {
5 I8 R. z/ h4 E+ j errMsg += ex.Message+",";
0 S/ E! U9 \( O& o }
; n) J, w% r5 o% X* S
- e4 R5 L/ M) z: V- Q! y2 p errCode = ! r7 G% r4 S/ s; U" @/ L
"1003";
# _4 t# ^3 t$ @8 c this.ExceptionMessage = errMsg;% N' L8 k8 w8 L
WriteErrorLog(errCode, errMsg);
4 \& P3 M, e- h " S) ?7 V$ |2 v
this.ExceptionMessage = "{ErrorCode:" + errCode + ",ErrorMessage:" + this.ExceptionMessage + "}";: T' N! L4 e, g" ^% P
6 x: S8 [9 f# i' h) q returnnull;* N# f" h9 }4 f3 [
}$ p1 G, o% a9 w3 Q; _- n
catch (Exception ex)
7 a6 N' v5 Y2 Z- _$ o6 |9 ^6 Z7 n {
. [7 t8 s* M9 o; U" t5 }3 @8 J this.ExceptionMessage = ex.Message;
. T+ L7 W6 p) l) J( L2 ? errCode = F$ U- V/ D4 [* ^# D7 p
"1004";
4 S2 l: ?+ `& ~ t WriteErrorLog(errCode, this.ExceptionMessage);; d$ F5 s4 `+ n! O- q+ M$ S
this.ExceptionMessage =
/ L( I2 j+ Y: ^+ _8 X4 W "{ErrorCode:" + errCode + ",ErrorMessage:" + this.ExceptionMessage + "}";
3 u2 n2 u- g( O. \; F returnnull;
8 [: B# K6 G& I' s9 w5 l' ]/ w }
* U4 f- |; Q; e5 l* z } . j. g. v+ z' o# Z; X
方法首先要获取客户端的clientId 和clientSecret 信息,这个信息需要指定到本次请求的Authorization 头信息里面;然后在请求正文里面,指定授权类型,这里应该是"password",再在正文里面添加用户名和密码参数。 6 J- E# u+ M6 u- X {, a" I* C% }
接着,调用HttpClient对象,访问授权服务器的 /api/token ,该地址正是前面介绍的授权服务器项目里面指定的最后,对请求返回的响应结果做复杂的异常处理,得到正确的返回值或者异常结果在本例中,获取的令牌有效期只有1分钟,超过时间就需要刷新令牌:。 # o0 Z8 e3 S& _
////// 使用指定的令牌,直接刷新访问令牌/////////public ) `6 D( h, `) ^9 I
TokenResponse RefreshToken(TokenResponse token)
( `: v9 p+ z; _" U, j0 }- R4 E {: n- f& c7 d' C
this.CurrentToken = token;
3 H% z% n7 p5 o' X6 k2 f q9 z' {2 D! z5 h
return GetToken("refresh_token", token.RefreshToken).Result;
2 _/ q/ }+ B% v! @) ~7 g2 Z# V4 p, e }3.4.2,TokenManager类--令牌的管理由于令牌过期后需要刷新令牌获取新的访问令牌,否则应用使用过期的令牌访问就会出错,因此我们应该在令牌超期之前就检查令牌是否马上到期,在到期之前的前一秒我们就立即刷新令牌,用新的令牌来访问资源服务器;但是刷新令牌可能导致之前一个线程使用的令牌失效,造成访问未授权的问题,毕竟授权服务跟资源服务器分离之后,这个可能性是比较高的,因此我们需要对令牌的使用进行管理,降低发生问题的风险。 # P2 s6 V" z7 A& B' @. C7 u& r
首先看到 PWMIS.OAuth2.Tools.TokenManager 文件的 CreateToken 生成令牌的方法:////// 使用密码模式,给当前用户创建一个访问令牌/// $ t/ \% _' U) d( ?5 j N( F% I
///用户登录密码///验证码/// - l$ O2 J: |# }% N
publicasync Task CreateToken(string password,string validationCode=null)
- Z3 @, ]: Y, W. m! Y8 A {
7 E4 \2 j; s1 V9 d! N OAuthClient oc =
% U: b0 ? p! L5 ~! m! H- w new OAuthClient();2 O. _3 |+ W; e; z
oc.SessionID = this.SessionID;
, r7 d3 P j! r4 F, K. Q) F var tokenRsp= await oc.GetTokenOfPasswardGrantType(
+ t7 q" Z4 m( D this.UserName, password, validationCode);
/ P% F: F1 `6 i# i! b if (tokenRsp != null)& q' L% Y9 E! L$ [
{
5 k6 F; f3 ` W6 z1 M5 l- K UserTokenInfo uti =
. S4 L4 G* t+ l8 a" h+ j5 @# b$ \ new UserTokenInfo(this.UserName, tokenRsp);+ s: v( L3 M" V# k, @: X V. m. O
dictUserToken[this.UserName] = uti;' S7 g' g- I5 b. S7 b1 f
}6 E6 P O6 Q7 t9 w0 m
& J, u' \" |" h! d: D" \ else+ F4 d+ R4 @6 D. j, G4 Q3 F
{
# H/ z8 Q- \# U& k B, ]+ J; m; m this.TokenExctionMessage = oc.ExceptionMessage;. X' ?" @# @4 ?- B3 E
}9 s7 A* O9 @' W7 f c
0 f' {( x( |( t5 p; `/ J8 W
return tokenRsp;
( \& S% }) o: j7 j; a) \" [ }生成的令牌存储在一个字段中,通过登录用户名来获取对应的令牌然后看TakeToken 方法,它首先尝试获取一个当前用户的令牌,如果令牌快过期,就尝试刷新令牌:。 + ]; F* ?; G% N; L0 W1 z
/// /// 取一个访问令牌/// /// 如果没有或者获取令牌失败,返回空public TokenResponse TakeToken()0 o, C, G% A: f% d7 T
{
7 e0 j# o0 L& c u, N( L& A- W
M- U, l3 J# P U! }: l if (dictUserToken.ContainsKey(this.UserName))
9 E& @* C; j; ^) [$ z/ k+ ^ {$ E; J5 e5 E) c4 b
UserTokenInfo uti = dictUserToken[ , S6 z+ U" F4 d0 k5 p
this.UserName];- | [# k$ C2 p$ n- n. w: C/ e
this.OldToken = uti.Token;
' D# C: j" Q/ g
7 u! R$ _0 M9 r4 Z2 m //如果令牌超期,刷新令牌if (DateTime.Now.Subtract(uti.FirstUseTime).TotalSeconds >= uti.Token.expires_in || NeedRefresh)
( ?( e2 O5 K' A8 ^% @- V; P' J$ ^$ M {
, Q" a& P5 F! T+ I, P9 U( }# R lock (uti.SyncObject)7 \2 }; L: c7 g& g% g; u- \$ [
{: r' P! M) r; Y9 f( W
: z2 O( f5 ]) z1 f; F9 P3 L
//防止线程重入,再次判断if (DateTime.Now.Subtract(uti.FirstUseTime).TotalSeconds >= uti.Token.expires_in || NeedRefresh)& S0 X9 v4 B' Z$ [+ d/ P
{) u. C$ O0 m1 Q+ o; O1 S
+ i; ]2 E4 z* p8 I1 y3 q# ^( B3 M
//等待之前的用户使用完令牌再刷新while (uti.UseCount > 0)' h& H! v6 {+ n9 X( k" C7 I
{ i- a6 B( C1 e5 c$ |1 A/ w
* Z' Y. j/ k1 x5 w0 Y if (DateTime.Now.Subtract(uti.LastUseTime).TotalSeconds > 5)
) O, b, g( @+ Q- ^$ H* C {0 z8 M& @# [ E. }2 N p2 ~
4 C) E9 j0 f5 Z/ j- S //如果发出请求超过5秒使用计数还大于0,可以认为资源服务器响应缓慢,最终请求此资源可能会拒绝访问this.TokenExctionMessage = "Resouce Server maybe Request TimeOut."
' n S% S. P& k% u" M1 [% Q. V ;) n& {# f( Y7 k5 y* M" `6 K
OAuthClient.WriteErrorLog("00", "**警告** "+DateTime.Now.ToString()+
2 _! l# D9 p6 Q. S! W ":用户"+this.UserName+" 最近一次使用当前令牌("$ f L$ F6 u1 @2 T$ l. s& H
+uti.Token.AccessToken +")已经超时(10秒),使用次数:"
/ J0 n5 c m8 X +uti.UseCount+",线程ID:"+System.Threading.Thread.CurrentThread.ManagedThreadId+"\r\n**下面将刷新令牌,但可能导致之前还未处理完的资源服务器访问被拒绝访问。
# M1 u; Y1 X4 J7 _+ S ");6 ]! A6 c2 Y! o3 e4 l
break;
/ G) _5 l* j6 S, Q/ X7 U }4 Z$ B) m! q$ L7 c% E, U& o
System.Threading.Thread.Sleep(
6 w9 \6 d# k* V 100);6 R! h/ a3 D. G ]4 Z' `$ c
}. P/ L% h ^& f8 \( V
//刷新令牌try, l/ D* h) M5 R4 E' |7 f$ \# |( I* ~
{( s2 P* b9 W- A0 p4 S( {
OAuthClient oc = new OAuthClient();0 H! A9 V0 F2 g! n; V
9 l, _5 k1 J1 a) D2 s- ~1 j
var newToken = oc.RefreshToken(uti.Token);# S) I: J4 F$ ~8 w# H- B3 P0 Q
if (newToken == null)0 q; P. }" k0 D' x4 @9 Y
. H4 g: p, ^2 a1 l ^5 J. J
throw new Exception("Refresh Token Error:" + oc.ExceptionMessage);7 X0 P. }8 {' Y! g' k2 r
else , {6 s- x" C) g6 `, L
if( string.IsNullOrEmpty( newToken.AccessToken))
5 k; R C8 l2 F. v# H% u throw new Exception(
( U8 {8 E8 \7 v9 c "Refresh Token Error:Empty AccessToken. Other Message:" + oc.ExceptionMessage);6 C; K" H9 m# ^; k
5 h; e3 c2 n9 A0 v4 i" O, E& _# p1 d
uti.ResetToken(newToken);
, K7 A+ ^/ h) o5 Q6 U6 X 9 p; {+ n$ \- D0 f' Y7 T( C
this.TokenExctionMessage = oc.ExceptionMessage;- l1 t' ^. [ V/ ^& E4 D7 b
}
S1 b* i# p+ V7 @ $ V) e$ @5 K) |3 A6 w, r
catch (Exception ex)2 s4 i5 _* g1 d5 w
{8 |# E6 p& h. M f( i& y1 R, i
this.TokenExctionMessage = ex.Message;2 N0 v$ N/ a( K# k2 {; ]; ^
9 I. |3 L: x- U$ E1 I
returnnull;% s9 b x7 e0 ~; b0 z
}1 \' x; V+ p: n, @" u' H/ F
NeedRefresh = false;/ S' v- d2 k/ t
}1 B. P! f1 _4 U8 s. e' S K& u
} 1 ]2 p3 }( J3 {" ?. \5 w0 u
//end lock
( O4 ~' y! I7 W$ u* z% J9 u }
9 G4 G% I; ^9 j+ A: F( e
& r# n1 y0 F- a& l: S3 x this.CurrentUserTokenInfo = uti;
- d; c3 Z; ], @# b2 P uti.BeginUse();
: t3 _: E9 H" u* s4 g2 V; q ! D& H8 g* Q: G" m: ~0 d/ I
//this.CurrentTokenLock.Set();return uti.Token;% b; e8 u) Q% `, o
}2 p/ x+ `0 I( f7 @$ m8 B
else
" L! G/ }# |: \( Z. u {
1 [: r$ A) A1 k' @, {9 w , f: |9 A: \0 w/ i$ ]' M5 O
//throw new Exception(this.UserName+" 还没有访问令牌");this.TokenExctionMessage = "UserNoToken";
9 }. `$ A! P4 K/ W 。
# V; v; P _$ ` d& L' j, i returnnull;
2 ] ^2 Z& R" B) }, D/ ?* w' w }* }$ O; h! n6 m
}有了令牌管理功能,客户端生成和获取一个访问令牌就方便了,下面看看客户端如何来使用它3.5,项目 Demo.OAuth2.Port项目 Demo.OAuth2.Port 在本解决方案里面有3个作用:
# |% J4 S/ d8 W( ]' P- o 提供静态资源的访问,比如调用WebAPI的Vue.js 功能代码;提供后端API路由功能,作为前端所有API访问的网关代理;存储用户的登录票据,关联用户的访问令牌这里我们着重讲解第3点功能,网关代理功能另外详细介绍。 * D ]' M- C5 y. p1 L4 t5 p
在方案中,用户的访问令牌缓存在Port站点的进程中,每当用户登录成功后,就生成一个用户访问令牌跟当前用户票据关联看到项目的控制器 LogonController 的用户登录Action: [。 , \- {: V, w4 w4 X
HttpPost]
8 z8 e9 s4 S3 K4 R8 A4 A# Q+ W [AsyncTimeout(60000)]# x* Y- `- `. f7 e) E
publicasync Task Index(LogonModel model
! l) ^! [4 B7 B; H% m0 D: n. i )
& Y( J4 ^. T2 O* q6 q {: R6 V+ v2 k7 E8 g9 [: ]
LogonResultModel result = new LogonResultModel();
w5 f9 r0 o, q/ t" u
! U: z5 g- [% }3 ? //首先,调用授权服务器,以密码模式获取访问令牌 2 l4 D V6 k& i
//授权服务器会携带用户名和密码到认证服务器去验证用户身份//验证服务器验证通过,授权服务器生成访问令牌给当前站点程序//当前站点标记此用户登录成功,并将访问令牌存储在当前站点的用户会话中//当前用户下次访问别的站点的WebAPI的时候,携带此访问令牌。 1 O) a- W) F- s% |! A) f' N
# Y4 G7 H/ `. f6 f5 x9 c: N. S5 U
. k. y' J+ }5 A- }4 K TokenManager tm = new TokenManager(model.UserName, Session.SessionID);
9 _' q3 x$ U h8 _8 @- |7 ?" N. G) S 2 l" j0 A) W7 e ~+ y3 S
var tokenResponse = await tm.CreateToken(model.Password,model.ValidationCode);
% p) |* h5 s9 j* o4 b# ` if (tokenResponse != _& @; L% K+ {# N, a6 Z
null && !string.IsNullOrEmpty(tokenResponse.AccessToken))
( J9 P6 {8 t; J. V {& I" D O/ D3 s, q: |1 ?: U
result.UserId =
% j x2 T5 `, V5 c- h 123;# o8 s6 |9 `, m$ I ]
result.UserName = model.UserName;
% p$ q5 O7 u5 I3 ^9 M- N7 t result.LogonMessage = "OK";
, K) r, r; e5 R& z M3 K( J
- w* m6 G3 s* s# ]& t7 E6 l /* OWin的方式7 r* x7 S% K. K; a# @( e
ClaimsIdentity identity = new ClaimsIdentity("Basic");
# c9 S9 a D+ [: O, S$ t8 R$ K# ^ identity.AddClaim(new Claim(ClaimTypes.Name, model.UserName));( \# D, ?9 c) Q& M) |& \* ~6 d
ClaimsPrincipal principal = new ClaimsPrincipal(identity);+ F; G' i j1 ?
HttpContext.User = principal;. O8 A# j, r. h# w3 B
*/
: c5 E2 w, [' y: `4 _0 o+ b
- y5 u5 C4 z: M% [# }7 k. Q FormsAuthentication.SetAuthCookie(model.UserName, false);* e/ Z: C$ U6 w
}
9 r6 {' {5 _: w9 X+ P% W
# M/ \- [# U1 X; n& i0 `9 V else8 ^8 O+ a6 O4 x1 k# \
{% H, }( @1 r3 X4 a# Y
result.LogonMessage = tm.TokenExctionMessage;( F' X- x% g% H/ F: _. f
}
3 `$ j$ T( Z* ~6 Q7 \! k- W3 k " F" K% Z u" e1 b. O6 A
return Json(result);% I7 t/ v; E+ z0 @
}Port站点作为授权服务器的客户端,需要配置客户端信息,看到Web.config文件的配置:
9 d$ g/ w; ]" n) e value="3.0.0.0" /> 9 t! K* u3 V8 a
/> ; T% X! w; {1 j0 j" J
="PWMIS.OAuth2.Port"/>
6 z6 g8 n7 w5 c1 c' j value="http://localhost:60186"/>, v( `: M8 V- n2 l
# @1 j6 H5 _4 H3 o8 s6 @& x
. ]4 K. M* U3 j# r# c2 J- j
$ S" b% b" i% a `( B
0 \( |: X1 A( @% X* @( t2 F |