PHP 怎么SCIM 协议

wen PHP项目 1

本文目录导读:

PHP 怎么SCIM 协议

  1. 使用现成的库
  2. 手动实现 SCIM REST API
  3. 完整的SCIM服务端实现
  4. SCIM客户端实现
  5. 错误处理和验证
  6. 认证和授权
  7. 测试示例
  8. 最佳实践建议

SCIM(System for Cross-domain Identity Management)是一种用于身份管理的标准化协议,主要用于用户、组等身份资源的跨域同步,在PHP中实现SCIM协议,主要有以下几种方式:

使用现成的库

SCIM 2.0 PHP库

// 使用 composer 安装
composer require "scim2/core"
composer require "scim2/server"
composer require "scim2/client"

服务端实现示例:

use SCIM\Server\SCIMServer;
use SCIM\Server\Adapter\InMemoryAdapter;
// 创建内存适配器
$adapter = new InMemoryAdapter();
// 创建用户
$userData = [
    'userName' => 'john.doe',
    'name' => [
        'givenName' => 'John',
        'familyName' => 'Doe'
    ],
    'emails' => [
        ['value' => 'john.doe@example.com', 'type' => 'work']
    ]
];
$user = $adapter->createUser($userData);
// 获取用户
$retrievedUser = $adapter->getUser($user['id']);

Symfony SCIM Bundle

// 使用 Symfony 的 SCIM bundle
composer require "tobiri/scim-bundle"
// config/packages/scim.yaml
scim:
    user_class: App\Entity\User
    group_class: App\Entity\Group

手动实现 SCIM REST API

基本架构

class SCIMController extends AbstractController
{
    #[Route('/scim/v2/Users', name: 'scim_users')]
    public function handleUsers(Request $request): JsonResponse
    {
        $method = $request->getMethod();
        switch ($method) {
            case 'GET':
                return $this->listUsers($request);
            case 'POST':
                return $this->createUser($request);
            default:
                return $this->json(['error' => 'Method not allowed'], 405);
        }
    }
    #[Route('/scim/v2/Users/{id}', name: 'scim_user_detail')]
    public function handleUserDetail(string $id, Request $request): JsonResponse
    {
        $method = $request->getMethod();
        switch ($method) {
            case 'GET':
                return $this->getUser($id);
            case 'PUT':
                return $this->updateUser($id, $request);
            case 'PATCH':
                return $this->patchUser($id, $request);
            case 'DELETE':
                return $this->deleteUser($id);
            default:
                return $this->json(['error' => 'Method not allowed'], 405);
        }
    }
}

数据模型

class SCIMUser
{
    private string $id;
    private string $userName;
    private array $name;
    private array $emails;
    private array $roles;
    private bool $active;
    // 转换为 SCIM 格式
    public function toSCIM(): array
    {
        return [
            'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User'],
            'id' => $this->id,
            'userName' => $this->userName,
            'name' => $this->name,
            'emails' => $this->emails,
            'active' => $this->active,
            'meta' => [
                'resourceType' => 'User',
                'created' => $this->createdAt->format(DATE_ATOM),
                'lastModified' => $this->updatedAt->format(DATE_ATOM)
            ]
        ];
    }
}

完整的SCIM服务端实现

class SCIMService
{
    private EntityManagerInterface $em;
    private UserRepository $userRepository;
    private GroupRepository $groupRepository;
    // 用户CRUD操作
    public function createUser(SCIMRequest $request): SCIMResponse
    {
        $data = $request->getAttributes();
        // 验证 schema
        if (!$this->isValidUserSchema($data)) {
            throw new SCIMValidationException('Invalid schema');
        }
        // 检查用户名唯一性
        if ($this->userRepository->findOneBy(['username' => $data['userName']])) {
            throw new SCIMConflictException('Username already exists');
        }
        // 创建用户实体
        $user = new User();
        $user->setUsername($data['userName']);
        $user->setEmail($data['emails'][0]['value']);
        // 映射自定义属性
        if (isset($data['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'])) {
            $enterpriseData = $data['urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'];
            $user->setDepartment($enterpriseData['department'] ?? null);
            // 更多自定义属性映射
        }
        $this->em->persist($user);
        $this->em->flush();
        // 返回 SCIM 格式响应
        return new SCIMResponse(
            $this->userToSCIM($user),
            201
        );
    }
    // PATCH 请求处理
    public function patchUser(string $id, SCIMRequest $request): SCIMResponse
    {
        $user = $this->userRepository->find($id);
        if (!$user) {
            throw new SCIMNotFoundException('User not found');
        }
        // 使用 JSON Patch 或 SCIM Patch 操作
        $operations = $request->getOperations();
        foreach ($operations as $operation) {
            switch ($operation['op']) {
                case 'add':
                    $this->applyAddOperation($user, $operation);
                    break;
                case 'replace':
                    $this->applyReplaceOperation($user, $operation);
                    break;
                case 'remove':
                    $this->applyRemoveOperation($user, $operation);
                    break;
            }
        }
        $this->em->flush();
        return new SCIMResponse($this->userToSCIM($user));
    }
    // 查询和过滤
    public function searchUsers(SCIMSearchRequest $request): SCIMResponse
    {
        $filters = $request->getFilters();
        $startIndex = $request->getStartIndex() ?? 1;
        $count = $request->getCount() ?? 100;
        // 构建查询
        $qb = $this->em->createQueryBuilder();
        $qb->select('u')
           ->from(User::class, 'u');
        // 应用过滤条件
        if (!empty($filters)) {
            foreach ($filters as $filter) {
                switch ($filter['field']) {
                    case 'userName':
                        $qb->andWhere('u.username = :username')
                           ->setParameter('username', $filter['value']);
                        break;
                    case 'email':
                        $qb->andWhere('u.email LIKE :email')
                           ->setParameter('email', '%' . $filter['value'] . '%');
                        break;
                    // 更多过滤条件
                }
            }
        }
        // 分页
        $qb->setFirstResult($startIndex - 1)
           ->setMaxResults($count);
        $users = $qb->getQuery()->getResult();
        // 格式化响应
        return new SCIMResponse([
            'schemas' => [
                'urn:ietf:params:scim:api:messages:2.0:ListResponse'
            ],
            'totalResults' => count($users),
            'startIndex' => $startIndex,
            'itemsPerPage' => $count,
            'Resources' => array_map(fn($user) => $this->userToSCIM($user), $users)
        ]);
    }
    // 批量操作
    public function bulkOperation(SCIMBulkRequest $request): SCIMResponse
    {
        $operations = $request->getOperations();
        $results = [];
        foreach ($operations as $index => $operation) {
            try {
                switch ($operation['method']) {
                    case 'POST':
                        $results[] = $this->createUser(new SCIMRequest($operation['data']));
                        break;
                    case 'PUT':
                        $results[] = $this->updateUser(
                            $operation['path'],
                            new SCIMRequest($operation['data'])
                        );
                        break;
                    case 'DELETE':
                        $results[] = $this->deleteUser($operation['path']);
                        break;
                }
            } catch (Exception $e) {
                $results[] = [
                    'bulkId' => $operation['bulkId'],
                    'status' => $e->getCode(),
                    'response' => ['error' => $e->getMessage()]
                ];
            }
        }
        return new SCIMResponse([
            'schemas' => ['urn:ietf:params:scim:api:messages:2.0:BulkResponse'],
            'Operations' => $results
        ]);
    }
}

