0%

C#搭建Oauth2.0认证流程以及代码示例 - WebEnh - 博客园

Excerpt

我认为对于一个普遍问题,必有对应的一个简洁优美的解决方案。当然这也许只是我的一厢情愿,因为根据宇宙法则,所有事物总归趋于混沌,而OAuth协议就是混沌中的产物,不管是1.0、1.0a还是2.0,单看版本号就让人神伤。 对接过各类开放平台的朋友对OAuth应该不会陌生。当年我小试了下淘宝API,各种t


<2024年9月>
1234567
891011121314
15161718192021
22232425262728
293012345
6789101112

C#搭建Oauth2.0认证流程以及代码示例

Posted on 2017-06-27 11:22  WebEnh  阅读(19363)  评论()  编辑  收藏  举报

复制代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
我认为对于一个普遍问题,必有对应的一个简洁优美的解决方案。当然这也许只是我的一厢情愿,因为根据宇宙法则,所有事物总归趋于混沌,而OAuth协议就是混沌中的产物,不管是1.<span>0</span>、<span>1</span>.0a还是2.<span>0</span><span>,单看版本号就让人神伤。


对接过各类开放平台的朋友对OAuth应该不会陌生。当年我小试了下淘宝API,各种token、key、secret、code、id,让我眼花缭乱,不明所以,虽然最终调通,但那种照猫画虎的感觉颇不好受。最近公司计划,开放接口的授权协议从1.0升到2.</span><span>0</span><span>,这个任务不巧就落在了我的头上。


声明:我并没有认真阅读过OAuth2.0协议规范,本文对OAuth2.0的阐述或有不当之处,请谅解。本文亦不保证叙述的正确性,欢迎指正。


OAuth2.0包含四种角色:


用户,又叫资源所有者
客户端,俗称第三方商户
授权服务端,颁发AccessToken
资源服务端,根据AccessToken开放相应的资源访问权限
本文涉及到三种授权模式:


Authorization Code模式:这是现在互联网应用中最常见的授权模式。客户端引导用户在授权服务端输入凭证获取用户授权(AccessToken),进而访问用户资源。需要注意的是,在用户授权后,授权服务端先回传客户端授权码,然后客户端再使用授权码换取AccessToken。为什么不直接返回AccessToken呢?主要是由于用户授权后,授权服务端重定向到客户端地址,此时数据只能通过QueryString方式向客户端传递,在地址栏URL中可见,不安全,于是分成了两步,第二步由客户端主动请求获取最终的令牌。
Client Credentials Flow:客户端乃是授权服务端的信任合作方,不需要用户参与授权,事先就约定向其开放指定资源(不特定于用户)的访问权限。客户端通过证书或密钥(或其它约定形式)证明自己的身份,获取AccessToken,用于后续访问。
Username and Password Flow:客户端被用户和授权服务端高度信任,用户直接在客户端中输入用户名密码,然后客户端传递用户名密码至授权服务端获取AccessToken,便可访问相应的用户资源。这在内部多系统资源共享、同源系统资源共享等场景下常用,比如单点登录,在登录时就获取了其它系统的AccessToken,避免后续授权,提高了用户体验。
上述模式涉及到三类凭证:


AuthorizationCode:授权码,授权服务端和客户端之间传输。
AccessToken:访问令牌,授权服务端发给客户端,客户端用它去到资源服务端请求资源。
RefreshToken:刷新令牌,授权服务端和客户端之间传输。
对客户端来说,授权的过程就是获取AccessToken的过程。


总的来说,OAuth并没有新鲜玩意,仍是基于加密、证书诸如此类的技术,在OAuth出来之前,这些东东就已经被大伙玩的差不多了。OAuth给到我们的最大好处就是统一了流程标准,一定程度上促进了互联网的繁荣。


