MantisBT 2.28.4 REST API 使用详解

MantisBT 2.28.4 REST API 使用详解:内置 Slim 路由 /api/rest/ 全端点实测——API 令牌创建/使用/撤销全生命周期、用户/项目/问题/备注四域增删改查、分页排序约定、错误码对照,附 OpenAPI 3.0.3 规范与 Postman 集合定位、AllowOverride/CGIPassAuth/Nginx try_files 部署排障实录,全部命令与响应 JSON 逐字来自实测。

MantisBTMantisBT工单系统2026/10/21

MantisBT 2.28.4 REST API 使用详解

目标:把 MantisBT 2.28.4 内置 REST API(/api/rest/)的认证机制、令牌生命周期、四大端点域与错误行为一次讲清并逐条实测——含 OpenAPI 规范文件定位、Apache/Nginx 部署排障实录,全部命令与响应来自 Ubuntu 22.04 + Apache 2.4 + MariaDB + PHP 8.1 的实测服务器。

一、环境信息

  • 操作系统:Ubuntu 22.04(x86_64)
  • MantisBT:2.28.4(官方 tarball 安装,部署于 /var/www/html/mantisbt)
  • Web:Apache 2.4.52(mod_php);数据库:MariaDB 10.6.23;PHP:8.1.2-1ubuntu2.27
  • API 基址:http://localhost:8132/mantisbt/api/rest/
  • 演示账号:administrator / q1w2e3r4;实验项目 demo-app(id=1)、用户 dev1(id=2)、问题 #1/#2/#3/#4
  • 安装部署过程见《Ubuntu 22.04 安装 MantisBT 指南》;实例参数调优见《MantisBT 2.28.4 配置参数详解》。
  • 本文使用的 API 令牌属于一次性实验环境(收工即销毁);生产环境请替换为你自己实例的令牌,并视同密码保管。

二、前置条件

2.1 服务可达与客户端工具

客户端用系统自带的 curl 即可(无需任何 SDK);调试复杂请求可导入发行包自带的 Postman 集合(见 3.1)。先确认站点可达:

curl -s -m 30 -o /dev/null -w '%{http_code}\n' http://localhost:8132/mantisbt/login_page.php

实测输出:

200

2.2 创建 API 令牌

Web 界面路径:右上角用户菜单 → 个人资料 → API 令牌 标签页,输入令牌名称后点「创建 API 令牌」。实测创建结果页原文:

用于访问API的令牌。注意此密钥将只会显示一次。

kspt3Sunv39aLO8EnxJbecnwQ_aQg_MF

令牌机制要点(全部实测核验):

  • 令牌为 32 位 uri-safe 字符串(含 - 与 _,不是纯十六进制),明文只在创建时返回这一次,之后无法找回;
  • 数据库 mantis_api_token_table 只存 sha256 散列。实测比对:对令牌 kspt3Sunv39aLO8EnxJbecnwQ_aQg_MF 计算 sha256 得 044194c42622022924299955f303ce903039e1a5b3deebad59c2776cdaa893b7,与库内 doc-api-test 行的 hash 字段完全一致;散列函数定义在 core/api_token_api.php 第 104 行 api_token_hash()(hash( 'sha256', $p_token ));
  • 令牌也可以通过 REST API 创建/撤销,见 5.1 的全生命周期实测。

💡 技术拓展:API 令牌 vs 会话 Cookie。Web 界面用 PHP 会话 Cookie + CSRF 令牌(每次登录会话不同,表单必须带 form security token);REST API 用静态 API 令牌放进 Authorization 头,无 CSRF 概念、长期有效,适合脚本、CI 与第三方集成。两者的「密码」语义不同:会话过期重新登录即可,令牌泄露必须吊销重建。

2.3 部署前提

REST 路由依赖发行包 api/rest/.htaccess(Slim 重写 + CGIPassAuth),因此部署侧必须满足:

  • Apache:AllowOverride All + rewrite 模块启用。本文环境由 /etc/apache2/conf-available/mantisbt-override.conf 提供:
<Directory /var/www/html>
    AllowOverride All
    CGIPassAuth On
</Directory>
  • Nginx:无 .htaccess 机制,需在站点配置显式把 /api/rest 路由到 index.php(配置见安装指南方案 B)。

两项配置缺失时的具体故障现象与修复过程见第七节排障实录。

三、API 总览

3.1 协议与规范文件

MantisBT 的 REST API 基于 Slim 3 微框架实现,入口 api/rest/index.php,路由注册在 api/rest/restcore/ 下按域拆分。发行包在 api/rest/ 目录自带四份规范/工具文件(实测 ls 输出):

文件大小用途
mantisbt_openapi.yaml41345 BOpenAPI 3.0.3 规范(本版 API 的权威定义)
swagger.json9943 BSwagger 2.0 格式(供 Swagger UI 沙箱用)
api.postman_collection.json289785 BPostman 请求集合
env.postman_environment.json681 BPostman 环境变量模板

规范文件是静态资源,可直连下载(实测):

curl -s -m 30 -o /dev/null -w '%{http_code} %{size_download}B\n' \
  http://localhost:8132/mantisbt/api/rest/mantisbt_openapi.yaml

实测输出:

200 41345B

mantisbt_openapi.yaml 头部实测节选:

openapi: 3.0.3
info:
  title: Mantis Bug Tracker REST API
  version: 2.28.4
  description: |-
    REST API calls have to be authenticated by creating an API token for the user doing the calls,
    and then passing the API token in the 'Authorization' header.  However, for anonymous access,
    the API key and header are not needed.

💡 技术拓展:OpenAPI vs Swagger。Swagger 2.0 是旧规范,OpenAPI 3.0 是其继任者(改名后由 Linux 基金会旗下 OpenAPI Initiative 维护)。MantisBT 同时带两份文件:做接口文档、代码生成用 mantisbt_openapi.yaml;只有需要老版 Swagger UI 在线调试时才用 swagger.json,且它头部注明沙箱要求站点部署在根路径,子目录部署需手工把 basePath 从 /api/rest 改成 /mantisbt/api/rest。

3.2 认证方式

所有请求在 Authorization 头携带 API 令牌原文(无 Bearer 前缀)。匿名访问在 $g_anonymous_account 配置后可免令牌(官方 OpenAPI 描述原文提及 anonymous access 场景,本文未实测匿名通道)。API 侧没有独立限流参数,restcore/ 中间件仅五个(实测 ls 输出):ApiEnabledMiddleware.php、AuthMiddleware.php、CacheMiddleware.php、OfflineMiddleware.php、VersionMiddleware.php。

3.3 端点域清单

