perf(struct): ddd
This commit is contained in:
@@ -7,11 +7,12 @@
|
||||
- 登录页:`{IAM_FRONT_BASE_URL}/login?clientId={clientId}&tenantId={tenantId}&callback={encodeURIComponent(redirectUri)}`
|
||||
- 授权码(code):JWT(HS256),有效期 5 分钟,Redis 单次使用
|
||||
- 换取 token:业务服务端携带 `clientSecret` 调用
|
||||
- `POST {IAM_SERVICE_BASE_URL}/iam/api/v1/auth/code2token`
|
||||
- `POST {IAM_SERVICE_BASE_URL}/api/v1/auth/code2token`(外部第三方:必须携带 tenant_id 并校验 clientId/clientSecret)
|
||||
- `POST {IAM_SERVICE_BASE_URL}/api/v1/internal/auth/code2token`(内部服务:需 `X-Internal-Token` 预共享密钥,不强制 tenant_id)
|
||||
- 签发授权码:由 IAM 服务端完成(校验 redirectUri 是否在该 clientId 的 allowlist 中)
|
||||
- `POST {IAM_SERVICE_BASE_URL}/iam/api/v1/auth/login-code`
|
||||
- token 刷新:`POST {IAM_SERVICE_BASE_URL}/iam/api/v1/auth/refresh`
|
||||
- 退出:`POST {IAM_SERVICE_BASE_URL}/iam/api/v1/auth/logout`
|
||||
- `POST {IAM_SERVICE_BASE_URL}/api/v1/auth/login-code`
|
||||
- token 刷新:`POST {IAM_SERVICE_BASE_URL}/api/v1/auth/refresh`
|
||||
- 退出:`POST {IAM_SERVICE_BASE_URL}/api/v1/auth/logout`
|
||||
|
||||
## 2. 业务 Next.js(前端)接入:中间件跳转登录
|
||||
|
||||
@@ -20,45 +21,46 @@
|
||||
在业务项目根目录创建 `/src/middleware.ts`:
|
||||
|
||||
```ts
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
function isExpired(jwt: string): boolean {
|
||||
const parts = jwt.split(".")
|
||||
if (parts.length < 2) return true
|
||||
const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/")
|
||||
const padLen = (4 - (normalized.length % 4)) % 4
|
||||
const json = atob(normalized + "=".repeat(padLen))
|
||||
const payload = JSON.parse(json) as { exp?: number }
|
||||
if (!payload.exp) return true
|
||||
return Math.floor(Date.now() / 1000) >= payload.exp
|
||||
const parts = jwt.split(".");
|
||||
if (parts.length < 2) return true;
|
||||
const normalized = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
||||
const padLen = (4 - (normalized.length % 4)) % 4;
|
||||
const json = atob(normalized + "=".repeat(padLen));
|
||||
const payload = JSON.parse(json) as { exp?: number };
|
||||
if (!payload.exp) return true;
|
||||
return Math.floor(Date.now() / 1000) >= payload.exp;
|
||||
}
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const tenantId = req.headers.get("x-tenant-id") ?? ""
|
||||
const accessToken = req.cookies.get("accessToken")?.value ?? ""
|
||||
const tenantId = req.headers.get("x-tenant-id") ?? "";
|
||||
const accessToken = req.cookies.get("accessToken")?.value ?? "";
|
||||
|
||||
if (!accessToken || isExpired(accessToken)) {
|
||||
const currentUrl = req.nextUrl.clone()
|
||||
const currentUrl = req.nextUrl.clone();
|
||||
const callback = `${process.env.CMS_SERVICE_BASE_URL}/auth/callback?next=${encodeURIComponent(
|
||||
currentUrl.toString(),
|
||||
)}`
|
||||
)}`;
|
||||
const loginUrl = `${process.env.IAM_FRONT_BASE_URL}/login?clientId=${encodeURIComponent(
|
||||
process.env.CMS_CLIENT_ID ?? "",
|
||||
)}&tenantId=${encodeURIComponent(
|
||||
tenantId,
|
||||
)}&callback=${encodeURIComponent(callback)}`
|
||||
return NextResponse.redirect(loginUrl, 302)
|
||||
)}&callback=${encodeURIComponent(callback)}`;
|
||||
return NextResponse.redirect(loginUrl, 302);
|
||||
}
|
||||
|
||||
return NextResponse.next()
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/((?!_next/static|_next/image|favicon.ico|api/public).*)"],
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 示例使用 cookie `accessToken`;你也可以改为本地存储或自定义 header。
|
||||
- 校验是否过期仅做 payload 解析(不验签);服务端真正鉴权仍应使用 `auth-kit` + RS256 验签。
|
||||
|
||||
@@ -89,7 +91,7 @@ async fn sso_callback(Query(q): Query<CallbackQuery>) -> Redirect {
|
||||
|
||||
let http = reqwest::Client::new();
|
||||
let resp = http
|
||||
.post(format!("{}/iam/api/v1/auth/code2token", iam_base.trim_end_matches('/')))
|
||||
.post(format!("{}/api/v1/auth/code2token", iam_base.trim_end_matches('/')))
|
||||
.json(&serde_json::json!({
|
||||
"code": q.code,
|
||||
"clientId": client_id,
|
||||
@@ -131,15 +133,16 @@ pub fn router() -> Router {
|
||||
### 3.2 后端鉴权与自动刷新
|
||||
|
||||
推荐:
|
||||
|
||||
- 业务后端对受保护接口强制 `Authorization: Bearer <access_token>`(来自 cookie 或前端 header)。
|
||||
- 过期时由业务后端调用 `POST /iam/api/v1/auth/refresh`,轮换 refresh token,并回写 cookie。
|
||||
- 过期时由业务后端调用 `POST /api/v1/auth/refresh`,轮换 refresh token,并回写 cookie。
|
||||
|
||||
## 4. clientId / clientSecret 管理
|
||||
|
||||
由平台管理员通过 IAM 平台接口创建与轮换(仅示例,实际需要具备平台权限码 `iam:client:*`):
|
||||
|
||||
```bash
|
||||
curl -X POST "$IAM_SERVICE_BASE_URL/iam/api/v1/platform/clients" \
|
||||
curl -X POST "$IAM_SERVICE_BASE_URL/api/v1/platform/clients" \
|
||||
-H "Authorization: Bearer $PLATFORM_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "clientId": "cms", "name": "CMS", "redirectUris": ["https://cms-api.example.com/auth/callback"] }'
|
||||
@@ -148,14 +151,14 @@ curl -X POST "$IAM_SERVICE_BASE_URL/iam/api/v1/platform/clients" \
|
||||
轮换:
|
||||
|
||||
```bash
|
||||
curl -X POST "$IAM_SERVICE_BASE_URL/iam/api/v1/platform/clients/cms/rotate-secret" \
|
||||
curl -X POST "$IAM_SERVICE_BASE_URL/api/v1/platform/clients/cms/rotate-secret" \
|
||||
-H "Authorization: Bearer $PLATFORM_TOKEN"
|
||||
```
|
||||
|
||||
更新允许回调地址(redirectUris):
|
||||
|
||||
```bash
|
||||
curl -X PUT "$IAM_SERVICE_BASE_URL/iam/api/v1/platform/clients/cms/redirect-uris" \
|
||||
curl -X PUT "$IAM_SERVICE_BASE_URL/api/v1/platform/clients/cms/redirect-uris" \
|
||||
-H "Authorization: Bearer $PLATFORM_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "redirectUris": ["https://cms-api.example.com/auth/callback"] }'
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
"clientId": "cms",
|
||||
"clientSecret": "2adbc0d720b687a6d05df32942c2919b0adcbd579c23ecd9cbb27f7a7a7e3326"
|
||||
}
|
||||
{
|
||||
"clientId": "cms2",
|
||||
"clientSecret": "76ac6841b28271389ba1ff133fb9295a08c197edadc6d7cfdadb8e155ef30dc1"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -33,8 +37,8 @@
|
||||
"code": 0,
|
||||
"message": "Success",
|
||||
"data": {
|
||||
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImxvY2FsLWRldiJ9.eyJzdWIiOiI3YTgzYmJjMC0yZjA1LTQwMDItOGFmZC0yYWI1M2RkZDMxNWIiLCJ0ZW5hbnRfaWQiOiI0ZDc3OTQxNC1kYTA0LTQ5YzMtYjM0Mi1kYWJmOTNiNmExMTkiLCJleHAiOjE3NzAwMjU5OTQsImlhdCI6MTc3MDAyNTA5NCwiaXNzIjoiaWFtLXNlcnZpY2UiLCJyb2xlcyI6WyJBZG1pbiIsIm1hbmFnZXIiXSwicGVybWlzc2lvbnMiOlsiY21zOmFydGljbGU6Y3JlYXRlIiwiY21zOmFydGljbGU6ZWRpdCIsImNtczphcnRpY2xlOnB1Ymxpc2giLCJjbXM6Y2F0ZWdvcnk6bWFuYWdlIiwiY21zOm1lZGlhOm1hbmFnZSIsImNtczpzZXR0aW5nczptYW5hZ2UiLCJpYW06Y2xpZW50OnJlYWQiLCJpYW06Y2xpZW50OndyaXRlIiwicm9sZTpyZWFkIiwicm9sZTp3cml0ZSIsInRlbmFudDpyZWFkIiwidGVuYW50OndyaXRlIiwidXNlcjpwYXNzd29yZDpyZXNldDphbnkiLCJ1c2VyOnJlYWQiLCJ1c2VyOndyaXRlIl0sImFwcHMiOlsiY21zIl0sImFwcHNfdmVyc2lvbiI6MX0.NNfdO14PRxkLa5Kkiz5SZ0tDnbrXvVTgOsU65Xg8jSbowrRsbdO3N_fBpEaSxJ3n2DhtD0uYZyRABuCBVWCncxk0RDUWXhoHVXucEFA1Br6I4niZTfIbnv-L1M-Q1fNvPGZE2DQ8Os9K5b2F91kpcwkaR-vQgE9oyFeq1xhQ-MR7YeQLXgLk9UQpWyD2Yj3VIWyFYiG94JX9eI6iJsOJZayqSXaeid50c5R4Z9lq9SQ07ZmFTqZFitCrPrQRY_wh6OeeQrHF33HMKC3yQ1jq4XyiNlDIzLIzDerUpK5UtLdz9Cntt31yg-2tsj2nSMUZLssllMZZaPjFUTMFeu0egQ",
|
||||
"refresh_token": "bd60d869926bac781dd04ad4b340f79624c4da35373c85865bd4627093714e2e",
|
||||
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImxvY2FsLWRldiJ9.eyJzdWIiOiI3YTgzYmJjMC0yZjA1LTQwMDItOGFmZC0yYWI1M2RkZDMxNWIiLCJ0ZW5hbnRfaWQiOiI0ZDc3OTQxNC1kYTA0LTQ5YzMtYjM0Mi1kYWJmOTNiNmExMTkiLCJleHAiOjE3NzAwOTc2NzYsImlhdCI6MTc3MDA5Njc3NiwiaXNzIjoiaWFtLXNlcnZpY2UiLCJyb2xlcyI6WyJBZG1pbiIsIm1hbmFnZXIiXSwicGVybWlzc2lvbnMiOlsiY21zOmFydGljbGU6Y3JlYXRlIiwiY21zOmFydGljbGU6ZWRpdCIsImNtczphcnRpY2xlOnB1Ymxpc2giLCJjbXM6Y2F0ZWdvcnk6bWFuYWdlIiwiY21zOm1lZGlhOm1hbmFnZSIsImNtczpzZXR0aW5nczptYW5hZ2UiLCJpYW06Y2xpZW50OnJlYWQiLCJpYW06Y2xpZW50OndyaXRlIiwicm9sZTpyZWFkIiwicm9sZTp3cml0ZSIsInRlbmFudDpyZWFkIiwidGVuYW50OndyaXRlIiwidXNlcjpwYXNzd29yZDpyZXNldDphbnkiLCJ1c2VyOnJlYWQiLCJ1c2VyOndyaXRlIl0sImFwcHMiOlsiY21zIl0sImFwcHNfdmVyc2lvbiI6MX0.Tt7fEQj8wtS5XzJ-GuwRF5yiOrtGaRr9P_V5RqNZagLXj0eRikf9U4oNkFS2uy8Pp75Ks4jL816DzeLXQsXZJfWEtvlTp0QmwyOhYIN1p-yyGS9Pitl0gb7wobjStGDyMSD-ctbHgEsU41qLrQd6ZQkpFl2IpaCSfCN0JZCpc7_3BeI6YLwAw_K4-TFF_1OTNRPm4sT3RnEZYOHXm6EcOUk-MsDBy1itADCdEEUdCSoslK6FGHIpbhkgA56Z7Qy5909BxXW34I21c2rZX-R_iB9q_eKzWd0GqBMIZi33ITbRy4F7_CtCQSwFNUt6-lvVvzXYLHsVQchcpOdYtj3h6w",
|
||||
"refresh_token": "7483624e8942aee112e62b2b58ec902fb01731dd7b098b4af6001e350b2303f0",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 900
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user