PHP项目OAuth2.0授权流程如何代码实现

wen PHP项目 27

本文目录导读:

PHP项目OAuth2.0授权流程如何代码实现

  1. 使用OAuth2.0客户端库
  2. 完整授权流程实现
  3. 自定义OAuth提供商
  4. 安全注意事项
  5. 完整示例流程图

我来详细讲解PHP项目中OAuth2.0授权流程的完整实现。

使用OAuth2.0客户端库

推荐使用社区成熟的库,如 league/oauth2-clientphpseclib/oauth

安装依赖

composer require league/oauth2-client

完整授权流程实现

1 配置OAuth2服务提供商

config/oauth.php

<?php
return [
    'github' => [
        'clientId'     => 'your-client-id',
        'clientSecret' => 'your-client-secret',
        'redirectUri'  => 'https://your-domain/callback/github',
        'urlAuthorize'            => 'https://github.com/login/oauth/authorize',
        'urlAccessToken'          => 'https://github.com/login/oauth/access_token',
        'urlResourceOwnerDetails' => 'https://api.github.com/user',
        'scopes' => ['read:user', 'user:email']
    ],
    'google' => [
        'clientId'     => 'your-google-client-id',
        'clientSecret' => 'your-google-client-secret',
        'redirectUri'  => 'https://your-domain/callback/google',
        'urlAuthorize'            => 'https://accounts.google.com/o/oauth2/v2/auth',
        'urlAccessToken'          => 'https://oauth2.googleapis.com/token',
        'urlResourceOwnerDetails' => 'https://www.googleapis.com/oauth2/v2/userinfo',
        'scopes' => ['https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile']
    ]
];

2 OAuth服务类

services/OAuthService.php

<?php
namespace App\Services;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Token\AccessToken;
use App\Models\User;
use App\Repositories\UserRepository;
use GuzzleHttp\Client as HttpClient;
class OAuthService
{
    private $provider;
    private $userRepository;
    private $config;
    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
        $this->config = require __DIR__ . '/../config/oauth.php';
    }
    /**
     * 获取授权URL
     */
    public function getAuthorizationUrl(string $providerName): string
    {
        $provider = $this->getProvider($providerName);
        // 生成state参数防止CSRF
        $state = bin2hex(random_bytes(32));
        $_SESSION['oauth_state'] = $state;
        $_SESSION['oauth_provider'] = $providerName;
        $authorizationUrl = $provider->getAuthorizationUrl([
            'state' => $state,
            'scope' => $this->config[$providerName]['scopes']
        ]);
        return $authorizationUrl;
    }
    /**
     * 处理回调,获取access token和用户信息
     */
    public function handleCallback(string $providerName, string $code, string $state): array
    {
        // 验证state
        if (!isset($_SESSION['oauth_state']) || $state !== $_SESSION['oauth_state']) {
            throw new \Exception('Invalid state parameter');
        }
        $provider = $this->getProvider($providerName);
        try {
            // 用授权码换取access token
            $accessToken = $provider->getAccessToken('authorization_code', [
                'code' => $code
            ]);
            // 使用access token获取用户信息
            $resourceOwner = $provider->getResourceOwner($accessToken);
            $userData = $resourceOwner->toArray();
            // 查找或创建用户
            $user = $this->findOrCreateUser($providerName, $userData, $accessToken);
            // 清除session中的state
            unset($_SESSION['oauth_state']);
            unset($_SESSION['oauth_provider']);
            return [
                'user' => $user,
                'access_token' => $accessToken->getToken(),
                'refresh_token' => $accessToken->getRefreshToken(),
                'expires' => $accessToken->getExpires()
            ];
        } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
            throw new \Exception('OAuth Error: ' . $e->getMessage());
        }
    }
    /**
     * 刷新access token
     */
    public function refreshToken(string $providerName, string $refreshToken): AccessToken
    {
        $provider = $this->getProvider($providerName);
        $newAccessToken = $provider->getAccessToken('refresh_token', [
            'refresh_token' => $refreshToken
        ]);
        return $newAccessToken;
    }
    /**
     * 查找或创建用户
     */
    private function findOrCreateUser(string $provider, array $userData, AccessToken $accessToken): User
    {
        $email = $userData['email'] ?? $userData['login'] . '@github.oauth';
        // 根据提供商ID查找已有用户
        $user = $this->userRepository->findByOAuthProvider($provider, $userData['id']);
        if (!$user) {
            // 创建新用户
            $user = $this->userRepository->create([
                'name' => $userData['name'] ?? $userData['login'],
                'email' => $email,
                'password' => password_hash(bin2hex(random_bytes(16)), PASSWORD_DEFAULT),
                'email_verified_at' => now(),
                'oauth_provider' => $provider,
                'oauth_id' => (string)$userData['id'],
                'avatar_url' => $userData['avatar_url'] ?? null
            ]);
        }
        // 更新access token信息
        $this->userRepository->updateOAuthToken($user->id, [
            'access_token' => $accessToken->getToken(),
            'refresh_token' => $accessToken->getRefreshToken(),
            'token_expires_at' => date('Y-m-d H:i:s', $accessToken->getExpires())
        ]);
        return $user;
    }
    /**
     * 获取OAuth2提供商实例
     */
    private function getProvider(string $name): AbstractProvider
    {
        $config = $this->config[$name];
        switch ($name) {
            case 'github':
                return new \League\OAuth2\Client\Provider\Github([
                    'clientId'     => $config['clientId'],
                    'clientSecret' => $config['clientSecret'],
                    'redirectUri'  => $config['redirectUri']
                ]);
            case 'google':
                return new \League\OAuth2\Client\Provider\Google([
                    'clientId'     => $config['clientId'],
                    'clientSecret' => $config['clientSecret'],
                    'redirectUri'  => $config['redirectUri']
                ]);
            default:
                throw new \InvalidArgumentException("Unsupported provider: {$name}");
        }
    }
}

