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            本地驗證

5.2 公鑰輪轉

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

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

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

5.3 Token 黑名單管理

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 一致

七、驗證清單

整合完成後,逐項驗證:


八、常見問題

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 分支