跳至主要内容

AuthServer 資源服務與 BFF 整合手冊

公開發布說明:本文中的網域、手機號碼、OTP、Token 與密鑰值均為占位示例,不可直接用於正式環境。

目標受眾:需要與 AuthServer(統一登入認證中心)對接的後端資源服務、BFF 層開發者。 AuthServer 版本:基於 springcloud-aile/authserver 原始碼分析(release 分支)。


一、AuthServer 架構概覽​

┌─────────────────────────────────────────────────────┐
│ AuthServer (:8080) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ JWT 簽發 │ │ Token 驗證 │ │ 信任事件 │ │
│ │ (RS256) │ │ (introspect) │ │ (webhook) │ │
│ └──────┬──────┘ └──────┬───────┘ └──────┬──────┘ │
│ │ │ │ │
│ ┌──────┴────────────────┴──────────────────┴──────┐ │
│ │ JWKS 公鑰分發 │ │
│ │ GET /oauth2/jwks │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│ ▲ ▲
│ 登入 → JWT │ Token 驗證 │ 信任事件推送
▼ │ │
┌──────────┐ ┌──────────┴──┐ ┌───────┴──────────┐
│ 客戶端 │ │ 資源服務 │ │ 資源服務 webhook │
│ (BFF/App) │ │ (SpringCloud)│ │ (接收信任事件) │
└──────────┘ └─────────────┘ └──────────────────┘

核心特徵​

特徵說明
簽名演算法RS256(RSA 2048-bit 金鑰對)
Token 格式JWT(Header: {"alg":"RS256","type":"oauth2","kid":"<SHA-256>"}
Token 傳輸HTTP Header satoken: <jwt_string>
公鑰分發GET /oauth2/jwks 回傳 JWKS 格式公鑰
Token 有效期預設 30 天(jwt.expiration: 2592000 秒)
Token 撤銷支援 jti 級別和 accountId 級別黑名單
客戶端認證OAuth Client(clientId + 可選 clientSecret)

二、JWT Token 結構​

2.1 JWT Header​

{
"alg": "RS256",
"type": "oauth2",
"kid": "<SHA-256 of public key modulus, base64url>"
}

2.2 JWT Payload(Claims)​

Claim型別必填說明
issstring✓簽發者,預設 http://localhost:8080,正式環境應設定為 AuthServer 域名
substring✓帳號唯一標識(accountId)
mobilestring✓手機號碼(不含國家區號)
countryCodestring國家區號,如 +886
iatnumber✓簽發時間(Unix epoch 秒)
expnumber✓過期時間(Unix epoch 秒)
jtistring✓JWT 唯一 ID(UUID 格式),用於精確撤銷
login_methodstring登入方式:3=OTP, 4=PASSWORD, 2=CHANNEL, 7=QRCODE
channelstring應用渠道(來自 deviceData.channel 列舉值)
thirdChannelstring第三方登入渠道(僅第三方登入 token 包含)
scopeIdstring第三方渠道 scopeId(僅第三方登入 token 包含)
rememberMeboolean✓是否記住此裝置登入

2.3 Token 有效期​

預設 30 天,可透過設定調整:

# AuthServer 設定
jwt:
expiration: 2592000 # 秒,預設 30 天

建議:資源服務的本地快取/驗證邏輯應容忍 Token 過期時間的合理時鐘偏差(建議 ±5 分鐘)。


三、資源服務整合​

資源服務:指需要驗證請求方 JWT Token 的後端微服務(如 aile-service-account、aile-service-room 等)。

3.1 整合模式選擇​

模式適用場景複雜度
A. 本地驗證高吞吐服務,避免每次請求都調 AuthServer中等
B. 遠端 Introspect簡單整合,可容忍一次額外 HTTP 呼叫低
C. 混合模式本地驗證 + 信任事件訂閱,兼顧效能與安全性較高

3.2 模式 A:本地 JWT 驗證(推薦)​

原理:資源服務從 JWKS 端點取得 AuthServer 公鑰,在本地驗證 JWT 簽名和有效期。

Step 1:取得 AuthServer 公鑰​

curl -s https://auth.example.com/oauth2/jwks | jq '.'

響應示例:

{
"success": true,
"data": {
"keys": [
{
"kty": "RSA",
"kid": "abc123...base64url",
"use": "sig",
"alg": "RS256",
"n": "<modulus base64url>",
"e": "<exponent base64url>"
}
]
},
"requestId": "..."
}

Step 2:實現本地 JWT 驗證​

Spring Boot 實現(推薦 Filter/Interceptor 模式):

// 1. 定義 JWKS 快取設定
@Configuration
public class AuthServerJwksConfig {

@Value("${auth.server.jwks-uri:http://authserver/oauth2/jwks}")
private String jwksUri;

@Bean
public RSAPublicKey authServerPublicKey() {
// 從 JWKS 端點取得並快取公鑰
// 建議快取 TTL = 1 小時,定期重新整理
JWKSet jwkSet = fetchJwks(jwksUri);
return extractRsaPublicKey(jwkSet);
}
}

// 2. 實現認證攔截器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
String token = request.getHeader("satoken"); // 與 AuthServer 一致的 header 名

if (token == null) {
response.setStatus(401);
response.getWriter().write("{\"error\":\"MISSING_TOKEN\"}");
return;
}

try {
// 解析並驗證 JWT
JWT jwt = JWTUtil.parseToken(token);
// 使用公鑰驗證簽名
boolean valid = jwt.setSigner(JWTSignerUtil.createSigner("RS256", publicKey)).verify();
if (!valid) {
throw new JWTException("簽名無效");
}

// 檢查過期
JSONObject payload = jwt.getPayloads();
long exp = payload.getLong("exp", 0);
if (exp < System.currentTimeMillis() / 1000) {
throw new JWTException("令牌已過期");
}

// 提取使用者資訊
String accountId = payload.getStr("sub");
request.setAttribute("accountId", accountId);
request.setAttribute("tokenClaims", payload);

chain.doFilter(request, response);
} catch (Exception e) {
response.setStatus(401);
response.getWriter().write("{\"error\":\"INVALID_TOKEN\"}");
}
}
}

