【Webman+MySQL答题系统九】登录鉴权:HMAC Token 与中间件
果子发表于:2026-10-05 20:55:47浏览:1次
本篇目标
实现登录与接口鉴权:password_verify 校验、HMAC 签名 token 的签发与验证、中间件统一拦截,替换掉前面写死的 user_id=1。

一、鉴权流程

不引入第三方 JWT 库,手写一个"载荷 + HMAC 签名"的两段式 token,原理与 JWT 完全同构,看得见摸得着:
二、登录接口
<?php
namespace app\controller;
use support\Request;
use support\Db;
use app\service\Auth;
class AuthController
{
public function login(Request $request)
{
$username = trim((string)$request->post('username'));
$password = (string)$request->post('password');
$user = Db::table('user')->where('username', $username)->first();
if (!$user || !password_verify($password, $user->password)) {
return error('用户名或密码错误', 401);
}
return success([
'token' => Auth::issue((int)$user->id),
'nickname' => $user->nickname,
]);
}
}
三、token 服务
app/service/Auth.php:
<?php
namespace app\service;
class Auth
{
// 签发:7 天有效
public static function issue(int $uid): string
{
$payload = self::b64(json_encode([
'uid' => $uid,
'exp' => time() + 86400 * 7,
]));
return $payload . '.' . self::sign($payload);
}
// 校验:通过返回 uid,失败返回 null
public static function verify(string $token): ?int
{
$parts = explode('.', $token);
if (count($parts) !== 2) {
return null;
}
[$payload, $sign] = $parts;
if (self::sign($payload) !== $sign) { // 签名防篡改
return null;
}
$data = json_decode(self::b64($payload, true), true);
if (!$data || ($data['exp'] ?? 0) < time()) { // 过期检查
return null;
}
return (int)$data['uid'];
}
private static function sign(string $payload): string
{
return hash_hmac('sha256', $payload, env('APP_SECRET', 'quiz-secret'));
}
private static function b64(string $s, bool $decode = false)
{
return $decode ? base64_decode(strtr($s, '-_', '+/'))
: rtrim(strtr(base64_encode($s), '+/', '-_'), '=');
}
}
安全要点:
- 密钥从
.env读(APP_SECRET=换成随机长串),绝不硬编码进 Git; hash_equals替换!==可进一步防时序攻击,安全要求高的项目建议替换;- stateless token 无法主动吊销,需要"强制下线"能力就加 redis 黑名单,按需取舍。
四、鉴权中间件
app/middleware/AuthCheck.php:
<?php
namespace app\middleware;
use support\Request;
use support\Response;
use Webman\MiddlewareInterface;
use app\service\Auth;
class AuthCheck implements MiddlewareInterface
{
public function process(Request $request, callable $handler): Response
{
$token = preg_replace('/^Bearer\s+/i', '', $request->header('authorization', ''));
$uid = Auth::verify($token);
if (!$uid) {
return json(['code' => 401, 'msg' => '请先登录']);
}
$request->uid = $uid; // 后续控制器直接 $request->uid
return $handler($request);
}
}
五、挂载到路由分组
use app\middleware\AuthCheck;
use app\controller\AuthController;
Route::post('/api/auth/login', [AuthController::class, 'login']);
Route::group('/api', function () {
// 答题、统计等全部业务路由……
})->middleware([AuthCheck::class]);
然后把各控制器里的写死值替换掉:
// AnswerController::submit / StatController 等
$userId = (int)($request->uid ?? 1);
六、验证
// 1. 登录拿 token
curl -X POST http://127.0.0.1:8787/api/auth/login -d "username=zhangsan" -d "password=123456"
// {"code":0,"data":{"token":"eyJ...xxx","nickname":"张三"}}
// 2. 不带 token 访问受保护接口
curl "http://127.0.0.1:8787/api/stat/my_records"
// {"code":401,"msg":"请先登录"}
// 3. 带 token 访问
curl "http://127.0.0.1:8787/api/stat/my_records" -H "Authorization: Bearer eyJ...xxx"
// {"code":0,...}
小结
登录、签发、校验、中间件拦截全部就位,user_id 不再写死。最后一篇做前后端联调,把整套系统跑起来。
栏目分类全部>
推荐文章
- 【Webman+MySQL答题系统九】登录鉴权:HMAC Token 与中间件
- ThinkPHP+uniAPP 公众号线上答题软件答题抽奖赢红包知识竞赛宣传活动链接系统
- element-ui 表格组件el-table操作toggleRowSelection事件会主动触发selection-change的坑
- 大模型的「幻觉」是什么?为什么会胡说八道
- Vue Button按钮点击下载文件的方法
- Phpstorm之快捷键
- 新闻网站源码 网站群系统+精美wap手机端(包含数据)淘宝在售源码
- RAG 检索增强生成入门:让大模型「开卷考试」
- 【Webman+MySQL答题系统二】环境搭建与项目初始化
- 【Webman+MySQL教程五】开发完整的 JSON 接口:统一返回、参数校验与异常处理

