> For the complete documentation index, see [llms.txt](https://www.axisnow.io/zh/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://www.axisnow.io/zh/docs/edge/policies/edge-runtime.md).

# 边缘运行时

以 Lua 脚本扩展自定义认证与自定义跳转

边缘运行时（Edge Runtime）是边缘上的一个轻量 Lua 沙箱。内置的认证方式和跳转规则覆盖不了的逻辑，您写成一个 `handle(ctx)` 函数交给它，边缘在处理每个请求时调用。脚本只能调用运行时提供的 API，不能接触边缘自身的变量、文件系统和网络。

和通用的边缘函数产品不同，边缘运行时不是一个独立的 Serverless 平台：脚本不独占请求，不自己生成响应，也不发起子请求。它只在插件动作里承担“算出一个结论”的那一步——这个请求的凭据对不对、该跳到哪里——其余仍由插件完成。

## 工作原理 <a href="#how-it-works" id="how-it-works"></a>

脚本挂在插件的[动作](/zh/docs/edge/policies/rules.md#actions)上，在请求生命周期里由插件触发：

<table data-search="false"><thead><tr><th width="220">位置</th><th>脚本要做的事</th><th width="260">返回值</th></tr></thead><tbody><tr><td><a href="/zh/docs/edge/policies/authn-authz.md">认证与授权</a> › 认证方式 › 自定义 › 自定义认证代码</td><td>读取请求里的凭据，按您自己的方案校验，给出放行或拒绝。</td><td><code>auth.result(opts)</code>，<code>opts.verified</code> 为 <code>true</code> 放行、<code>false</code> 拒绝</td></tr><tr><td><a href="/zh/docs/edge/policies/request-processing.md#redirect">请求处理 › URL 重定向</a> › 自定义脚本</td><td>根据请求算出跳转地址。状态码取动作里配置的值，脚本不能改。</td><td>目标 URL 字符串；返回 <code>nil</code> 表示这次不跳转，请求继续</td></tr></tbody></table>

一次请求命中带脚本的规则后，边缘做三件事：

* **编译并缓存**：脚本按内容只编译一次，之后的请求直接执行缓存的结果。保存时也会先编译：编译不通过、没有定义 `handle` 函数，或顶层代码运行出错，都保存不了。
* **构造上下文**：为这次请求准备 `ctx`——方法、URL、请求头、Cookie、查询参数、客户端 IP 与地理位置、TLS 信息。请求体只在脚本第一次读取时才加载。
* **在隔离环境里运行**：每次请求都用一个全新的环境执行 `handle(ctx)`，请求之间不共享任何变量；全局环境只读，除了定义 `handle` 之外不能往里写东西。运行受指令预算约束，超出即终止。

## 入口函数 <a href="#handler" id="handler"></a>

脚本只有一个约定：定义 `function handle(ctx)`，在里面读取 `ctx`，最后返回这个位置要求的结果。`url`、`crypto`、`jwt` 等工具模块是全局的，直接用，不需要 `require`。骨架如下：

```lua
-- 常量和辅助函数写在 handle 外面；顶层代码每次请求重新执行，不会跨请求保留状态
local SECRET = "…"

function handle(ctx)
    -- 1. 读请求：ctx.request（方法、路径、请求头、Cookie、查询参数、请求体）、ctx.client、ctx.geo、ctx.tls
    local token = ctx.request.headers:get("x-token")

    -- 2. 做判断：crypto 验签、jwt 解析、regex 匹配、time 判时效
    local ok = token ~= nil and crypto.secure_equal(token, SECRET)

    -- 3. 返回这个位置要求的结果
    --    自定义认证：auth.result({ verified = true 或 false, message = "原因码" })
    --    自定义跳转：return "https://…" 跳转，return nil 不跳转
    return auth.result({ verified = ok })
end
```

完整的例子见页末的[示例](#examples)。

## 运行时 API <a href="#runtime-api" id="runtime-api"></a>

### 请求上下文 <a href="#context" id="context"></a>

`ctx` 只在 `handle(ctx)` 里可用，全部只读。请求行：

<table data-search="false"><thead><tr><th width="300">成员</th><th>说明</th></tr></thead><tbody><tr><td><code>ctx.request.method</code></td><td>请求方法，大写</td></tr><tr><td><code>ctx.request.scheme</code></td><td><code>http</code> 或 <code>https</code></td></tr><tr><td><code>ctx.request.host</code></td><td>Host，不含端口</td></tr><tr><td><code>ctx.request.authority</code></td><td>Host 加端口</td></tr><tr><td><code>ctx.request.port</code></td><td>请求到达的端口</td></tr><tr><td><code>ctx.request.protocol</code></td><td><code>HTTP/1.0</code>、<code>HTTP/1.1</code>、<code>HTTP/2</code>、<code>HTTP/3</code>；与条件字段「HTTP 版本」的取值 <code>HTTP/2.0</code> 写法不同</td></tr><tr><td><code>ctx.request.http_version</code></td><td>数字形式的版本：1.0、1.1、2、3</td></tr></tbody></table>

URL 与路径：

<table data-search="false"><thead><tr><th width="300">成员</th><th>说明</th></tr></thead><tbody><tr><td><code>ctx.request.url</code></td><td>完整 URL</td></tr><tr><td><code>ctx.request.path</code></td><td>解码后的路径</td></tr><tr><td><code>ctx.request.raw_path</code></td><td>原始路径，未解码</td></tr><tr><td><code>ctx.request.raw_query</code></td><td>原始查询串，没有参数时是空串</td></tr><tr><td><code>ctx.request.signature_base(signature_input, label)</code></td><td>按 RFC 9421 为请求构造 signature base。支持单个 Signature-Input 成员、普通请求头和 <code>@method</code>、<code>@target-uri</code>、<code>@authority</code>、<code>@scheme</code>、<code>@request-target</code>、<code>@path</code>、<code>@query</code> 派生组件；<code>@query-param</code> 这类带参数的组件暂不支持</td></tr></tbody></table>

请求头和 Cookie 是带方法的对象：

<table data-search="false"><thead><tr><th width="300">成员</th><th>说明</th></tr></thead><tbody><tr><td><code>ctx.request.headers:get(name)</code></td><td>请求头的第一个值，名称不区分大小写；没有时为 <code>nil</code></td></tr><tr><td><code>ctx.request.headers:get_all(name)</code></td><td>同名请求头的全部值，列表</td></tr><tr><td><code>ctx.request.headers:has(name)</code></td><td>是否带有该请求头</td></tr><tr><td><code>ctx.request.headers:entries()</code></td><td>全部请求头</td></tr><tr><td><code>ctx.request.cookies:get(name)</code></td><td>Cookie 的第一个值</td></tr><tr><td><code>ctx.request.cookies:get_all(name)</code></td><td>同名 Cookie 的全部值，列表</td></tr><tr><td><code>ctx.request.cookies:all()</code></td><td>全部 Cookie</td></tr></tbody></table>

查询参数和请求体：

<table data-search="false"><thead><tr><th width="300">成员</th><th>说明</th></tr></thead><tbody><tr><td><code>ctx.request.query:get(name)</code></td><td>查询参数的第一个值。只有名称没有值的参数返回 <code>true</code>，判断前先看类型</td></tr><tr><td><code>ctx.request.query:get_all(name)</code></td><td>同名参数的全部值，列表</td></tr><tr><td><code>ctx.request.query:has(name)</code></td><td>是否带有该参数</td></tr><tr><td><code>ctx.request.query:entries()</code></td><td>全部查询参数</td></tr><tr><td><code>ctx.request.query:raw()</code></td><td>原始查询串</td></tr><tr><td><code>ctx.request.body:bytes()</code></td><td>请求体原文。第一次调用时才读取并缓存；没有请求体返回空串；超过 1 MiB 返回 <code>nil, err</code></td></tr><tr><td><code>ctx.request.body:text()</code></td><td>同 <code>bytes()</code>，按文本使用</td></tr><tr><td><code>ctx.request.body:size()</code></td><td>请求体字节数</td></tr><tr><td><code>ctx.request.body:digest(algo)</code></td><td>请求体的十六进制摘要，算法同 <code>crypto.hash</code></td></tr></tbody></table>

客户端与地理位置：

<table data-search="false"><thead><tr><th width="300">成员</th><th>说明</th></tr></thead><tbody><tr><td><code>ctx.client.ip</code></td><td>真实客户端 IP，与条件匹配字段里的客户端 IP 地址一致</td></tr><tr><td><code>ctx.client.port</code></td><td>客户端端口</td></tr><tr><td><code>ctx.client.peer_ip</code></td><td>与边缘直接建立连接的对端地址；有前置代理时是代理的地址</td></tr><tr><td><code>ctx.client.peer_port</code></td><td>与边缘直接建立连接的对端端口</td></tr><tr><td><code>ctx.client.ip_source</code></td><td>真实 IP 的来源：<code>real_ip</code>（由<a href="/zh/docs/edge/policies/real-ip.md">真实客户端 IP</a>插件还原）或 <code>remote_addr</code>（连接对端）</td></tr><tr><td><code>ctx.client.xff</code></td><td>X-Forwarded-For 请求头原文</td></tr><tr><td><code>ctx.geo.country</code></td><td>客户端 IP 所在的国家 / 地区代码，查不到时为 <code>nil</code></td></tr><tr><td><code>ctx.geo.region</code></td><td>省份代码</td></tr><tr><td><code>ctx.geo.city</code></td><td>城市代码</td></tr><tr><td><code>ctx.geo.continent</code></td><td>大洲代码</td></tr></tbody></table>

TLS 与客户端证书：

<table data-search="false"><thead><tr><th width="300">成员</th><th>说明</th></tr></thead><tbody><tr><td><code>ctx.tls.enabled</code></td><td>是否 HTTPS</td></tr><tr><td><code>ctx.tls.version</code></td><td>TLS 版本</td></tr><tr><td><code>ctx.tls.cipher</code></td><td>加密套件</td></tr><tr><td><code>ctx.tls.sni</code></td><td>SNI</td></tr><tr><td><code>ctx.tls.client_cert.present</code></td><td>客户端是否出示了证书</td></tr><tr><td><code>ctx.tls.client_cert.verified</code></td><td>客户端证书是否通过验证</td></tr><tr><td><code>ctx.tls.client_cert.subject</code></td><td>客户端证书主题</td></tr><tr><td><code>ctx.tls.client_cert.issuer</code></td><td>客户端证书颁发者</td></tr><tr><td><code>ctx.tls.client_cert.serial</code></td><td>客户端证书序列号</td></tr><tr><td><code>ctx.tls.client_cert.fingerprint</code></td><td>客户端证书指纹</td></tr></tbody></table>

日志：

<table data-search="false"><thead><tr><th width="300">成员</th><th>说明</th></tr></thead><tbody><tr><td><code>ctx.log.debug(msg, fields)</code></td><td>调试日志，debug 级别；<code>fields</code> 是可选的字段表。见<a href="#logging-and-debugging">日志与排错</a></td></tr><tr><td><code>ctx.log.info(msg, fields)</code></td><td>调试日志，info 级别</td></tr><tr><td><code>ctx.log.warn(msg, fields)</code></td><td>调试日志，warn 级别</td></tr><tr><td><code>ctx.log.error(msg, fields)</code></td><td>调试日志，error 级别</td></tr></tbody></table>

### 工具模块 <a href="#utilities" id="utilities"></a>

九个工具模块都是全局变量，直接调用。出错时一般返回 `nil` 和错误信息；`jwt.verify` 例外，它失败抛错。

#### url — URL <a href="#url-module" id="url-module"></a>

<table data-search="false"><thead><tr><th width="300">函数</th><th>说明</th></tr></thead><tbody><tr><td><code>url.parse(value)</code></td><td>解析成 URL 对象，字段有 <code>scheme</code>、<code>host</code>、<code>port</code>、<code>path</code>、<code>query</code>、<code>fragment</code>；可以 <code>set_host</code>、<code>set_path</code>，用 <code>to_string()</code> 还原成字符串</td></tr><tr><td><code>url.parse(value).query</code></td><td>查询参数对象，支持 <code>get</code>、<code>get_all</code>、<code>has</code>、<code>set</code>、<code>remove</code>、<code>entries</code>、<code>raw</code>、<code>sort</code>、<code>clone</code>、<code>to_string</code></td></tr><tr><td><code>url.build(parts)</code></td><td>由各部分拼出 URL</td></tr><tr><td><code>url.join(base, path)</code></td><td>拼接路径</td></tr><tr><td><code>url.resolve(base, relative)</code></td><td>解析相对地址</td></tr><tr><td><code>url.is_absolute(value)</code></td><td>是否绝对地址</td></tr><tr><td><code>url.normalize(value)</code></td><td>规范化</td></tr></tbody></table>

#### crypto — 密码学 <a href="#crypto-module" id="crypto-module"></a>

<table data-search="false"><thead><tr><th width="300">函数</th><th>说明</th></tr></thead><tbody><tr><td><code>crypto.hash(algo, data)</code></td><td>摘要，返回十六进制。算法：<code>md5</code>、<code>sha1</code>（仅为兼容旧方案）、<code>sha256</code>、<code>sha384</code>、<code>sha512</code></td></tr><tr><td><code>crypto.hmac(algo, key, data)</code></td><td>HMAC，返回十六进制。算法：<code>sha1</code>、<code>sha256</code>、<code>sha384</code>、<code>sha512</code>，也接受 <code>hmac-sha256</code> 这种写法</td></tr><tr><td><code>crypto.hmac_raw(algo, key, data)</code></td><td>HMAC 的原始二进制结果，用于 AWS SigV4 这类多级密钥派生，最后用 <code>codec.hex_encode</code> 转成十六进制</td></tr><tr><td><code>crypto.secure_equal(a, b)</code></td><td>恒定时间比较两个字符串。比对凭证一律用它，不要用 <code>==</code></td></tr><tr><td><code>crypto.random_bytes(n)</code></td><td>安全随机字节</td></tr><tr><td><code>crypto.uuid()</code></td><td>UUID v4</td></tr><tr><td><code>crypto.sign(algo, key, data)</code></td><td>非对称签名，密钥是 PEM。算法：<code>rsa-sha256</code>、<code>rsa-pss-sha256</code>、<code>ecdsa-sha256</code>、<code>ed25519</code></td></tr><tr><td><code>crypto.verify(algo, key, data, signature)</code></td><td>非对称验签，算法同上</td></tr><tr><td><code>crypto.jws_verify(expected_alg, key, compact_jws)</code></td><td>校验 RFC 7515 Compact JWS，必须显式给出预期算法，防止算法混淆。支持 <code>HS256</code>、<code>RS256</code>、<code>PS256</code>、<code>ES256</code>、<code>EdDSA</code>；成功返回 payload 和受保护头。不支持未编码 payload（<code>b64=false</code>）、分离 payload 和 critical 扩展</td></tr></tbody></table>

#### jwt — JWT <a href="#jwt-module" id="jwt-module"></a>

<table data-search="false"><thead><tr><th width="300">函数</th><th>说明</th></tr></thead><tbody><tr><td><code>jwt.decode(token)</code></td><td>只解码不验签，返回一张表，含 <code>header</code> 和 <code>payload</code> 两个字段</td></tr><tr><td><code>jwt.verify(token, options)</code></td><td>验签并校验声明，成功返回 payload，失败抛错。<code>options</code> 的字段：<code>key</code>（必填，PEM、对称密钥或 JWK）、<code>alg</code>（必填，显式指定防止算法降级）、<code>iss</code>、<code>aud</code>、<code>leeway</code>（秒）</td></tr></tbody></table>

#### codec — 编解码 <a href="#codec-module" id="codec-module"></a>

<table data-search="false"><thead><tr><th width="300">函数</th><th>说明</th></tr></thead><tbody><tr><td><code>codec.base64_encode(data)</code></td><td>Base64 编码</td></tr><tr><td><code>codec.base64_decode(data)</code></td><td>Base64 解码</td></tr><tr><td><code>codec.base64url_encode(data)</code></td><td>Base64 URL 安全变体编码</td></tr><tr><td><code>codec.base64url_decode(data)</code></td><td>Base64 URL 安全变体解码</td></tr><tr><td><code>codec.hex_encode(data)</code></td><td>十六进制编码</td></tr><tr><td><code>codec.hex_decode(data)</code></td><td>十六进制解码</td></tr><tr><td><code>codec.url_encode(value)</code></td><td>URL 编码</td></tr><tr><td><code>codec.url_decode(value)</code></td><td>URL 解码</td></tr><tr><td><code>codec.form_encode(table)</code></td><td>表单编码（<code>application/x-www-form-urlencoded</code>）</td></tr><tr><td><code>codec.form_decode(value)</code></td><td>表单解码</td></tr><tr><td><code>codec.utf8_encode(value)</code></td><td>UTF-8 编码</td></tr><tr><td><code>codec.utf8_decode(bytes)</code></td><td>UTF-8 解码</td></tr></tbody></table>

#### json — JSON <a href="#json-module" id="json-module"></a>

<table data-search="false"><thead><tr><th width="300">函数</th><th>说明</th></tr></thead><tbody><tr><td><code>json.encode(value)</code></td><td>编码，失败返回 <code>nil, err</code></td></tr><tr><td><code>json.decode(value)</code></td><td>解码，失败返回 <code>nil, err</code>，不抛错</td></tr><tr><td><code>json.try_decode(value)</code></td><td>同 <code>decode</code></td></tr><tr><td><code>json.array(t)</code></td><td>把 Lua 表标记为数组，空表才能编码成 <code>[]</code></td></tr><tr><td><code>json.null</code></td><td>JSON 的 <code>null</code></td></tr></tbody></table>

#### regex — 正则 <a href="#regex-module" id="regex-module"></a>

<table data-search="false"><thead><tr><th width="300">函数</th><th>说明</th></tr></thead><tbody><tr><td><code>regex.match(value, pattern)</code></td><td>匹配并返回捕获</td></tr><tr><td><code>regex.find(value, pattern)</code></td><td>查找位置</td></tr><tr><td><code>regex.replace(value, pattern, replacement)</code></td><td>替换</td></tr><tr><td><code>regex.split(value, pattern)</code></td><td>切分</td></tr></tbody></table>

#### time — 时间 <a href="#time-module" id="time-module"></a>

<table data-search="false"><thead><tr><th width="300">函数</th><th>说明</th></tr></thead><tbody><tr><td><code>time.now()</code></td><td>当前 Unix 时间，秒，带小数</td></tr><tr><td><code>time.now_ms()</code></td><td>当前 Unix 时间，毫秒</td></tr><tr><td><code>time.parse_iso8601(value)</code></td><td>解析 ISO 8601 时间</td></tr><tr><td><code>time.format_iso8601(ts)</code></td><td>格式化为 ISO 8601</td></tr><tr><td><code>time.parse_http_date(value)</code></td><td>解析 HTTP Date</td></tr><tr><td><code>time.format_http_date(ts)</code></td><td>格式化为 HTTP Date</td></tr><tr><td><code>time.add(ts, seconds)</code></td><td>时间加上若干秒</td></tr><tr><td><code>time.diff(a, b)</code></td><td>两个时间之差，秒</td></tr><tr><td><code>time.seconds(n)</code></td><td>n 秒，换算成秒</td></tr><tr><td><code>time.minutes(n)</code></td><td>n 分钟，换算成秒</td></tr><tr><td><code>time.hours(n)</code></td><td>n 小时，换算成秒</td></tr><tr><td><code>time.days(n)</code></td><td>n 天，换算成秒。典型用途是过期时间和时间戳容差</td></tr></tbody></table>

#### net — 网络地址 <a href="#net-module" id="net-module"></a>

<table data-search="false"><thead><tr><th width="300">函数</th><th>说明</th></tr></thead><tbody><tr><td><code>net.ip.parse(value)</code></td><td>解析 IP 地址</td></tr><tr><td><code>net.ip.in_cidr(ip, cidr)</code></td><td>是否落在网段内</td></tr><tr><td><code>net.ip.is_private(ip)</code></td><td>是否内网地址</td></tr><tr><td><code>net.ip.is_ipv4(ip)</code></td><td>是否 IPv4</td></tr><tr><td><code>net.ip.is_ipv6(ip)</code></td><td>是否 IPv6</td></tr></tbody></table>

#### utf8 — UTF-8 <a href="#utf8-module" id="utf8-module"></a>

<table data-search="false"><thead><tr><th width="300">函数</th><th>说明</th></tr></thead><tbody><tr><td><code>utf8.len(s)</code></td><td>字符数</td></tr><tr><td><code>utf8.offset(s, n, i)</code></td><td>第 n 个字符的字节位置</td></tr><tr><td><code>utf8.char(...)</code></td><td>码点转字符</td></tr><tr><td><code>utf8.codepoint(s, i, j)</code></td><td>字符转码点</td></tr></tbody></table>

### 结果 <a href="#results" id="results"></a>

结果通过全局对象 `auth` 返回，或直接 `return` 字符串：

<table data-search="false"><thead><tr><th width="300">写法</th><th>说明</th></tr></thead><tbody><tr><td><code>auth.result(opts)</code>，<code>opts.verified = true</code></td><td>自定义认证：放行。<code>verified</code> 必须是布尔值，缺了或类型不对脚本会出错。可选 <code>message</code>（写进访问日志）</td></tr><tr><td><code>auth.result(opts)</code>，<code>opts.verified = false</code></td><td>自定义认证：拒绝</td></tr><tr><td><code>return "https://…"</code> 或 <code>return "/path"</code></td><td>自定义跳转：跳到这个地址，状态码取动作配置</td></tr><tr><td><code>return nil</code></td><td>自定义跳转：不跳转，请求继续</td></tr></tbody></table>

跳转地址必须是绝对的 http(s) 地址或以 `/` 开头的路径，不超过 8192 字节；含控制字符或以 `//` 开头的一律拒绝。目标地址已带参数时用 `&` 续接；带 `#fragment` 时参数要放在 `#` 之前。

### Lua 语言 <a href="#lua" id="lua"></a>

可以直接使用的标准库：

<table data-search="false"><thead><tr><th width="300">名称</th><th>说明</th></tr></thead><tbody><tr><td><code>string</code></td><td>字符串标准库</td></tr><tr><td><code>math</code></td><td>数学标准库</td></tr><tr><td><code>table</code></td><td>表标准库</td></tr><tr><td><code>bit</code></td><td>位运算库</td></tr></tbody></table>

可以直接使用的全局函数：

<table data-search="false"><thead><tr><th width="300">名称</th><th>说明</th></tr></thead><tbody><tr><td><code>tonumber</code></td><td>转数字</td></tr><tr><td><code>tostring</code></td><td>转字符串</td></tr><tr><td><code>type</code></td><td>取类型</td></tr><tr><td><code>pairs</code></td><td>遍历表</td></tr><tr><td><code>ipairs</code></td><td>遍历数组</td></tr><tr><td><code>next</code></td><td>取下一个键值</td></tr><tr><td><code>select</code></td><td>取可变参数</td></tr><tr><td><code>unpack</code></td><td>展开数组</td></tr><tr><td><code>assert</code></td><td>断言</td></tr><tr><td><code>error</code></td><td>抛出错误</td></tr></tbody></table>

不开放的能力：

<table data-search="false"><thead><tr><th width="300">名称</th><th>说明</th></tr></thead><tbody><tr><td><code>print</code></td><td>不开放，调试输出用 <code>ctx.log</code></td></tr><tr><td><code>io</code>、<code>os</code></td><td>不开放，脚本不能访问文件系统和操作系统</td></tr><tr><td><code>require</code>、<code>package</code>、<code>load</code>、<code>loadfile</code>、<code>dofile</code></td><td>不开放，脚本不能加载外部代码</td></tr><tr><td><code>debug</code>、<code>ffi</code></td><td>不开放</td></tr><tr><td><code>pcall</code>、<code>xpcall</code>、<code>coroutine</code></td><td>不开放</td></tr></tbody></table>

## 使用限制 <a href="#limits" id="limits"></a>

<table data-search="false"><thead><tr><th width="220">限制</th><th width="200">值</th><th>说明</th></tr></thead><tbody><tr><td>指令预算</td><td>1000 万条 Lua 指令</td><td>约 20 ms 的解释执行时间，超出即终止。脚本适合鉴权、改地址这类轻量逻辑，不适合重计算</td></tr><tr><td>请求体读取</td><td>1 MiB</td><td>超过上限 <code>ctx.request.body</code> 返回错误，不返回截断内容</td></tr><tr><td>JSON 嵌套深度</td><td>32 层</td><td><code>json.encode</code> / <code>json.decode</code> 都受限</td></tr><tr><td>调试日志</td><td>64 条，每条 2048 字节</td><td>超出的条目丢弃，超长的消息截断</td></tr><tr><td>跳转地址长度</td><td>8192 字节</td><td>只对自定义跳转有效</td></tr><tr><td>网络访问</td><td>无</td><td>脚本不能发请求，也不能按 URL 拉取 JWKS 之类的远程密钥。验签要用的公钥、密钥都得写在脚本里</td></tr></tbody></table>

## 日志与排错 <a href="#logging-and-debugging" id="logging-and-debugging"></a>

脚本有两个输出口，用途不同：

* **调试日志**：`ctx.log`，单独存放，只在排查时看。四个级别 `debug` / `info` / `warn` / `error`，第二个参数可以带一张字段表。
* **访问日志字段**：`auth.result` 的 `message`，落到每条请求访问日志的 `extras.authn_authz.message` 字段，通过和拒绝都会记，超过 256 字节截断。它优先于边缘自己生成的失败原因，所以只在拒绝的分支里给 `message`，或者让文案随结果变化，不要写成固定值。

脚本运行出错（抛出异常、超出指令预算、返回值类型不对）时：自定义认证按认证失败处理，请求被拒绝，返回 401 Unauthorized；URL 重定向这次不跳转，请求继续往下走。两种情况都会记一条错误日志，排查时先看它。

{% hint style="warning" %}
不要把请求里的凭证拼进 message 或日志：Authorization 请求头、Cookie、URL 里的 token 会原样进入访问日志。标识一次失败用固定的原因码（invalid\_signature、expired），不要回显凭证本身。
{% endhint %}

## 示例 <a href="#examples" id="examples"></a>

### 校验签名 URL <a href="#example-signed-url" id="example-signed-url"></a>

**自定义认证代码**的例子。链接带 `sig` 和 `exp` 两个参数，边缘用同一把密钥重算签名、核对时效，签名不对或已过期就拒绝。

```lua
-- 签名 URL：校验签名与过期时间
local SECRET = "replace-with-your-own-secret"

function handle(ctx)
    local sig = ctx.request.query:get("sig")
    local exp = tonumber(ctx.request.query:get("exp"))
    if type(sig) ~= "string" or not exp then
        return auth.result({ verified = false, message = "missing_signature" })
    end

    -- 链接过期即拒绝；time.now() 是带小数的秒，先取整
    if math.floor(time.now()) > exp then
        return auth.result({ verified = false, message = "expired" })
    end

    -- 用同样的密钥和算法重算签名，再做恒定时间比对
    local expected = crypto.hmac("sha256", SECRET, ctx.request.path .. "|" .. tostring(exp))
    if not expected or not crypto.secure_equal(sig, expected) then
        return auth.result({ verified = false, message = "invalid_signature" })
    end

    return auth.result({ verified = true })
end
```

### 按访客所在国家跳转 <a href="#example-geo-redirect" id="example-geo-redirect"></a>

URL 重定向里**自定义脚本**的例子。显式选择优先于 Cookie，Cookie 优先于 IP 所在国家；已经在正确站点上的访客不跳。

```lua
function handle(ctx)
    local sites = { CN = "cn", JP = "jp", DE = "de" }

    -- ?site=jp 优先于 Cookie，Cookie 优先于 IP 所在国家
    local chosen = ctx.request.query:get("site") or ctx.request.cookies:get("site_pref")
    if type(chosen) ~= "string" then
        chosen = nil -- ?site 不带值时 get 返回 true
    end
    local locale = sites[string.upper(chosen or ctx.geo.country or "")]
    if not locale then
        return nil
    end

    -- 已经在正确的站点上就不跳
    local target = locale .. ".example.com"
    if string.lower(ctx.request.host or "") == target then
        return nil
    end

    -- 查询参数不会自动带过去，要自己拼；raw_query 没有参数时是空串
    local url = "https://" .. target .. ctx.request.path
    if ctx.request.raw_query ~= "" then
        url = url .. "?" .. ctx.request.raw_query
    end
    return url
end
```

## 下一步 <a href="#next-steps" id="next-steps"></a>

* 自定义认证的完整用法和防盗链、私有签名等场景，见[保护对外 API 和 URL 鉴权](/zh/docs/guide/authn-authz.md#custom-authentication)。
* 不需要写代码就能表达的条件，用规则条件更合适，见[条件匹配字段](/zh/docs/edge/policies/rules/condition-fields.md)和[操作符和函数](/zh/docs/edge/policies/rules/operators-and-functions.md)。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://www.axisnow.io/zh/docs/edge/policies/edge-runtime.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