Step 3:使用統一的 Token Header 名稱​

# 與 AuthServer 設定保持一致,預設為 satoken
sa-token.token-name=satoken

非 Java 實現參考:

Python(PyJWT + cryptography):

import jwt
import requests
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
import base64
import json

class AuthServerValidator:
def __init__(self, jwks_url, issuer):
self.jwks_url = jwks_url
self.issuer = issuer
self._public_keys = {} # kid -> public_key

def _fetch_jwks(self):
resp = requests.get(self.jwks_url)
jwks = resp.json()["data"]
for key in jwks["keys"]:
kid = key["kid"]
# 將 JWK 轉為 PEM
from cryptography.hazmat.primitives.asymmetric import rsa
n = int.from_bytes(base64.urlsafe_b64decode(key["n"] + "=="), "big")
e = int.from_bytes(base64.urlsafe_b64decode(key["e"] + "=="), "big")
public_key = rsa.RSAPublicNumbers(e, n).public_key(default_backend())
self._public_keys[kid] = public_key

def validate(self, token: str) -> dict:
# 先解析 header 取得 kid
unverified = jwt.get_unverified_header(token)
kid = unverified.get("kid")

if kid not in self._public_keys:
self._fetch_jwks()

public_key = self._public_keys[kid]
claims = jwt.decode(
token,
public_key,
algorithms=["RS256"],
issuer=self.issuer,
options={"verify_exp": True}
)
return claims

Go(golang-jwt):

import (
"crypto/rsa"
"encoding/base64"
"math/big"
"github.com/golang-jwt/jwt/v5"
)

func validateToken(tokenString string, publicKey *rsa.PublicKey, issuer string) (*jwt.MapClaims, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return publicKey, nil
},
jwt.WithIssuer(issuer),
jwt.WithLeeway(5*time.Minute), // 5 分鐘時鐘偏差容忍
)
if err != nil {
return nil, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token")
}
return &claims, nil
}

