PHP 怎么OIDC 认证

wen PHP项目 2

本文目录导读:

PHP 怎么OIDC 认证

  1. 使用官方OIDC库
  2. 使用Laravel框架
  3. 手动实现OIDC认证
  4. 验证JWT令牌
  5. 完整认证流程示例
  6. 最佳实践建议

我来详细介绍PHP实现OIDC(OpenID Connect)认证的几种方法:

使用官方OIDC库

使用 jumbojett/openid-connect-php(最流行)

// 安装
composer require jumbojett/openid-connect-php
<?php
require_once 'vendor/autoload.php';
use Jumbojett\OpenIDConnectClient;
// 初始化OIDC客户端
$oidc = new OpenIDConnectClient(
    'https://your-provider.com',  // 认证服务器URL
    'your-client-id',              // 客户端ID
    'your-client-secret'           // 客户端密钥
);
// 配置作用域
$oidc->addScope(['openid', 'profile', 'email']);
// 设置重定向URI
$oidc->setRedirectURL('https://your-app.com/callback.php');
// 认证用户
$oidc->authenticate();
// 获取用户信息
$userInfo = $oidc->requestUserInfo();
$name = $userInfo->name;
$email = $userInfo->email;
// 获取ID Token
$idToken = $oidc->getIdToken();

回调处理示例

<?php
// callback.php
session_start();
require_once 'vendor/autoload.php';
use Jumbojett\OpenIDConnectClient;
try {
    $oidc = new OpenIDConnectClient(
        'https://your-provider.com',
        'your-client-id',
        'your-client-secret'
    );
    $oidc->setRedirectURL('https://your-app.com/callback.php');
    // 处理认证回调
    $oidc->authenticate();
    // 验证并获取用户信息
    $userInfo = $oidc->requestUserInfo();
    // 保存用户会话
    $_SESSION['user'] = [
        'name' => $userInfo->name,
        'email' => $userInfo->email,
        'sub' => $userInfo->sub
    ];
    header('Location: dashboard.php');
    exit;
} catch (Exception $e) {
    // 错误处理
    error_log('OIDC Authentication Error: ' . $e->getMessage());
    header('Location: error.php');
    exit;
}

使用Laravel框架

使用 laravel/socialite

// 安装
composer require laravel/socialite
// app/Services/OIDCService.php
<?php
namespace App\Services;
use Laravel\Socialite\Facades\Socialite;
class OIDCService
{
    public function redirectToProvider()
    {
        return Socialite::driver('oidc')->redirect();
    }
    public function handleProviderCallback()
    {
        try {
            $user = Socialite::driver('oidc')->user();
            // 获取用户信息
            $name = $user->getName();
            $email = $user->getEmail();
            // 存储用户信息到数据库
            // ...
            return $user;
        } catch (\Exception $e) {
            throw new \Exception('OIDC认证失败: ' . $e->getMessage());
        }
    }
}

配置文件 config/services.php

'oidc' => [
    'client_id' => env('OIDC_CLIENT_ID'),
    'client_secret' => env('OIDC_CLIENT_SECRET'),
    'redirect' => env('OIDC_REDIRECT_URI'),
    'authorize_url' => env('OIDC_AUTHORIZE_URL'),
    'token_url' => env('OIDC_TOKEN_URL'),
    'userinfo_url' => env('OIDC_USERINFO_URL'),
    'jwks_url' => env('OIDC_JWKS_URL'),
],

手动实现OIDC认证

完整的OIDC流程实现