我接到任务后,本着善假于物的理念,先去网上搜了一遍,原本以为有很多资源,结果只搜到DotNetOpenAuth这个开源组件。更让人失望的是,官方API文档没找到(可能是我找的姿势不对,有知道的兄弟告知一声),网上其它资料也少的可怜,其间发现一篇 OAuth2学习及DotNetOpenAuth部分源码研究 ,欣喜若狂,粗粗浏览一遍,有收获,却觉得该组件未免过于繁杂(由于时间紧迫,我并没有深入研究,只是当前观点)。DotNetOpenAuth包含OpenID、OAuth1.</span><span>0</span>[a]/<span>2.0</span><span>,自带的例子有几处暗坑,不易(能)调通。下面介绍我在搭建基于该组件的OAuth2.0授权框架时的一些心得体会。


本文介绍的DotNetOpenAuth乃是对应.Net4.0的版本。


授权服务端


授权服务端交道打的最多的就是客户端,于是定义一个Client类,实现DotNetOpenAuth.OAuth2.IClientDescription接口,下面我们来看IClientDescription的定义:


</span><span>public</span> <span>interface</span><span> IClientDescription {


Uri DefaultCallback { </span><span>get</span><span>; }


</span><span>//</span><span>0:有secret 1:没有secret</span>
ClientType ClientType { <span>get</span><span>; }


</span><span>//</span><span>该client的secret是否为空</span>
<span>bool</span> HasNonEmptySecret { <span>get</span><span>; }


</span><span>//</span><span>检查传入的callback与该client的callback是否一致</span>
<span>bool</span><span> IsCallbackAllowed(Uri callback);


</span><span>//</span><span>检查传入的secret与该client的secret是否一致</span>
<span>bool</span> IsValidClientSecret(<span>string</span><span> secret);
}
其中隐含了许多信息。DefaultCallback表示客户端的默认回调地址(假如有的话),在接收客户端请求时,使用IsCallbackAllowed判断回调地址是否合法(比如查看该次回调地址和默认地址是否属于同一个域),过滤其它应用的恶意请求。若ClientType 为0,则表示客户端需持密钥(secret)表明自己的身份,授权服务端可以据此赋予此类客户端相对更多的权限。自定义的Client类一般需要多定义一个ClientSecret属性。DefaultCallback和ClientSecret在下文常有涉及。


DotNetOpenAuth预定义了一个接口——IAuthorizationServerHost,这是个重要的接口,定义如下:


</span><span>public</span> <span>interface</span><span> IAuthorizationServerHost
{
ICryptoKeyStore CryptoKeyStore { </span><span>get</span><span>; }
INonceStore NonceStore { </span><span>get</span><span>; }


AutomatedAuthorizationCheckResponse CheckAuthorizeClientCredentialsGrant(IAccessTokenRequest accessRequest);
AutomatedUserAuthorizationCheckResponse CheckAuthorizeResourceOwnerCredentialGrant(</span><span>string</span> userName, <span>string</span><span> password, IAccessTokenRequest accessRequest);
AccessTokenResult CreateAccessToken(IAccessTokenRequest accessTokenRequestMessage);
IClientDescription GetClient(</span><span>string</span><span> clientIdentifier);
</span><span>bool</span><span> IsAuthorizationValid(IAuthorizationDescription authorization);
}
简单地说,CryptoKeyStore用于存取对称加密密钥,用于授权码和刷新令牌的加密,由于客户端不需要对它们进行解密,所以密钥只存于授权服务端;关于AccessToken的传输则略有不同,关于这点我们待会说。理解NonceStore 属性需要知道 N once和 Timestamp的概念,Nonce与消息合并加密可防止重放攻击,Timestamp是为了避免可能的Nonce重复问题,也将一同参与加密,具体参看 nonce和timestamp在Http安全协议中的作用 ;这项技术放在这里主要是为了确保一个授权码只能被使用一次。 CheckAuthorizeClientCredentialsGrant方法在客户端凭证模式下使用,CheckAuthorizeResourceOwnerCredentialGrant在用户名密码模式下使用,经测试,IsAuthorizationValid方法只在授权码模式下被调用,这三个方法的返回值标示是否通过授权。


当授权通过后,通过CreateAccessToken生成AccessToken并返回给客户端,客户端于是就可以用AccessToken访问资源服务端了。那当资源服务端接收到AccessToken时,需要做什么工作呢?首先,它要确认这个AccessToken是由合法的授权服务端颁发的,否则,攻击者就能使用DotNetOpenAuth另外建一个授权服务端,后果可想而知。说到身份认证,最成熟的就是RSA签名技术,即授权服务端私钥对AccessToken签名,资源服务端接收后使用授权服务端的公钥验证。我们还可以使用 资源服务器公</span>/<span>私钥对来加解密AccessToken(签名在加密后),这对于OAuth2.0来说没任何意义,而是为OAuth1.0服务的。


