1e73afce
李曜臣
代码优化
|
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
|
# 2026-07-16 泰额版代码优化
本文档说明 **泰额版后端** 当日对 `tenant-select` 出参、默认管理员凭据加密及 Web 登录入参的改造。仅涉及泰额版 `Yi.Abp.Net8`,前端需同步对接。
---
## 一、变更概述
| 项 | 说明 |
|----|------|
| 接口 | `GET /api/app/th-multi-tenancy/tenant-select` |
| 目的 | 登录页展示租户列表时不再暴露租户 Id、数据库连接串;改为返回创建时间与可解密的默认管理员凭据 |
| 登录 | `POST /api/app/th-web-auth/login` 支持 `tenantName` 替代 `tenantId` |
| 配置 | 新增 `FoodLabeling:TenantSelectCrypto:SecretKey` |
---
## 二、tenant-select 出参变更
### 2.1 新旧字段对比
| 旧字段 | 新字段 | 说明 |
|--------|--------|------|
| `id` | — | **已移除**,不再返回租户 Guid |
| `name` | `name` | 保留,租户名称 |
| — | `creationTime` | **新增**,租户创建时间(主库 `yitenant`) |
| — | `loginAccount` | **新增**,默认管理员账号,固定为 `admin`(与 `UserDataSeed` 一致) |
| — | `password` | **新增**,AES-CBC 加密后的 Base64 密文(**非**库内不可逆哈希) |
| — | `passwordSalt` | **新增**,AES IV 的 Base64,前端解密时使用 |
| `tenantConnectionString` / 连接串相关 | — | **从未在此接口返回**;仍不暴露 |
### 2.2 出参示例
```json
[
{
"name": "Default",
"creationTime": "2026-01-15T10:30:00",
"loginAccount": "admin",
"password": "xK3mP9vL2nQ8wR1sT4uV7yZ0aB5cD6eF8gH9iJ0kL1m=",
"passwordSalt": "AbCdEfGhIjKlMnOpQrStUvWxYz0123=="
}
]
```
> 每条记录的 `password` / `passwordSalt` 因随机 IV 而不同;明文来源均为 `RbacOptions.AdminPassword`(当前默认 `123456`)。
---
## 三、密码加盐加密与前端解密
### 3.1 算法约定
| 项 | 值 |
|----|-----|
| 算法 | AES-256-CBC |
| 填充 | PKCS7 |
| Key | `SHA256(UTF8(SecretKey))` → 32 字节 |
| IV | 每次加密随机 16 字节,Base64 写入 `passwordSalt` |
| 密文 | Base64 写入 `password` |
| 明文 | `RbacOptions.AdminPassword`(appsettings,与种子管理员一致) |
### 3.2 后端配置
`appsettings.json`:
```json
"FoodLabeling": {
"TenantSelectCrypto": {
"SecretKey": "FoodLabelingThTenantSelect2026Key!"
}
}
```
- `SecretKey` 为前后端约定的字符串,**不要求**本身为 32 字节;后端用 SHA256 派生 AES-256 密钥。
- 生产环境请更换为足够长度的随机字符串,并与前端构建配置保持一致。
### 3.3 前端解密(CryptoJS 示例)
```javascript
import CryptoJS from 'crypto-js';
const SECRET_KEY = 'FoodLabelingThTenantSelect2026Key!'; // 与后端 TenantSelectCrypto:SecretKey 一致
function decryptTenantPassword(passwordBase64, passwordSaltBase64) {
const key = CryptoJS.SHA256(CryptoJS.enc.Utf8.parse(SECRET_KEY));
const iv = CryptoJS.enc.Base64.parse(passwordSaltBase64);
const cipher = CryptoJS.enc.Base64.parse(passwordBase64);
const decrypted = CryptoJS.AES.decrypt(
{ ciphertext: cipher },
key,
{ iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 },
);
return decrypted.toString(CryptoJS.enc.Utf8);
}
```
### 3.4 前端解密(Web Crypto 伪代码)
```javascript
async function decryptTenantPassword(passwordBase64, passwordSaltBase64) {
const secretKey = 'FoodLabelingThTenantSelect2026Key!';
const keyMaterial = await crypto.subtle.importKey(
'raw',
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(secretKey)),
{ name: 'AES-CBC' },
false,
['decrypt'],
);
const iv = Uint8Array.from(atob(passwordSaltBase64), (c) => c.charCodeAt(0));
const cipher = Uint8Array.from(atob(passwordBase64), (c) => c.charCodeAt(0));
const plain = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, keyMaterial, cipher);
return new TextDecoder().decode(plain);
}
```
### 3.5 安全说明
- 接口为 **匿名可访问**,密文仅降低明文密码在传输/展示中的直接暴露风险,**不能**替代 HTTPS 与生产环境改密策略。
- 解密后的密码仅用于登录表单预填或一键登录;请勿持久化明文密码。
---
## 四、Web 登录:tenantName 替代 tenantId
`tenant-select` 不再返回 `id` 后,Web 登录须使用 **租户名称**。
### 4.1 入参变更
| 字段 | 必填 | 说明 |
|------|------|------|
| `tenantId` | 否 | 与 `tenantName` 二选一 |
| `tenantName` | 否 | 与 `tenantId` 二选一,推荐 |
| `userName` | 是 | 账号或邮箱;`admin` 或 `admin@example.com` 均可 |
| `password` | 是 | 明文密码 |
二者均为空时返回:`请指定租户 Id 或租户名称`。
### 4.2 登录示例(curl)
```bash
curl -X POST "http://127.0.0.1:19002/api/app/th-web-auth/login" \
-H "Content-Type: application/json" \
-d "{\"tenantName\":\"Default\",\"userName\":\"admin\",\"password\":\"123456\"}"
```
### 4.3 出参(不变)
```json
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "...",
"tenantId": "11111111-1111-1111-1111-111111111111",
"tenantName": "Default"
}
```
Token 内仍写入真实 `TenantId` Guid;后续业务 API 行为不变。
---
## 五、泰额 App 登录说明
`POST /api/app/th-app-auth/login` **本次未改**,仍要求入参 `tenantId`(Guid)。
若 App 端也需要去掉 tenantId,需单独改造 `ThAppLoginInputVo` 与 `ThAppAuthAppService`,并与移动端联调。
---
## 六、相关接口(当日其它优化,可选)
- **开通租户** `POST /api/app/th-tenant-provisioning/provision`:创建租户后可在后台异步初始化业务库(`DatabaseInitializing` / `InitializeTenantDatabase`)。
- **同步初始化** `POST /api/app/th-tenant-provisioning/initialize-tenant-database/{tenantId}`:补跑租户库 CodeFirst + Seed。
与本次 `tenant-select` 改造无直接冲突;平台管理员仍通过 provision 创建租户,登录页通过 `tenant-select` + `tenantName` 登录。
---
## 七、改动文件列表(后端)
| 文件 | 变更 |
|------|------|
| `FoodLabeling.Th.Application.Contracts/Dtos/MultiTenancy/ThTenantSelectDto.cs` | 出参字段调整 |
| `FoodLabeling.Th.Application.Contracts/Options/FoodLabelingThTenantSelectCryptoOptions.cs` | 新增加密配置 |
| `FoodLabeling.Th.Application/MultiTenancy/TenantSelectCredentialCipher.cs` | AES-CBC 加密 |
| `FoodLabeling.Th.Application/MultiTenancy/ThTenantSelectConsts.cs` | 默认 admin 账号常量 |
| `FoodLabeling.Th.Application/MultiTenancy/TenantResolveHelper.cs` | 支持 tenantName 解析 |
| `FoodLabeling.Th.Application/Services/ThMultiTenancyAppService.cs` | tenant-select 映射与加密 |
| `FoodLabeling.Th.Application.Contracts/Dtos/Auth/ThWebLoginInputVo.cs` | TenantId 可选 + TenantName |
| `FoodLabeling.Th.Application/Services/ThWebAuthAppService.cs` | 按名称登录 |
| `FoodLabeling.Th.Application/FoodLabelingThApplicationModule.cs` | 注册加密配置 |
| `src/Yi.Abp.Web/appsettings.json` | TenantSelectCrypto 节点 |
---
## 八、前端对接 checklist
- [ ] 更新 `ThTenantSelectDto` 类型,移除 `id`,增加 `creationTime` / `loginAccount` / `password` / `passwordSalt`
- [ ] 配置与后端一致的 `SecretKey`,解密 `password` 后填入登录表单(若需)
- [ ] 登录请求改传 `tenantName: selected.name`,不再依赖 `tenantId`
- [ ] 登录成功后仍保存响应中的 `tenantId` 供业务 API / `__tenant` 头使用
|