路由注册实测自 restcore/*.php(grep 'g_app->'),按域汇总:

域路由文件代表端点本文实测
用户users_rest.phpGET /users/me、GET /users/{id}、POST /users/me/token、DELETE /users/me/token/{id}已实测
项目projects_rest.phpGET /projects、POST /projects、版本/子项目/项目用户管理GET 已实测
问题issues_rest.phpGET/POST/PATCH/DELETE /issues、/{id}/notes、/{id}/tags、/{id}/monitors、/{id}/relationships、文件上传已实测(含 PATCH)
过滤器filters_rest.phpGET /filters、DELETE /filters/{id}已实测 GET
配置config_rest.phpGET /config、PATCH /config已实测 GET
语言串lang_rest.phpGET /lang已实测
页面数据pages_rest.phpGET /pages/issues/view/{id}路由核验

3.4 通用行为约定

  • 响应为 application/json;线上传输的中文以 \uXXXX Unicode 转义出现(如 \u7ba1\u7406\u5458),本文回显为便于阅读已解码为原文,字段名、结构与数值逐字对应实测;
  • 列表与单条查询都是数组包装:用户查询返回 {"users":[...]},问题查询返回 {"issues":[...]}——即使按 id 只查一条;
  • 时间为 ISO 8601 UTC(如 2026-10-21T07:07:43+00:00);
  • 认证中间件先于路由执行:未认证访问任何路径一律 401(含无路由的根路径 /api/rest/,实测 401);认证后访问不存在的路由返回 Slim 的 HTML「Page Not Found」。因此排障时 401 说明路由可达(认证失败)、404 才是路由不通。

四、认证与首次调用

4.1 失败对照:无令牌与错误令牌

TOKEN='kspt3Sunv39aLO8EnxJbecnwQ_aQg_MF'
BASE='http://localhost:8132/mantisbt/api/rest'

curl -s -m 30 -o /dev/null -w '%{http_code}\n' "$BASE/issues"
curl -s -m 30 -H 'Authorization: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' -o /dev/null -w '%{http_code}\n' "$BASE/issues"

实测输出:

401
401

注意:401 的响应体为空(无 JSON 错误信息,Content-Type 为 text/html),排查认证问题只能靠状态码。

4.2 首次成功调用

curl -s -m 30 -H "Authorization: $TOKEN" "$BASE/users/me"

实测输出:

{"id":1,"name":"administrator","email":"root@localhost","language":"chinese_simplified","timezone":"UTC","access_level":{"id":90,"name":"administrator","label":"管理员"},"created_at":"2026-10-21T07:07:43+00:00","projects":[{"id":1,"name":"demo-app"}]}

字段解读:access_level.label 来自本地化枚举串(中文界面下显示「管理员」,与配置参数详解篇 5.4 的枚举双串机制同源);projects 是当前用户可见的项目列表。

五、各 API 域详解

5.1 用户域 /users

按 id / 用户名查询:

curl -s -m 30 -H "Authorization: $TOKEN" "$BASE/users/username/dev1"
curl -s -m 30 -H "Authorization: $TOKEN" "$BASE/users/2"

实测输出(两条返回一致,均为数组包装):

{"users":[{"id":2,"name":"dev1","real_name":"开发一号","email":"dev1@localhost","language":"chinese_simplified","timezone":"UTC","access_level":{"id":55,"name":"developer","label":"开发人员"},"created_at":"2026-10-21T07:19:46+00:00"}]}

裸调 GET /users 会 400——用户域没有「列出全部用户」的端点,必须带 id 或用户名:

curl -s -m 30 -H "Authorization: $TOKEN" "$BASE/users"

实测输出:

{"message":"User not specified","code":29,"localized":"用于“user_id”的值无效"}

令牌全生命周期实测(users_rest.php 路由:POST /users/me/token 创建、DELETE /users/me/token/{id} 撤销):

第一步,用现有令牌创建新令牌:

curl -s -m 30 -X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"d5-curl-lifecycle"}' -w '\nSTATUS %{http_code}\n' "$BASE/users/me/token"

实测输出(新令牌明文仅此一次):

{"id":3,"name":"d5-curl-lifecycle","token":"jIC17ZKloxdp_YmsMqCQ95LS6lQi95OA","user":{"id":1,"name":"administrator","email":"root@localhost","email_pending":"administrator@mantis.local"}}
STATUS 201

第二步,用新令牌调用:

curl -s -m 30 -H "Authorization: jIC17ZKloxdp_YmsMqCQ95LS6lQi95OA" -o /dev/null -w '%{http_code}\n' "$BASE/users/me"

实测输出:200

第三步,撤销新令牌:

curl -s -m 30 -X DELETE -H "Authorization: $TOKEN" -o /dev/null -w '%{http_code}\n' "$BASE/users/me/token/3"

实测输出:204(无响应体)

第四步,被撤销令牌再调用:

curl -s -m 30 -H "Authorization: jIC17ZKloxdp_YmsMqCQ95LS6lQi95OA" -o /dev/null -w '%{http_code}\n' "$BASE/users/me"

实测输出:401

步骤请求状态码说明
创建POST /users/me/token201响应含一次性明文 token
使用GET /users/me200立即可用
撤销DELETE /users/me/token/3204空响应体
复用GET /users/me401立即失效

5.2 项目域 /projects

curl -s -m 30 -H "Authorization: $TOKEN" "$BASE/projects"

实测输出:

{"projects":[{"id":1,"name":"demo-app","status":{"id":10,"name":"development","label":"开发中"},"description":"Demo project for document testing","enabled":true,"view_state":{"id":10,"name":"public","label":"公开"},"inherit_global":true,"access_level":{"id":90,"name":"administrator","label":"管理员"},"custom_fields":[],"versions":[],"categories":[{"id":1,"name":"General","project":{"id":0,"name":null},"status":"1"},{"id":2,"name":"General","project":{"id":1,"name":"demo-app"},"status":"1"}]}]}

注意 categories 里有两条 General:id=1 是全局分类(project.id=0),id=2 是 demo-app 的项目级分类。REST 创建问题时用 {"category":{"name":"General"}} 按名称匹配即可。

5.3 问题域 /issues

分页列表(默认按最后更新时间倒序):

curl -s -m 30 -H "Authorization: $TOKEN" "$BASE/issues?page_size=2&page=1"

实测输出(节选,响应同时携带每个问题的 notes 与 history):

{"issues":[{"id":2,"summary":"REST API 创建的测试问题","description":"通过 POST /api/rest/issues 创建,用于文档实测。","project":{"id":1,"name":"demo-app"},"category":{"id":1,"name":"General"},"reporter":{"id":1,"name":"administrator","email":"root@localhost","email_pending":"administrator@mantis.local"},"status":{"id":10,"name":"new","label":"新建","color":"#fcbdbd"},"resolution":{"id":10,"name":"open","label":"未处理"},"priority":{"id":30,"name":"normal","label":"中"},"severity":{"id":50,"name":"minor","label":"小错误"},"sticky":false,"created_at":"2026-10-21T07:31:56+00:00","updated_at":"2026-10-21T07:32:07+00:00","notes":[……节选……],"history":[……节选……]},……节选……]}

排序实测:采集该响应时库内 3 个问题的 updated_at 分别为 #1 08:01:06、#2 07:32:07、#3 07:58:58,page_size=2&page=1 返回的是 #1、#3——严格按 updated_at 倒序取前 2 条。

单条查询(注意同样是数组包装):

curl -s -m 30 -H "Authorization: $TOKEN" "$BASE/issues/1"

实测输出(节选,含分派人、备注、附件结构):

{"issues":[{"id":1,"summary":"登录页验证码在低分辨率下重叠","description":"在 1280x720 分辨率下,登录页验证码与输入框重叠,无法辨认。","steps_to_reproduce":"打开 login_page.php","project":{"id":1,"name":"demo-app"},"category":{"id":1,"name":"General"},"reporter":{"id":1,"name":"administrator","email":"root@localhost","email_pending":"administrator@mantis.local"},"handler":{"id":2,"name":"dev1","real_name":"开发一号","email":"dev1@localhost"},"status":{"id":50,"name":"assigned","label":"已分配","color":"#c2dfff"},"resolution":{"id":10,"name":"open","label":"未处理"},"platform":"PC","os":"Ubuntu 22.04","sticky":false,"created_at":"2026-10-21T07:11:42+00:00","updated_at":"2026-10-21T07:23:18+00:00","notes":[{"id":1,"reporter":{"id":1,"name":"administrator","email":"root@localhost","email_pending":"administrator@mantis.local"},"text":"复现步骤文件见附件","view_state":{"id":10,"name":"public","label":"公开"},"attachments":[{"id":1,"reporter":{"id":1,"name":"administrator","email":"root@localhost","email_pending":"administrator@mantis.local"},"created_at":"2026-10-21T07:23:18+00:00","filename":"step.txt","size":58,"content_type":"text/plain; charset=us-ascii"}],"type":"note","created_at":"2026-10-21T07:23:18+00:00","updated_at":"2026-10-21T07:23:18+00:00"}],"history":[……节选……]}]}

查询不存在的 id:

curl -s -m 30 -H "Authorization: $TOKEN" "$BASE/issues/99999"

实测输出(localized 字段是服务端按语言包翻译好的报错文案):

{"message":"Issue #99999 not found","code":1100,"localized":"没有找到问题 99999 。"}

创建问题(必填 summary、description、project、category;其余字段走枚举默认值):

curl -s -m 30 -X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"summary":"REST API 创建问题验证","description":"通过 POST /api/rest/issues 以 API 令牌创建,用于《REST API 使用详解》实测闭环。","project":{"id":1},"category":{"name":"General"}}' \
  -w '\nSTATUS %{http_code}\n' "$BASE/issues"

实测输出(节选;新建问题落为 #4,severity 默认 minor、status 默认 new,history 只有一条「新建问题」):

{"issue":{"id":4,"summary":"REST API 创建问题验证","description":"通过 POST /api/rest/issues 以 API 令牌创建,用于《REST API 使用详解》实测闭环。","project":{"id":1,"name":"demo-app"},"category":{"id":1,"name":"General"},"reporter":{"id":1,"name":"administrator","email":"root@localhost","email_pending":"administrator@mantis.local"},"status":{"id":10,"name":"new","label":"新建","color":"#fcbdbd"},"resolution":{"id":10,"name":"open","label":"未处理"},"priority":{"id":30,"name":"normal","label":"中"},"severity":{"id":50,"name":"minor","label":"小错误"},"reproducibility":{"id":70,"name":"have not tried","label":"没有试验"},"sticky":false,"created_at":"2026-10-21T08:05:17+00:00","updated_at":"2026-10-21T08:05:17+00:00","history":[{"created_at":"2026-10-21T08:05:17+00:00","user":{"id":1,"name":"administrator","email":"root@localhost","email_pending":"administrator@mantis.local"},"type":{"id":1,"name":"issue-new"},"message":"新建问题"}]}}
STATUS 201

更新问题(PATCH,实测把 #4 的 severity 从 minor 改为 major):

curl -s -m 30 -X PATCH -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"severity":{"name":"major"}}' -o /dev/null -w '%{http_code}\n' "$BASE/issues/4"

实测输出:200

PATCH 响应体返回更新后的完整问题(数组包装)。实测更新后关键字段与末条 history:

"severity": {"id": 60, "name": "major", "label": "很严重"}
{"created_at":"2026-10-21T08:07:07+00:00","user":{"id":1,"name":"administrator","email":"root@localhost","email_pending":"administrator@mantis.local"},"field":{"name":"severity","label":"严重性"},"type":{"id":0,"name":"field-updated"},"old_value":{"id":50,"name":"minor","label":"小错误"},"new_value":{"id":60,"name":"major","label":"很严重"},"message":"严重性","change":"小错误 => 很严重"}

5.4 备注域 /issues/{id}/notes

字段名陷阱实测:创建备注的字段是 text 而不是 body。先看错误示范:

curl -s -m 30 -X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"body":"REST API 备注验证"}' -w '\nSTATUS %{http_code}\n' "$BASE/issues/1/notes"

实测输出:

{"message":"Issue note not specified.","code":11,"localized":"必填字段“问题注释”为空。请重新检查您的输入。"}
STATUS 400

字段名依据:core/commands/IssueNoteAddCommand.php 头部 docblock 示例即 "text": "This is a test issue note",第 164 行 trim( $this->payload( 'text', '' ) )。

正确请求:

curl -s -m 30 -X POST -H "Authorization: $TOKEN" -H 'Content-Type: application/json' \
  -d '{"text":"REST API 添加备注验证:POST /issues/1/notes 携带 text 字段(API 令牌通道)。"}' \
  -w '\nSTATUS %{http_code}\n' "$BASE/issues/1/notes"

实测输出(节选;顶层同时返回新备注 note 与整个问题 issue):

{"note":{"id":4,"reporter":{"id":1,"name":"administrator","email":"root@localhost","email_pending":"administrator@mantis.local"},"text":"REST API 添加备注验证:POST /issues/1/notes 携带 text 字段(API 令牌通道)。","view_state":{"id":10,"name":"public","label":"公开"},"attachments":[],"type":"note","created_at":"2026-10-21T08:05:17+00:00","updated_at":"2026-10-21T08:05:17+00:00"},"issue":{……节选……}}
STATUS 201

删除备注:

curl -s -m 30 -X DELETE -H "Authorization: $TOKEN" -o /dev/null -w '%{http_code}\n' "$BASE/issues/1/notes/4"

实测输出:200(响应体返回删除后的完整问题 JSON;其 history 中留下成对记录——实测两条:"type":{"id":2,"name":"note-added"},"note":{"id":4} 与 "type":{"id":4,"name":"note-deleted"},"note":{"id":4})。

5.5 其余端点域速览

对 config、lang、filters 三个只读端点的实测(均为当前账号视角的空集合/当前语言):

curl -s -m 30 -H "Authorization: $TOKEN" "$BASE/config"
curl -s -m 30 -H "Authorization: $TOKEN" "$BASE/lang"
curl -s -m 30 -H "Authorization: $TOKEN" "$BASE/filters"

实测输出:

{"configs":[]}
{"strings":[],"language":"chinese_simplified"}
{"filters":[]}

GET /config 支持查询参数拉取指定配置项、GET /lang 支持按参数返回指定语言串、GET /filters 返回当前用户保存的过滤器;问题域还有标签挂载(POST /issues/{id}/tags)、监控人(/{id}/monitors)、关联(/{id}/relationships)与 base64 文件上传(payload 携带 files 数组,服务端 files_base64_to_temp() 落临时目录)等子端点,均已在 3.3 表中标注实测状态,使用前可对照 mantisbt_openapi.yaml 的请求/响应 schema。

六、分页与通用约定

  • 分页参数:page_size 每页条数 + page 页码(从 1 起)。实测 page_size=2&page=1 精确返回 2 条;
  • 排序:问题列表默认按 updated_at 倒序(5.3 实测验证),无显式排序参数;
  • 总数感知:响应没有 X-Total-Count 之类的总数头(实测全量响应头仅 Content-Type: application/json),客户端翻页到返回空数组即知到底;
  • 错误码对照(全部实测):
场景状态码响应体实测 code/localized
无/错令牌401空——
备注缺 text 字段400JSONcode 11「必填字段“问题注释”为空」
裸调 /users400JSONcode 29「用于“user_id”的值无效」
问题不存在404JSONcode 1100「没有找到问题 99999 。」
令牌撤销后复用401空——

七、部署侧排障实录

以下两组故障均在本文环境真实触发并复原,回显为逐字实拍。

7.1 AllowOverride None → REST 全部 404

Ubuntu 的 Apache 默认 AllowOverride None,发行包 api/rest/.htaccess(内容实测)整体失效——Slim 重写与 CGIPassAuth 都不生效:

# Based on Slim Framework recommendation @ http://docs.slimframework.com/routing/rewrite/
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

# Requests failing due to missing Authorization header, see #26365
CGIPassAuth On

把覆盖配置临时改为 AllowOverride None 并 reload 后实测(复原前抓取):

curl -s -m 30 -H "Authorization: $TOKEN" -o /dev/null -w 'REST users/me HTTP %{http_code}\n' \
  http://localhost:8132/mantisbt/api/rest/users/me
REST users/me HTTP 404

响应是 Apache 原生 404 页(<title>404 Not Found),说明请求根本没进 index.php。修复三步:覆盖配置恢复 AllowOverride All → a2enmod rewrite → reload,实测恢复后同请求返回 200。

7.2 Authorization 头被丢弃 → 401

Apache 2.4 + mod_php 下,PHP 侧 $_SERVER['HTTP_AUTHORIZATION'] 默认收不到 Authorization 头。实测对照:在无目录级 .htaccess 覆盖的 /var/www/html/ 下放探针页 hdrprobe.php(输出 $_SERVER['HTTP_AUTHORIZATION'] ?? '(missing)'):

  • 全局 CGIPassAuth On 生效时:HTTP_AUTHORIZATION: probe-test-123
  • 注释掉全局 CGIPassAuth 并 reload 后:HTTP_AUTHORIZATION: (missing)——头被 Apache 吞掉

而 api/rest/ 在同一时刻仍返回 200,原因是发行包 api/rest/.htaccess 自带 CGIPassAuth On(对应上游 issue #26365)且 AllowOverride All 使其生效——两处证据互相印证:修复 = AllowOverride All(让 .htaccess 生效)+ CGIPassAuth On(让认证头到达 PHP)。CGIPassAuth 已内置在发行包 .htaccess 中,手工补的只有 AllowOverride All。

7.3 Nginx 方案的 REST 路由

Nginx 没有 .htaccess,/api/rest/* 必须显式重写到 Slim 入口(配置实拍,见安装指南方案 B):

location /api/rest {
    try_files $uri /api/rest/index.php$is_args$args;
}

缺这条时全部 REST 请求 404;配好后未带令牌访问返回 401 即为「路由可达、等待认证」的正常信号(安装指南方案 B 的 rest-auth 401 冒烟即此用法)。

7.4 令牌值取错:页面上的 32 位串不一定是令牌

实测在令牌创建结果页做 DOM 解析时,页面里会出现两个 32 位串:PHP 会话 ID(纯十六进制)与 API 令牌(uri-safe,含 -/_)。拿会话 ID 当令牌必然 401。可靠定位法:对候选串逐一计算 sha256,与 mantis_api_token_table.hash 比对(2.2 节实测命中 044194c42622... 前缀即为真令牌)。

八、常见问题(FAQ)

Q1:令牌忘了/丢了怎么办? 无法找回。明文只在创建响应出现一次(GUI 与 REST 一致),只能撤销旧令牌重新创建;脚本侧把令牌放进配置管理或密钥服务,不要硬编码在代码里。

Q2:令牌确定正确却一直 401? 按 7.2 排查:先确认 Authorization 头真的到达 PHP(探针页打印 $_SERVER['HTTP_AUTHORIZATION']),Apache 场景检查 AllowOverride All 与 CGIPassAuth On 是否同时满足;注意令牌放头里不加 Bearer 前缀(实测裸令牌可用)。

Q3:响应里的中文全是 \u7ba1\u7406\u5458 这样的转义? 服务端 JSON 编码的正常行为,不是乱码。任何标准 JSON 解析器(jq、requests.json()、JSON.parse)都会自动还原为中文。

Q4:按 id 查一条问题为什么返回 issues 数组? 通用列表结构的兼容设计,客户端取 issues[0] 即可(5.3 实测)。判断「查没查到」直接看数组长度,404 只有在 id 完全不存在时出现。

Q5:想要问题的总数做分页进度怎么办? REST 响应不带总数头(第六节实测)。变通:按 page_size 逐页翻到空数组计数;或直接查库 SELECT COUNT(*) FROM mantis_bug_table;(本文实测口径)。

Q6:能否不开令牌直接让第三方匿名读问题? OpenAPI 描述提到匿名访问可免令牌,前提是配置参数详解篇 5.5 的 $g_anonymous_account 指向专用只读账号;该通道本文未实测,生产启用前请先小范围验证权限边界。

九、参考链接

  • 发行包 OpenAPI 规范:api/rest/mantisbt_openapi.yaml(OpenAPI 3.0.3,version 2.28.4)
  • 发行包 Postman 集合:api/rest/api.postman_collection.json、api/rest/env.postman_environment.json
  • 令牌实现:core/api_token_api.php(api_token_hash() 第 104 行,sha256)
  • 备注字段定义:core/commands/IssueNoteAddCommand.php(docblock "text" 示例)
  • 路由注册:api/rest/restcore/{users,projects,issues,filters,config,lang,pages}_rest.php
  • 上游 Authorization 头修复记录:MantisBT issue #26365(.htaccess 注释引用)
  • 本专栏相关篇目:《MantisBT 2.28.4 介绍与选型指南》《Ubuntu 22.04 安装 MantisBT 指南》《MantisBT 2.28.4 配置参数详解》