HTTP 认证
HTTP 认证机制允许 Web 服务器在响应请求时要求客户端提供身份凭证。PHP 通过 $_SERVER['PHP_AUTH_USER']、$_SERVER['PHP_AUTH_PW'] 和 $_SERVER['HTTP_AUTHORIZATION'] 等超全局变量来获取认证信息,配合 header('WWW-Authenticate: ...') 触发浏览器的认证对话框。本节涵盖 HTTP Basic、Digest 认证以及现代 Token 认证的实现方式。
基础概念
HTTP 认证类型
| 认证方式 | 安全性 | 凭证传输 | 适用场景 |
|---|---|---|---|
| Basic | 低(Base64 编码,非加密) | Base64(username:password) | 内部工具、HTTPS 环境 |
| Digest | 中(MD5 哈希) | MD5 哈希 | 兼容旧系统 |
| Bearer Token | 高(取决于实现) | Authorization: Bearer | API 认证 |
| OAuth 2.0 | 高 | 授权码/令牌 | 第三方接入 |
| API Key | 中 | Header/Query 中的密钥 | 简单 API |
安全警告
HTTP Basic 认证始终应配合 HTTPS 使用。Base64 不是加密,只是编码,攻击者可以轻易解码。在非 HTTPS 环境下使用 Basic 认证等同于明文传输密码。
HTTP Basic 认证
工作原理
1. 客户端请求受保护资源
2. 服务端返回 401 + WWW-Authenticate 头
3. 浏览器弹出用户名/密码对话框
4. 用户输入后,浏览器发送 Base64 编码的凭证
5. 服务端验证后返回受保护内容基本实现
php
<?php
declare(strict_types=1);
// === 方式一:使用 PHP 内置变量(CGI/FastCGI)===
if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) {
// 发送 401 状态码和认证挑战
http_response_code(401);
header('WWW-Authenticate: Basic realm="My Protected Area"');
echo '需要认证才能访问此页面';
exit;
}
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
// 验证凭证
if ($username === 'admin' && $password === 'secret123') {
echo "欢迎,{$username}!认证成功。";
} else {
http_response_code(401);
header('WWW-Authenticate: Basic realm="My Protected Area"');
echo '用户名或密码错误';
exit;
}使用数据库验证
php
<?php
declare(strict_types=1);
/**
* 基于 MySQL 数据库的 HTTP Basic 认证
*/
function authenticateBasic(PDO $pdo): ?array
{
// 检查认证信息是否存在
if (!isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
http_response_code(401);
header('WWW-Authenticate: Basic realm="API Access"');
exit(json_encode(['error' => 'Authentication required']));
}
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
// 从数据库获取用户记录
$stmt = $pdo->prepare('SELECT id, username, password_hash, status FROM users WHERE username = ?');
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
http_response_code(401);
header('WWW-Authenticate: Basic realm="API Access"');
exit(json_encode(['error' => 'Invalid credentials']));
}
// 使用 password_verify 验证密码
if (!password_verify($password, $user['password_hash'])) {
http_response_code(401);
header('WWW-Authenticate: Basic realm="API Access"');
exit(json_encode(['error' => 'Invalid credentials']));
}
// 检查账户状态
if ($user['status'] !== 'active') {
http_response_code(403);
exit(json_encode(['error' => 'Account is disabled']));
}
return $user;
}
// 使用示例
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
$user = authenticateBasic($pdo);
echo "欢迎,{$user['username']} (ID: {$user['id']})";CGI/FastCGI 下的 Authorization 头问题
php
<?php
declare(strict_types=1);
/**
* 在 CGI/FastCGI 模式下,Apache 可能不自动填充 PHP_AUTH_USER/PHP_AUTH_PW
* 需要手动解析 HTTP_AUTHORIZATION 头
*/
// 方案一:从 HTTP_AUTHORIZATION 头解析
function getBasicCredentials(): ?array
{
if (isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
return ['user' => $_SERVER['PHP_AUTH_USER'], 'pass' => $_SERVER['PHP_AUTH_PW']];
}
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (empty($authHeader) && isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$authHeader = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
}
if (str_starts_with($authHeader, 'Basic ')) {
$decoded = base64_decode(substr($authHeader, 6), true);
if ($decoded === false) {
return null;
}
$parts = explode(':', $decoded, 2);
if (count($parts) === 2) {
return ['user' => $parts[0], 'pass' => $parts[1]];
}
}
return null;
}
// 方案二:.htaccess 规则传递 Authorization 头(Apache)
// 在 .htaccess 中添加:
// SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
// RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
// 方案三:php.ini 配置(CGI 模式)
// cgi.fix_pathinfo=1
// 在 .htaccess 中:
// <IfModule mod_rewrite.c>
// RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
// </IfModule>登出 Basic 认证
php
<?php
declare(strict_types=1);
/**
* HTTP Basic 认证的登出非常棘手
* 因为浏览器会自动缓存凭证,直到浏览器关闭
*/
// 方法一:发送 401 强制重新输入(伪登出)
function basicAuthLogout(): never
{
http_response_code(401);
header('WWW-Authenticate: Basic realm="Logout", charset="UTF-8"');
echo '<p>您已登出。请关闭浏览器以完全清除凭证。</p>';
exit;
}
// 方法二:使用错误的凭证使浏览器丢弃缓存的凭证
function forceLogout(): never
{
// 发送 401 并要求重新认证
header('HTTP/1.1 401 Unauthorized');
header('WWW-Authenticate: Basic realm="Please logout"');
// 发送一段误导浏览器清除缓存的脚本
echo '<html><head><script>';
echo 'window.onload = function() {';
echo ' var xhr = new XMLHttpRequest();';
echo ' xhr.open("GET", location.href, true, "logout", "logout");';
echo ' xhr.send();';
echo '};';
echo '</script></head><body>正在登出...</body></html>';
exit;
}
// 方法三:推荐方案 - 使用 Session/Cookie 替代 Basic 认证
// Basic 认证不适合需要登出功能的场景HTTP Digest 认证
基本实现
php
<?php
declare(strict_types=1);
/**
* HTTP Digest 认证
* 比 Basic 认证安全,密码以 MD5 哈希形式传输
*/
$users = [
'admin' => '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', // password("password") 的 SHA1
];
$realm = 'Protected Area';
$opaque = md5($realm); // 客户端回传的随机值
// 解析 Authorization 头
function parseDigestHeader(string $header): array
{
$parts = [];
$pattern = '/(\w+)=(?:"([^"]+)"|([^\s,]+))/';
preg_match_all($pattern, $header, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$parts[$match[1]] = $match[2] ?: $match[3];
}
return $parts;
}
// 生成有效的响应
function digestResponse(
string $username,
string $realm,
string $password,
string $method,
string $uri,
string $nonce,
string $nc,
string $cnonce,
string $qop,
): string {
$ha1 = md5("{$username}:{$realm}:{$password}");
$ha2 = md5("{$method}:{$uri}");
$response = md5("{$ha1}:{$nonce}:{$nc}:{$cnonce}:{$qop}:{$ha2}");
return $response;
}
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (empty($authHeader) || !str_starts_with($authHeader, 'Digest ')) {
http_response_code(401);
$nonce = md5(uniqid((string) mt_rand(), true));
header('WWW-Authenticate: Digest realm="' . $realm . '", qop="auth", nonce="' . $nonce . '", opaque="' . $opaque . '"');
exit('需要 Digest 认证');
}
$digest = parseDigestHeader(substr($authHeader, 7));
$username = $digest['username'] ?? '';
if (!isset($users[$username])) {
http_response_code(401);
exit('无效用户');
}
$password = 'password'; // 实际应从安全存储获取
$validResponse = digestResponse(
username: $username,
realm: $realm,
password: $password,
method: $_SERVER['REQUEST_METHOD'],
uri: $digest['uri'],
nonce: $digest['nonce'],
nc: $digest['nc'],
cnonce: $digest['cnonce'],
qop: $digest['qop'],
);
if (!hash_equals($validResponse, $digest['response'])) {
http_response_code(401);
exit('认证失败');
}
echo "欢迎,{$username}!Digest 认证成功。";Digest 认证的局限性
Digest 认证在现代 Web 安全中已不推荐使用:
- MD5 哈希已不安全
- 需要在服务端存储明文或可逆的密码
- 大多数现代应用使用 Bearer Token 或 OAuth 2.0 替代
Bearer Token 认证
基本实现
php
<?php
declare(strict_types=1);
/**
* Bearer Token 认证
* 客户端在 Authorization 头中发送 Token
*/
// === 从请求中提取 Token ===
function getBearerToken(): ?string
{
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
// 标准 Authorization 头
if (str_starts_with($authHeader, 'Bearer ')) {
return trim(substr($authHeader, 7));
}
// 兼容方式:通过 GET/POST 参数传递
$token = $_GET['token'] ?? $_POST['token'] ?? null;
if ($token !== null) {
return $token;
}
// 兼容方式:Cookie
$token = $_COOKIE['auth_token'] ?? null;
if ($token !== null) {
return $token;
}
return null;
}
// === 使用 ===
$token = getBearerToken();
if ($token === null) {
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['error' => 'Missing authentication token']);
exit;
}
// 验证 Token(示例:使用数据库查询)
// 实际项目中推荐使用 JWT,详见 API 认证章节
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
$stmt = $pdo->prepare(
'SELECT u.id, u.username, t.expires_at
FROM users u
JOIN api_tokens t ON u.id = t.user_id
WHERE t.token = ? AND t.expires_at > NOW()'
);
$stmt->execute([$token]);
$userToken = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$userToken) {
http_response_code(401);
echo json_encode(['error' => 'Invalid or expired token']);
exit;
}
echo "认证用户: {$userToken['username']} (ID: {$userToken['id']})";API Key 认证
php
<?php
declare(strict_types=1);
/**
* API Key 认证
* 简单直接,适合服务间通信和公开 API
*/
class ApiKeyAuth
{
public function __construct(
private readonly PDO $pdo,
) {
}
/**
* 从请求中获取 API Key
*/
public function getApiKey(): ?string
{
// 1. Authorization 头(推荐)
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'ApiKey ')) {
return trim(substr($authHeader, 6));
}
// 2. X-API-Key 头
$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? '';
if (!empty($apiKey)) {
return $apiKey;
}
// 3. Query 参数(不推荐,会出现在日志中)
$apiKey = $_GET['api_key'] ?? null;
if ($apiKey !== null) {
return $apiKey;
}
return null;
}
/**
* 验证 API Key
*/
public function validate(string $apiKey): ?array
{
// 使用 hash_equals 防止时序攻击
$stmt = $this->pdo->prepare(
'SELECT id, name, user_id, permissions, rate_limit
FROM api_keys
WHERE key_hash = ? AND is_active = 1'
);
// 对 API Key 进行哈希后查询(不在数据库存储明文)
$keyHash = hash('sha256', $apiKey);
$stmt->execute([$keyHash]);
$key = $stmt->fetch(PDO::FETCH_ASSOC);
return $key ?: null;
}
/**
* 速率限制检查
*/
public function checkRateLimit(string $apiKey, int $limit = 100): bool
{
// 使用 Redis 实现速率限制(示例)
// $redis = new Redis();
// $redis->connect('127.0.0.1', 6379);
// $key = "rate_limit:{$apiKey}:" . date('YmdHi');
// $current = $redis->incr($key);
// if ($current === 1) {
// $redis->expire($key, 60);
// }
// return $current <= $limit;
return true; // 示例中跳过
}
}
// 使用中间件模式
$auth = new ApiKeyAuth($pdo);
$apiKey = $auth->getApiKey();
if ($apiKey === null) {
http_response_code(401);
echo json_encode(['error' => 'API Key required. Pass via X-API-Key header or Authorization: ApiKey {key}']);
exit;
}
$keyInfo = $auth->validate($apiKey);
if ($keyInfo === null) {
http_response_code(401);
echo json_encode(['error' => 'Invalid API Key']);
exit;
}
echo "API Key: {$keyInfo['name']},权限: {$keyInfo['permissions']}";认证中间件
可复用的认证中间件
php
<?php
declare(strict_types=1);
/**
* 认证中间件基类
*/
abstract class AuthMiddleware
{
abstract public function authenticate(): ?array;
public function requireAuth(): array
{
$user = $this->authenticate();
if ($user === null) {
$this->unauthorized();
}
return $user;
}
protected function unauthorized(string $message = 'Authentication required'): never
{
http_response_code(401);
header('Content-Type: application/json');
echo json_encode(['error' => $message]);
exit;
}
protected function forbidden(string $message = 'Access denied'): never
{
http_response_code(403);
header('Content-Type: application/json');
echo json_encode(['error' => $message]);
exit;
}
}
/**
* Bearer Token 认证中间件
*/
class BearerAuthMiddleware extends AuthMiddleware
{
public function __construct(
private readonly PDO $pdo,
) {
}
public function authenticate(): ?array
{
$token = $this->extractToken();
if ($token === null) {
return null;
}
$stmt = $this->pdo->prepare(
'SELECT u.id, u.username, u.role
FROM users u
JOIN auth_tokens t ON u.id = t.user_id
WHERE t.token = ? AND t.expires_at > NOW()'
);
$stmt->execute([$token]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
}
private function extractToken(): ?string
{
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'Bearer ')) {
return substr($authHeader, 7);
}
return null;
}
}
/**
* 角色权限检查
*/
function requireRole(array $user, string ...$roles): void
{
if (!in_array($user['role'], $roles, true)) {
http_response_code(403);
echo json_encode(['error' => '权限不足']);
exit;
}
}
// 使用示例
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
$auth = new BearerAuthMiddleware($pdo);
// 需要认证的 API
$user = $auth->requireAuth();
echo "当前用户: {$user['username']}";
// 需要管理员权限
requireRole($user, 'admin', 'superadmin');
echo "管理员操作执行中...";实战示例:REST API 认证流程
php
<?php
declare(strict_types=1);
/**
* 完整的 API 认证流程
* 包含用户名密码登录、Token 签发和 Token 验证
*/
class ApiAuth
{
public function __construct(
private readonly PDO $pdo,
private readonly string $tokenSecret = 'your-secret-key-change-this',
private readonly int $tokenLifetime = 86400, // 24 小时
) {
}
/**
* 用户登录,签发 Token
*/
public function login(string $username, string $password): array
{
// 获取用户
$stmt = $this->pdo->prepare('SELECT id, username, password_hash, status FROM users WHERE username = ?');
$stmt->execute([$username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user || !password_verify($password, $user['password_hash'])) {
throw new RuntimeException('用户名或密码错误');
}
if ($user['status'] !== 'active') {
throw new RuntimeException('账户已被禁用');
}
// 生成 Token
$token = $this->generateToken($user['id']);
// 存储 Token
$expiresAt = date('Y-m-d H:i:s', time() + $this->tokenLifetime);
$stmt = $this->pdo->prepare(
'INSERT INTO auth_tokens (user_id, token, expires_at, created_at) VALUES (?, ?, ?, NOW())'
);
$stmt->execute([$user['id'], $token, $expiresAt]);
return [
'token' => $token,
'expires_at' => $expiresAt,
'user' => [
'id' => $user['id'],
'username' => $user['username'],
],
];
}
/**
* 验证 Token,返回用户信息
*/
public function verifyToken(string $token): ?array
{
$stmt = $this->pdo->prepare(
'SELECT t.id, t.user_id, t.expires_at, u.username, u.role
FROM auth_tokens t
JOIN users u ON t.user_id = u.id
WHERE t.token = ? AND t.expires_at > NOW()'
);
$stmt->execute([$token]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$result) {
return null;
}
return [
'id' => $result['user_id'],
'username' => $result['username'],
'role' => $result['role'],
'token_id' => $result['id'],
];
}
/**
* 注销 Token
*/
public function logout(string $token): bool
{
$stmt = $this->pdo->prepare('DELETE FROM auth_tokens WHERE token = ?');
return $stmt->execute([$token]) && $stmt->rowCount() > 0;
}
/**
* 生成随机 Token
*/
private function generateToken(int $userId): string
{
$payload = json_encode([
'user_id' => $userId,
'iat' => time(),
'exp' => time() + $this->tokenLifetime,
]);
// 简单的 Token 生成(生产环境推荐 JWT)
$signature = hash_hmac('sha256', $payload, $this->tokenSecret);
return base64_encode($payload) . '.' . $signature;
}
/**
* 从请求中提取 Token
*/
public static function extractTokenFromRequest(): ?string
{
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'Bearer ')) {
return substr($authHeader, 7);
}
return null;
}
}
// === API 路由 ===
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
$auth = new ApiAuth($pdo);
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
header('Content-Type: application/json');
match ($uri) {
'/api/login' => match ($method) {
'POST' => handleLogin(),
default => (function () { http_response_code(405); echo json_encode(['error' => 'Method Not Allowed']); })(),
},
'/api/logout' => match ($method) {
'POST' => handleLogout(),
default => (function () { http_response_code(405); echo json_encode(['error' => 'Method Not Allowed']); })(),
},
'/api/profile' => match ($method) {
'GET' => handleProfile(),
default => (function () { http_response_code(405); echo json_encode(['error' => 'Method Not Allowed']); })(),
},
default => (function () { http_response_code(404); echo json_encode(['error' => 'Not Found']); })(),
};
function handleLogin(): never
{
global $auth;
$data = json_decode(file_get_contents('php://input'), true) ?? [];
try {
$result = $auth->login($data['username'] ?? '', $data['password'] ?? '');
echo json_encode(['success' => true, 'data' => $result], JSON_UNESCAPED_UNICODE);
} catch (RuntimeException $e) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
function handleLogout(): never
{
global $auth;
$token = ApiAuth::extractTokenFromRequest();
if ($token === null) {
http_response_code(401);
echo json_encode(['error' => 'Token required']);
exit;
}
$auth->logout($token);
echo json_encode(['success' => true, 'message' => '已登出']);
exit;
}
function handleProfile(): never
{
global $auth;
$token = ApiAuth::extractTokenFromRequest();
if ($token === null) {
http_response_code(401);
echo json_encode(['error' => 'Token required']);
exit;
}
$user = $auth->verifyToken($token);
if ($user === null) {
http_response_code(401);
echo json_encode(['error' => 'Invalid or expired token']);
exit;
}
echo json_encode(['success' => true, 'data' => $user]);
exit;
}注意事项
1. 时序攻击防范
php
<?php
// 错误:使用 == 比较 Token(可能受时序攻击影响)
// if ($token == $storedToken) { ... }
// 正确:使用 hash_equals 进行恒定时间比较
if (hash_equals($storedToken, $providedToken)) {
// Token 有效
}
// password_verify 内部已使用 hash_equals,无需额外处理
if (password_verify($password, $hash)) {
// 密码正确
}2. 认证信息不要写入日志
php
<?php
// 错误:将 Authorization 头写入日志
error_log("Auth: " . $_SERVER['HTTP_AUTHORIZATION']); // 泄露 Token
// 正确:记录认证事件,不记录凭证
error_log("Authentication attempt for user: {$username}, result: success/failure");3. HTTPS 是强制性的
php
<?php
// 在所有需要认证的端点强制 HTTPS
function enforceHttps(): void
{
if (
(empty($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on')
&& ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') !== 'https'
) {
$httpsUrl = 'https://' . ($_SERVER['HTTP_HOST'] ?? 'localhost') . $_SERVER['REQUEST_URI'];
http_response_code(301);
header('Location: ' . $httpsUrl);
exit;
}
}最佳实践
1. 选择合适的认证方式
php
<?php
// 场景推荐:
//
// 1. 浏览器访问的 Web 应用 -> Session + Cookie
// 2. SPA(前后端分离)-> Bearer Token (JWT)
// 3. 服务间通信 -> API Key / mTLS
// 4. 第三方接入 -> OAuth 2.0
// 5. 简单内部工具 -> Basic Auth (仅 HTTPS)
// 6. 移动应用 -> Bearer Token (JWT) + Refresh Token2. 认证失败信息不要暴露具体原因
php
<?php
// 错误:告诉攻击者用户名存在
if (!userExists($username)) {
die('用户名不存在');
}
if (!passwordCorrect($password)) {
die('密码错误');
}
// 正确:统一的错误信息
if (!validCredentials($username, $password)) {
die('用户名或密码错误'); // 不区分是用户名还是密码错误
}下一节
继续学习:表单提交与验证