覆盖每种 API 认证方式的测试端点
Basic、Digest、Bearer 令牌、API 密钥、HMAC 签名、JWT、OAuth 1.0a 和带 PKCE 的 OAuth 2.0,附凭据。
示例最后一次针对 API 运行于
想在不搭建身份提供方的情况下测试 API 客户端的认证,可以用 https://api.testingapis.com/api/auth登录后查看 上的公共测试端点。每种方式一个端点:HTTP Basic、Digest、固定的 Bearer 令牌、放在请求头、查询参数或 Cookie 中的 API 密钥,以及 HMAC 请求签名。JWT 登录提供刷新令牌和用于反向测试的过期令牌,OAuth 2.0 服务器支持 PKCE 和客户端凭据, OAuth 1.0a 服务器会校验签名请求。凭据是固定且公开的,列在下面,也可以通过 GET /api/auth登录后查看 查看。凭据错误会返回规范的 401,有效令牌但缺少相应角色会返回 403。
端点与凭据
| 方式 | 端点 | 凭据 |
|---|---|---|
| HTTP Basic | GET /api/auth/basic登录后查看 | apibee / password123 |
| HTTP Digest(MD5,qop=auth) | GET /api/auth/digest登录后查看 | apibee / password123 |
| Bearer 令牌 | GET /api/auth/bearer登录后查看 | Authorization: Bearer apibee-token-123 |
| API 密钥(请求头) | GET /api/auth/api-key/header登录后查看 | X-API-Key: apibee-key-123 |
| API 密钥(查询参数) | GET /api/auth/api-key/query登录后查看 | ?api_key=apibee-key-123 |
| API 密钥(Cookie) | GET /api/auth/api-key/cookie登录后查看 | Cookie: api_key=apibee-key-123 |
| HMAC-SHA256 签名 | POST /api/auth/hmac登录后查看 | 密钥 apibee-hmac-secret |
| 角色与 scope | GET /api/auth/roles/admin登录后查看 | admin-token, user-token, readonly-token |
| JWT | POST /api/auth/jwt/login登录后查看 | admin / admin123、user / user123 |
| OAuth 2.0 | /api/auth/oauth/*登录后查看 | apibee-client / apibee-secret,公共客户端 apibee-public |
| OAuth 1.0a | /api/auth/oauth1/*登录后查看 | Consumer apibee-consumer-key / apibee-consumer-secret,令牌 apibee-access-token / apibee-token-secret |
Basic 与 Digest
密码缺失或错误时返回 401,并带有 WWW-Authenticate 响应头,正是它让浏览器和 HTTP 库提示输入凭据或带上凭据重试:
$ curl -i -u apibee:password123 https://api.testingapis.com/api/auth/basic
HTTP/1.1 200 OK
{"authenticated":true,"scheme":"basic","user":"apibee"}
$ curl -i https://api.testingapis.com/api/auth/basic
HTTP/1.1 401 Unauthorized
www-authenticate: Basic realm="apibee", charset="UTF-8"
{"status":401,"error":"Unauthorized","message":"Missing or malformed Basic Authorization header."}Digest 需要两次往返。第一个 401 带有一个 nonce,客户端用密码的哈希而不是密码本身来回应。curl 的 --digest 会完成这两步:
$ curl -i --digest -u apibee:password123 https://api.testingapis.com/api/auth/digest
HTTP/1.1 401 Unauthorized
www-authenticate: Digest realm="apibee", qop="auth", algorithm=MD5, nonce="MTc5...", opaque="069d..."
HTTP/1.1 200 OK
{"authenticated":true,"scheme":"digest","user":"apibee","qop":"auth","nc":"00000001"}要测试需要应对“隐藏受保护资源”的服务器的客户端,/api/auth/basic-hidden/{user}/{pass}登录后查看 在凭据不匹配时返回 404 而不是 401。
Bearer 令牌与 API 密钥
三个 API 密钥端点在不同位置接受同一个密钥,所以你可以检查客户端是否把它放在了 API 期望的位置。错误的 Bearer 令牌会返回 WWW-Authenticate: Bearer error="invalid_token"。
/api/auth/api-key/header401 与 403
401 表示服务器不知道你是谁:没有凭据,或者凭据不对。403 表示它很清楚你是谁,但你没有权限。客户端应该区别对待两者。收到 401 后可以重新登录或刷新令牌;收到 403 后,重新登录也无济于事。角色端点会分别返回这两种情况:
$ curl -i https://api.testingapis.com/api/auth/roles/admin
HTTP/1.1 401 Unauthorized
www-authenticate: Bearer realm="apibee"
$ curl -i -H "Authorization: Bearer user-token" https://api.testingapis.com/api/auth/roles/admin
HTTP/1.1 403 Forbidden
{"status":403,"error":"Forbidden","message":"This endpoint requires the 'admin' role.","roles":["user"],"scopes":["read","write"]}
$ curl -H "Authorization: Bearer admin-token" https://api.testingapis.com/api/auth/roles/admin
{"authorized":true,"user":"Alice Admin","roles":["admin","user"],"scopes":["read","write","delete"]}DELETE /api/auth/roles/resource登录后查看 检查的是 scope 而不是角色。readonly-token 只有 read,所以会收到一个 403,并指出缺少的 delete scope。
HMAC 请求签名
支付和 webhook 类 API 常常对请求签名,而不是直接发送密钥。这里的签名是对 timestamp + "." + raw body 计算的 HMAC-SHA256,以小写十六进制表示,放在 X-Signature 中,时间戳放在 X-Timestamp 中。要对你实际发送的字节签名。签名之后重新序列化 JSON,是最常见的签名失效原因。
import { createHmac } from "node:crypto";
const secret = "apibee-hmac-secret";
const body = JSON.stringify({ orderId: 1001, amount: 49.99 });
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = createHmac("sha256", secret)
.update(`${timestamp}.${body}`)
.digest("hex");
const res = await fetch("https://api.testingapis.com/api/auth/hmac", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Timestamp": timestamp,
"X-Signature": signature,
},
body,
});
console.log(res.status, await res.json());200 { authenticated: true, scheme: 'hmac-sha256', timestamp: 1791341858, bodyLength: 31 }
# The same signature sent with a different body:
401 {
status: 401,
error: 'Unauthorized',
message: 'Signature mismatch.',
hint: 'X-Signature = lowercase hex HMAC-SHA256(secret, X-Timestamp + "." + raw request body). Optional \'sha256=\' prefix.',
bodyLength: 30
}想手动核对签名,可以用哈希生成器在浏览器中计算 HMAC-SHA256。
JWT 登录、过期与刷新
/api/auth/jwt/login登录后查看 返回一个有效期 15 分钟的签名访问令牌和一个刷新令牌。刷新令牌会轮换:每次刷新都返回一个新的,旧的随即失效。/api/auth/jwt/expired登录后查看 提供一个签名正确但已经过期的令牌,让你不用等 15 分钟就能测试过期路径。
const BASE = "https://api.testingapis.com/api/auth/jwt";
const login = await fetch(`${BASE}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "user", password: "user123" }),
});
const tokens = await login.json();
// { accessToken, tokenType: "Bearer", expiresIn: 900, refreshToken, user }
const auth = { Authorization: `Bearer ${tokens.accessToken}` };
console.log((await fetch(`${BASE}/me`, { headers: auth })).status); // 200
console.log((await fetch(`${BASE}/admin`, { headers: auth })).status); // 403: role is "user"
// A correctly signed token that expired an hour ago, for the negative test.
const { accessToken: expired } = await (await fetch(`${BASE}/expired`)).json();
const old = await fetch(`${BASE}/me`, { headers: { Authorization: `Bearer ${expired}` } });
console.log(old.status, old.headers.get("www-authenticate"));
// 401 Bearer error="invalid_token", error_description="The token expired at '...'"
// Refresh tokens rotate: use the new one next time.
const refreshed = await fetch(`${BASE}/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken: tokens.refreshToken }),
});
console.log(refreshed.status); // 200把其中任意一个令牌粘贴到 JWT 解码器中,即可查看它的声明和过期时间。
带 PKCE 的 OAuth 2.0
OAuth 服务器支持带 PKCE 的授权码模式、客户端凭据、密码和刷新令牌模式,以及令牌自省和撤销。授权会以用户 apibee 的身份自动批准,所以整个流程可以在一个脚本中跑完。localhost 或 127.0.0.1(任意端口)上的重定向 URI 以及 Postman 的回调地址都会收到真实的 302 重定向。
带 PKCE 的授权码模式
import { createHash, randomBytes } from "node:crypto";
const BASE = "https://api.testingapis.com/api/auth/oauth";
const redirectUri = "http://localhost:3000/callback";
// 1. A random code verifier and its S256 challenge.
const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
// 2. Send the user to /authorize. This server approves at once and redirects
// to the redirect URI with ?code=...&state=...
const authorize = new URL(`${BASE}/authorize`);
authorize.search = new URLSearchParams({
response_type: "code",
client_id: "apibee-public",
redirect_uri: redirectUri,
scope: "read profile",
state: "xyz123",
code_challenge: challenge,
code_challenge_method: "S256",
});
const redirect = await fetch(authorize, { redirect: "manual" });
const code = new URL(redirect.headers.get("location")).searchParams.get("code");
// 3. Exchange the code and the verifier for tokens.
const tokenRes = await fetch(`${BASE}/token`, {
method: "POST",
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: "apibee-public",
code,
redirect_uri: redirectUri,
code_verifier: verifier,
}),
});
const tokens = await tokenRes.json();
// 4. Call a protected endpoint.
const me = await fetch(`${BASE}/userinfo`, {
headers: { Authorization: `Bearer ${tokens.access_token}` },
});
console.log(me.status, await me.json());200 {
sub: 'apibee',
name: 'TestingAPIs Tester',
preferred_username: 'apibee',
email: null,
email_verified: null
}PKCE 客户端应处理的两种失败:
# Sending the same code a second time
400 { error: 'invalid_grant', error_description: 'Authorization code is invalid, expired or already used.' }
# Sending a code_verifier that doesn't match the challenge
400 { error: 'invalid_grant', error_description: 'PKCE verification failed: code_verifier does not match code_challenge.' }客户端凭据
用于服务之间的调用,不涉及任何用户:
$ curl -X POST https://api.testingapis.com/api/auth/oauth/token \
-d "grant_type=client_credentials&client_id=apibee-client&client_secret=apibee-secret&scope=read"
{"access_token":"oat_3fc4...","token_type":"Bearer","expires_in":3600,"scope":"read"}OAuth 1.0a 签名请求
OAuth 1.0a 不是单独发送令牌,而是用两个密钥(consumer 密钥和令牌密钥)为每个请求签名。测试服务器接受 HMAC-SHA1、 HMAC-SHA256、HMAC-SHA512 和 PLAINTEXT 签名。可以使用永不过期的固定访问令牌 apibee-access-token / apibee-token-secret,也可以走 three-legged 流程获取自己的令牌。授权会以用户 apibee 的身份自动批准。
POST /api/auth/oauth1/request_token登录后查看,只用 consumer 的 key 和密钥签名,oauth_callback设为一个 URL 或oob。GET /api/auth/oauth1/authorize?oauth_token=…登录后查看 会带着oauth_verifier重定向到 localhost 或 Postman 的回调地址;对于oob,则以 JSON 返回。POST /api/auth/oauth1/access_token登录后查看,用 request token 及其密钥签名,并带上oauth_verifier。访问令牌有效期为一小时。
The protected resource, with the static access token:
1. New request: GET https://api.testingapis.com/api/auth/oauth1
2. Authorization → Type: OAuth 1.0
Signature Method: HMAC-SHA1
Consumer Key: apibee-consumer-key
Consumer Secret: apibee-consumer-secret
Access Token: apibee-access-token
Token Secret: apibee-token-secret
Add authorization data to: Request Headers
3. Send. The response has "authenticated": true.
The three-legged flow, with the same Authorization settings:
1. POST https://api.testingapis.com/api/auth/oauth1/request_token
Leave Access Token and Token Secret empty.
Advanced → Callback URL: oob
2. Open GET https://api.testingapis.com/api/auth/oauth1/authorize?oauth_token=<oauth_token from step 1>
and copy oauth_verifier from the JSON.
3. POST https://api.testingapis.com/api/auth/oauth1/access_token
Access Token: oauth_token from step 1
Token Secret: oauth_token_secret from step 1
Advanced → Verifier: oauth_verifier from step 2
4. Use the oauth_token and oauth_token_secret it returns as
Access Token and Token Secret for GET /api/auth/oauth1.cURL 标签页中的 PLAINTEXT 不需要签名库:签名就是签名密钥本身,即 consumer_secret&token_secret,其中 & 以 %26 发送。先用它确认凭据无误,再切换到 HMAC-SHA1 测试你的签名代码。
当签名不匹配时,401 会包含服务器构造的签名基础字符串,方便你和客户端生成的进行对比。值得测试的失败情况:
# Signed with the wrong token secret
401 signature_invalid: Signature mismatch.
details.signatureBaseString: GET&https%3A%2F%2Fapi.testingapis.com%2Fapi%2Fauth%2Foauth1&oauth_consumer_key%3D...
# The same oauth_nonce sent twice
401 nonce_used: This oauth_nonce was already used with this timestamp. Send a new nonce.
# The same request token exchanged twice
401 token_used: This request token was already exchanged for an access token.OAuth 1.0a 签名规则说明了参数应放在哪里、时间戳的有效窗口以及每个 oauth_problem 代码。
值得编写的反向测试
- 没有凭据:应返回 401 和
WWW-Authenticate响应头。 - 密码错误、密钥错误、令牌被篡改:返回 401,绝不能是 500。
- 用户有效但缺少角色或 scope:返回 403,客户端不应让用户退出登录。
- JWT 过期:客户端刷新一次,然后重试原来的请求。
- 刷新令牌被使用两次:第二次使用失败。
- OAuth 授权码被使用两次,或 verifier 错误:
invalid_grant。 - 针对另一个请求体计算的 HMAC 签名:401。
- OAuth 1.0a 请求重复使用 nonce 或签名错误:401
nonce_used或signature_invalid。
这些测试背后的概念见认证与授权课程(英文)。