NewHome/includes/auth.php
2026-09-14 22:06:21 +08:00

146 lines
4.7 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* auth.php —— 后台登录会话工具
* 密码存储算法PHP password_hash 强散列bcrypt/argon2兼容旧库 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']);
}