</span><span>public</span><span> AccessTokenResult CreateAccessToken(IAccessTokenRequest accessTokenRequestMessage)
{
</span><span>var</span> accessToken = <span>new</span><span> AuthorizationServerAccessToken();
</span><span>int</span> minutes = <span>0</span><span>;
</span><span>string</span> setting = ConfigurationManager.AppSettings[<span>"</span><span>AccessTokenLifeTime</span><span>"</span><span>];
minutes </span>= <span>int</span>.TryParse(setting, <span>out</span> minutes) ? minutes : <span>10</span>;<span>//</span><span>10分钟</span>
accessToken.Lifetime =<span> TimeSpan.FromMinutes(minutes);


</span><span>//</span><span>这里设置加密公钥
</span><span>//</span><span>accessToken.ResourceServerEncryptionKey = new RSACryptoServiceProvider();
</span><span>//</span><span>accessToken.ResourceServerEncryptionKey.ImportParameters(ResourceServerEncryptionPublicKey);


</span><span>//</span><span>签名私钥,这是必须的(在后续版本中可以设置accessToken.SymmetricKeyStore替代)</span>
accessToken.AccessTokenSigningKey =<span> CreateRSA();


</span><span>var</span> result = <span>new</span><span> AccessTokenResult(accessToken);
</span><span>return</span><span> result;
}
前面说了,所有授权模式都是为了获取AccessToken,授权码模式和用户名密码模式还有个RefreshToken,当然授权码模式独有Authorization Code。一般来说,这三个东西,对于客户端是一个经过加密编码的字符串,对于服务端是可序列化的对象,存储相关授权信息。需要注意的是客户端证书模式没有RefreshToken,这是为什么呢?我们不妨想想为什么授权码模式和用户名密码模式有个RefreshToken,或者说RefreshToken的作用是什么。以下是我个人推测:


首先要明确,AccessToken一般是不会永久有效的。因为,AccessToken并没有承载可以验证客户端身份的完备信息,并且资源服务端也不承担验证客户端身份的职责,一旦AccessToken被他人获取,那么就有可能被恶意使用。失效机制有效减少了产生此类事故可能造成的损失。当AccessToken失效后,需要重新获取。对于授权码模式和用户名密码模式来说,假如没有RefreshToken,就意味这需要用户重新输入用户名密码进行再次授权。如果AccessToken有效期够长,比如几天,倒不觉得有何不妥,有些敏感应用只设置数分钟,就显得不够人性化了。为了解决这个问题,引入RefreshToken,它会在AccessToken失效后,在不需要用户参与的情况下,重新获取新的AccessToken,这里有个前提就是RefreshToken的有效期(如果有的话)要比AccessToken长,可设为永久有效。那么,RefreshToken泄露了会带来问题吗?答案是不会,除非你同时泄露了客户端身份凭证。需要同时具备RefreshToken和客户端凭证信息,才能获取新的AccessToken,我们甚至可以将旧的AccessToken当作RefreshToken。同理可推,由于不需要用户参与授权,在客户端证书模式下,客户端在AccessToken失效后只需提交自己的身份凭证重新请求新AccessToken即可,根本不需要RefreshToken。


授权码模式,用户授权后(此时并生成返回AccessToken,而是返回授权码),授权服务端要保存相关的授权信息,为此定义一个ClientAuthorization类:


</span><span>public</span> <span>class</span><span> ClientAuthorization
{
</span><span>public</span> <span>int</span> ClientId { <span>get</span>; <span>set</span><span>; }


</span><span>public</span> <span>string</span> UserId { <span>get</span>; <span>set</span><span>; }


</span><span>public</span> <span>string</span> Scope { <span>get</span>; <span>set</span><span>; }


</span><span>public</span> DateTime? ExpirationDateUtc { <span>get</span>; <span>set</span><span>; }
}
ClientId和UserId就不说了,Scope是授权范围,可以是一串Uri,也可以是其它标识,只要后台代码能通过它来判断待访问资源是否属于授权范围即可。ExpirationDateUtc乃是授权过期时间,即当该时间到期后,需要用户重新授权(有RefreshToken)也没用,为null表示永不过期。