Node.js(jsonwebtoken + jwks-rsa):

import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';

const client = jwksClient({
jwksUri: 'https://auth.example.com/oauth2/jwks',
cache: true,
cacheMaxEntries: 5,
cacheMaxAge: 3600000, // 1 小時
});

function getKey(header: jwt.JwtHeader, callback: jwt.SigningKeyCallback) {
client.getSigningKey(header.kid, (err, key) => {
if (err) return callback(err);
callback(null, key.getPublicKey());
});
}

export async function validateToken(token: string): Promise<jwt.JwtPayload> {
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, {
algorithms: ['RS256'],
issuer: 'https://auth.example.com',
}, (err, decoded) => {
if (err) return reject(err);
resolve(decoded as jwt.JwtPayload);
});
});
}

3.3 模式 B:遠端 Introspect 驗證​

適用場景:不想管理公鑰快取,每次驗證都調 AuthServer。

資源服務 ──GET /oauth2/introspect──▶ AuthServer
│ Header: satoken: <jwt>
│
◀── 200 { active: true/false, sub, exp, iat, jti }
# 驗證 Token
curl -s https://auth.example.com/oauth2/introspect \
-H "satoken: <使用者的JWT>" | jq '.'

響應:

{
"success": true,
"data": {
"active": true,
"sub": "acc_xxx",
"iss": "http://localhost:8080",
"exp": 1750000000,
"iat": 1720000000,
"jti": "uuid-xxx"
},
"requestId": "..."
}

注意:Introspect 端點本身需要攜帶有效的 satoken 頭。對於服務間呼叫,建議使用模式 A(本地驗證)+ 模式 C(信任事件),而不是讓每個資源服務都持有一個"服務帳號"的 Token 去做 introspect。

3.4 模式 C:混合模式(本地驗證 + 信任事件訂閱)​

推薦用於正式環境。在模式 A 的基礎上增加 Token 撤銷事件的實時感知。

Step 1:註冊資源服務 webhook​

curl -s -X POST https://auth.example.com/internal/v1/resource-servers/register \
-H "X-AS-Registration-Token: <registration-token>" \
-H "Content-Type: application/json" \
-d '{
"serviceCode": "aile-service-account",
"serviceName": "Account Service",
"webhookUrl": "https://account-service.example.com/auth/events",
"webhookSecret": "<webhook-hmac-secret>",
"eventTypes": ["TokenInvalidated", "AccountTokensInvalidated", "ResourcePublicKeyRemoved"],
"enabled": true
}'

Step 2:實現 webhook 接收端點​

@RestController
@RequestMapping("/auth/events")
public class AuthEventController {

@Value("${auth.server.webhook.secret}")
private String webhookSecret;

@PostMapping
public ResponseEntity<Void> handleEvent(
@RequestHeader("X-AS-Event-Id") String eventId,
@RequestHeader("X-AS-Event-Type") String eventType,
@RequestHeader("X-AS-Timestamp") String timestamp,
@RequestHeader("X-AS-Signature") String signature,
@RequestBody String body
) {
// 1. 驗證 HMAC 簽名
String expectedSig = hmacSha256(webhookSecret, timestamp + "." + body);
if (!MessageDigest.isEqual(expectedSig.getBytes(), signature.getBytes())) {
return ResponseEntity.status(403).build();
}

// 2. 處理事件
JsonNode event = objectMapper.readTree(body);
switch (eventType) {
case "TokenInvalidated":
String jti = event.get("jti").asText();
localBlacklist.add(jti); // 加入本地黑名單
break;
case "AccountTokensInvalidated":
String accountId = event.get("accountId").asText();
accountBlacklist.add(accountId);
break;
case "ResourcePublicKeyRemoved":
refreshPublicKeyCache(); // 重新整理公鑰快取
break;
}

return ResponseEntity.ok().build();
}

private String hmacSha256(String secret, String data) {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
}
}

Step 3:驗證時加入本地黑名單檢查​

