← 中文资源

简体中文版本

令牌受众校验

aud 声明指名令牌写给哪个 API;接受为其他服务铸造的令牌——或干脆跳过受众检查——会让一张令牌四处通行。

阅读英文原文

概念定义

令牌是信不是钥匙串:它是写给某个收件人的。aud 声明就是信封上的地址。签名无误、但写给别的收件人的信递给你,也不是你的信。

工作机制

校验是声明链:iss 说明谁铸造、aud 说明给哪个 API、exp 与 nbf 限定时间、azp 与 scope 限定权限。每个 API 把自己的受众值当作服务器常量检查。网关可以转发令牌但不能洗白它——转发不等于校验。

使用场景

车队里每个 API,包括内部服务。集中式 SDK 应把受众设为必填;自写中间件是检查最常丢失的地方。集成测试应提交跨受众令牌并断言被拒。

常见误区

签名有效不等于令牌属于你;共享签发者恰恰是为了让每个 API 的校验各不相同。把一个受众值复用到多个 API 图省事,等于拆掉隔离。泄露令牌的爆炸半径随每个跳过 aud 的服务成倍放大。委托流程中的多受众与令牌交换需要显式处理;走内省端点的服务若跳过返回的受众字段,会在服务端复刻同一个洞。

代码示例

import unicodedata

SCRIPT_RANGES = {
    "LATIN": [(0x0041, 0x024F), (0x1E00, 0x1EFF)],
    "CYRILLIC": [(0x0400, 0x04FF)],
    "GREEK": [(0x0370, 0x03FF)],
}


def script_of(char: str) -> str:
    code = ord(char)
    for name, ranges in SCRIPT_RANGES.items():
        if any(start <= code <= end for start, end in ranges):
            return name
    return "OTHER"


def find_mixed_scripts(text: str) -> list[str]:
    scripts = {script_of(char) for char in text if char.isalpha()}
    return sorted(scripts - {"OTHER"})


def is_suspicious(text: str) -> bool:
    scripts = find_mixed_scripts(text)
    return "LATIN" in scripts and len(scripts) > 1


def explain(text: str) -> list[dict[str, str]]:
    return [
        {
            "char": char,
            "name": unicodedata.name(char, "UNKNOWN"),
            "script": script_of(char),
        }
        for char in text
        if char.isalpha() and script_of(char) not in {"LATIN", "OTHER"}
    ]


if __name__ == "__main__":
    for sample in ["paypal", "pаypal", "admin"]:
        print(sample, find_mixed_scripts(sample), is_suspicious(sample))
    print(explain("pаypal"))
import hmac
import secrets

ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'


def api_key(prefix: str = 'sk') -> str:
    body = ''.join(secrets.choice(ALPHABET) for _ in range(40))
    return f'{prefix}_{body}'


def url_token(nbytes: int = 32) -> str:
    return secrets.token_urlsafe(nbytes)


def otp_code(digits: int = 6) -> str:
    # 朴素做法 random.randint 可预测,必须用 secrets
    floor = 10 ** (digits - 1)
    return str(floor + secrets.randbelow(9 * floor))


def constant_time_match(candidate: str, expected: str) -> bool:
    if len(candidate) != len(expected):
        return False
    return hmac.compare_digest(
        candidate.encode(), expected.encode()
    )


def scoped_token(scope: str, nbytes: int = 16) -> dict:
    return {
        'token': url_token(nbytes),
        'scope': scope,
        'hint': f'scope={scope}',
    }


key = api_key()
code = otp_code()
print('key length:', len(key), 'otp:', code)
print('compare ok:', constant_time_match(code, code))
print('compare tampered:', constant_time_match(code, '123456'))