资源服务端


在所有的授权模式下,资源服务端都只专注一件和OAuth相关的事情——验证AccessToken。这个步骤相对来说就简单很多,以Asp.net WebAPI为例。在此之前建议对Asp.net WebAPI消息拦截机制不熟悉的朋友浏览一遍 ASP.NET Web API之消息[拦截]处理 。这里我们新建一个继承自DelegatingHandler的类作为例子:


</span><span>public</span> <span>class</span><span> BearerTokenHandler : DelegatingHandler
{
</span><span>///</span> <span>&lt;summary&gt;</span>
<span>///</span><span> 验证访问令牌合法性,由授权服务器私钥签名,资源服务器通过对应的公钥验证
</span><span>///</span> <span>&lt;/summary&gt;</span>
<span>private</span> <span>static</span> <span>readonly</span> RSAParameters AuthorizationServerSigningPublicKey = <span>new</span> RSAParameters();<span>//</span><span>just a 例子</span>


<span>private</span><span> RSACryptoServiceProvider CreateAuthorizationServerSigningServiceProvider()
{
</span><span>var</span> authorizationServerSigningServiceProvider = <span>new</span><span> RSACryptoServiceProvider();
authorizationServerSigningServiceProvider.ImportParameters(AuthorizationServerSigningPublicKey);
</span><span>return</span><span> authorizationServerSigningServiceProvider;
}


</span><span>protected</span> <span>override</span> Task&lt;HttpResponseMessage&gt;<span> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
</span><span>if</span> (request.Headers.Authorization != <span>null</span><span>)
{
</span><span>if</span> (request.Headers.Authorization.Scheme == <span>"</span><span>Bearer</span><span>"</span><span>)
{
</span><span>var</span> resourceServer = <span>new</span> ResourceServer(<span>new</span> StandardAccessTokenAnalyzer(<span>this</span>.CreateAuthorizationServerSigningServiceProvider(), <span>null</span><span>));
</span><span>var</span> principal = resourceServer.GetPrincipal(request);<span>//</span><span>可以在此传入待访问资源标识参与验证</span>
HttpContext.Current.User =<span> principal;
Thread.CurrentPrincipal </span>=<span> principal;
}
}


</span><span>return</span> <span>base</span><span>.SendAsync(request, cancellationToken);
}
}
需要注意,AccessToken乃是从头信息Authorization获取,格式为“Bearer:AccessToken”,在下文“ 原生方式获取AccessToken ”中有进一步描述。ResourceServer.GetPrincipal方法使用授权服务端的公钥验证AccessToken的合法性,同时解密AccessToken,若传入参数有scope,则还会判断scope是否属于授权范围内,通过后将会话标识赋给当前会话,该会话标识乃是当初用户授权时的用户信息,这样就实现了用户信息的传递。一般来说若返回的principal为null,就可以不必执行后续逻辑了。


客户端


可以认为DotNetOpenAuth.OAuth2.Client是DotNetOpenAuth给C#客户端提供的默认SDK。我们以授权码模式为例。先声明一个IAuthorizationState接口对象,IAuthorizationState接口是用来保存最终换取AccessToken成功后授权服务端返回的信息,其部分定义如下:


</span><span>public</span> <span>interface</span><span> IAuthorizationState {
Uri Callback { </span><span>get</span>; <span>set</span><span>; }
</span><span>string</span> RefreshToken { <span>get</span>; <span>set</span><span>; }
</span><span>string</span> AccessToken { <span>get</span>; <span>set</span><span>; }
DateTime</span>? AccessTokenIssueDateUtc { <span>get</span>; <span>set</span><span>; }
DateTime</span>? AccessTokenExpirationUtc { <span>get</span>; <span>set</span><span>; }
HashSet</span>&lt;<span>string</span>&gt; Scope { <span>get</span><span>; }
}
AccessTokenExpirationUtc是AccessToken过期时间,以Utc时间为准。若该对象为null,则表示尚未授权,我们需要去授权服务端请求。