// 在 JwtAuthenticationFilter 中增加黑名單檢查
if (localBlacklist.contains(jti) || accountBlacklist.contains(accountId)) {
response.setStatus(401);
response.getWriter().write("{\"error\":\"TOKEN_REVOKED\"}");
return;
}

3.5 Token 撤銷​

資源服務需要知道何時 Token 被撤銷。兩種方式:

方式機制
主動查詢呼叫 GET /oauth2/introspect,檢查 active 欄位
被動接收註冊 webhook,接收 TokenInvalidated / AccountTokensInvalidated 事件

AuthServer 的 Token 黑名單儲存在 Redis 中,資源服務無法直接存取。建議在資源服務側維護一個程序內(或本地快取)的黑名單,透過 webhook 事件來更新。


四、BFF 層整合​

BFF(Backend For Frontend) 層:面向客戶端的 API 閘道器或聚合層,負責接收客戶端請求、管理會話、轉發給下游資源服務。

4.1 BFF 的職責​

客戶端 App / Web
│
▼
┌──────────────────────┐
│ BFF (aile-gateway │
│ 或 自建 BFF) │
│ │
│ ① 管理使用者登入會話 │
│ ② 透傳 satoken 到 │
│ 下游資源服務 │
│ ③ 呼叫 AuthServer │
│ 登入/驗證 API │
└──────┬───────────────┘
│ Feign / HTTP + satoken header
▼
┌──────────────┐
│ 資源服務 │
│ (本地驗證JWT) │
└──────────────┘

4.2 BFF 呼叫 AuthServer 登入 API​

場景一:OTP 簡訊驗證碼登入​

# Step 1: 傳送 OTP(取得 onceToken)
curl -s -X POST https://auth.example.com/api/v1/otp/send \
-H "Content-Type: application/json" \
-d '{
"countryCode": "<country-code>",
"mobile": "<mobile-number>"
}'

# 響應:{ "data": { "onceToken": "ot_xxx" } }

# Step 2: 使用 OTP + onceToken 完成登入
curl -s -X POST https://auth.example.com/api/v1/login/otp \
-H "Content-Type: application/json" \
-H "deviceData: {...}" \
-d '{
"onceToken": "ot_xxx",
"otp": "<otp-code>",
"clientId": "aile-web"
}'

# 響應:
# 200: { "data": { "token": "<JWT>", "newAccount": false, "accountId": "acc_xxx" } }
# 201: 新註冊使用者(newAccount=true)

場景二:帳號密碼登入​

curl -s -X POST https://auth.example.com/api/v1/login/password \
-H "Content-Type: application/json" \
-d '{
"countryCode": "<country-code>",
"mobile": "<mobile-number>",
"password": "<password>",
"clientId": "aile-web"
}'

# 響應:{ "data": { "token": "<JWT>", "tokenType": "Bearer", "expiresIn": 2592000, "accountId": "acc_xxx", "phone": "<mobile-number>" } }

場景三:第三方渠道登入(微信/LINE/Apple 等)​

curl -s -X POST https://auth.example.com/api/v1/login/channel \
-H "Content-Type: application/json" \
-d '{
"thirdChannel": "WECHAT",
"scopeId": "openid_xxx",
"clientId": "aile-web"
}'

# 已繫結手機號:{ "data": { "needBindPhone": false, "token": "<JWT>", ... } }
# 未繫結手機號:{ "data": { "needBindPhone": true, "bindToken": "bt_xxx", "bindTokenExpiresIn": 600 } }

4.3 BFF 透傳 Token 到下游資源服務​

BFF 拿到 AuthServer 簽發的 JWT 後,應將其透傳給下游資源服務:

// Feign 攔截器自動注入 satoken header
@Component
public class AuthTokenFeignInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate template) {
// 從當前請求上下文取得 token
String token = AuthContext.getCurrentToken();
if (token != null) {
template.header("satoken", token); // 與 AuthServer 設定一致
}
}
}
// 請求上下文(ThreadLocal)
public class AuthContext {
private static final ThreadLocal<String> TOKEN_HOLDER = new ThreadLocal<>();
private static final ThreadLocal<String> ACCOUNT_ID_HOLDER = new ThreadLocal<>();

public static void setToken(String token) { TOKEN_HOLDER.set(token); }
public static String getCurrentToken() { return TOKEN_HOLDER.get(); }
public static void setAccountId(String id) { ACCOUNT_ID_HOLDER.set(id); }
public static String getCurrentAccountId() { return ACCOUNT_ID_HOLDER.get(); }
public static void clear() { TOKEN_HOLDER.remove(); ACCOUNT_ID_HOLDER.remove(); }
}
// BFF Filter
@WebFilter(urlPatterns = "/api/*")
public class BffAuthFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
String token = request.getHeader("satoken");

if (token == null) {
response.setStatus(401);
return;
}

try {
// 可選:先驗證一次 JWT
TokenClaims claims = tokenService.parseAndValidate(token);
AuthContext.setToken(token);
AuthContext.setAccountId(claims.getSubject());
chain.doFilter(request, response);
} catch (Exception e) {
response.setStatus(401);
} finally {
AuthContext.clear();
}
}
}

4.4 登出​

curl -s -X POST https://auth.example.com/api/v1/login/logout \
-H "satoken: <JWT>"

# 響應始終 200(冪等)

登出後,AuthServer 將 JWT 加入黑名單。如果資源服務訂閱了 TokenInvalidated webhook 事件,也會收到相應通知。

4.5 Account Scope 查詢​

BFF 在新使用者註冊前可查詢使用者是否已存在於生態中:

curl -s -X POST https://auth.example.com/account/scope/v1/real/check \
-H "Content-Type: application/json" \
-d '{
"channel": "WECHAT",
"scopeId": "openid_xxx"
}'

# 響應:{ "data": true } // true=已存在且繫結手機號, false=不存在或未繫結

4.6 OAuth Client 註冊(BFF 作為客戶端)​

如果你的 BFF 是新客戶端,需要在 AuthServer 註冊:

# 透過 Admin API 註冊(需要 admin 許可權)
curl -s -X POST https://auth.example.com/admin/v1/oauth-clients \
-H "Content-Type: application/json" \
-H "satoken: <admin-token>" \
-d '{
"clientId": "my-bff-app",
"clientName": "My BFF Application",
"requireSecret": true,
"clientSecret": "<oauth-client-secret>",
"allowOtpLogin": true,
"allowPasswordLogin": true,
"allowChannelLogin": true,
"remark": "自建 BFF 客戶端"
}'

客戶端設定項說明:

欄位說明
clientId客戶端唯一標識
clientName顯示名稱
requireSecret是否要求 clientSecret(true=機密客戶端, false=公開客戶端)
allowOtpLogin是否允許 OTP 登入
allowPasswordLogin是否允許密碼登入
allowChannelLogin是否允許渠道登入
enabled是否啟用

五、安全最佳實踐​

5.1 Token 傳輸安全​

客戶端 ──HTTPS──▶ BFF ──內部網路 HTTP──▶ 資源服務
satoken satoken 本地驗證
  • 客戶端 ↔ BFF:必須使用 HTTPS,防止 Token 被中間人截獲。
  • BFF ↔ 資源服務:內網環境可 HTTP,但應透過網路策略隔離。
  • 不要在 URL 參數中傳遞 Token。

5.2 公鑰輪轉​

AuthServer 支援金鑰輪轉(透過 JWKS 回傳多把 key,用 kid 區分):

  1. AuthServer 更換金鑰對(更新 jwt.private-key / jwt.public-key 設定)。
  2. JWKS 端點回傳新 kid。
  3. 資源服務從 JWKS 重新整理公鑰快取。
  4. 新簽發的 JWT 使用新 kid,舊 JWT 在有效期內仍可用舊公鑰驗證。

建議:資源服務的公鑰快取 TTL 設定為 1 小時以內,確保能及時感知金鑰輪轉。

