146 lines
4.6 KiB
PHP
146 lines
4.6 KiB
PHP
<?php
|
||
/**
|
||
* auth.php —— 后台登录会话工具
|
||
* 密码存储算法(按需求):md5(base64(明文))
|
||
*/
|
||
require_once __DIR__ . '/db.php';
|
||
|
||
if (session_status() === PHP_SESSION_NONE) {
|
||
session_start();
|
||
}
|
||
|
||
/** 是否已登录 */
|
||
function is_logged_in(): bool
|
||
{
|
||
return !empty($_SESSION['admin']);
|
||
}
|
||
|
||
/** 未登录则跳转登录页(后台各页顶部调用) */
|
||
function require_login(string $loginPage = 'login.php'): void
|
||
{
|
||
if (!is_logged_in()) {
|
||
header('Location: ' . $loginPage);
|
||
exit;
|
||
}
|
||
}
|
||
|
||
/** 密码散列:使用 PHP 内置强散列(bcrypt/argon2),自动加盐与迭代 */
|
||
function hp_password_hash(string $plain): string
|
||
{
|
||
return password_hash($plain, PASSWORD_DEFAULT);
|
||
}
|
||
|
||
/** 旧版散列(md5(base64()))——仅用于兼容迁移,禁止用于新写入 */
|
||
function hp_password_hash_legacy(string $plain): string
|
||
{
|
||
return md5(base64_encode($plain));
|
||
}
|
||
|
||
/** 验证用户(旧版散列验证通过后自动升级为强散列) */
|
||
function hp_verify_user(string $username, string $plain): bool
|
||
{
|
||
$st = db()->prepare('SELECT password_hash FROM users WHERE username = ? LIMIT 1');
|
||
$st->execute([$username]);
|
||
$row = $st->fetch();
|
||
if (!$row) {
|
||
// 用户不存在也执行一次等价开销,削弱用户名枚举的时序差异
|
||
password_verify($plain, '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi');
|
||
return false;
|
||
}
|
||
$stored = (string) $row['password_hash'];
|
||
|
||
// 旧库:32 位十六进制 md5 → 校验通过后透明升级
|
||
if (preg_match('/^[0-9a-f]{32}$/i', $stored)) {
|
||
if (!hash_equals($stored, hp_password_hash_legacy($plain))) {
|
||
return false;
|
||
}
|
||
$upd = db()->prepare('UPDATE users SET password_hash = ? WHERE username = ?');
|
||
$upd->execute([hp_password_hash($plain), $username]);
|
||
return true;
|
||
}
|
||
|
||
return password_verify($plain, $stored);
|
||
}
|
||
|
||
/** 指定用户(默认当前登录用户)是否必须修改密码(初始/重置密码未改时为 true) */
|
||
function hp_password_needs_change(?string $username = null): bool
|
||
{
|
||
$username = ($username !== null) ? $username : (string) ($_SESSION['admin'] ?? '');
|
||
if ($username === '') {
|
||
return false;
|
||
}
|
||
try {
|
||
$st = db()->prepare('SELECT must_change_pwd FROM users WHERE username = ? LIMIT 1');
|
||
$st->execute([$username]);
|
||
return ((int) $st->fetchColumn()) === 1;
|
||
} catch (Throwable $e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/** 执行登录(成功返回 true;成功/失败均写入登录日志与设备信息) */
|
||
function hp_login(string $username, string $plain, string $deviceClient = ''): bool
|
||
{
|
||
$uaRaw = (string) ($_SERVER['HTTP_USER_AGENT'] ?? '');
|
||
if (hp_verify_user($username, $plain)) {
|
||
session_regenerate_id(true);
|
||
$_SESSION['admin'] = $username;
|
||
// 记录本次登录信息(供后台“登录信息”卡片展示)
|
||
$_SESSION['login_at'] = date('Y-m-d H:i:s');
|
||
$_SESSION['login_ip'] = client_ip();
|
||
$_SESSION['login_ua'] = login_ua_brief($uaRaw);
|
||
$_SESSION['login_device'] = login_device_brief($deviceClient, $uaRaw);
|
||
login_log_write($username, 'ok', $deviceClient);
|
||
return true;
|
||
}
|
||
login_log_write($username, 'fail', $deviceClient);
|
||
return false;
|
||
}
|
||
|
||
/** 修改当前用户密码 */
|
||
function hp_change_password(string $oldPlain, string $newPlain): array
|
||
{
|
||
if (!is_logged_in()) {
|
||
return [false, '未登录'];
|
||
}
|
||
if (!hp_verify_user((string) $_SESSION['admin'], $oldPlain)) {
|
||
return [false, '原密码不正确'];
|
||
}
|
||
if (strlen($newPlain) < 6) {
|
||
return [false, '新密码至少 6 位'];
|
||
}
|
||
$st = db()->prepare('UPDATE users SET password_hash = ?, must_change_pwd = 0 WHERE username = ?');
|
||
$st->execute([hp_password_hash($newPlain), (string) $_SESSION['admin']]);
|
||
return [true, '密码修改成功'];
|
||
}
|
||
|
||
/** 退出登录 */
|
||
function hp_logout(): void
|
||
{
|
||
$_SESSION = [];
|
||
if (ini_get('session.use_cookies')) {
|
||
$p = session_get_cookie_params();
|
||
setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']);
|
||
}
|
||
session_destroy();
|
||
}
|
||
|
||
/** CSRF token */
|
||
function csrf_token(): string
|
||
{
|
||
if (empty($_SESSION['csrf'])) {
|
||
$_SESSION['csrf'] = bin2hex(random_bytes(16));
|
||
}
|
||
return (string) $_SESSION['csrf'];
|
||
}
|
||
|
||
function csrf_field(): string
|
||
{
|
||
return '<input type="hidden" name="csrf" value="' . htmlspecialchars(csrf_token(), ENT_QUOTES, 'UTF-8') . '">';
|
||
}
|
||
|
||
function csrf_verify(): bool
|
||
{
|
||
return isset($_POST['csrf']) && is_string($_POST['csrf']) && hash_equals(csrf_token(), $_POST['csrf']);
|
||
}
|