</span><span>private</span> <span>static</span> AuthorizationServerDescription _authServerDescription = <span>new</span><span> AuthorizationServerDescription
{
TokenEndpoint </span>= <span>new</span><span> Uri(MvcApplication.TokenEndpoint),
AuthorizationEndpoint </span>= <span>new</span><span> Uri(MvcApplication.AuthorizationEndpoint),
};


</span><span>private</span> <span>static</span> WebServerClient _client = <span>new</span> WebServerClient(_authServerDescription, <span>"</span><span>democlient</span><span>"</span>, <span>"</span><span>samplesecret</span><span>"</span><span>);


[HttpPost]
</span><span>public</span><span> ActionResult Index()
{
</span><span>if</span> (Authorization == <span>null</span><span>)
{
</span><span>return</span><span> _client.PrepareRequestUserAuthorization().AsActionResult();
}
</span><span>return</span><span> View();
}
AuthorizationServerDescription包含两个属性,AuthorizationEndpoint是用户显式授权的地址,一般即用户输用户名密码的地;TokenEndpoint是用授权码换取AccessToken的地址,注意该地址须用POST请求。“democlient”和“samplesecret”是示例用的客户端ID和客户端Secret。WebServerClient.PrepareRequestUserAuthorization方法将会首先返回code和state到当前url,以querystring的形式(若用户授权的话)。


code即是授权码,state参数不好理解,这涉及到CSRF,可参看浅谈CSRF攻击方式,state就是为了预防CSRF而引入的随机数。客户端生成该值,将其附加到state参数的同时,存入用户Cookie中,用户授权完毕后,该参数会同授权码一起返回到客户端,然后客户端将其值同Cookie中的值比较,若一样则表示该次授权为当前用户操作,视为有效。由于不同域的cookie无法共享,因此其它站点并不能知道state的确切的值,CSRF攻击也就无从谈起了。简单地说,state参数起到一个标示消息是否合法的作用。结合获取授权码这步来说,授权服务端返回的url为 http:</span><span>//</span><span>localhost:22187/?code=xxxxxxxxx&amp;state=_PzGpfJzyQI9DkdoyWeWr 格式,若忽略state,那么攻击方将code替换成自己的授权码,最终客户端获取的AccessToken是攻击方的AccessToken,由于AccessToken同用户关联,也就是说,后续客户端做的其实是另一个用户资源(也许是攻击方注册的虚拟用户),如果操作中包括新增或更新,那么真实用户信息就会被攻击方获取到。可参看 OAuth2 Cross Site Request Forgery, and state parameter 。</span>
<span>

有了code就可以去换取AccessToken了:


</span><span>public</span> ActionResult Index(<span>string</span> code,<span>string</span><span> state)
{
</span><span>if</span> (!<span>string</span>.IsNullOrEmpty(code) &amp;&amp; !<span>string</span><span>.IsNullOrEmpty(state))
{
</span><span>var</span> authorization =<span> _client.ProcessUserAuthorization(Request);
Authorization </span>=<span> authorization;
</span><span>return</span><span> View(authorization);
}
</span><span>return</span><span> View();
}
如前所述,Authorization不为null即表示整个授权流程成功完成。然后就可以用它来请求资源了。


</span><span>public</span><span> ActionResult Invoke()
{
</span><span>var</span> request = <span>new</span> HttpRequestMessage(<span>new</span> HttpMethod(<span>"</span><span>GET</span><span>"</span>), <span>"</span><span>http://demo.openapi.cn/bookcates</span><span>"</span><span>);
</span><span>using</span> (<span>var</span> httpClient = <span>new</span><span> HttpClient(_client.CreateAuthorizingHandler(Authorization)))
{
</span><span>using</span> (<span>var</span> resourceResponse =<span> httpClient.SendAsync(request))
{
ViewBag.Result </span>=<span> resourceResponse.Result.Content.ReadAsStringAsync().Result;
}
}
</span><span>return</span><span> View(Authorization);
}
WebServerClient.CreateAuthorizingHandler方法返回一个DelegatingHandler,主要用来当AccessToken过期时,使用RefreshToken刷新换取新的AccessToken;并设置Authorization头信息,下文有进一步说明。


