PHP项目OpenID Connect认证

wen PHP项目 4

本文目录导读:

PHP项目OpenID Connect认证

  1. OpenID Connect基础概念
  2. 使用官方库实现(推荐)
  3. 完整实现示例
  4. 配置文件示例
  5. 测试和调试

我来为您详细讲解如何在PHP项目中实现OpenID Connect认证。

OpenID Connect基础概念

OpenID Connect (OIDC) 是建立在OAuth 2.0之上的身份认证层,主要包含:

  • ID Token:JWT格式的身份令牌
  • Access Token:访问令牌
  • Refresh Token:刷新令牌
  • UserInfo Endpoint:用户信息端点

使用官方库实现(推荐)

使用 league/oauth2-client

composer require league/oauth2-client

使用 jumbojett/openid-connect-php

composer require jumbojett/openid-connect-php

完整实现示例

1 基础配置

<?php
// config.php
class OIDCConfig {
    // Provider配置
    const PROVIDER_URL = 'https://your-provider.com';
    const CLIENT_ID = 'your-client-id';
    const CLIENT_SECRET = 'your-client-secret';
    const REDIRECT_URI = 'https://your-app.com/callback.php';
    // 请求的权限范围
    const SCOPES = ['openid', 'profile', 'email', 'offline_access'];
}
?>

2 使用jumbojett库的完整实现

<?php
// login.php - 发起认证请求
require_once 'vendor/autoload.php';
require_once 'config.php';
use Jumbojett\OpenIDConnectClient;
session_start();
// 创建OIDC客户端
$oidc = new OpenIDConnectClient(
    OIDCConfig::PROVIDER_URL,
    OIDCConfig::CLIENT_ID,
    OIDCConfig::CLIENT_SECRET
);
// 设置重定向URI
$oidc->setRedirectURL(OIDCConfig::REDIRECT_URI);
// 设置请求的作用域
$oidc->addScope(OIDCConfig::SCOPES);
// 添加认证参数(可选)
$oidc->addAuthParam(['prompt' => 'login']);
// 保存状态用于防CSRF
$_SESSION['oidc_state'] = $oidc->getState();
// 发起认证请求
$oidc->authenticate();
?>
<?php
// callback.php - 处理认证回调
require_once 'vendor/autoload.php';
require_once 'config.php';
use Jumbojett\OpenIDConnectClient;
session_start();
// 验证state防止CSRF
if (isset($_GET['state']) && $_SESSION['oidc_state'] !== $_GET['state']) {
    die('State mismatch, possible CSRF attack');
}
try {
    $oidc = new OpenIDConnectClient(
        OIDCConfig::PROVIDER_URL,
        OIDCConfig::CLIENT_ID,
        OIDCConfig::CLIENT_SECRET
    );
    $oidc->setRedirectURL(OIDCConfig::REDIRECT_URI);
    // 处理认证回调
    $oidc->authenticate();
    // 获取用户信息
    $userInfo = $oidc->requestUserInfo();
    // 获取ID Token
    $idToken = $oidc->getIdToken();
    // 获取Access Token
    $accessToken = $oidc->getAccessToken();
    // 获取Refresh Token(如果配置了offline_access)
    $refreshToken = $oidc->getRefreshToken();
    // 保存用户会话
    $_SESSION['user'] = [
        'sub' => $userInfo->sub,
        'email' => $userInfo->email ?? '',
        'name' => $userInfo->name ?? '',
        'given_name' => $userInfo->given_name ?? '',
        'family_name' => $userInfo->family_name ?? '',
        'picture' => $userInfo->picture ?? '',
        'access_token' => $accessToken,
        'id_token' => $idToken,
        'refresh_token' => $refreshToken,
        'login_time' => time()
    ];
    // 重定向到受保护的页面
    header('Location: /dashboard.php');
    exit;
} catch (Exception $e) {
    error_log('OIDC authentication error: ' . $e->getMessage());
    header('Location: /login-error.php?error=' . urlencode($e->getMessage()));
    exit;
}
?>

3 使用league/oauth2-client实现

