You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在PHP/Slim项目中使用thephpleague/OAuth2实现授权?

在Slim项目中用thephpleague/oauth2-server实现基础授权的分步指南

针对Slim初学者,以下是实现基础OAuth2授权的最简步骤,适合开发环境快速验证:

1. 安装依赖

首先用Composer安装所需包:

composer require league/oauth2-server slim/psr7

slim/psr7是为了满足OAuth2服务器对PSR-7请求/响应的要求。

2. 生成加密密钥

OAuth2服务器需要加密密钥保障令牌安全,用OpenSSL命令生成:

# 生成私钥(用于签署令牌)
openssl genrsa -out config/keys/private.key 2048
# 生成公钥(可选,用于验证令牌)
openssl rsa -in config/keys/private.key -pubout -out config/keys/public.key
# 生成加密密钥(用于加密授权码等)
openssl rand -hex 16 > config/keys/encryption.key

确保config/keys目录存在,生产环境要限制该目录的访问权限。

3. 初始化OAuth2服务器

在项目中创建初始化服务器的文件src/OAuthServer.php:

<?php
namespace App;

use League\OAuth2\Server\AuthorizationServer;
use League\OAuth2\Server\CryptKey;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;

class OAuthServer
{
    public static function create(
        ClientRepositoryInterface $clientRepo,
        AccessTokenRepositoryInterface $tokenRepo,
        AuthCodeRepositoryInterface $authCodeRepo,
        RefreshTokenRepositoryInterface $refreshTokenRepo,
        ScopeRepositoryInterface $scopeRepo
    ): AuthorizationServer {
        $server = new AuthorizationServer(
            $clientRepo,
            $tokenRepo,
            $scopeRepo,
            new CryptKey(__DIR__ . '/../config/keys/private.key'),
            file_get_contents(__DIR__ . '/../config/keys/encryption.key')
        );

        // 启用授权码模式(适合Web应用的基础授权场景)
        $server->enableGrantType(
            new \League\OAuth2\Server\Grant\AuthCodeGrant(
                $authCodeRepo,
                $refreshTokenRepo,
                new \DateInterval('PT10M') // 授权码有效期10分钟
            ),
            new \DateInterval('PT1H') // 访问令牌有效期1小时
        );

        return $server;
    }
}

4. 实现基础存储库(开发环境用内存存储)

OAuth2服务器依赖多个存储库接口,开发阶段用内存实现,生产环境需替换为数据库存储:

客户端存储库(src/Repository/ClientRepository.php)

<?php
namespace App\Repository;

use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;

class ClientRepository implements ClientRepositoryInterface
{
    public function getClientEntity($clientIdentifier)
    {
        // 开发环境硬编码测试客户端
        if ($clientIdentifier !== 'test-client') {
            return null;
        }

        $client = new \League\OAuth2\Server\Entities\ClientEntity();
        $client->setIdentifier('test-client');
        $client->setName('Test Client');
        $client->setRedirectUri('http://localhost:8080/callback');
        $client->setConfidential(false); // 公开客户端(如前端应用)

        return $client;
    }

    public function validateClient($clientIdentifier, $clientSecret, $grantType)
    {
        // 开发环境跳过密钥验证,生产需对比存储的客户端密钥
        return $clientIdentifier === 'test-client';
    }
}

用户存储库(src/Repository/UserRepository.php)

用于验证用户身份:

<?php
namespace App\Repository;

use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface;

class UserRepository implements UserRepositoryInterface
{
    public function getUserEntityByUserCredentials($username, $password, $grantType, ClientEntityInterface $clientEntity)
    {
        // 开发环境硬编码测试用户
        if ($username === 'test-user' && $password === 'test-pass') {
            $user = new class implements UserEntityInterface {
                public function getIdentifier()
                {
                    return 1;
                }
            };
            return $user;
        }

        return null;
    }
}

范围存储库(src/Repository/ScopeRepository.php)

<?php
namespace App\Repository;

use League\OAuth2\Server\Repositories\ScopeRepositoryInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;

class ScopeRepository implements ScopeRepositoryInterface
{
    public function getScopeEntityByIdentifier($scopeIdentifier)
    {
        // 开发环境只支持基础权限范围
        if ($scopeIdentifier !== 'basic') {
            return null;
        }

        $scope = new \League\OAuth2\Server\Entities\ScopeEntity();
        $scope->setIdentifier('basic');
        return $scope;
    }

    public function finalizeScopes(array $scopes, $grantType, ClientEntityInterface $clientEntity, $userIdentifier = null)
    {
        return $scopes;
    }
}

令牌存储库示例(src/Repository/AccessTokenRepository.php)

<?php
namespace App\Repository;

use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;

class AccessTokenRepository implements AccessTokenRepositoryInterface
{
    protected $tokens = [];

    public function persistNewAccessToken(AccessTokenEntityInterface $accessTokenEntity)
    {
        $this->tokens[$accessTokenEntity->getIdentifier()] = $accessTokenEntity;
    }