SCIM客户端实现

class SCIMClient
{
    private string $baseUrl;
    private string $bearerToken;
    // 创建用户
    public function createUser(array $userData): array
    {
        $ch = curl_init($this->baseUrl . '/Users');
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => json_encode($userData),
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $this->bearerToken
            ]
        ]);
        $response = curl_exec($ch);
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        return json_decode($response, true);
    }
    // 更新用户 (PATCH)
    public function patchUser(string $id, array $patchData): array
    {
        $ch = curl_init($this->baseUrl . '/Users/' . $id);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => 'PATCH',
            CURLOPT_POSTFIELDS => json_encode($patchData),
            CURLOPT_HTTPHEADER => [
                'Content-Type: application/json',
                'Authorization: Bearer ' . $this->bearerToken
            ]
        ]);
        $response = curl_exec($ch);
        curl_close($ch);
        return json_decode($response, true);
    }
}

错误处理和验证

class SCIMErrorHandler
{
    public static function handleException(Exception $e): JsonResponse
    {
        $statusCode = 500;
        $scimError = [
            'schemas' => ['urn:ietf:params:scim:api:messages:2.0:Error'],
            'detail' => $e->getMessage()
        ];
        switch (get_class($e)) {
            case SCIMNotFoundException::class:
                $statusCode = 404;
                $scimError['status'] = '404';
                break;
            case SCIMValidationException::class:
                $statusCode = 400;
                $scimError['status'] = '400';
                $scimError['scimType'] = 'invalidSyntax';
                break;
            case SCIMConflictException::class:
                $statusCode = 409;
                $scimError['status'] = '409';
                $scimError['scimType'] = 'uniqueness';
                break;
            case SCIMFilterException::class:
                $statusCode = 400;
                $scimError['status'] = '400';
                $scimError['scimType'] = 'invalidFilter';
                break;
        }
        return new JsonResponse($scimError, $statusCode);
    }
}

认证和授权

class SCIMAuthenticator
{
    public function authenticate(Request $request): ?User
    {
        // 支持 Bearer Token
        $authHeader = $request->headers->get('Authorization');
        if (!$authHeader || !preg_match('/^Bearer\s+(.+)$/', $authHeader, $matches)) {
            return null;
        }
        $token = $matches[1];
        // 验证 token
        return $this->validateToken($token);
    }
    private function validateToken(string $token): ?User
    {
        // 检查是否为有效的 Service Token
        if (in_array($token, $this->getServiceTokens())) {
            return new ServiceUser();
        }
        // 或通过 OAuth2 验证
        $claims = $this->jwtDecoder->decode($token);
        return $this->userRepository->findByEmail($claims['email']);
    }
}

测试示例

class SCIMServiceTest extends TestCase
{
    public function testCreateUser()
    {
        $payload = [
            'schemas' => ['urn:ietf:params:scim:schemas:core:2.0:User'],
            'userName' => 'test.user',
            'emails' => [
                ['value' => 'test@example.com', 'primary' => true]
            ]
        ];
        $client = $this->createClient();
        $client->request(
            'POST',
            '/scim/v2/Users',
            [],
            [],
            ['CONTENT_TYPE' => 'application/json'],
            json_encode($payload)
        );
        $this->assertResponseStatusCodeSame(201);
        $response = json_decode($client->getResponse()->getContent(), true);
        $this->assertArrayHasKey('id', $response);
        $this->assertEquals('test.user', $response['userName']);
    }
}

最佳实践建议

  1. 验证所有SCIM Schema - 确保请求符合SCIM标准
  2. 实现完整的错误处理 - 返回标准化的SCIM错误格式
  3. 支持过滤和分页 - 实现完整的过滤语法支持
  4. 处理并发 - 使用版本号实现乐观锁
  5. 日志记录 - 记录所有SCIM操作用于审计
  6. 性能优化 - 为常用查询添加索引

如果需要更具体的实现细节或帮助,可以提供更多关于您使用场景的信息。

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