功能说明
parseWwwAuthenticate 解析 401 响应中的 WWW-Authenticate 头,返回认证方案及其参数数组。它能处理 Basic realm、Bearer realm 与 error、error_description 等参数,也能识别同一头中用逗号分隔的多个挑战。适合 API 客户端根据服务端挑战选择认证方式、展示错误原因以及在测试中断言认证流程。
关键代码
解析器逐字符推进,空白跳过函数与 token 读取函数保持位置显式。方案名读取后,若下一项是参数名则继续解析等号与值,值可以是 token 或带转义的引号字符串。处理逗号时需要向前看:只有下一段仍是 name=value 才属于当前挑战,否则视为新方案。参数名统一小写,重复参数后值覆盖前值。
使用注意
实现不支持 token68 形式的挑战,例如 Negotiate 后直接跟 base64 令牌,遇到这种头会抛错而不是部分解析。多个挑战的区分依赖逗号后的前瞻,参数值中未加引号的逗号会导致歧义。错误描述可能包含敏感信息,展示给终端用户前应做筛选。该解析器只读取头,不执行认证或验证令牌。
验证方式
测试 Basic realm 单参数、Bearer 多参数、带转义的引号值与参数中的逗号。验证 Bearer 与 Basic 并列时返回两个挑战,以及逗号前后空格不规则的情况。测试空头、缺失等号、未闭合引号、只给方案名与 token68 形式,确认抛出对应错误。最后对浏览器或服务器真实响应头做快照测试。
代码示例
export interface AuthChallenge {
scheme: string;
params: Record<string, string>;
}
function skipWhitespace(header: string, index: number): number {
while (index < header.length && /\s/.test(header[index]!)) index += 1;
return index;
}
function readToken(header: string, index: number): { value: string; end: number } {
const match = /^[^\s,="]+/.exec(header.slice(index));
if (!match) throw new SyntaxError("expected token at position " + index);
return { value: match[0], end: index + match[0].length };
}
function readQuoted(header: string, index: number): { value: string; end: number } {
let value = "";
index += 1;
while (index < header.length) {
const character = header[index]!;
if (character === "\\" && index + 1 < header.length) {
value += header[index + 1];
index += 2;
continue;
}
if (character === '"') return { value, end: index + 1 };
value += character;
index += 1;
}
throw new SyntaxError("unterminated quoted string");
}
function readValue(header: string, index: number): { value: string; end: number } {
return header[index] === '"' ? readQuoted(header, index) : readToken(header, index);
}
export function parseWwwAuthenticate(header: string): AuthChallenge[] {
if (typeof header !== "string" || header.trim() === "") {
throw new TypeError("header must be a non-empty string");
}
const challenges: AuthChallenge[] = [];
let index = 0;
while (index < header.length) {
index = skipWhitespace(header, index);
if (header[index] === ",") {
index = skipWhitespace(header, index + 1);
continue;
}
if (index >= header.length) break;
const schemeToken = readToken(header, index);
const scheme = schemeToken.value;
index = skipWhitespace(header, schemeToken.end);
const params: Record<string, string> = {};
if (index < header.length && header[index] !== ",") {
const name = readToken(header, index);
index = skipWhitespace(header, name.end);
if (header[index] !== "=") {
throw new SyntaxError("expected = after parameter " + name.value);
}
index = skipWhitespace(header, index + 1);
const value = readValue(header, index);
params[name.value.toLowerCase()] = value.value;
index = value.end;
}
while (true) {
index = skipWhitespace(header, index);
if (header[index] !== ",") break;
const afterComma = skipWhitespace(header, index + 1);
if (afterComma >= header.length) {
index = afterComma;
break;
}
const lookahead = readToken(header, afterComma);
const afterName = skipWhitespace(header, lookahead.end);
if (header[afterName] !== "=") break;
index = skipWhitespace(header, afterName + 1);
const value = readValue(header, index);
params[lookahead.value.toLowerCase()] = value.value;
index = value.end;
}
challenges.push({ scheme, params });
}
return challenges;
}