03207d5d
wwk
1
|
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
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
|
using NCC.Authorization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace NCC.DataEncryption
{
/// <summary>
/// JWT 加解密
/// </summary>
public class JWTEncryption
{
/// <summary>
/// 生成 Token
/// </summary>
/// <param name="payload"></param>
/// <param name="expiredTime">过期时间(分钟)</param>
/// <returns></returns>
public static string Encrypt(IDictionary<string, object> payload, long? expiredTime = null)
{
var (Payload, JWTSettings) = CombinePayload(payload, expiredTime);
return Encrypt(JWTSettings.IssuerSigningKey, Payload, JWTSettings.Algorithm);
}
/// <summary>
/// 生成 Token
/// </summary>
/// <param name="issuerSigningKey"></param>
/// <param name="payload"></param>
/// <param name="algorithm"></param>
/// <returns></returns>
public static string Encrypt(string issuerSigningKey, IDictionary<string, object> payload, string algorithm = SecurityAlgorithms.HmacSha256)
{
return Encrypt(issuerSigningKey, JsonSerializer.Serialize(payload), algorithm);
}
/// <summary>
/// 生成 Token
/// </summary>
/// <param name="issuerSigningKey"></param>
/// <param name="payload"></param>
/// <param name="algorithm"></param>
/// <returns></returns>
public static string Encrypt(string issuerSigningKey, string payload, string algorithm = SecurityAlgorithms.HmacSha256)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(issuerSigningKey));
var credentials = new SigningCredentials(securityKey, algorithm);
var tokenHandler = new JsonWebTokenHandler();
return tokenHandler.CreateToken(payload, credentials);
}
/// <summary>
/// 生成刷新 Token
/// </summary>
/// <param name="accessToken"></param>
/// <param name="expiredTime">刷新 Token 有效期(分钟)</param>
/// <returns></returns>
public static string GenerateRefreshToken(string accessToken, int expiredTime = 43200)
{
// 分割Token
var tokenParagraphs = accessToken.Split('.', StringSplitOptions.RemoveEmptyEntries);
var s = RandomNumberGenerator.GetInt32(10, tokenParagraphs[1].Length / 2 + 2);
var l = RandomNumberGenerator.GetInt32(3, 13);
var payload = new Dictionary<string, object>
{
{ "f",tokenParagraphs[0] },
{ "e",tokenParagraphs[2] },
{ "s",s },
{ "l",l },
{ "k",tokenParagraphs[1].Substring(s,l) }
};
return Encrypt(payload, expiredTime);
}
/// <summary>
/// 通过过期Token 和 刷新Token 换取新的 Token
/// </summary>
/// <param name="expiredToken"></param>
/// <param name="refreshToken"></param>
/// <param name="expiredTime">过期时间(分钟)</param>
/// <param name="clockSkew">刷新token容差值,秒做单位</param>
/// <returns></returns>
public static string Exchange(string expiredToken, string refreshToken, long? expiredTime = null, long clockSkew = 5)
{
// 交换刷新Token 必须原Token 已过期
var (_isValid, _, _) = Validate(expiredToken);
if (_isValid) return default;
// 判断刷新Token 是否过期
var (isValid, refreshTokenObj, _) = Validate(refreshToken);
if (!isValid) return default;
// 解析 HttpContext
var httpContext = GetCurrentHttpContext();
// 判断这个刷新Token 是否已刷新过
var blacklistRefreshKey = "BLACKLIST_REFRESH_TOKEN:" + refreshToken;
var distributedCache = httpContext?.RequestServices?.GetService<IDistributedCache>();
// 处理token并发容错问题
var nowTime = DateTimeOffset.UtcNow;
var cachedValue = distributedCache?.GetString(blacklistRefreshKey);
var isRefresh = !string.IsNullOrWhiteSpace(cachedValue); // 判断是否刷新过
if (isRefresh)
{
var refreshTime = new DateTimeOffset(long.Parse(cachedValue), TimeSpan.Zero);
// 处理并发时容差值
if ((nowTime - refreshTime).TotalSeconds > clockSkew) return default;
}
// 分割过期Token
var tokenParagraphs = expiredToken.Split('.', StringSplitOptions.RemoveEmptyEntries);
if (tokenParagraphs.Length < 3) return default;
// 判断各个部分是否匹配
if (!refreshTokenObj.GetPayloadValue<string>("f").Equals(tokenParagraphs[0])) return default;
if (!refreshTokenObj.GetPayloadValue<string>("e").Equals(tokenParagraphs[2])) return default;
if (!tokenParagraphs[1].Substring(refreshTokenObj.GetPayloadValue<int>("s"), refreshTokenObj.GetPayloadValue<int>("l")).Equals(refreshTokenObj.GetPayloadValue<string>("k"))) return default;
// 获取过期 Token 的存储信息
var oldToken = ReadJwtToken(expiredToken);
var payload = oldToken.Claims.Where(u => !StationaryClaimTypes.Contains(u.Type))
.ToDictionary(u => u.Type, u => (object)u.Value, new MultiClaimsDictionaryComparer());
// 交换成功后登记刷新Token,标记失效
if (!isRefresh)
{
distributedCache?.SetString(blacklistRefreshKey, nowTime.Ticks.ToString(), new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.FromUnixTimeSeconds(refreshTokenObj.GetPayloadValue<long>(JwtRegisteredClaimNames.Exp))
});
}
return Encrypt(payload, expiredTime);
}
/// <summary>
/// 自动刷新 Token 信息
/// </summary>
/// <param name="context"></param>
/// <param name="httpContext"></param>
/// <param name="expiredTime">新 Token 过期时间(分钟)</param>
/// <param name="refreshTokenExpiredTime">新刷新 Token 有效期(分钟)</param>
/// <param name="tokenPrefix"></param>
/// <param name="clockSkew"></param>
/// <returns></returns>
public static bool AutoRefreshToken(AuthorizationHandlerContext context, DefaultHttpContext httpContext, long? expiredTime = null, int refreshTokenExpiredTime = 43200, string tokenPrefix = "Bearer ", long clockSkew = 5)
{
// 如果验证有效,则跳过刷新
if (context.User.Identity.IsAuthenticated) return true;
// 判断是否含有匿名特性
if (httpContext.GetEndpoint()?.Metadata?.GetMetadata<AllowAnonymousAttribute>() != null) return true;
// 获取过期Token 和 刷新Token
var expiredToken = GetJwtBearerToken(httpContext, tokenPrefix: tokenPrefix);
var refreshToken = GetJwtBearerToken(httpContext, "X-Authorization", tokenPrefix: tokenPrefix);
if (string.IsNullOrWhiteSpace(expiredToken) || string.IsNullOrWhiteSpace(refreshToken)) return false;
// 交换新的 Token
var accessToken = Exchange(expiredToken, refreshToken, expiredTime, clockSkew);
if (string.IsNullOrWhiteSpace(accessToken)) return false;
// 读取新的 Token Clamis
var claims = ReadJwtToken(accessToken)?.Claims;
if (claims == null) return false;
// 创建身份信息
var claimIdentity = new ClaimsIdentity("AuthenticationTypes.Federation");
claimIdentity.AddClaims(claims);
var claimsPrincipal = new ClaimsPrincipal(claimIdentity);
// 设置 HttpContext.User 并登录
httpContext.User = claimsPrincipal;
httpContext.SignInAsync(claimsPrincipal);
string accessTokenKey = "access-token"
, xAccessTokenKey = "x-access-token"
, accessControlExposeKey = "Access-Control-Expose-Headers";
// 返回新的 Token
httpContext.Response.Headers[accessTokenKey] = accessToken;
// 返回新的 刷新Token
httpContext.Response.Headers[xAccessTokenKey] = GenerateRefreshToken(accessToken, refreshTokenExpiredTime);
// 处理 axios 问题
httpContext.Response.Headers.TryGetValue(accessControlExposeKey, out var acehs);
httpContext.Response.Headers[accessControlExposeKey] = string.Join(',', StringValues.Concat(acehs, new StringValues(new[] { accessTokenKey, xAccessTokenKey })).Distinct());
return true;
}
/// <summary>
/// 验证 Token
/// </summary>
/// <param name="accessToken"></param>
/// <returns></returns>
public static (bool IsValid, JsonWebToken Token, TokenValidationResult validationResult) Validate(string accessToken)
{
var jwtSettings = GetJWTSettings();
if (jwtSettings == null) return (false, default, default);
// 加密Key
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.IssuerSigningKey));
var creds = new SigningCredentials(key, jwtSettings.Algorithm);
// 创建Token验证参数
var tokenValidationParameters = CreateTokenValidationParameters(jwtSettings);
if (tokenValidationParameters.IssuerSigningKey == null) tokenValidationParameters.IssuerSigningKey = creds.Key;
// 验证 Token
var tokenHandler = new JsonWebTokenHandler();
try
{
var tokenValidationResult = tokenHandler.ValidateToken(accessToken, tokenValidationParameters);
if (!tokenValidationResult.IsValid) return (false, null, tokenValidationResult);
var jsonWebToken = tokenValidationResult.SecurityToken as JsonWebToken;
return (true, jsonWebToken, tokenValidationResult);
}
catch
{
return (false, default, default);
}
}
/// <summary>
/// 验证 Token
/// </summary>
/// <param name="httpContext"></param>
/// <param name="token"></param>
/// <param name="headerKey"></param>
/// <param name="tokenPrefix"></param>
/// <returns></returns>
public static bool ValidateJwtBearerToken(DefaultHttpContext httpContext, out JsonWebToken token, string headerKey = "Authorization", string tokenPrefix = "Bearer ")
{
// 获取 token
var accessToken = GetJwtBearerToken(httpContext, headerKey, tokenPrefix);
if (string.IsNullOrWhiteSpace(accessToken))
{
token = null;
return false;
}
// 验证token
var (IsValid, Token, _) = Validate(accessToken);
token = IsValid ? Token : null;
return IsValid;
}
/// <summary>
/// 读取 Token,不含验证
/// </summary>
/// <param name="accessToken"></param>
/// <returns></returns>
public static JsonWebToken ReadJwtToken(string accessToken)
{
var tokenHandler = new JsonWebTokenHandler();
if (tokenHandler.CanReadToken(accessToken))
{
return tokenHandler.ReadJsonWebToken(accessToken);
}
return default;
}
/// <summary>
/// 获取 JWT Bearer Token
/// </summary>
/// <param name="httpContext"></param>
/// <param name="headerKey"></param>
/// <param name="tokenPrefix"></param>
/// <returns></returns>
public static string GetJwtBearerToken(DefaultHttpContext httpContext, string headerKey = "Authorization", string tokenPrefix = "Bearer ")
{
// 判断请求报文头中是否有 "Authorization" 报文头
var bearerToken = httpContext.Request.Headers[headerKey].ToString();
if (string.IsNullOrWhiteSpace(bearerToken)) return default;
var prefixLenght = tokenPrefix.Length;
return bearerToken.StartsWith(tokenPrefix, true, null) && bearerToken.Length > prefixLenght ? bearerToken[prefixLenght..] : default;
}
/// <summary>
/// 获取 JWT 配置
/// </summary>
/// <returns></returns>
public static JWTSettingsOptions GetJWTSettings()
{
return FrameworkApp.GetMethod("GetOptions").MakeGenericMethod(typeof(JWTSettingsOptions)).Invoke(null, new object[] { null }) as JWTSettingsOptions ?? SetDefaultJwtSettings(new JWTSettingsOptions());
}
/// <summary>
/// 生成Token验证参数
/// </summary>
/// <param name="jwtSettings"></param>
/// <returns></returns>
public static TokenValidationParameters CreateTokenValidationParameters(JWTSettingsOptions jwtSettings)
{
return new TokenValidationParameters
{
// 验证签发方密钥
ValidateIssuerSigningKey = jwtSettings.ValidateIssuerSigningKey.Value,
// 签发方密钥
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.IssuerSigningKey)),
// 验证签发方
ValidateIssuer = jwtSettings.ValidateIssuer.Value,
// 设置签发方
ValidIssuer = jwtSettings.ValidIssuer,
// 验证签收方
ValidateAudience = jwtSettings.ValidateAudience.Value,
// 设置接收方
ValidAudience = jwtSettings.ValidAudience,
// 验证生存期
ValidateLifetime = jwtSettings.ValidateLifetime.Value,
// 过期时间容错值
ClockSkew = TimeSpan.FromSeconds(jwtSettings.ClockSkew.Value),
};
}
/// <summary>
/// 组合 Claims 负荷
/// </summary>
/// <param name="payload"></param>
/// <param name="expiredTime">过期时间,单位:分钟</param>
/// <returns></returns>
private static (IDictionary<string, object> Payload, JWTSettingsOptions JWTSettings) CombinePayload(IDictionary<string, object> payload, long? expiredTime = null)
{
var jwtSettings = GetJWTSettings();
var datetimeOffset = DateTimeOffset.UtcNow;
if (!payload.ContainsKey(JwtRegisteredClaimNames.Iat))
{
payload.Add(JwtRegisteredClaimNames.Iat, datetimeOffset.ToUnixTimeSeconds());
}
if (!payload.ContainsKey(JwtRegisteredClaimNames.Nbf))
{
payload.Add(JwtRegisteredClaimNames.Nbf, datetimeOffset.ToUnixTimeSeconds());
}
if (!payload.ContainsKey(JwtRegisteredClaimNames.Exp))
{
var minute = expiredTime ?? jwtSettings?.ExpiredTime ?? 20;
payload.Add(JwtRegisteredClaimNames.Exp, DateTimeOffset.UtcNow.AddMinutes(minute).ToUnixTimeSeconds());
}
if (!payload.ContainsKey(JwtRegisteredClaimNames.Iss))
{
payload.Add(JwtRegisteredClaimNames.Iss, jwtSettings?.ValidIssuer);
}
if (!payload.ContainsKey(JwtRegisteredClaimNames.Aud))
{
payload.Add(JwtRegisteredClaimNames.Aud, jwtSettings?.ValidAudience);
}
return (payload, jwtSettings);
}
/// <summary>
/// 设置默认 Jwt 配置
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
internal static JWTSettingsOptions SetDefaultJwtSettings(JWTSettingsOptions options)
{
options.ValidateIssuerSigningKey ??= true;
if (options.ValidateIssuerSigningKey == true)
{
options.IssuerSigningKey ??= "7k5yOxSMHVdYjs61gkgUY3W9DHbgk7tokaZlP3QIlfk34D1H7jYEOcLybClW1aKl";
}
options.ValidateIssuer ??= true;
if (options.ValidateIssuer == true)
{
options.ValidIssuer ??= "yinmaisoft";
}
options.ValidateAudience ??= true;
if (options.ValidateAudience == true)
{
options.ValidAudience ??= "powerby NCC";
}
options.ValidateLifetime ??= true;
if (options.ValidateLifetime == true)
{
options.ClockSkew ??= 10;
}
options.ExpiredTime ??= 20;
options.Algorithm ??= SecurityAlgorithms.HmacSha256;
return options;
}
/// <summary>
/// 获取当前的 HttpContext
/// </summary>
/// <returns></returns>
private static HttpContext GetCurrentHttpContext()
{
return FrameworkApp.GetProperty("HttpContext").GetValue(null) as HttpContext;
}
/// <summary>
/// 固定的 Claim 类型
/// </summary>
private static readonly string[] StationaryClaimTypes = new[] { JwtRegisteredClaimNames.Iat, JwtRegisteredClaimNames.Nbf, JwtRegisteredClaimNames.Exp, JwtRegisteredClaimNames.Iss, JwtRegisteredClaimNames.Aud };
/// <summary>
/// 框架 App 静态类
/// </summary>
internal static Type FrameworkApp { get; set; }
/// <summary>
/// 获取框架上下文
/// </summary>
/// <returns></returns>
internal static Assembly GetFrameworkContext(Assembly callAssembly)
{
if (FrameworkApp != null) return FrameworkApp.Assembly;
// 获取 NCC 程序集名称
var jnfpAssemblyName = callAssembly.GetReferencedAssemblies()
.FirstOrDefault(u => u.Name == "NCC.Code")
?? throw new InvalidOperationException("No `NCC` assembly installed in the current project was detected.");
// 加载 NCC 程序集
var NCCAssembly = AssemblyLoadContext.Default.LoadFromAssemblyName(jnfpAssemblyName);
// 获取 NCC.App 静态类
FrameworkApp = NCCAssembly.GetType("NCC.App");
return NCCAssembly;
}
}
}
|