ThinkPHP 8.0 手动实现登录需严格串联五环节:1. 表单提交后显式调用验证器check();2. 用password_verify()比对哈希密码;3. JWT签发含uid/iat/exp/iss/aud,密钥从环境变量读取;4. 中间件提取Bearer Token并校验签名与声明,注入auth属性;5. Redis维护token_version实现单设备登录控制。

一、登录接口先做严格数据校验
验证器必须显式调用 check() 才生效,否则规则形同虚设:
- 用
php think make:validate Login生成验证器,定义用户名/密码格式、必填、最小长度等规则 - 控制器中不能只写
validate(Login::class),要完整链式调用:validate(Login::class)->scene('login')->check($data) - 失败时捕获
ValidateException,返回统一错误结构,避免静默放行
二、密码验证用 password_verify(),别手写比对
从数据库查出用户后,直接比对哈希密码,不拼接明文、不忽略大小写:
- 查库用
Db::name('user')->where('username', $data['username'])->find(),确保字段索引已建 - 验证密码:
password_verify($data['password'], $user['password_hash']) - 若失败,统一返回“账号或密码错误”,不区分是用户名不存在还是密码错(防枚举)
三、签发 JWT 要带必要声明,且密钥从环境读取
JWT 不是简单 base64 编码,必须含标准字段并由服务端可信密钥签名:
- payload 至少包含:
uid(用户ID)、iat(签发时间)、exp(过期时间,建议 2 小时)、iss(签发方)、aud(受众) - 密钥从
$_ENV['JWT_SECRET']读取,并用trim()去首尾空格,防止不可见字符导致验签失败 - 使用
firebase/php-jwt:^6.10(非 v7.x),签发时传new Key($secret, 'HS256')和算法数组['HS256']
四、中间件里完成 token 校验与用户注入
认证中间件只负责“识别身份”,不处理跳转、不查权限、不修改响应体:
立即学习“PHP免费学习笔记(深入)”;
- 从
$request->header('authorization')提取 token,用正则/Bearer\s+(.+)/i安全提取,避免空格异常 - decode 前检查 header 中
alg是否为HS256;decode 后强制校验iss和aud - 校验通过后,把
uid、role等字段存入请求属性:$request->withAttribute('auth', ['uid' => $payload->uid]) - 拒绝时统一抛
HttpException(401),交由全局异常处理器返回 JSON
五、支持单设备登录需加 Redis 版本控制
仅靠 JWT 的 exp 无法实现“新登录挤掉旧登录”,必须引入有状态协调:
- 用户每次登录成功,递增数据库中
token_version字段,并写入 Redis:SET jwt:version:{uid} {new_version} EX 7500 - JWT payload 中携带该版本号(如
v字段) - 中间件校验时,先 decode 得到
uid和v,再查 Redis:GET jwt:version:{uid},值不一致即返回“账号已在其他设备登录” - 登出时只需
DEL jwt:version:{uid},下次请求自动失效



















