feat(用户安全): 支持要求更改密码并强制登录后改密
- 新增 users.force_password_change 字段与迁移 - 用户新增/编辑/批量导入支持要求更改密码 - 登录后未改密用户仅允许访问改密相关接口
This commit is contained in:
parent
e10f050dfc
commit
777c682a4e
@ -16,6 +16,29 @@ class AuthController extends Controller
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth:api')->except('login');
|
||||
$this->middleware(function (Request $request, \Closure $next) {
|
||||
/** @var User|null $user */
|
||||
$user = Auth::guard('api')->user();
|
||||
if (! $user || ! $user->force_password_change) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$allowed = [
|
||||
'api/auth/me',
|
||||
'api/auth/password',
|
||||
'api/auth/logout',
|
||||
];
|
||||
|
||||
if (in_array($request->path(), $allowed, true)) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'code' => 423,
|
||||
'message' => '请先修改密码后再继续操作',
|
||||
'data' => ['force_password_change' => true],
|
||||
], 423);
|
||||
})->except('login');
|
||||
}
|
||||
|
||||
#[Apidoc\Title('登录'), Apidoc\Method('POST'), Apidoc\Url('/auth/login')]
|
||||
@ -70,6 +93,7 @@ class AuthController extends Controller
|
||||
'data' => [
|
||||
'user' => $user?->load('roles'),
|
||||
'permissions' => $user?->getAllPermissions()->pluck('name')->values(),
|
||||
'force_password_change' => (bool) ($user?->force_password_change ?? false),
|
||||
],
|
||||
]);
|
||||
}
|
||||
@ -104,6 +128,7 @@ class AuthController extends Controller
|
||||
]);
|
||||
|
||||
$user->password = $validated['password'];
|
||||
$user->force_password_change = false;
|
||||
$user->save();
|
||||
$this->auditLog($request, 'password_update');
|
||||
|
||||
|
||||
@ -148,6 +148,7 @@ class UserController extends Controller
|
||||
'email' => trim($this->firstMatchedValue($normalizedRow, ['email', 'mail', '邮箱'])),
|
||||
'phone' => trim($this->firstMatchedValue($normalizedRow, ['phone', 'mobile', '手机号', '手机', '电话'])),
|
||||
'password' => trim($this->firstMatchedValue($normalizedRow, ['password', 'passwd', 'pwd', '密码'])),
|
||||
'force_password_change' => $this->parseBooleanValue($this->firstMatchedValue($normalizedRow, ['force_password_change', 'require_password_change', '要求更改密码', 'force_change_password'])),
|
||||
];
|
||||
|
||||
$validator = Validator::make($payload, [
|
||||
@ -387,6 +388,16 @@ class UserController extends Controller
|
||||
return '';
|
||||
}
|
||||
|
||||
private function parseBooleanValue(string $value): bool
|
||||
{
|
||||
$normalized = strtolower(trim($value));
|
||||
if ($normalized === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($normalized, ['1', 'true', 'yes', 'y', 'on', '是', '真'], true);
|
||||
}
|
||||
|
||||
private function buildUsersImportTemplateXlsx(string $xlsxPath): void
|
||||
{
|
||||
$zip = new \ZipArchive;
|
||||
@ -418,9 +429,9 @@ class UserController extends Controller
|
||||
.'</Relationships>';
|
||||
|
||||
$rows = [
|
||||
['nickname', 'email', 'phone', 'password', 'role_ids'],
|
||||
['张三', 'zhangsan@example.com', '13800138000', 'Pass@123456', '1'],
|
||||
['李四', 'lisi@example.com', '13900139000', 'Pass@123456', '1,2'],
|
||||
['nickname', 'email', 'phone', 'password', 'role_ids', 'force_password_change'],
|
||||
['张三', 'zhangsan@example.com', '13800138000', 'Pass@123456', '1', '0'],
|
||||
['李四', 'lisi@example.com', '13900139000', 'Pass@123456', '1,2', '1'],
|
||||
];
|
||||
|
||||
$sheetData = '';
|
||||
|
||||
@ -18,6 +18,7 @@ class StoreUserRequest extends FormRequest
|
||||
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
||||
'phone' => ['nullable', 'string', 'max:32', 'unique:users,phone'],
|
||||
'password' => ['required', 'string', 'min:6'],
|
||||
'force_password_change' => ['sometimes', 'boolean'],
|
||||
'role_ids' => ['sometimes', 'array'],
|
||||
'role_ids.*' => ['integer', 'exists:roles,id'],
|
||||
];
|
||||
|
||||
@ -21,6 +21,7 @@ class UpdateUserRequest extends FormRequest
|
||||
'email' => ['sometimes', 'required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($userId)],
|
||||
'phone' => ['nullable', 'string', 'max:32', Rule::unique('users', 'phone')->ignore($userId)],
|
||||
'password' => ['sometimes', 'required', 'string', 'min:6'],
|
||||
'force_password_change' => ['sometimes', 'boolean'],
|
||||
'role_ids' => ['sometimes', 'array'],
|
||||
'role_ids.*' => ['integer', 'exists:roles,id'],
|
||||
];
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
@ -23,6 +23,7 @@ class User extends Authenticatable implements JWTSubject
|
||||
'email',
|
||||
'phone',
|
||||
'password',
|
||||
'force_password_change',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
@ -62,6 +63,7 @@ class User extends Authenticatable implements JWTSubject
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'force_password_change' => 'boolean',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('force_password_change')
|
||||
->default(false)
|
||||
->after('password')
|
||||
->comment('是否要求下次登录后必须修改密码');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('force_password_change');
|
||||
});
|
||||
}
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user