@elf-express/admin-net-mcp 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +83 -88
- package/knowledge/Furion_Teaching_Manual/04-1-/351/205/215/347/275/256.md +442 -0
- package/knowledge/Furion_Teaching_Manual/04-2-/351/201/270/351/240/205.md +363 -0
- package/knowledge/Furion_Teaching_Manual/05-1-/345/213/225/346/205/213WebAPI.md +825 -0
- package/knowledge/Furion_Teaching_Manual/05-2-HttpContext.md +217 -0
- package/knowledge/Furion_Teaching_Manual/05-3-/347/257/251/351/201/270/345/231/250/346/224/224/346/210/252/345/231/250AOP.md +581 -0
- package/knowledge/Furion_Teaching_Manual/05-4-/350/253/213/346/261/202/347/250/275/346/240/270/346/227/245/350/252/214.md +129 -0
- package/knowledge/Furion_Teaching_Manual/05-5-/344/270/255/344/273/213/350/273/237/351/253/224Middleware.md +328 -0
- package/knowledge/Furion_Teaching_Manual/05-6-Vue-React-Angular/344/273/213/351/235/242/344/273/243/347/220/206.md +317 -0
- package/knowledge/Furion_Teaching_Manual/06-1/350/246/217/347/257/204/345/214/226/346/216/245/345/217/243.md +1458 -0
- package/knowledge/Furion_Teaching_Manual/06-2/347/254/254/344/270/211/346/226/271API_Scalar.md +91 -0
- package/knowledge/Furion_Teaching_Manual/07-/345/217/213/345/245/275/344/276/213/345/244/226/350/231/225/347/220/206.md +511 -0
- package/knowledge/Furion_Teaching_Manual/08-1-/350/263/207/346/226/231/351/251/227/350/255/211/345/237/272/347/244/216/344/275/277/347/224/250.md +587 -0
- package/knowledge/Furion_Teaching_Manual/10-1-SqlSugar/346/225/264/345/220/210.md +336 -0
- package/knowledge/Furion_Teaching_Manual/11-SaaS /345/244/232/347/247/237/346/210/266/347/255/206/350/250/230.md" +271 -0
- package/knowledge/Furion_Teaching_Manual/12-furion-dependency-injection.md +408 -0
- package/knowledge/Furion_Teaching_Manual/13-/347/211/251/344/273/266/350/263/207/346/226/231/346/230/240/345/260/204/357/274/210Mapster/357/274/211.md +162 -0
- package/knowledge/Furion_Teaching_Manual/14-/345/210/206/345/270/203/345/274/217/347/274/223/345/255/230.md +311 -0
- package/knowledge/Furion_Teaching_Manual/15-/345/256/211/345/205/250/351/211/264/346/235/203.md +832 -0
- package/knowledge/Furion_Teaching_Manual/17-/346/252/242/350/246/226/347/257/204/346/234/254/345/274/225/346/223/216.md +327 -0
- package/knowledge/Furion_Teaching_Manual/18-/346/227/245/350/252/214/350/250/230/351/214/204.md +639 -0
- package/knowledge/Furion_Teaching_Manual/19-1-HTTP/351/201/240/347/253/257/350/253/213/346/261/202/345/237/272/347/244/216/344/275/277/347/224/250.md +621 -0
- package/knowledge/Furion_Teaching_Manual/19-2-HTTP/351/201/240/347/253/257/350/253/213/346/261/202/351/200/262/351/232/216/346/214/207/345/215/227.md +928 -0
- package/knowledge/Furion_Teaching_Manual/19-3-HTTP/351/201/240/347/253/257/350/253/213/346/261/202/345/270/270/350/246/213/345/225/217/351/241/214.md +362 -0
- package/knowledge/Furion_Teaching_Manual/20-/350/263/207/346/226/231/345/212/240/350/247/243/345/257/206.md +286 -0
- package/knowledge/Furion_Teaching_Manual/20-/351/231/204/351/214/204-KSort/350/263/207/346/226/231/347/260/275/345/220/215/345/256/214/346/225/264/345/216/237/345/247/213/347/242/274.md +305 -0
- package/knowledge/Furion_Teaching_Manual/21-/345/205/250/347/220/203/345/214/226/345/222/214/346/234/254/345/234/260/345/214/226.md +342 -0
- package/knowledge/Furion_Teaching_Manual/22-/344/272/213/344/273/266/345/214/257/346/265/201/346/216/222EventBus.md +448 -0
- package/knowledge/Furion_Teaching_Manual/23-JSON/345/272/217/345/210/227/345/214/226.md +340 -0
- package/knowledge/Furion_Teaching_Manual/24-/345/215/263/346/231/202/351/200/232/350/250/212SignalR.md +247 -0
- package/knowledge/Furion_Teaching_Manual/25-/350/274/224/345/212/251/350/247/222/350/211/262/346/234/215/345/213/231WorkerService.md +295 -0
- package/knowledge/Furion_Teaching_Manual/26-1-/346/216/222/347/250/213/344/275/234/346/245/255/345/256/232/346/231/202/344/273/273/345/213/231.md +631 -0
- package/knowledge/Furion_Teaching_Manual/26-2-Cron/350/241/250/351/201/224/345/274/217.md +203 -0
- package/knowledge/Furion_Teaching_Manual/26-3-/344/273/273/345/213/231/344/275/207/345/210/227TaskQueue.md +215 -0
- package/knowledge/Furion_Teaching_Manual/27-/345/210/206/346/225/243/345/274/217ID/347/224/237/346/210/220.md +86 -0
- package/knowledge/Furion_Teaching_Manual/28-/346/250/241/347/265/204/345/214/226/351/226/213/347/231/274.md +68 -0
- package/knowledge/Furion_Teaching_Manual/29-/346/265/201/350/256/212/347/211/251/344/273/266Clay.md +313 -0
- package/knowledge/Furion_Teaching_Manual/30-/350/204/253/346/225/217/350/231/225/347/220/206/357/274/210Sensitive Detection).md" +168 -0
- package/knowledge/Furion_Teaching_Manual/32-/346/234/203/350/251/261/345/222/214/347/213/200/346/205/213/347/256/241/347/220/206.md +147 -0
- package/knowledge/Furion_Teaching_Manual/33-IPC/347/250/213/345/272/217/351/200/232/350/250/212.md +98 -0
- package/knowledge/Furion_Teaching_Manual/34-2-Docker/351/203/250/347/275/262.md +313 -0
- package/knowledge/Furion_Teaching_Manual/34-3-Nginx/351/203/250/347/275/262.md +157 -0
- package/knowledge/Furion_Teaching_Manual/34-8-WindowsService/351/203/250/347/275/262.md +112 -0
- package/knowledge/Furion_Teaching_Manual/35-1-Docker/347/222/260/345/242/203/346/214/201/347/272/214/351/203/250/347/275/262Jenkins.md +169 -0
- package/knowledge/Furion_Teaching_Manual/36-1-/345/226/256/345/205/203/346/270/254/350/251/246/346/225/264/345/220/210/346/270/254/350/251/246.md +275 -0
- package/knowledge/Furion_Teaching_Manual/36-3-/345/237/272/346/272/226/346/270/254/350/251/246Benchmarking.md +80 -0
- package/knowledge/attributes.md +153 -0
- package/knowledge/config.md +147 -0
- package/knowledge/entity.md +115 -0
- package/knowledge/event.md +124 -0
- package/knowledge/plugin.md +136 -0
- package/knowledge/service.md +146 -0
- package/knowledge/sqlsugar.md +172 -0
- package/knowledge/vue-typescript.md +338 -0
- package/package.json +3 -2
package/knowledge/Furion_Teaching_Manual/15-/345/256/211/345/205/250/351/211/264/346/235/203.md
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
# 15. 安全鉴权
|
|
2
|
+
|
|
3
|
+
## 📝 模块更新日志
|
|
4
|
+
|
|
5
|
+
### 新特性
|
|
6
|
+
|
|
7
|
+
- **新增** JWT 授权支持 RS\*/PS\*/EC\* 加密算法支持 `4.9.8.24` ⏱️2026.03.15 `ce2cd0c`
|
|
8
|
+
- **新增** JWT 自动刷新时可添加自定义回调函数参数 `onRefreshing` `4.9.7.47` ⏱️2024.04.20 `dfe85db`
|
|
9
|
+
- **新增** 授权失败可以设置 Http 状态码 `context.Fail(statusCode)` `4.9.3.14` ⏱️2024.05.14 `542eb8c`
|
|
10
|
+
- **新增** JWT 授权配置 `RequireExpirationTime` 属性,解决 JWT 过期时间不能大于 13年 问题 `4.9.1.46` ⏱️2024.03.13 `#I9840M`
|
|
11
|
+
|
|
12
|
+
### 突破性变化
|
|
13
|
+
|
|
14
|
+
- **调整** 授权处理程序 `AppAuthorizeHandler` 接口的 `HandleAsync` 方法签名,新增 `DefaultHttpContext` 参数 `4.9.3` ⏱️2024.05.10 `52d3c2c` `edc51f4`
|
|
15
|
+
|
|
16
|
+
### 问题修复
|
|
17
|
+
|
|
18
|
+
- **修复** JWT 验证或刷新时出现中断性异常问题 `4.9.8.20` ⏱️2026.03.05 `5f12d53`
|
|
19
|
+
- **修复** JWT 授权自动刷新后没有同步回原 `AuthorizationHandlerContext context` `4.9.8.19` ⏱️2026.03.03 `#IEJFOU` `f9bf7a9`
|
|
20
|
+
- **修复** 生成 JWT 令牌时自定义 `IssuerSigningKey` 密钥导致缺失字段问题 `4.9.7.118` ⏱️2025.09.06 `94b8a60`
|
|
21
|
+
- **修复** 自定义授权 Handler 发生异常且启用 `app.UseUnifyResultStatusCodes()` 中间件时会导致重复写入响应体问题 `4.9.5.26` ⏱️2024.11.22 `4045aa7`
|
|
22
|
+
- **修复** 客户端设置 JWT Token 时如果 Bearer 后面跟多个空格导致验证失败问题 `4.9.2.8` ⏱️2024.04.08 `@xuejf168` `!874`
|
|
23
|
+
- **修复** 不注册 `AddJwt` 不能使用 `JWTEncryption.Encrypt` 方法问题 `4.9.1.53` ⏱️2024.03.16 `5882cf9`
|
|
24
|
+
- **修复** 使用刷新 Token 也能通过鉴权检查严重安全 Bug `4.8.8.42` ⏱️2023.08.28 `#I7TII4`
|
|
25
|
+
|
|
26
|
+
### 其他更改
|
|
27
|
+
|
|
28
|
+
- **改进** 创建 JWT 安全密钥的逻辑 `4.9.8.23` ⏱️2026.03.15 `05fd2a8`
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 15.1 什么是鉴权
|
|
33
|
+
|
|
34
|
+
鉴权实际上就是一种身份认证。
|
|
35
|
+
|
|
36
|
+
由用户提供凭据,然后将其与存储在操作系统、数据库、应用或资源中的凭据进行比较。 在授权过程中,如果凭据匹配,则用户身份验证成功,可执行已向其授权的操作。 授权指判断允许用户执行的操作的过程。 也可以将身份验证理解为进入空间(例如服务器、数据库、应用或资源)的一种方式,而授权是用户可以对该空间(服务器、数据库或应用)内的哪些对象执行哪些操作。
|
|
37
|
+
|
|
38
|
+
### 15.1.1 常见的鉴权方式
|
|
39
|
+
|
|
40
|
+
**HTTP Basic Authentication**
|
|
41
|
+
|
|
42
|
+
这是 HTTP 协议实现的基本认证方式,我们在浏览网页时,从浏览器正上方弹出的对话框要求我们输入账号密码,正是使用了这种认证方式。
|
|
43
|
+
|
|
44
|
+
**Session + Cookie**
|
|
45
|
+
|
|
46
|
+
利用服务器端的 session(会话)和浏览器端的 cookie 来实现前后端的认证,由于 http 请求时是无状态的,服务器正常情况下是不知道当前请求之前有没有来过,这个时候我们如果要记录状态,就需要在服务器端创建一个会话(session),将同一个客户端的请求都维护在各自的会话中,每当请求到达服务器端的时候,先去查一下该客户端有没有在服务器端创建 session,如果有则已经认证成功了,否则就没有认证。
|
|
47
|
+
|
|
48
|
+
**Token**
|
|
49
|
+
|
|
50
|
+
客户端在首次登录以后,服务端再次接收 HTTP 请求的时候,就只认 Token 了,请求只要每次把 Token 带上就行了,服务器端会拦截所有的请求,然后校验 Token 的合法性,合法就放行,不合法就返回 401(鉴权失败)。
|
|
51
|
+
|
|
52
|
+
Token 验证比较灵活,适用于大部分场景。常用的 Token 鉴权方式的解决方案是 **JWT**,JWT 是通过对带有相关用户信息的进行加密,加密的方式比较灵活,可以根据需求具体设计。
|
|
53
|
+
|
|
54
|
+
**OAuth**
|
|
55
|
+
|
|
56
|
+
OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。我们常见的提供 OAuth 认证服务的厂商有支付宝、QQ 和微信。
|
|
57
|
+
|
|
58
|
+
OAuth 协议又有 1.0 和 2.0 两个版本。相比较 1.0 版,2.0 版整个授权验证流程更简单更安全,也是目前最主要的用户身份验证和授权方式。
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 15.2 如何使用
|
|
63
|
+
|
|
64
|
+
### 配置之前
|
|
65
|
+
|
|
66
|
+
在添加授权服务之前,请先确保 `Startup.cs` 中 `Configure` 是否添加了以下两个中间件:
|
|
67
|
+
|
|
68
|
+
```csharp
|
|
69
|
+
app.UseAuthentication();
|
|
70
|
+
app.UseAuthorization();
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 15.2.1 添加 Cookie 身份验证
|
|
74
|
+
|
|
75
|
+
> **使用说明**:如果您使用的是 WebAPI,则该小节可忽略,通常 WebAPI 使用的是 JWT 授权方式,而非 Cookie。
|
|
76
|
+
|
|
77
|
+
```csharp
|
|
78
|
+
// Cookies单独身份验证
|
|
79
|
+
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
|
80
|
+
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, b =>
|
|
81
|
+
{
|
|
82
|
+
b.LoginPath = "/Home/Login";
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 15.2.2 添加 Jwt 身份验证
|
|
87
|
+
|
|
88
|
+
安装 `Furion.Extras.Authentication.JwtBearer` 扩展包。
|
|
89
|
+
|
|
90
|
+
在 `Startup.cs` 中注册 `AddJwt` 服务,**注意,必须在 `.AddControllers()` 之前注册!!**
|
|
91
|
+
|
|
92
|
+
```csharp
|
|
93
|
+
// 默认授权机制,需授权的即可(方法)需贴 `[Authorize]` 特性
|
|
94
|
+
services.AddJwt();
|
|
95
|
+
|
|
96
|
+
// 启用全局授权,这样每个接口都必须授权才能访问,无需贴 `[Authorize]` 特性,推荐!!!
|
|
97
|
+
// services.AddJwt<JwtHandler>(enableGlobalAuthorize:true);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
> **注**:如果项目使用了 `services.AddSignalR();` 服务,那么该服务必须在 `services.AddJwt` 之后注册。
|
|
101
|
+
|
|
102
|
+
#### 默认 JwtHandler 代码
|
|
103
|
+
|
|
104
|
+
```csharp
|
|
105
|
+
using Furion.Authorization;
|
|
106
|
+
using Microsoft.AspNetCore.Authorization;
|
|
107
|
+
using Microsoft.AspNetCore.Http;
|
|
108
|
+
using System.Threading.Tasks;
|
|
109
|
+
|
|
110
|
+
namespace FurionApi.Web.Core;
|
|
111
|
+
|
|
112
|
+
public class JwtHandler : AppAuthorizeHandler
|
|
113
|
+
{
|
|
114
|
+
public override Task<bool> PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
|
|
115
|
+
{
|
|
116
|
+
// 这里写您的授权判断逻辑,授权通过返回 true,否则返回 false
|
|
117
|
+
// 默认授权失败返回状态码为 403,如需自定义状态码可通过 context.StatusCode(状态码) 或 context.Fail(状态码) 设置
|
|
118
|
+
// 并结合 `app.UseUnifyResultStatusCodes()` 使用,Furion 4.9.3.14+ 支持
|
|
119
|
+
|
|
120
|
+
return Task.FromResult(true);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### 自定义 Jwt 配置(默认无需配置)
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"JWTSettings": {
|
|
130
|
+
"ValidateIssuerSigningKey": true,
|
|
131
|
+
"IssuerSigningKey": "你的密钥(公钥)",
|
|
132
|
+
"IssuerSigningPrivateKey": null,
|
|
133
|
+
"ValidateIssuer": true,
|
|
134
|
+
"ValidIssuer": "签发方",
|
|
135
|
+
"ValidateAudience": true,
|
|
136
|
+
"ValidAudience": "签收方",
|
|
137
|
+
"ValidateLifetime": true,
|
|
138
|
+
"ExpiredTime": 20,
|
|
139
|
+
"ClockSkew": 5,
|
|
140
|
+
"Algorithm": "HS256",
|
|
141
|
+
"RequireExpirationTime": true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
各字段说明:
|
|
147
|
+
|
|
148
|
+
| 字段 | 类型 | 说明 |
|
|
149
|
+
|------|------|------|
|
|
150
|
+
| `ValidateIssuerSigningKey` | bool | 是否验证密钥,默认 true |
|
|
151
|
+
| `IssuerSigningKey` | string | 密钥(公钥),必须是复杂密钥,长度大于16,.NET8+ 长度需大于 32 |
|
|
152
|
+
| `IssuerSigningPrivateKey` | string | 私钥,仅使用 RS\*/PS\*/ES\* 加密算法时才需要配置,Furion 4.9.8.24+ 支持 |
|
|
153
|
+
| `ValidateIssuer` | bool | 是否验证签发方,默认 true |
|
|
154
|
+
| `ValidIssuer` | string | 签发方 |
|
|
155
|
+
| `ValidateAudience` | bool | 是否验证签收方,默认 true |
|
|
156
|
+
| `ValidAudience` | string | 签收方 |
|
|
157
|
+
| `ValidateLifetime` | bool | 是否验证过期时间,默认 true,建议 true |
|
|
158
|
+
| `ExpiredTime` | long | 过期时间,单位分钟,默认 20 分钟,最大支持 13 年 |
|
|
159
|
+
| `ClockSkew` | long | 过期时间容错值,单位秒,默认 5 秒 |
|
|
160
|
+
| `Algorithm` | string | 加密算法,默认 HS256 |
|
|
161
|
+
| `RequireExpirationTime` | bool | 验证过期时间,设置 false 将永不过期,Furion 4.9.1.46+ 支持 |
|
|
162
|
+
|
|
163
|
+
> ⚠️ **系统安全注意事项**:Furion 框架为了方便开发,已经自动添加了 Jwt 默认配置。建议每个项目都应该单独配置 `IssuerSigningKey`、`ValidIssuer`、`ValidAudience` 这三个。否则同样用了 Furion 框架生成的 Token 可能存在相互访问各自系统的风险。
|
|
164
|
+
|
|
165
|
+
#### Algorithm 算法支持列表
|
|
166
|
+
|
|
167
|
+
目前支持的加密算法:HS256、HS384、HS512、PS256、PS384、PS512、RS256(4.9.8.24+)、RS384(4.9.8.24+)、RS512(4.9.8.24+)、ES256、ES256K、ES384、ES512、EdDSA。
|
|
168
|
+
|
|
169
|
+
详情请查阅 `SecurityAlgorithms`。
|
|
170
|
+
|
|
171
|
+
#### RS\* 加密算法配置示例(如 RS256)
|
|
172
|
+
|
|
173
|
+
公钥和私钥必须是 PEM 格式(在线生成 RSA 密钥:https://www.ufreetools.com/zh/tool/rsa-key-pair-generator)
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"JWTSettings": {
|
|
178
|
+
"ValidateIssuerSigningKey": true,
|
|
179
|
+
"IssuerSigningKey": "-----BEGIN PUBLIC KEY-----\r\n...(你的公钥)...\r\n-----END PUBLIC KEY-----\r\n",
|
|
180
|
+
"IssuerSigningPrivateKey": "-----BEGIN RSA PRIVATE KEY-----\r\n...(你的私钥)...\r\n-----END RSA PRIVATE KEY-----\r\n",
|
|
181
|
+
"ValidateIssuer": true,
|
|
182
|
+
"ValidIssuer": "签发方",
|
|
183
|
+
"ValidateAudience": true,
|
|
184
|
+
"ValidAudience": "签收方",
|
|
185
|
+
"ValidateLifetime": true,
|
|
186
|
+
"ExpiredTime": 20,
|
|
187
|
+
"ClockSkew": 5,
|
|
188
|
+
"Algorithm": "RS256",
|
|
189
|
+
"RequireExpirationTime": true
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
> **注意**:换行使用 `\r\n` 或 `\n`。
|
|
195
|
+
|
|
196
|
+
#### ❤️ 生成 Token
|
|
197
|
+
|
|
198
|
+
通常我们需要在登录成功之后生成 JWT Token 并返回,可通过 `JWTEncryption.Encrypt` 静态方法生成:
|
|
199
|
+
|
|
200
|
+
> **关于 Token 的值**:字典 `Dictionary` 中的值支持所有基元类型和基元类型组成的值,但应尽可能避免使用数组值。
|
|
201
|
+
|
|
202
|
+
```csharp
|
|
203
|
+
// 生成 token
|
|
204
|
+
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>()
|
|
205
|
+
{
|
|
206
|
+
{ "UserId", user.Id }, // 存储Id
|
|
207
|
+
{ "Account", user.Account }, // 存储用户名
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 15.2.3 混合身份验证
|
|
212
|
+
|
|
213
|
+
有时候我们一个系统中需要多种混合验证方式,这时候我们需要配置一个**主验证方式**,所以需要修改 `options.DefaultAuthenticateScheme` 和 `options.DefaultChallengeScheme` 为主验证方式。
|
|
214
|
+
|
|
215
|
+
如需第二种方式,只需要通过 `[Authorize(JwtBearerDefaults.AuthenticationScheme)]` 贴即可。
|
|
216
|
+
|
|
217
|
+
#### JWT 和 Cookies 混合身份验证
|
|
218
|
+
|
|
219
|
+
```csharp
|
|
220
|
+
services.AddJwt<JwtHandler>(options =>
|
|
221
|
+
{
|
|
222
|
+
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
|
223
|
+
options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
|
224
|
+
})
|
|
225
|
+
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
|
|
226
|
+
{
|
|
227
|
+
options.LoginPath = "/Home/Login";
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### JWT 和 Windows 身份验证混合验证
|
|
232
|
+
|
|
233
|
+
```csharp
|
|
234
|
+
services.AddJwt<JwtHandler>(options =>
|
|
235
|
+
{
|
|
236
|
+
options.DefaultAuthenticateScheme = NegotiateDefaults.AuthenticationScheme;
|
|
237
|
+
options.DefaultChallengeScheme = NegotiateDefaults.AuthenticationScheme;
|
|
238
|
+
})
|
|
239
|
+
.AddNegotiate();
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### 应用例子
|
|
243
|
+
|
|
244
|
+
```csharp
|
|
245
|
+
// 贴了 [Authorize] 则表示应用 JwtBearerDefaults.AuthenticationScheme
|
|
246
|
+
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
|
247
|
+
public class ApiServices : IDynamicApiController
|
|
248
|
+
{
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 不贴则应用主验证,也即是 DefaultAuthenticateScheme 设置的
|
|
252
|
+
public class HomeController : Controller
|
|
253
|
+
{
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 15.2.4 自定义策略授权
|
|
258
|
+
|
|
259
|
+
在一些复杂的应用系统中,可能存在多套授权机制,比如特定区域使用 JWT 授权,MVC 架构项目使用 Cookies 授权,还有一些特殊的接口使用自定义授权。
|
|
260
|
+
|
|
261
|
+
#### 自定义授权需求类和处理器
|
|
262
|
+
|
|
263
|
+
```csharp
|
|
264
|
+
public class CustomAuthorizationRequirement : IAuthorizationRequirement
|
|
265
|
+
{
|
|
266
|
+
// 可以在此处定义任何你需要验证的信息或逻辑
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public class CustomAuthorizationHandler : AuthorizationHandler<CustomAuthorizationRequirement>
|
|
270
|
+
{
|
|
271
|
+
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomAuthorizationRequirement requirement)
|
|
272
|
+
{
|
|
273
|
+
// 实现你的自定义验证逻辑
|
|
274
|
+
|
|
275
|
+
if (/* 验证条件满足 */)
|
|
276
|
+
{
|
|
277
|
+
context.Succeed(requirement);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return Task.CompletedTask;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### 注册自定义授权处理器
|
|
286
|
+
|
|
287
|
+
```csharp
|
|
288
|
+
services.AddAuthorization(options =>
|
|
289
|
+
{
|
|
290
|
+
options.AddPolicy("CustomPolicy", policy =>
|
|
291
|
+
{
|
|
292
|
+
policy.Requirements.Add(new CustomAuthorizationRequirement());
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
services.AddSingleton<IAuthorizationHandler, CustomAuthorizationHandler>();
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
#### 使用自定义授权策略
|
|
300
|
+
|
|
301
|
+
```csharp
|
|
302
|
+
[ApiController]
|
|
303
|
+
public class MyController : ControllerBase
|
|
304
|
+
{
|
|
305
|
+
[HttpGet("/api/some-endpoint")]
|
|
306
|
+
[Authorize(Policy = "CustomPolicy")]
|
|
307
|
+
public IActionResult SomeEndpoint()
|
|
308
|
+
{
|
|
309
|
+
// ...
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
[HttpGet("/api/protected-endpoint")]
|
|
313
|
+
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
|
|
314
|
+
public IActionResult ProtectedEndpoint()
|
|
315
|
+
{
|
|
316
|
+
// ...
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### 15.2.5 Basic 授权(HTTP 基本认证)
|
|
322
|
+
|
|
323
|
+
在 HTTP 中,HTTP 基本认证(Basic Authentication)是一种允许网页浏览器或其他客户端程序以(用户名:口令密码)请求资源的身份验证方式。
|
|
324
|
+
|
|
325
|
+
#### 创建 Basic 验证处理程序
|
|
326
|
+
|
|
327
|
+
```csharp
|
|
328
|
+
namespace YourProject.Core;
|
|
329
|
+
|
|
330
|
+
public class BasicAuthenticationDefaults
|
|
331
|
+
{
|
|
332
|
+
public const string AuthenticationScheme = "BasicAuthentication";
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
|
336
|
+
{
|
|
337
|
+
private readonly IMyUserService _userService;
|
|
338
|
+
|
|
339
|
+
public BasicAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options
|
|
340
|
+
, ILoggerFactory logger
|
|
341
|
+
, UrlEncoder encoder
|
|
342
|
+
, IMyUserService userService)
|
|
343
|
+
: base(options, logger, encoder)
|
|
344
|
+
{
|
|
345
|
+
_userService = userService;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
|
349
|
+
{
|
|
350
|
+
// 检查请求报文头是否包含 Authorization 头,且值以 "Basic " 开头
|
|
351
|
+
if (!Context.Request.Headers.TryGetValue("Authorization", out StringValues authorizationHeader)
|
|
352
|
+
|| !authorizationHeader.ToString().StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
|
|
353
|
+
{
|
|
354
|
+
return AuthenticateResult.NoResult();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 读取请求报文头 Authorization 的信息并进行 Base64 解码
|
|
358
|
+
var token = authorizationHeader.ToString()[6..].Trim();
|
|
359
|
+
var credentialString = Encoding.UTF8.GetString(Convert.FromBase64String(token));
|
|
360
|
+
|
|
361
|
+
// 通过 : 分隔符分割出用户名和密码
|
|
362
|
+
var credentials = credentialString.Split(':');
|
|
363
|
+
string username = credentials[0];
|
|
364
|
+
string password = credentials[1];
|
|
365
|
+
|
|
366
|
+
// 验证用户名密码
|
|
367
|
+
if (await _userService.ValidateCredentials(username, password))
|
|
368
|
+
{
|
|
369
|
+
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, username) };
|
|
370
|
+
var identity = new ClaimsIdentity(claims, Scheme.Name);
|
|
371
|
+
var principal = new ClaimsPrincipal(identity);
|
|
372
|
+
var ticket = new AuthenticationTicket(principal, null, Scheme.Name);
|
|
373
|
+
|
|
374
|
+
return AuthenticateResult.Success(ticket);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return AuthenticateResult.Fail("Invalid username or password.");
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
> **IMyUserService 说明**:`IMyUserService` 在这里只是一个实现验证用户名和密码的逻辑服务例。你可以是注入仓储服务,如 `IRepository<User>`,也可以是任何可以认证用户名和密码的服务。
|
|
383
|
+
|
|
384
|
+
```csharp
|
|
385
|
+
public interface IMyUserService
|
|
386
|
+
{
|
|
387
|
+
Task<bool> ValidateCredentials(string username, string password);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
public class MyUserService : IMyUserService, IScoped
|
|
391
|
+
{
|
|
392
|
+
public Task<bool> ValidateCredentials(string username, string password)
|
|
393
|
+
{
|
|
394
|
+
// 这里验证用户名和密码...
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
#### 注册 BasicAuthenticationHandler
|
|
400
|
+
|
|
401
|
+
```csharp
|
|
402
|
+
services.AddAuthentication(BasicAuthenticationDefaults.AuthenticationScheme)
|
|
403
|
+
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>(BasicAuthenticationDefaults.AuthenticationScheme
|
|
404
|
+
, configureOptions =>
|
|
405
|
+
{
|
|
406
|
+
// 更多配置
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
#### 使用 Basic 授权
|
|
411
|
+
|
|
412
|
+
**全局授权:**
|
|
413
|
+
|
|
414
|
+
```csharp
|
|
415
|
+
services.Configure<MvcOptions>(options =>
|
|
416
|
+
{
|
|
417
|
+
options.Filters.Add(new AuthorizeFilter());
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**局部授权:**
|
|
422
|
+
|
|
423
|
+
```csharp
|
|
424
|
+
[Authorize]
|
|
425
|
+
public class AdminController : Controller
|
|
426
|
+
{
|
|
427
|
+
// ...
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
**多套授权中使用:**
|
|
432
|
+
|
|
433
|
+
```csharp
|
|
434
|
+
[Authorize(AuthenticationSchemes = BasicAuthenticationDefaults.AuthenticationScheme)]
|
|
435
|
+
public class AdminController : Controller
|
|
436
|
+
{
|
|
437
|
+
// ...
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
#### 在 Swagger 中启用 Basic 授权
|
|
442
|
+
|
|
443
|
+
```json
|
|
444
|
+
{
|
|
445
|
+
"SpecificationDocumentSettings": {
|
|
446
|
+
"SecurityDefinitions": [
|
|
447
|
+
{
|
|
448
|
+
"Id": "Bearer",
|
|
449
|
+
"Type": "Http",
|
|
450
|
+
"Name": "Authorization",
|
|
451
|
+
"Description": "JWT Authorization header using the Bearer scheme.",
|
|
452
|
+
"BearerFormat": "JWT",
|
|
453
|
+
"Scheme": "bearer",
|
|
454
|
+
"In": "Header",
|
|
455
|
+
"Requirement": {
|
|
456
|
+
"Scheme": {
|
|
457
|
+
"Reference": { "Id": "Bearer", "Type": "SecurityScheme" },
|
|
458
|
+
"Accesses": []
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
"Id": "basic",
|
|
464
|
+
"Type": "Http",
|
|
465
|
+
"Name": "basic",
|
|
466
|
+
"Description": "Basic Authorization header using the username and password.",
|
|
467
|
+
"Scheme": "basic",
|
|
468
|
+
"In": "Header",
|
|
469
|
+
"Requirement": {
|
|
470
|
+
"Scheme": {
|
|
471
|
+
"Reference": { "Id": "basic", "Type": "SecurityScheme" },
|
|
472
|
+
"Accesses": []
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
]
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
#### 客户端(JavaScript)如何设置授权信息
|
|
482
|
+
|
|
483
|
+
```javascript
|
|
484
|
+
// 将用户名和密码组合成一个字符串,用冒号分隔
|
|
485
|
+
var authString = username + ':' + password;
|
|
486
|
+
|
|
487
|
+
// 对组合后的字符串进行 Base64 编码
|
|
488
|
+
var encodedAuthString = btoa(authString);
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
在发送请求时在 Headers 中添加:
|
|
492
|
+
|
|
493
|
+
```javascript
|
|
494
|
+
headers: {
|
|
495
|
+
'Content-Type': 'application/json',
|
|
496
|
+
'Authorization': 'Basic ' + encodedAuthString
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## 15.3 高级自定义授权
|
|
503
|
+
|
|
504
|
+
Furion 框架提供了非常灵活的高级策略鉴权和授权方式,通过该策略授权方式可以实现任何自定义授权。
|
|
505
|
+
|
|
506
|
+
### 15.3.1 AppAuthorizeHandler
|
|
507
|
+
|
|
508
|
+
Furion 框架提供了 `AppAuthorizeHandler` 策略授权处理程序提供基类,只需要创建自己的 Handler 继承它即可。
|
|
509
|
+
|
|
510
|
+
```csharp
|
|
511
|
+
using Furion.Authorization;
|
|
512
|
+
using Furion.Core;
|
|
513
|
+
using Microsoft.AspNetCore.Authorization;
|
|
514
|
+
using Microsoft.AspNetCore.Http;
|
|
515
|
+
using Microsoft.IdentityModel.JsonWebTokens;
|
|
516
|
+
|
|
517
|
+
namespace Furion.Web.Core
|
|
518
|
+
{
|
|
519
|
+
public class JwtHandler : AppAuthorizeHandler
|
|
520
|
+
{
|
|
521
|
+
public override Task<bool> PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
|
|
522
|
+
{
|
|
523
|
+
// 此处已经自动验证 Jwt token 的有效性了,无需手动验证
|
|
524
|
+
return Task.FromResult(CheckAuthorzie(httpContext));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private static bool CheckAuthorzie(DefaultHttpContext httpContext)
|
|
528
|
+
{
|
|
529
|
+
// 获取权限特性
|
|
530
|
+
var securityDefineAttribute = httpContext.GetMetadata<SecurityDefineAttribute>();
|
|
531
|
+
if (securityDefineAttribute == null) return true;
|
|
532
|
+
|
|
533
|
+
return "查询数据库返回是否有权限";
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
注册:
|
|
540
|
+
|
|
541
|
+
```csharp
|
|
542
|
+
services.AddJwt<JwtHandler>();
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
### 15.3.2 完全自定义授权
|
|
546
|
+
|
|
547
|
+
有些时候可能针对不同的平台采用不一样的授权方式,比如合作信任的第三方机构可以免授权,这时候只需要重写 `HandleAsync` 方法即可:
|
|
548
|
+
|
|
549
|
+
```csharp
|
|
550
|
+
using Furion.Authorization;
|
|
551
|
+
using Furion.Core;
|
|
552
|
+
using Microsoft.AspNetCore.Authorization;
|
|
553
|
+
using Microsoft.AspNetCore.Http;
|
|
554
|
+
using System.Threading.Tasks;
|
|
555
|
+
|
|
556
|
+
namespace Furion.Web.Core
|
|
557
|
+
{
|
|
558
|
+
public class JwtHandler : AppAuthorizeHandler
|
|
559
|
+
{
|
|
560
|
+
// Furion 4.9.3 之前版本使用:
|
|
561
|
+
// public override async Task HandleAsync(AuthorizationHandlerContext context)
|
|
562
|
+
public override async Task HandleAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
|
|
563
|
+
{
|
|
564
|
+
var isAuthenticated = context.User.Identity.IsAuthenticated;
|
|
565
|
+
|
|
566
|
+
if (是第三方)
|
|
567
|
+
{
|
|
568
|
+
foreach (var requirement in context.PendingRequirements)
|
|
569
|
+
{
|
|
570
|
+
context.Succeed(requirement);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else context.Fail();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
---
|
|
580
|
+
|
|
581
|
+
## 15.4 授权特性及全局授权
|
|
582
|
+
|
|
583
|
+
默认情况下,所有的路由都是允许匿名访问的,所以如果需要对某个 Action 或 Controller 设定授权访问,只需要在 Action 或 Controller 贴 `[AppAuthorize]` 或 `[Authorize]` 特性即可。
|
|
584
|
+
|
|
585
|
+
如果需要对特定的 Action 或 Controller 允许匿名访问,则贴 `[AllowAnonymous]` 即可。
|
|
586
|
+
|
|
587
|
+
### 15.4.1 全局授权
|
|
588
|
+
|
|
589
|
+
```csharp
|
|
590
|
+
services.AddJwt<JwtHandler>(enableGlobalAuthorize: true);
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### 15.4.2 匿名访问
|
|
594
|
+
|
|
595
|
+
如果需要对特定的 Action 或 Controller 允许匿名访问,则贴 `[AllowAnonymous]` 即可。
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## 15.5 自动刷新 Token
|
|
600
|
+
|
|
601
|
+
### 15.5.1 后端登录部分
|
|
602
|
+
|
|
603
|
+
当用户登录成功之后,返回 `accessToken` 字符串,之后通过 `JWTEncryption.GenerateRefreshToken()` 获取刷新 Token,并通过响应报文头返回:
|
|
604
|
+
|
|
605
|
+
```csharp
|
|
606
|
+
// token
|
|
607
|
+
var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>()
|
|
608
|
+
{
|
|
609
|
+
{ "UserId", user.Id },
|
|
610
|
+
{ "Account", user.Account },
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// 获取刷新 token
|
|
614
|
+
var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, 43200);
|
|
615
|
+
// 第二个参数是刷新 token 的有效期(分钟),默认三十天,最大支持13年
|
|
616
|
+
|
|
617
|
+
// 设置响应报文头
|
|
618
|
+
httpContextAccessor.HttpContext.Response.Headers["access-token"] = accessToken;
|
|
619
|
+
httpContextAccessor.HttpContext.Response.Headers["x-access-token"] = refreshToken;
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
用户登录成功之后把 `accessToken` 和 `refreshToken` 一起返回给客户端存储起来。
|
|
623
|
+
|
|
624
|
+
### 15.5.2 后端授权 Handler 部分
|
|
625
|
+
|
|
626
|
+
```csharp
|
|
627
|
+
using Furion.Authorization;
|
|
628
|
+
using Furion.Core;
|
|
629
|
+
using Furion.DataEncryption;
|
|
630
|
+
using Microsoft.AspNetCore.Authorization;
|
|
631
|
+
using Microsoft.AspNetCore.Http;
|
|
632
|
+
using Microsoft.Extensions.DependencyInjection;
|
|
633
|
+
using System.Threading.Tasks;
|
|
634
|
+
|
|
635
|
+
namespace Furion.Web.Core
|
|
636
|
+
{
|
|
637
|
+
public class JwtHandler : AppAuthorizeHandler
|
|
638
|
+
{
|
|
639
|
+
// Furion 4.9.3 之前版本使用:
|
|
640
|
+
// public override async Task HandleAsync(AuthorizationHandlerContext context)
|
|
641
|
+
public override async Task HandleAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
|
|
642
|
+
{
|
|
643
|
+
// 自动刷新 token
|
|
644
|
+
// 支持传入 onRefreshing: (newToken, newRefreshToken) => {},Furion 4.9.7.47+ 支持
|
|
645
|
+
if (JWTEncryption.AutoRefreshToken(context, httpContext))
|
|
646
|
+
{
|
|
647
|
+
await AuthorizeHandleAsync(context);
|
|
648
|
+
}
|
|
649
|
+
else context.Fail();
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
public override Task<bool> PipelineAsync(AuthorizationHandlerContext context, DefaultHttpContext httpContext)
|
|
653
|
+
{
|
|
654
|
+
return Task.FromResult(true);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
### 15.5.3 客户端部分
|
|
661
|
+
|
|
662
|
+
客户端每次请求需将 `accessToken` 和 `refreshToken` 放到请求报文头中传送到服务端,格式为:
|
|
663
|
+
|
|
664
|
+
```
|
|
665
|
+
Authorization: Bearer 你的token
|
|
666
|
+
X-Authorization: Bearer 你的刷新token
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
> ⚠️ **特别注意**:`Authorization` 和 `X-Authorization` 都必须添加 `Bearer` 前缀。
|
|
670
|
+
|
|
671
|
+
Furion 框架提供了 vue/react/angular 客户端请求参考代码:https://gitee.com/dotnetchina/Furion/tree/v4/clients
|
|
672
|
+
|
|
673
|
+
> **小建议**:建议使用自动生成 Vue/React/Angular 代理方式:5.6 Vue/React/Angular 接口代理
|
|
674
|
+
|
|
675
|
+
> **其他补充**:在正常开发中,`refreshToken` 无需每次请求携带,而是 `accessToken` 即将过期之后携带即可。可以在客户端自行判断 `accessToken` 是否即将过期。
|
|
676
|
+
|
|
677
|
+
如果 Token 过期,那么 Furion 将自动根据有效期内的 `refreshToken` 自动生成新的 AccessToken,并在**响应报文头**中返回:
|
|
678
|
+
|
|
679
|
+
```
|
|
680
|
+
access-token: 新的token
|
|
681
|
+
x-access-token: 新的刷新token
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
> **存储新的 Token**:前端需要获取响应报文头新的 token 和刷新 token 替换之前在客户处存储旧的 token 和刷新 token。
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
688
|
+
## 15.6 获取 Jwt 存储的信息
|
|
689
|
+
|
|
690
|
+
```csharp
|
|
691
|
+
// 获取 Jwt 存储的信息
|
|
692
|
+
var userId = App.User?.FindFirstValue("键");
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
> 注意引入 `System.Security.Claims` 命名空间。
|
|
696
|
+
|
|
697
|
+
> **获取不到 Token 信息说明**:请确保 `.AddJwt` 服务已注册且启用了全局授权或该接口(方法)贴有 `[Authorize]` 特性。
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
701
|
+
## 15.7 前端解密 JWT 信息
|
|
702
|
+
|
|
703
|
+
通常在用户登录成功后我们会将 JWT Token 存储到浏览器中,这时候就需要在浏览器端解析 token 里面存储的信息。
|
|
704
|
+
|
|
705
|
+
**TypeScript 版本:**
|
|
706
|
+
|
|
707
|
+
```typescript
|
|
708
|
+
/**
|
|
709
|
+
* 解密 JWT token 的信息
|
|
710
|
+
* @param token jwt token 字符串
|
|
711
|
+
* @returns <any>object
|
|
712
|
+
*/
|
|
713
|
+
function decryptJWT(token: string): any {
|
|
714
|
+
token = token.replace(/_/g, "/").replace(/-/g, "+");
|
|
715
|
+
var json = decodeURIComponent(escape(window.atob(token.split(".")[1])));
|
|
716
|
+
return JSON.parse(json);
|
|
717
|
+
}
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
**JavaScript 版本:**
|
|
721
|
+
|
|
722
|
+
```javascript
|
|
723
|
+
/**
|
|
724
|
+
* 解密 JWT token 的信息
|
|
725
|
+
* @param token jwt token 字符串
|
|
726
|
+
* @returns <any>object
|
|
727
|
+
*/
|
|
728
|
+
function decryptJWT(token) {
|
|
729
|
+
token = token.replace(/_/g, "/").replace(/-/g, "+");
|
|
730
|
+
var json = decodeURIComponent(escape(window.atob(token.split(".")[1])));
|
|
731
|
+
return JSON.parse(json);
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
> **小知识**:可以在解密之后读取过期时间 `exp` 来解决请求时是否需要带刷新 Token,比如即将过期前 5 分钟。
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
## 15.8 Jwt 身份验证过程监听
|
|
740
|
+
|
|
741
|
+
有时候我们希望能够自定义或者监听 Jwt 验证过程,比如验证失败后在 Response 中添加 Headers,或者对接第三方验证时要求提供 apikey 等方式。
|
|
742
|
+
|
|
743
|
+
```csharp
|
|
744
|
+
services.AddJwt<AuthHandler>(jwtBearerConfigure: options =>
|
|
745
|
+
{
|
|
746
|
+
options.Events = new JwtBearerEvents
|
|
747
|
+
{
|
|
748
|
+
// 添加额外 Token 读取处理
|
|
749
|
+
OnMessageReceived = context =>
|
|
750
|
+
{
|
|
751
|
+
return Task.CompletedTask;
|
|
752
|
+
},
|
|
753
|
+
// Token 验证通过处理
|
|
754
|
+
OnTokenValidated = context =>
|
|
755
|
+
{
|
|
756
|
+
return Task.CompletedTask;
|
|
757
|
+
},
|
|
758
|
+
// Token 验证失败处理
|
|
759
|
+
OnAuthenticationFailed = context =>
|
|
760
|
+
{
|
|
761
|
+
return Task.CompletedTask;
|
|
762
|
+
},
|
|
763
|
+
// 客户端未提供 Token 或 Token 格式不正确处理
|
|
764
|
+
OnChallenge = context =>
|
|
765
|
+
{
|
|
766
|
+
return Task.CompletedTask;
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
});
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
### 15.8.1 实现 Url 参数验证 Token
|
|
773
|
+
|
|
774
|
+
正常情况下,JWT 都是通过请求头的 `Authorization` 设置,我们可以通过下列代码实现支持 Url 设置 Token:
|
|
775
|
+
|
|
776
|
+
```csharp
|
|
777
|
+
services.AddJwt<AuthHandler>(jwtBearerConfigure: options =>
|
|
778
|
+
{
|
|
779
|
+
options.Events = new JwtBearerEvents
|
|
780
|
+
{
|
|
781
|
+
OnMessageReceived = context =>
|
|
782
|
+
{
|
|
783
|
+
var httpContext = context.HttpContext;
|
|
784
|
+
|
|
785
|
+
if (httpContext.Request.Query.ContainsKey("Authorization"))
|
|
786
|
+
{
|
|
787
|
+
var token = httpContext.Request.Query["Authorization"];
|
|
788
|
+
context.Token = token;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return Task.CompletedTask;
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
});
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
这样就可以通过 `https://www.xxxx.com?Authorization=你的Token` 访问了。
|
|
798
|
+
|
|
799
|
+
---
|
|
800
|
+
|
|
801
|
+
## 15.9 关于 Blazor + WebAPI 混合授权
|
|
802
|
+
|
|
803
|
+
一些应用使用了 Blazor + WebAPI 模板后并启用全局授权,可能会遇到 401/403 授权失败的提示,这时只需要在启动层 `YourProject.Web.Entry` 下的 `Pages/_Host.cshtml` 顶部添加以下代码即可:
|
|
804
|
+
|
|
805
|
+
```razor
|
|
806
|
+
@using Microsoft.AspNetCore.Authorization
|
|
807
|
+
@attribute [AllowAnonymous]
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
---
|
|
811
|
+
|
|
812
|
+
## 15.10 启用全局授权导致重复检查问题
|
|
813
|
+
|
|
814
|
+
在一些复杂的应用系统中,可能存在多种鉴权方式,如 JWT 和 Basic,通常我们会启用全局鉴权:
|
|
815
|
+
|
|
816
|
+
```csharp
|
|
817
|
+
services.AddJwt<JwtHandler>(enableGlobalAuthorize: true);
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
这时候如果局部授权贴了 `[Authorize]` 特性,就会导致重复进入 `JwtHandler` 的情况,解决方式:
|
|
821
|
+
|
|
822
|
+
```csharp
|
|
823
|
+
// 1. 移除 enableGlobalAuthorize:true
|
|
824
|
+
services.AddJwt<JwtHandler>();
|
|
825
|
+
|
|
826
|
+
// 2. 添加 .RequireAuthorization()
|
|
827
|
+
// 如果使用的是 WebApplication,那么直接使用 app.MapControllers().RequireAuthorization();
|
|
828
|
+
app.UseEndpoints(endpoints =>
|
|
829
|
+
{
|
|
830
|
+
endpoints.MapControllers().RequireAuthorization();
|
|
831
|
+
});
|
|
832
|
+
```
|