<?php
// oidc_league.php - 使用league/oauth2-client
require_once 'vendor/autoload.php';
require_once 'config.php';
use League\OAuth2\Client\Provider\GenericProvider;
session_start();
class OIDCOAuth2Client {
    private $provider;
    public function __construct() {
        $this->provider = new GenericProvider([
            'clientId'                => OIDCConfig::CLIENT_ID,
            'clientSecret'            => OIDCConfig::CLIENT_SECRET,
            'redirectUri'             => OIDCConfig::REDIRECT_URI,
            'urlAuthorize'            => OIDCConfig::PROVIDER_URL . '/authorize',
            'urlAccessToken'          => OIDCConfig::PROVIDER_URL . '/token',
            'urlResourceOwnerDetails' => OIDCConfig::PROVIDER_URL . '/userinfo',
            'scopes'                  => OIDCConfig::SCOPES
        ]);
    }
    public function authenticate() {
        // 检查授权回调
        if (!isset($_GET['code'])) {
            // 获取授权URL
            $authorizationUrl = $this->provider->getAuthorizationUrl();
            $_SESSION['oauth2state'] = $this->provider->getState();
            header('Location: ' . $authorizationUrl);
            exit;
        }
        // 验证state
        if (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
            unset($_SESSION['oauth2state']);
            exit('Invalid state');
        }
        try {
            // 获取访问令牌
            $accessToken = $this->provider->getAccessToken('authorization_code', [
                'code' => $_GET['code']
            ]);
            // 获取用户信息
            $resourceOwner = $this->provider->getResourceOwner($accessToken);
            $userData = $resourceOwner->toArray();
            // 保存会话
            $_SESSION['user'] = [
                'id' => $userData['sub'],
                'email' => $userData['email'],
                'name' => $userData['name'],
                'access_token' => $accessToken->getToken(),
                'expires' => $accessToken->getExpires()
            ];
            return true;
        } catch (Exception $e) {
            error_log('Authentication failed: ' . $e->getMessage());
            return false;
        }
    }
}
// 使用示例
$client = new OIDCOAuth2Client();
if ($client->authenticate()) {
    header('Location: /dashboard.php');
    exit;
} else {
    header('Location: /login-error.php');
    exit;
}
?>

4 用户会话管理和中间件

<?php
// auth_middleware.php - 认证中间件
class AuthMiddleware {
    public static function requireLogin() {
        session_start();
        if (!self::isLoggedIn()) {
            header('Location: /login.php');
            exit;
        }
        // 检查令牌是否过期
        if (self::isTokenExpired()) {
            self::refreshToken();
        }
        return $_SESSION['user'];
    }
    public static function isLoggedIn() {
        return isset($_SESSION['user']['id']);
    }
    public static function isTokenExpired() {
        if (!isset($_SESSION['user']['token_expires'])) {
            return true;
        }
        return time() >= $_SESSION['user']['token_expires'];
    }
    public static function refreshToken() {
        // 使用refresh token刷新
        require_once 'vendor/autoload.php';
        $refreshToken = $_SESSION['user']['refresh_token'] ?? null;
        if (!$refreshToken) {
            self::logout();
            return false;
        }
        try {
            $oidc = new OpenIDConnectClient(
                OIDCConfig::PROVIDER_URL,
                OIDCConfig::CLIENT_ID,
                OIDCConfig::CLIENT_SECRET
            );
            // 刷新令牌
            $newToken = $oidc->refreshToken($refreshToken);
            // 更新会话
            $_SESSION['user']['access_token'] = $newToken->getAccessToken();
            $_SESSION['user']['token_expires'] = time() + $newToken->getExpiresIn();
            $_SESSION['user']['refresh_token'] = $newToken->getRefreshToken();
            return true;
        } catch (Exception $e) {
            error_log('Token refresh failed: ' . $e->getMessage());
            self::logout();
            return false;
        }
    }
    public static function logout() {
        session_destroy();
        header('Location: /login.php');
        exit;
    }
}
?>

5 安全最佳实践