    public function revokeAccessToken($tokenId)
    {
        unset($this->tokens[$tokenId]);
    }

    public function isAccessTokenRevoked($tokenId)
    {
        return !isset($this->tokens[$tokenId]);
    }

    public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null)
    {
        $token = new \League\OAuth2\Server\Entities\AccessTokenEntity();
        $token->setClient($clientEntity);
        foreach ($scopes as $scope) {
            $token->addScope($scope);
        }
        if ($userIdentifier !== null) {
            $token->setUserIdentifier($userIdentifier);
        }
        $token->setExpiryDateTime((new \DateTime())->add(new \DateInterval('PT1H')));
        return $token;
    }
}

AuthCodeRepository和RefreshTokenRepository可参照此模式实现,逻辑类似。

5. 在Slim中注册路由和服务器

修改public/index.php,初始化Slim应用并配置OAuth相关路由:

<?php
use Slim\Factory\AppFactory;
use App\OAuthServer;
use App\Repository\{
    ClientRepository,
    AccessTokenRepository,
    AuthCodeRepository,
    RefreshTokenRepository,
    ScopeRepository,
    UserRepository
};
use League\OAuth2\Server\Exception\OAuthServerException;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

require __DIR__ . '/../vendor/autoload.php';

// 初始化Slim应用
AppFactory::setResponseFactory(new \Slim\Psr7\ResponseFactory());
$app = AppFactory::create();

// 初始化OAuth服务器
$clientRepo = new ClientRepository();
$tokenRepo = new AccessTokenRepository();
$authCodeRepo = new AuthCodeRepository();
$refreshTokenRepo = new RefreshTokenRepository();
$scopeRepo = new ScopeRepository();
$server = OAuthServer::create($clientRepo, $tokenRepo, $authCodeRepo, $refreshTokenRepo, $scopeRepo);

// 授权端点(用户登录并授权)
$app->get('/authorize', function (Request $request, Response $response) use ($server) {
    try {
        $authRequest = $server->validateAuthorizationRequest($request);

        // 开发环境模拟用户登录,生产需替换为登录页面跳转逻辑
        $userRepo = new UserRepository();
        $user = $userRepo->getUserEntityByUserCredentials('test-user', 'test-pass', 'authorization_code', $authRequest->getClient());
        $authRequest->setUser($user);

        // 开发环境自动授权,生产需让用户手动确认授权
        $authRequest->setAuthorizationApproved(true);

        $response = $server->completeAuthorizationRequest($authRequest, $response);
        return $response;
    } catch (OAuthServerException $e) {
        return $e->generateHttpResponse($response);
    } catch (\Exception $e) {
        $body = $response->getBody();
        $body->write($e->getMessage());
        return $response->withStatus(500)->withBody($body);
    }
});

// 令牌端点(交换授权码获取访问令牌)
$app->post('/token', function (Request $request, Response $response) use ($server) {
    try {
        return $server->respondToAccessTokenRequest($request, $response);
    } catch (OAuthServerException $e) {
        return $e->generateHttpResponse($response);
    } catch (\Exception $e) {
        $body = $response->getBody();
        $body->write($e->getMessage());
        return $response->withStatus(500)->withBody($body);
    }
});

// 受保护的测试端点
$app->get('/protected', function (Request $request, Response $response) {
    $user = $request->getAttribute('oauth_user_id');
    $response->getBody()->write("Hello, user #{$user}! This is a protected endpoint.");
    return $response;
})->add(function (Request $request, $handler) use ($server) {
    // 令牌验证中间件
    try {
        $request = $server->validateAuthenticatedRequest($request);
        return $handler->handle($request);
    } catch (OAuthServerException $e) {
        return $e->generateHttpResponse($handler->handle($request));
    }
});

// 运行应用
$app->run();

6. 测试流程

  1. 访问授权端点:http://localhost:8080/authorize?client_id=test-client&redirect_uri=http://localhost:8080/callback&response_type=code&scope=basic
    开发环境会自动完成授权,跳转到回调地址并携带授权码code。
  2. 发送POST请求到令牌端点http://localhost:8080/token,用form-data提交参数:
    • grant_type: authorization_code
    • client_id: test-client
    • code: 上一步获取的授权码
    • redirect_uri: http://localhost:8080/callback
      会返回包含access_token的JSON响应。
  3. 访问受保护端点http://localhost:8080/protected,请求头添加Authorization: Bearer {access_token},即可看到授权后的内容。

生产环境注意事项

  • 将所有内存存储库替换为数据库实现(如Doctrine或Eloquent)
  • 加密密钥不要提交到版本控制,严格管理访问权限
  • 替换硬编码用户凭证,实现正规的用户登录系统
  • 启用HTTPS保障数据传输安全

内容的提问来源于stack exchange,提问作者Dawid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 23:27:27