原生方式获取AccessToken


既然是开放平台,面对的客户端种类自然多种多样,DotNetOpenAuth.OAuth2.Client显然就不够用了,我也不打算为了这个学遍所有程序语言。所幸OAuth基于http,不管任何语言开发的客户端,获取AccessToken的步骤本质上就是提交http请求和接收http响应的过程,客户端SDK只是将这个过程封装得更易用一些。下面就让我们以授权码模式为例,一窥究竟。


参照前述事例,当我们第一次(新的浏览器会话)在客户端点击“请求授权”按钮后,会跳转到授权服务端的授权界面。


可以看到,url中带了client_id、redirect_uri、state、response_type四个参数,若要请求限定的授权范围,还可以传入scope参数。其中response_type设为code表示请求的是授权码。


</span><span>1</span> <span>private</span> <span>string</span> GetNonCryptoRandomDataAsBase64(<span>int</span><span> binaryLength)
</span><span>2</span><span> {
</span><span>3</span> <span>byte</span>[] buffer = <span>new</span> <span>byte</span><span>[binaryLength];
</span><span>4</span><span> _random.NextBytes(buffer);
</span><span>5</span> <span>string</span> uniq =<span> Convert.ToBase64String(buffer);
</span><span>6</span> <span>return</span><span> uniq;
</span><span>7</span><span> }
</span><span>8</span>
<span>9</span> <span>public</span><span> ActionResult DemoRequestCode()
</span><span>10</span><span> {
</span><span>11</span> <span>string</span> xsrfKey = <span>this</span>.GetNonCryptoRandomDataAsBase64(<span>16</span>);<span>//</span><span>生成随机数</span>
<span>12</span> <span>string</span> url = MvcApplication.AuthorizationEndpoint + <span>"</span><span>?</span><span>"</span> +
<span>13</span> <span>string</span>.Format(<span>"</span><span>client_id={0}&amp;redirect_uri={1}&amp;response_type={2}&amp;state={3}</span><span>"</span><span>,
</span><span>14</span> <span>"</span><span>democlient</span><span>"</span>, <span>"</span><span>http://localhost:22187/</span><span>"</span>, <span>"</span><span>code</span><span>"</span><span>, xsrfKey);
</span><span>15</span> HttpCookie xsrfKeyCookie = <span>new</span><span> HttpCookie(XsrfCookieName, xsrfKey);
</span><span>16</span> xsrfKeyCookie.HttpOnly = <span>true</span><span>;
</span><span>17</span> xsrfKeyCookie.Secure =<span> FormsAuthentication.RequireSSL;
</span><span>18</span><span> Response.Cookies.Add(xsrfKeyCookie);
</span><span>19</span>
<span>20</span> <span>return</span><span> Redirect(url);
</span><span>21</span><span> }
授权码返回后,先检查state参数,若通过则换取AccessToken:


</span><span>private</span> <span>bool</span> VerifyState(<span>string</span><span> state)
{
</span><span>var</span> cookie =<span> Request.Cookies[XsrfCookieName];
</span><span>if</span> (cookie == <span>null</span><span>)
</span><span>return</span> <span>false</span><span>;


</span><span>var</span> xsrfCookieValue =<span> cookie.Value;
</span><span>return</span> xsrfCookieValue ==<span> state;
}


</span><span>private</span><span> AuthenticationHeaderValue SetAuthorizationHeader()
{
</span><span>string</span> concat = <span>"</span><span>democlient:samplesecret</span><span>"</span><span>;
</span><span>byte</span>[] bits =<span> Encoding.UTF8.GetBytes(concat);
</span><span>string</span> base64 =<span> Convert.ToBase64String(bits);
</span><span>return</span> <span>new</span> AuthenticationHeaderValue(<span>"</span><span>Basic</span><span>"</span><span>, base64);
}