<?php
// OIDCClient.php
class OIDCClient {
    private $clientId;
    private $clientSecret;
    private $authorizeUrl;
    private $tokenUrl;
    private $userInfoUrl;
    private $redirectUri;
    private $jwksUrl;
    public function __construct($config) {
        $this->clientId = $config['client_id'];
        $this->clientSecret = $config['client_secret'];
        $this->authorizeUrl = $config['authorize_url'];
        $this->tokenUrl = $config['token_url'];
        $this->userInfoUrl = $config['userinfo_url'];
        $this->redirectUri = $config['redirect_uri'];
        $this->jwksUrl = $config['jwks_url'];
    }
    // 生成认证URL
    public function getAuthorizationUrl() {
        $params = [
            'response_type' => 'code',
            'client_id' => $this->clientId,
            'redirect_uri' => $this->redirectUri,
            'scope' => 'openid profile email',
            'state' => $this->generateState(),
            'nonce' => bin2hex(random_bytes(16))
        ];
        // 保存state到session用于CSRF防护
        $_SESSION['oidc_state'] = $params['state'];
        $_SESSION['oidc_nonce'] = $params['nonce'];
        return $this->authorizeUrl . '?' . http_build_query($params);
    }
    // 处理回调
    public function handleCallback($code, $state) {
        // 验证state
        if ($state !== ($_SESSION['oidc_state'] ?? '')) {
            throw new Exception('Invalid state parameter');
        }
        // 交换code获取token
        $tokens = $this->exchangeCode($code);
        // 验证ID Token
        $claims = $this->verifyIdToken($tokens['id_token']);
        // 获取用户信息
        $userInfo = $this->getUserInfo($tokens['access_token']);
        return [
            'tokens' => $tokens,
            'claims' => $claims,
            'user_info' => $userInfo
        ];
    }
    // 交换认证码
    private function exchangeCode($code) {
        $ch = curl_init($this->tokenUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'redirect_uri' => $this->redirectUri,
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret
        ]);
        $response = curl_exec($ch);
        curl_close($ch);
        return json_decode($response, true);
    }
    // 验证JWT
    private function verifyIdToken($idToken) {
        $claims = $this->decodeJWTPayload($idToken);
        // 验证claims
        if ($claims['iss'] !== $this->authorizeUrl) {
            throw new Exception('Invalid issuer');
        }
        if (time() < $claims['iat'] || time() > $claims['exp']) {
            throw new Exception('Token expired');
        }
        if ($claims['aud'] !== $this->clientId) {
            throw new Exception('Invalid audience');
        }
        // 验证nonce
        if ($claims['nonce'] !== ($_SESSION['oidc_nonce'] ?? '')) {
            throw new Exception('Invalid nonce');
        }
        return $claims;
    }
    // 解码JWT
    private function decodeJWTPayload($jwt) {
        $parts = explode('.', $jwt);
        $payload = base64_decode(str_replace(['-', '_'], ['+', '/'], $parts[1]));
        return json_decode($payload, true);
    }
    // 获取用户信息
    private function getUserInfo($accessToken) {
        $ch = curl_init($this->userInfoUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Authorization: Bearer ' . $accessToken
        ]);
        $response = curl_exec($ch);
        curl_close($ch);
        return json_decode($response, true);
    }
    private function generateState() {
        return bin2hex(random_bytes(16));
    }
}

验证JWT令牌

使用 firebase/php-jwt

<?php
// 安装
composer require firebase/php-jwt
// JWT验证示例
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class JWTVerifier {
    private $jwks;
    private $issuer;
    private $audience;
    public function verify($token) {
        $tks = explode('.', $token);
        $header = json_decode(base64_decode($tks[0]), true);
        $payload = json_decode(base64_decode($tks[1]), true);
        // 获取匹配的密钥
        $key = $this->getKeyFromJWKS($header['kid']);
        try {
            $decoded = JWT::decode($token, new Key($key['n'], 'RS256'));
            // 验证其他claims
            return $this->verifyClaims($decoded);
        } catch (Exception $e) {
            throw new Exception('JWT验证失败: ' . $e->getMessage());
        }
    }
    private function verifyClaims($token) {
        // 验证发行者
        if ($token->iss !== $this->issuer) {
            throw new Exception('无效的发行者');
        }
        // 验证受众
        if (isset($token->aud) && !in_array($this->audience, (array)$token->aud)) {
            throw new Exception('无效的受众');
        }
        // 验证过期时间
        if (isset($token->exp) && time() >= $token->exp) {
            throw new Exception('令牌已过期');
        }
        return $token;
    }
}

完整认证流程示例

<?php
// index.php - 登录入口
session_start();
require_once 'OIDCClient.php';
$config = [
    'client_id' => 'your-client-id',
    'client_secret' => 'your-client-secret',
    'authorize_url' => 'https://provider.com/authorize',
    'token_url' => 'https://provider.com/token',
    'userinfo_url' => 'https://provider.com/userinfo',
    'redirect_uri' => 'https://your-app.com/callback.php',
    'jwks_url' => 'https://provider.com/.well-known/jwks.json'
];
$auth = new OIDCClient($config);
// 检查用户是否已认证
if (isset($_SESSION['user'])) {
    echo "欢迎, " . $_SESSION['user']['name'];
} else {
    $authUrl = $auth->getAuthorizationUrl();
    echo "<a href='{$authUrl}'>使用OIDC登录</a>";
}
<?php
// callback.php - 回调处理
session_start();
require_once 'OIDCClient.php';
$config = [...]; // 同上的配置
$auth = new OIDCClient($config);
try {
    if (isset($_GET['code'])) {
        $result = $auth->handleCallback($_GET['code'], $_GET['state']);
        $_SESSION['user'] = [
            'sub' => $result['user_info']['sub'],
            'name' => $result['user_info']['name'] ?? '',
            'email' => $result['user_info']['email'] ?? '',
            'access_token' => $result['tokens']['access_token']
        ];
        header('Location: dashboard.php');
    } else if (isset($_GET['error'])) {
        throw new Exception('认证失败: ' . $_GET['error_description']);
    }
} catch (Exception $e) {
    echo "错误: " . $e->getMessage();
}

最佳实践建议

  1. 安全考虑

    • 始终使用HTTPS
    • 验证state和nonce参数
    • 妥善保管client_secret
    • 使用短生命周期的token
  2. 性能优化

    • 缓存JWKS密钥
    • 使用Redis等缓存token验证结果
  3. 错误处理

    • 统一错误处理机制
    • 记录详细日志
  4. 测试

    • 使用Mock Provider测试
    • 集成测试认证流程

选择哪种方法取决于你的PHP框架和使用场景,生产环境建议使用成熟的库(如第一个方案)以获得更好的安全性和可靠性。

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