← 中文资源

简体中文版本

WWW-Authenticate 响应头解析

解析认证方案与带引号的参数,支持同一响应头中并列多个 Basic、Bearer 等挑战。

阅读英文原文

功能说明

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;
}