</span><span>public</span> ActionResult Demo(<span>string</span> code, <span>string</span><span> state)
{
</span><span>if</span> (!<span>string</span>.IsNullOrEmpty(code) &amp;&amp; !<span>string</span>.IsNullOrEmpty(state) &amp;&amp;<span> VerifyState(state))
{
</span><span>var</span> httpClient = <span>new</span><span> HttpClient();
</span><span>var</span> httpContent = <span>new</span> FormUrlEncodedContent(<span>new</span> Dictionary&lt;<span>string</span>, <span>string</span>&gt;<span>()
{
{</span><span>"</span><span>code</span><span>"</span><span>, code},
{</span><span>"</span><span>redirect_uri</span><span>"</span>, <span>"</span><span>http://localhost:22187/</span><span>"</span><span>},
{</span><span>"</span><span>grant_type</span><span>"</span>,<span>"</span><span>authorization_code</span><span>"</span><span>}
});
httpClient.DefaultRequestHeaders.Authorization </span>= <span>this</span><span>.SetAuthorizationHeader();


</span><span>var</span> response =<span> httpClient.PostAsync(MvcApplication.TokenEndpoint, httpContent).Result;
Authorization </span>= response.Content.ReadAsAsync&lt;AuthorizationState&gt;<span>().Result;
</span><span>return</span><span> View(Authorization);
}
</span><span>return</span><span> View();
}
如上所示,以Post方式提交,三个参数,code即是授权码,redirect_uri和获取授权码时传递的redirect_uri要保持一致,grant_type设置为“authorization_code”。注意 SetAuthorizationHeader方法 ,需要设置请求头的Authorization属性,key为“Basic”,值为以Base64编码的“客户端ID:客户端Secret”字符串, 至于为何要如此规定,暂时没有探究 。 成功后返回的信息可以转为前面说的IAuthorizationState接口对象。


如前所述,当AccessToken过期后,需要用RefreshToken刷新。


</span><span>private</span> <span>void</span><span> RefreshAccessToken()
{
</span><span>var</span> httpClient = <span>new</span><span> HttpClient();
</span><span>var</span> httpContent = <span>new</span> FormUrlEncodedContent(<span>new</span> Dictionary&lt;<span>string</span>, <span>string</span>&gt;<span>()
{
{</span><span>"</span><span>refresh_token</span><span>"</span><span>, Authorization.RefreshToken},
{</span><span>"</span><span>grant_type</span><span>"</span>,<span>"</span><span>refresh_token</span><span>"</span><span>}
});
httpClient.DefaultRequestHeaders.Authorization </span>= <span>this</span><span>.SetAuthorizationHeader();


</span><span>var</span> response =<span> httpClient.PostAsync(MvcApplication.TokenEndpoint, httpContent).Result;
Authorization </span>= response.Content.ReadAsAsync&lt;AuthorizationState&gt;<span>().Result;
}
其中grant_type须设置为”refresh_token”,请求头信息设置同前。


获取AccessToken后, 就可以用于访问用户资源了。


</span><span>public</span><span> ActionResult DemoInvoke()
{
</span><span>var</span> httpClient = <span>new</span><span> HttpClient();
</span><span>if</span> (<span>this</span>.Authorization.AccessTokenExpirationUtc.HasValue &amp;&amp; <span>this</span>.Authorization.AccessTokenExpirationUtc.Value &lt;<span> DateTime.UtcNow)
{
</span><span>this</span><span>.RefreshAccessToken();
}
</span><span>var</span> bearerToken = <span>this</span><span>.Authorization.AccessToken;


httpClient </span>= <span>new</span><span> HttpClient();
httpClient.DefaultRequestHeaders.Authorization </span>= <span>new</span> AuthenticationHeaderValue(<span>"</span><span>Bearer</span><span>"</span><span>, bearerToken);
</span><span>var</span> request = <span>new</span> HttpRequestMessage(<span>new</span> HttpMethod(<span>"</span><span>GET</span><span>"</span>), <span>"</span><span>http://demo.openapi.cn/bookcates</span><span>"</span><span>);
</span><span>using</span> (<span>var</span> resourceResponse =<span> httpClient.SendAsync(request))
{
ViewBag.Result </span>=<span> resourceResponse.Result.Content.ReadAsStringAsync().Result;
}
</span><span>return</span><span> View(Authorization);
}
用法很简单, Authorization 请求头,key设为“Bearer”,值为AccessToken即可。</span>

复制代码