本文目录导读:

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']);
}
}
最佳实践建议
- 验证所有SCIM Schema - 确保请求符合SCIM标准
- 实现完整的错误处理 - 返回标准化的SCIM错误格式
- 支持过滤和分页 - 实现完整的过滤语法支持
- 处理并发 - 使用版本号实现乐观锁
- 日志记录 - 记录所有SCIM操作用于审计
- 性能优化 - 为常用查询添加索引
如果需要更具体的实现细节或帮助,可以提供更多关于您使用场景的信息。