FastAdmin noNeedLogin 配了 ['*'] 还是 401,元凶是 new 了另一个 Controller
一个 FastAdmin(ThinkPHP 5)接口,$noNeedLogin = ['*'] 写得清清楚楚,PHP-FPM 也重载了,Nginx 没有 auth_basic,在同目录建了一个空白探针 Controller 返回 200——但这个接口就是稳定吐 401。
找了半小时,所有"显而易见"的排查点都是正常的,只有这一个 action 例外。
现象
HTTP 401
{"code":401,"msg":"请登录后操作","time":"...","data":null}
对比实验:把 action 里的业务逻辑全注释掉,直接 $this->success('ok'),立刻返回 200。业务逻辑一恢复,又 401。
根因
FastAdmin 的基类 application/common/controller/Api.php 的构造函数无条件调用 _initialize():
public function __construct(Request $request = null)
{
$this->request = is_null($request) ? Request::instance() : $request;
$this->_initialize(); // ← 无论什么情况都会跑
}_initialize() 里会做 token 鉴权:
if (!$this->auth->match($this->noNeedLogin)) {
$this->auth->init($token);
if (!$this->auth->isLogin()) {
$this->error(__('Please login first'), null, 401); // → throw HttpResponseException
}
}关键在于:$this->noNeedLogin 解析的是被 new 出来的那个对象的类,而不是外层调用方的类。
问题代码长这样:
class DebugTask extends Api
{
protected $noNeedLogin = ['*']; // 外层免登录
public function run()
{
$sc = new BusinessController(); // ← 触发器
$result = $sc->someMethod(...);
$this->success('ok', $result);
}
}BusinessController 继承 Api,默认 $noNeedLogin = [](需要登录)。new BusinessController() 一执行,Api::__construct 就跑了,_initialize 拿着 [] 做鉴权,发现当前请求没有 token,于是 throw new HttpResponseException。
这个异常直接被 ThinkPHP 框架层捕获并转成 Response 发出去——外层的 DebugTask::run() 根本没有机会继续执行,仿佛外层的 ['*'] 被"绕过"了。
FastAdmin 里所有继承 Api 的 Controller,实例化时都会在 __construct 里重新走一遍 _initialize 鉴权。你的外层 Controller 放了多少白名单都没用——只要 new 了一个要求登录的 Api 子类,请求就会在那里被截断,抛出 HttpResponseException,并由框架直接吐 401 给客户端。
这个行为不是 FastAdmin 的 bug,而是它的设计——但几乎所有人第一次遇到时都不会往这里想。
正解
方案一:把可复用逻辑提取成 Service(推荐)
Controller 不是代码复用单元,Service 才是。把业务方法移到一个普通 PHP 类里,没有继承 Api,也就没有 _initialize:
// application/service/BusinessService.php
namespace app\service;
class BusinessService
{
public static function compute(string $input): array
{
// 纯逻辑,不依赖 Api、不依赖 $this->auth
return [...];
}
}
// DebugTask.php
public function run()
{
$result = \app\service\BusinessService::compute($input);
$this->success('ok', $result);
}方案二:把目标方法改成 static(快速修复)
如果方法不依赖实例状态,加一个 static 关键字,直接类方法调用,不 new,不触发构造函数:
// BusinessController.php
public static function someMethod(string $input): array { ... }
// DebugTask.php
$result = \app\api\controller\BusinessController::someMethod($input);方案三:ReflectionClass::newInstanceWithoutConstructor(应急)
来不及重构时用反射跳过构造函数。注意目标方法里如果依赖 $this->auth / $this->request 会是 null:
$ref = new \ReflectionClass(\app\api\controller\BusinessController::class);
$obj = $ref->newInstanceWithoutConstructor(); // 跳过 __construct,跳过 _initialize
$result = $obj->someMethod($input);在 FastAdmin / ThinkPHP 5 里,复用业务逻辑的正确位置是 application/service/ 下的 Service 类,或把方法标为 static 直接调用。绝对不要 new 另一个 Api 子类来"借用"它的方法——那等于在你的 action 里埋了一个隐式的鉴权检查点。
排查捷径
怀疑某个接口有这个问题时,30 秒可以复现:临时注释掉 action 里所有的 new XxxController(),如果立刻从 401 变 200,根因确认。
批量排查存量代码:
grep -rn 'new.*Controller(' application/api/controller/找到的结果如果所在文件里有 $noNeedLogin = ['*'],就是潜在的 401 炸弹。
一句话外卖
FastAdmin 里每一次
new Api子类()都是一道独立的鉴权门——它检查的是被实例化那个类自己的$noNeedLogin,而不是调用方的。