接口鉴权必须在中间件中完成JWT解析、时间戳验证、黑名单检查、用户上下文注入、权限比对五步,且注册顺序须为AuthToken在CheckPermission前,路由命名与权限表name严格一致,响应按请求类型动态适配。

接口鉴权必须在中间件里做,且不能只校验 token 是否存在——JWT 解析、时间戳验证、黑名单检查、用户上下文注入、权限比对这五步缺一不可,否则就是裸奔。
中间件注册顺序决定鉴权是否生效
认证中间件(如 AuthToken)必须放在权限中间件(如 CheckPermission)之前,且不能早于路由匹配完成阶段。在 app/middleware.php 的 'http' 数组中,它应排在 SessionInit::class 和 AllowCrossDomain::class 之后,但不能在已有鉴权类之后(比如 CheckAuth::class 后面再加一个,会重复拦截或取不到路由名)。
错误示例:['auth', 'permission'] 全局注册后,又在路由分组里写 ->middleware(['auth', 'permission']),会导致双倍校验;正确做法是只保留一种注册方式,推荐路由级绑定,更可控。
- 若用全局注册,
AuthToken必须在CheckPermission前,否则$request->getAttribute('auth')为空 -
$request->rule()->getName()在中间件里调用前,必须确认$request->rule()不为 null,否则报Call to a member function getName() on null - 不要在构造函数里查 session 或调用
Auth::check(),此时$request尚未绑定完整上下文
JWT 解析必须显式校验算法与时间字段
用 firebase/php-jwt 时,JWT::decode() 必须传入明确的算法数组(如 ['HS256']),不能依赖默认值,否则可能被 none 算法绕过;同时必须手动校验 iat 和 exp,不能只靠库抛异常——因为 PHP 时间不同步、服务端和客户端时区不一致、或 time() 被覆写都可能导致误判。
立即学习“PHP免费学习笔记(深入)”;
- 签发时用
time(),校验前也用time(),避免混用date('U')或字符串时间戳 - 解码前先用正则提取 token:
preg_match('/^Bearer\s+(?<token>[^\s]+)/i', $authHeader, $matches)</token>,防止空格、换行、Basic混入 - 解码后立刻检查
$payload->iss和$payload->aud,防跨域复用(如后台 token 被前端 API 拿去调用) - 别只 catch
JWTException,还要捕获UnexpectedValueException(token 被篡改)、InvalidArgumentException(base64 解码失败)
权限中间件必须基于路由名而非 URL 路径匹配
用 $request->url()->getPath() 或拼接 $request->controller() . '.' . $request->action() 做权限判断,极易因大小写、资源路由(如 article/create)、多应用前缀(admin. vs api.)出错。唯一稳定的方式是统一使用 $request->rule()->getName(),前提是所有需鉴权的路由都显式命名,例如:
Route::get('admin/user/list', 'Admin/UserController@list')->name('admin.user.list');
权限表 auth_rule.name 字段必须与之完全一致(含点号分隔、小写),否则缓存查不到、数据库比对失效。
- 游客直接重定向到登录页:
return redirect('/login')->with('msg', '请先登录'); - 超管(
is_super = 1)短路放行:if ($user['is_super']) return $next($request); - 非超管用户查缓存:
Cache::get("auth_role_rules_{$roleId}"),没命中再走四表关联查询(user → role → role_auth → auth_rule) - 白名单路径(如
/api/login、/public/*)用str_starts_with($path, $prefix)快速跳过,别进权限逻辑
响应格式必须按请求类型动态适配
同一个鉴权中间件既要处理浏览器访问(返回 403 页面),又要服务 Vue/React 的 fetch 请求(返回 JSON 401),硬编码 return json(...) 或 redirect(...) 都会翻车。得根据 $request->isAjax() 和 Accept 请求头判断:
- 若
$request->header('accept') === 'application/json'或$request->isAjax()为 true,返回json(['code' => 401, 'msg' => 'Unauthorized'])->code(401) - 否则返回
redirect('/login')->with('msg', '请先登录') - 别用
Cache::get('xxx') === null判断缓存是否存在——File 缓存驱动对空值也写盘,要用Cache::has('xxx') - 权限校验失败时,别 throw 新异常,直接 return 响应对象,确保中间件链终止
最常被忽略的是 jti 黑名单机制:签发 token 时必须生成服务端 jti(uniqid(true)),每次 decode 前先查 Redis 是否存在 jwt:blacklist:{$payload->jti},否则“退出登录”就只是前端删 token,后端毫无感知。



