<?php
// security.php - 安全配置
class Security {
    // 验证ID Token
    public static function validateIdToken($idToken) {
        $parts = explode('.', $idToken);
        if (count($parts) !== 3) {
            return false;
        }
        list($header, $payload, $signature) = $parts;
        // 解码payload
        $decodedPayload = json_decode(base64_decode(strtr($payload, '-_', '+/')), true);
        // 验证过期时间
        if ($decodedPayload['exp'] < time()) {
            return false;
        }
        // 验证issuer
        if ($decodedPayload['iss'] !== OIDCConfig::PROVIDER_URL) {
            return false;
        }
        // 验证audience
        if ($decodedPayload['aud'] !== OIDCConfig::CLIENT_ID) {
            return false;
        }
        return $decodedPayload;
    }
    // 生成安全的随机状态
    public static function generateState() {
        return bin2hex(random_bytes(32));
    }
    // CSRF防护
    public static function verifyCSRF($token) {
        return hash_equals($_SESSION['csrf_token'] ?? '', $token);
    }
}
// 认证成功后的用户授权检查
class Authorization {
    public static function checkPermission($user, $requiredRole) {
        $userRoles = $user['roles'] ?? [];
        return in_array($requiredRole, $userRoles);
    }
    public static function requireRole($role) {
        $user = AuthMiddleware::requireLogin();
        if (!self::checkPermission($user, $role)) {
            http_response_code(403);
            die('Access denied');
        }
        return $user;
    }
}
?>

6 完整的登录页面示例

<!-- login.php -->
<?php
require_once 'config.php';
session_start();
?>
<!DOCTYPE html>
<html>
<head>登录</title>
    <meta charset="utf-8">
    <style>
        body { font-family: Arial, sans-serif; margin: 40px auto; max-width: 400px; }
        .login-btn { display: block; width: 100%; padding: 15px; background: #4CAF50; color: white; text-decoration: none; text-align: center; border-radius: 5px; }
        .error { color: red; margin: 10px 0; }
    </style>
</head>
<body>
    <h1>用户登录</h1>
    <?php if (isset($_GET['error'])): ?>
        <div class="error"><?php echo htmlspecialchars($_GET['error']); ?></div>
    <?php endif; ?>
    <a href="/initiate_oidc.php" class="login-btn">使用OIDC账户登录</a>
</body>
</html>
<?php
// initiate_oidc.php - 发起OIDC认证
require_once 'vendor/autoload.php';
require_once 'config.php';
session_start();
$oidc = new OpenIDConnectClient(
    OIDCConfig::PROVIDER_URL,
    OIDCConfig::CLIENT_ID,
    OIDCConfig::CLIENT_SECRET
);
$oidc->setRedirectURL(OIDCConfig::REDIRECT_URI);
$oidc->addScope(['openid', 'profile', 'email']);
$oidc->authenticate();
?>

配置文件示例

<?php
// config_example.php - 配置示例
return [
    'oidc' => [
        'provider_url' => 'https://your-identity-provider.com',
        'client_id' => 'your-client-id',
        'client_secret' => 'your-client-secret',
        'redirect_uri' => 'https://your-app.com/callback.php',
        'scopes' => ['openid', 'profile', 'email'],
        'response_type' => 'code',
        'grant_type' => 'authorization_code',
        'allow_refresh_tokens' => true
    ],
    'session' => [
        'name' => 'oidc_session',
        'lifetime' => 3600, // 1小时
        'secure' => true,
        'http_only' => true,
        'same_site' => 'Lax'
    ],
    'cache' => [
        'enabled' => true,
        'ttl' => 300 // 5分钟缓存JWT
    ]
];
?>

测试和调试

<?php
// test_oidc.php - 测试脚本
require_once 'vendor/autoload.php';
function testOIDCConfiguration() {
    // 测试OIDC配置
    echo "测试OIDC配置...\n";
    // 1. 检查发现端点
    $issuer = OIDCConfig::PROVIDER_URL;
    $wellKnownUrl = $issuer . '/.well-known/openid-configuration';
    $ch = curl_init($wellKnownUrl);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    if (!$response) {
        echo "❌ 无法访问OIDC发现端点\n";
        return false;
    }
    $config = json_decode($response, true);
    if (!isset($config['authorization_endpoint'], $config['token_endpoint'], $config['userinfo_endpoint'])) {
        echo "❌ OIDC发现端点配置不完整\n";
        return false;
    }
    echo "✅ OIDC提供者配置正确\n";
    return true;
}
testOIDCConfiguration();
?>

这就是一个完整的PHP OpenID Connect认证实现,关键要点:

  1. 选择合适的库:建议使用 jumbojett/openid-connect-php 因为它专门针对OIDC实现
  2. 安全第一:始终验证state、令牌签名和过期时间
  3. 会话管理:合理管理access token和refresh token的生命周期
  4. 错误处理:提供友好的错误提示和日志记录

需要根据您的具体OIDC提供者(如Keycloak、Auth0、Azure AD等)调整配置。

抱歉,评论功能暂时关闭!