用户认证
一句话:检查登录状态、获取用户信息、生成和验证Token。
检查登录状态
php
// 检查是否已登录
$isLoggedIn = Anon_Check::isLoggedIn();
// 自动检查 Session 和 Cookie,如果 Cookie 有效会自动恢复 Session
// 用户注销
Anon_Check::logout();
// 设置认证 Cookie
Anon_Check::setAuthCookies($userId, $username, $rememberMe);
// $rememberMe: true=30天, false=会话结束
// Cookie 自动设置顶级域名,支持跨子域名共享
// 清除认证 Cookie
Anon_Check::clearAuthCookies();
// 启动会话
Anon_Check::startSessionIfNotStarted();获取当前用户
php
// 获取用户ID(从会话或Cookie)
$userId = Anon_Http_Request::getUserId();
// 返回: int|null
// 获取完整用户信息(未登录自动返回401)
$userInfo = Anon_Http_Request::requireAuth();
// 返回: ['uid' => 1, 'name' => 'admin', 'email' => '...', ...]登录示例
php
// server/app/Router/Auth/Login.php
try {
Anon_Http_Request::requireMethod('POST');
$data = Anon_Http_Request::validate([
'username' => '用户名不能为空',
'password' => '密码不能为空',
]);
$inputData = Anon_Http_Request::getInput();
$rememberMe = filter_var($inputData['rememberMe'] ?? false, FILTER_VALIDATE_BOOLEAN);
$db = Anon_Database::getInstance();
$user = $db->getUserInfoByName($data['username']);
if (!$user || !password_verify($data['password'], $user['password'])) {
Anon_Http_Response::unauthorized('用户名或密码错误');
}
Anon_Check::startSessionIfNotStarted();
$_SESSION['user_id'] = (int)$user['uid'];
$_SESSION['username'] = $user['name'];
Anon_Check::setAuthCookies((int)$user['uid'], $user['name'], $rememberMe);
// 登录时总是生成新Token
$token = Anon_Http_Request::generateUserToken((int)$user['uid'], $user['name'], $rememberMe);
Anon_Http_Response::success([
'user_id' => (int)$user['uid'],
'username' => $user['name'],
'token' => $token ?? '',
], '登录成功');
} catch (Exception $e) {
Anon_Http_Response::handleException($e);
}注册示例
php
// server/app/Router/Auth/Register.php
try {
$data = Anon_Http_Request::validate([
'username' => '用户名不能为空',
'email' => '邮箱不能为空',
'password' => '密码不能为空'
]);
// 验证码检查(如果启用)
if (class_exists('Anon_Auth_Captcha') && Anon_Auth_Captcha::isEnabled()) {
$inputData = Anon_Http_Request::getInput();
if (empty($inputData['captcha'] ?? '')) {
Anon_Http_Response::error('验证码不能为空', null, 400);
}
if (!Anon_Auth_Captcha::verify($inputData['captcha'] ?? '')) {
Anon_Http_Response::error('验证码错误', null, 400);
}
Anon_Auth_Captcha::clear();
}
// 防刷限制检查
$rateLimitConfig = Anon_System_Env::get('app.rateLimit.register', []);
$rateLimitResult = Anon_Auth_RateLimit::checkRegisterLimit($rateLimitConfig);
if (!$rateLimitResult['allowed']) {
Anon_Http_Response::error($rateLimitResult['message'], [
'remaining' => $rateLimitResult['remaining'],
'resetAt' => $rateLimitResult['resetAt'],
'type' => $rateLimitResult['type']
], 429);
}
$username = trim($data['username']);
$email = trim($data['email']);
$password = $data['password'];
// 验证用户名格式
if (strlen($username) < 3 || strlen($username) > 20) {
Anon_Http_Response::error('用户名长度必须在3-20个字符之间', null, 400);
}
if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
Anon_Http_Response::error('用户名只能包含字母、数字和下划线', null, 400);
}
// 验证邮箱格式
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
Anon_Http_Response::error('邮箱格式不正确', null, 400);
}
// 验证密码强度
if (strlen($password) < 6) {
Anon_Http_Response::error('密码长度至少6个字符', null, 400);
}
$db = Anon_Database::getInstance();
// 检查用户名是否已存在
if ($db->getUserInfoByName($username)) {
Anon_Http_Response::error('用户名已存在', null, 400);
}
// 检查邮箱是否已存在
if ($db->getUserInfoByEmail($email)) {
Anon_Http_Response::error('邮箱已被注册', null, 400);
}
// 加密密码并创建用户
$hashedPassword = password_hash($password, PASSWORD_BCRYPT);
$success = $db->addUser($username, $email, $hashedPassword, 'user');
if (!$success) {
Anon_Http_Response::error('注册失败,请稍后重试', null, 500);
}
Anon_Http_Response::success([
'username' => $username,
'email' => $email,
'remaining' => $rateLimitResult['remaining']
], '注册成功');
} catch (Exception $e) {
Anon_Http_Response::handleException($e, '注册处理过程中发生错误');
}Token 配置
php
// server/app/useApp.php
'app' => [
'token' => [
'enabled' => true, // 是否启用Token验证
'refresh' => false, // 是否自动刷新Token
'whitelist' => [ // Token验证白名单
'/auth/login',
'/auth/logout',
'/auth/check-login',
'/auth/token',
'/auth/captcha'
],
],
],Token 接口
系统提供了专门的 Token 获取接口 /auth/token,用于获取当前登录用户的 Token。
接口说明
- 路径:
/auth/token - 方法:
GET - 需要登录: 是(
requireLogin: true) - 返回:
{ "code": 200, "data": { "token": "..." }, "message": "获取 Token 成功" }
使用场景
前端检查登录状态后获取 Token
- 先调用
/auth/check-login检查登录状态 - 如果已登录,再调用
/auth/token获取 Token - 将 Token 保存到 Cookie 或 localStorage
- 先调用
Token 刷新
- 当 Token 即将过期时,前端可以调用此接口获取新的 Token
- 如果用户已登录,会返回新的 Token
示例
php
// server/app/Router/Auth/Token.php
<?php
if (!defined('ANON_ALLOWED_ACCESS')) exit;
const Anon_RouterMeta = [
'header' => true,
'requireLogin' => true,
'method' => 'GET',
];
try {
$isLoggedIn = Anon_Check::isLoggedIn();
$token = '';
if ($isLoggedIn) {
$userId = Anon_Http_Request::getUserId();
$username = $_SESSION['username'] ?? '';
if ($userId && $username) {
$token = Anon_Http_Request::getUserToken($userId, $username);
}
}
$message = $token ? '获取 Token 成功' : '用户未登录,无法获取 Token';
Anon_Http_Response::success([
'token' => $token,
], $message);
} catch (Exception $e) {
Anon_Http_Response::handleException($e, '获取 Token 时发生错误');
}配置说明:
enabled: 是否启用Token验证refresh: 是否在验证后自动刷新Tokentrue: 每次验证成功后生成新Token,通过响应头X-New-Token返回false: 不刷新,Token保持到过期(推荐多设备场景)
whitelist: Token验证白名单路由
生成 Token
智能获取或生成(推荐)
php
// 根据refresh配置决定:有有效Token就返回,没有就生成
$token = Anon_Http_Request::getUserToken($userId, $username, $rememberMe);强制生成新Token(登录时使用)
php
// 登录时总是生成新Token
$token = Anon_Http_Request::generateUserToken($userId, $username, $rememberMe);手动生成(不推荐)
php
$token = Anon_Auth_Token::generate(['user_id' => 1], 3600); // 1小时
$token = Anon_Auth_Token::generate(['user_id' => 1], 86400 * 30); // 30天验证 Token
Token验证自动在路由执行前进行,验证失败返回403。
特性:
- Token验证通过后,如果包含用户信息,系统自动设置登录状态
- 每个登录会话都有独立的Token
- Token只能从HTTP Header获取:
X-API-Token或Authorization: Bearer - 如果启用了
refresh,验证成功后会在响应头返回新Token:X-New-Token
Token刷新机制
当token.refresh设置为true时:
- 每次Token验证成功后,自动生成新的Token
- 新Token通过响应头
X-New-Token返回给客户端 - 客户端需要检查并更新本地存储的Token
- 旧Token仍然有效,直到过期
适用场景:
refresh: false(默认):多设备登录、Web应用推荐refresh: true:单设备应用、移动App、高安全要求场景
手动验证
php
// 要求Token(无效自动返回403)
Anon_Http_Request::requireToken();
// 手动验证Token
$payload = Anon_Auth_Token::verify();
if ($payload) {
$userId = $payload['data']['user_id'] ?? null;
}
// 从请求中获取Token
$token = Anon_Auth_Token::getTokenFromRequest();
// 从Header: X-API-Token 或 Authorization: Bearer使用示例
php
// 获取用户信息时使用getUserToken
// server/app/Router/User/Info.php
try {
$userInfo = Anon_Http_Request::requireAuth();
// 根据refresh配置决定智能获取或生成Token
$token = Anon_Http_Request::getUserToken((int)$userInfo['uid'], $userInfo['name']);
if ($token !== null) {
$userInfo['token'] = $token;
}
Anon_Http_Response::success($userInfo, '获取用户信息成功');
} catch (Exception $e) {
Anon_Http_Response::handleException($e, '获取用户信息发生错误');
}跨端登录状态共享
系统支持"一端登录,多端都有状态"的功能,用户在一个客户端登录并选择"记住我"后,其他客户端访问 API 时会自动获得登录状态。
工作原理
Cookie 自动设置
- 登录时设置认证 Cookie(
user_id和username) - Cookie 自动设置顶级域名(如
.example.com),支持跨子域名共享 - 选择"记住我"时,Cookie 有效期 30 天;否则为会话 Cookie
- 登录时设置认证 Cookie(
自动验证和恢复
Anon_Check::isLoggedIn()会自动检查 Cookie- 如果 Cookie 有效,自动恢复 Session,用户自动登录
- Cookie 验证会查询数据库确保用户存在且用户名匹配
跨域支持
- 跨域请求时,Cookie 的
SameSite自动设置为None(需要 HTTPS) - CORS 配置自动设置
Access-Control-Allow-Credentials: true - 前端需要设置
credentials: 'include'(前端已自动配置)
- 跨域请求时,Cookie 的
Cookie 配置
Cookie 自动根据环境配置:
- 同域名/子域名:
SameSite=Lax,支持跨子域名(如app.example.com和api.example.com) - 跨域 HTTPS:
SameSite=None,Secure=true,支持完全跨域 - Domain 设置:自动设置为顶级域名(如
.example.com),IP 地址使用完整域名
前端配置
前端已自动配置 credentials: 'include',确保 Cookie 自动发送:
typescript
// Vue / React / Next.js / Nuxt
const response = await fetch('/api/endpoint', {
credentials: 'include' // 自动发送 Cookie
});使用场景
同一域名下的不同子域名
- 用户在
app.example.com登录 - 访问
api.example.com时自动获得登录状态
- 用户在
跨域请求(需要 HTTPS)
- 用户在
https://app.example.com登录 - 从
https://mobile.example.com访问 API 时自动获得登录状态
- 用户在
不同端口
- 同一域名下的不同端口自动共享 Cookie
安全说明
- Cookie 使用
HttpOnly标志,防止 JavaScript 访问 - Cookie 验证时会查询数据库,确保用户存在且有效
- 跨域时要求 HTTPS,确保传输安全
- 支持通过 Hook 自定义 Cookie 选项:
auth_cookie_options
示例
php
// 登录时设置记住我
$rememberMe = true;
Anon_Check::setAuthCookies($userId, $username, $rememberMe);
// 其他客户端访问 API 时
$isLoggedIn = Anon_Check::isLoggedIn(); // 自动从 Cookie 恢复登录状态
if ($isLoggedIn) {
$userId = Anon_Http_Request::getUserId(); // 自动获取用户ID
}白名单
支持精确匹配和通配符:
- 精确匹配:
/api/public - 通配符:
/api/public/*