3 控制器实现

controllers/AuthController.php

<?php
namespace App\Controllers;
use App\Services\OAuthService;
use App\Repositories\UserRepository;
class AuthController
{
    private $oauthService;
    public function __construct()
    {
        $this->oauthService = new OAuthService(new UserRepository());
    }
    /**
     * 跳转到OAuth提供商授权页面
     */
    public function redirect(string $provider)
    {
        $authorizationUrl = $this->oauthService->getAuthorizationUrl($provider);
        header('Location: ' . $authorizationUrl);
        exit;
    }
    /**
     * 处理OAuth回调
     */
    public function callback(string $provider)
    {
        try {
            $code = $_GET['code'];
            $state = $_GET['state'];
            $result = $this->oauthService->handleCallback($provider, $code, $state);
            // 创建会话
            $_SESSION['user_id'] = $result['user']->id;
            $_SESSION['access_token'] = $result['access_token'];
            // 返回JSON或重定向
            header('Content-Type: application/json');
            echo json_encode([
                'success' => true,
                'message' => '登录成功',
                'user' => $result['user']
            ]);
        } catch (\Exception $e) {
            http_response_code(400);
            echo json_encode([
                'success' => false,
                'message' => $e->getMessage()
            ]);
        }
    }
    /**
     * 用户登出
     */
    public function logout()
    {
        session_destroy();
        header('Location: /login');
        exit;
    }
}

4 路由配置

routes/web.php

<?php
// 授权入口
Route::get('/auth/{provider}', 'AuthController@redirect');
// 回调处理
Route::get('/callback/{provider}', 'AuthController@callback');
// 登录页面(可选)
Route::get('/login', function() {
    echo '<a href="/auth/github">使用GitHub登录</a><br>';
    echo '<a href="/auth/google">使用Google登录</a>';
});
// 登出
Route::post('/logout', 'AuthController@logout');

自定义OAuth提供商

如果需要支持自定义API的OAuth2:

<?php
namespace App\OAuth;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use Psr\Http\Message\ResponseInterface;
class CustomProvider extends AbstractProvider
{
    private $apiDomain;
    public function __construct(array $options = [], array $collaborators = [])
    {
        parent::__construct($options, $collaborators);
        $this->apiDomain = $options['apiDomain'] ?? 'https://api.example.com';
    }
    public function getBaseAuthorizationUrl(): string
    {
        return $this->apiDomain . '/oauth/authorize';
    }
    public function getBaseAccessTokenUrl(array $params): string
    {
        return $this->apiDomain . '/oauth/token';
    }
    public function getResourceOwnerDetailsUrl(AccessToken $token): string
    {
        return $this->apiDomain . '/api/user';
    }
    protected function getDefaultScopes(): array
    {
        return ['basic'];
    }
    protected function checkResponse(ResponseInterface $response, $data): void
    {
        if ($response->getStatusCode() >= 400) {
            throw new IdentityProviderException(
                $data['error'] ?? $response->getReasonPhrase(),
                $response->getStatusCode(),
                $response
            );
        }
    }
    protected function createResourceOwner(array $response, AccessToken $token): CustomResourceOwner
    {
        return new CustomResourceOwner($response);
    }
}

安全注意事项

<?php
// 1. 始终使用HTTPS
if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
    header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
    exit;
}
// 2. State参数验证
// 在回调处理中验证state
// 3. 存储access token安全
// 使用加密存储
$encryptedToken = openssl_encrypt(
    $token,
    'aes-256-cbc',
    $_ENV['TOKEN_ENCRYPTION_KEY'],
    0,
    $iv
);
// 4. 定期刷新token
if ($accessToken->hasExpired()) {
    $newToken = $oauthService->refreshToken($provider, $refreshToken);
}
// 5. 限制重定向URI
// 在provider配置中只允许特定的回调URL

完整示例流程图

用户点击"使用GitHub登录"
        ↓
跳转到GitHub授权页面
        ↓
用户确认授权
        ↓
GitHub重定向回你的回调URL
        ↓
用授权码换取access token
        ↓
使用access token获取用户信息
        ↓
查找或创建本地用户
        ↓
创建用户会话
        ↓
返回成功响应或重定向到首页

这个实现涵盖了OAuth2.0授权码流程的完整步骤,包括state验证、token管理、用户信息同步等关键部分。

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