
laravel 8 中使用 tymon/jwt-auth 实现 jwt 认证时,登录接口持续返回 401 unauthorized,根本原因通常是用户密码未使用 laravel 默认的 bcrypt 哈希方式存储,而误用了 md5 等不兼容的加密算法。
在 Laravel 的认证体系中,Auth::attempt()(以及 JWT 的 guard()->attempt())底层依赖 IlluminateAuthEloquentUserProvider::validateCredentials() 方法,该方法会调用 $user->getAuthPassword() 获取密码字段值,并使用 Hash::check() 进行比对。而 Hash::check() 仅原生支持 bcrypt(及 argon2id、argon2i 等 Laravel 支持的哈希算法),完全不识别 MD5、SHA1 或明文密码。
因此,即使你的数据库中 password 字段存在值,若其是通过 md5(‘xxx’) 或其他非 bcrypt 方式写入的(例如手动 SQL 插入、旧系统迁移、或错误调用 Hash::make() 之外的函数),$this->guard()->attempt($credentials) 将始终返回 false,最终触发 401 Unauthorized 响应。
✅ 正确做法:确保所有用户密码均通过 Laravel 的 Hash::make() 生成(默认即为 bcrypt):
// 创建用户时(推荐使用 factory 或 seeder) use IlluminateSupportFacadesHash; $user = User::create([ 'name' => 'John Doe', 'email' => 'john@example.com', 'password' => Hash::make('secret123'), // ✅ 正确:生成 bcrypt 哈希 ]);
⚠️ 常见错误示例(将导致 401):
// ❌ 错误:MD5 不被 Laravel 认证组件识别 'password' => md5('secret123') // ❌ 错误:使用过时的 bcrypt 手动调用(缺少盐值管理与版本兼容性) 'password' => bcrypt('secret123') // 虽然结果是 bcrypt,但不推荐;应统一用 Hash::make() // ❌ 错误:明文存储 'password' => 'secret123'
? 验证密码哈希格式:
bcrypt 哈希值以 $2y$、$2a$ 或 $2b$ 开头(Laravel 8+ 默认为 $2y$)。可在数据库中检查 users.password 字段值是否符合该模式:
SELECT id, email, password FROM users LIMIT 3; -- ✅ 正常输出示例:$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi -- ❌ 异常示例:5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
?️ 快速修复已存在的 MD5 用户(仅限开发/测试环境):
若数据库中已有大量 MD5 密码需迁移,可编写一次性 Artisan 命令进行重哈希(注意:需用户原始密码已不可逆,此操作仅适用于你掌握明文密码的场景;生产环境应引导用户重置密码):
php artisan make:command MigrateMd5Passwords
在 app/console/Commands/MigrateMd5Passwords.php 中:
public function handle() { // 假设你有明文密码映射表,或仅用于种子数据 $users = User::where('password', 'like', '[a-f0-9]{32}')->get(); // 粗略匹配 MD5 长度 foreach ($users as $user) { // ⚠️ 仅当明确知道原始明文时才可执行! // $plainPassword = $this->getOriginalPasswordFor($user->email); // $user->password = Hash::make($plainPassword); // $user->save(); $this->warn("User {$user->email} has non-bcrypt password — manual reset required."); } }
? 补充检查点:
- 确认 config/auth.php 中 guards.api.driver 已设为 ‘jwt’(你已正确配置);
- 确保 User 模型实现了 JWTSubject 接口并正确定义了 getJWTIdentifier() 和 getJWTCustomClaims()(你已实现);
- 检查 .env 中 JWT_SECRET 是否已生成:php artisan jwt:secret(首次安装 JWT 包后必须运行);
- API 路由需通过 auth:api 中间件保护受控资源(me、refresh 等),但 login 路由本身不应加 auth:api(否则无法完成初始认证)——你当前路由配置正确。
总结:JWT 登录 401 的最常见元凶不是配置或代码逻辑,而是密码存储格式失配。始终坚持使用 Hash::make() 创建密码,并通过数据库校验哈希前缀,即可彻底规避该问题。