5.3 Token 黑名單管理​

  • jti 黑名單:精確撤銷單個 Token(使用者主動登出)。
  • accountId 黑名單:撤銷某帳號所有 Token(密碼修改、帳號停用)。
  • 黑名單儲存在 Redis 中。資源服務不應直接存取 AuthServer 的 Redis,而應透過 webhook 事件取得撤銷通知。
  • 本地黑名單建議僅保留 token 剩餘有效期內(最長 30 天)的資料,定期清理過期條目。

5.4 時鐘同步​

JWT 的 iat 和 exp 基於 Unix 時間戳。所有服務(AuthServer + 資源服務 + BFF)必須透過 NTP 保持時鐘同步。


六、設定參考​

6.1 AuthServer 關鍵設定​

# bootstrap.yml / Nacos
server:
port: 8080

jwt:
issuer: https://auth.your-domain.com # 正式環境改為實際域名
expiration: 2592000 # 30 天
private-key-path: ${JWT_PRIVATE_KEY_PATH} # 私鑰由安全儲存掛載
public-key-path: ${JWT_PUBLIC_KEY_PATH} # 公鑰由部署環境掛載

sa-token:
token-name: satoken # HTTP Header 名稱

as:
resource-server:
registration-token: "<registration-token>" # 資源服務註冊所需令牌

6.2 資源服務關鍵設定​

# 資源服務側
auth:
server:
jwks-uri: https://auth.example.com/oauth2/jwks
issuer: https://auth.your-domain.com
local-blacklist:
ttl: 2592000 # 黑名單 TTL,建議設為 token 有效期
max-size: 10000 # 最大快取條目
webhook:
secret: "${AUTH_WEBHOOK_SECRET}" # HMAC 簽名金鑰

6.3 BFF 關鍵設定​

# BFF 側
auth:
server:
base-url: https://auth.example.com
client:
id: my-bff-app
secret: "${AUTH_CLIENT_SECRET}"
token:
header-name: satoken # 與 AuthServer 一致

七、驗證清單​

整合完成後,逐項驗證:

  • 公鑰取得:GET /oauth2/jwks 回傳正確的 RSA 公鑰(含 kid, n, e)。
  • JWT 簽名驗證:資源服務用公鑰驗證 AuthServer 簽發的 JWT 透過。
  • JWT 過期驗證:過期 Token 被正確拒絕。
  • 黑名單驗證:撤銷的 Token(jti 或 accountId)被正確拒絕。
  • 登入流程:BFF → AuthServer 登入 API 正常回傳 JWT。
  • Token 透傳:BFF 將 satoken 正確透傳給下游資源服務。
  • 登出流程:登出後舊 Token 被撤銷。
  • Webhook 事件:資源服務能接收並正確驗證 AuthServer 推送的信任事件簽名。
  • 公鑰輪轉:AuthServer 更換金鑰後,資源服務能在快取 TTL 內切換到新公鑰。

八、常見問題​

Q1:資源服務能直接存取 AuthServer 的資料庫/Redis 嗎?

不能,也不應該。資源服務應透過 JWKS 端點取得公鑰,透過 webhook 接收信任事件。AuthServer 的內部儲存是實現細節,不應暴露給其他服務。

Q2:Introspect 端點需要認證嗎?

GET /oauth2/introspect 端點本身被 SaTokenJwtAuthInterceptor 保護(見 WebMvcAuthConfig.java),需要一個有效的 satoken 頭。這適合使用者 Token 的自檢場景。對於服務間驗證,建議使用本地 JWT 驗證(模式 A)。

Q3:如果 Token 被盜用怎麼辦?

AuthServer 支援 Token 撤銷(revoke):POST /oauth2/revoke 將 Token 加入黑名單。資源服務透過 webhook 事件感知撤銷,拒絕已撤銷的 Token。

Q4:客戶端需要同時支援 Web 和 App,如何區分?

透過 deviceData header 中的 channel 欄位區分。Token 的 channel claim 會記錄簽發時的渠道資訊。

Q5:Token 過期了怎麼辦?

客戶端需要重新登入取得新 Token。BFF 可以攔截 401 響應,引導客戶端重新整理登入態。


文件版本: v1.0
最後更新: 2026-07-14
基於原始碼: springcloud-aile/authserver release 分支