amocrm / amocrm-api-php

Библиотека на PHP для работы с API amoCRM
MIT License
148 stars 108 forks source link

Работа с несколькими аккаунтами - apiClientFactory #238

Open ikulakov opened 3 years ago

ikulakov commented 3 years ago

Добрый вечер,

Очень мало описано в документации по apiClientFactory. Могли бы подробнее рассказать о процессе авторизации для 2х и более аккаунтов?

Пример реализации интерфейса OAuthServiceInterface, в частности функции saveOAuthToken. Как в данном случае будет происходить получение и хранение access token

Спасибо!

ikulakov commented 3 years ago

интерфейсы реализовал следующим образом, поправьте если ошибаюсь

class UserAuth implements OAuthConfigInterface
{

    public $clientId;
    public $clientSecret;
    public $redirectUri;

    public function __construct($clientId, $clientSecret, $redirectUri)
    {
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
        $this->redirectUri = $redirectUri;
    }

    public function getIntegrationId(): string
    {
        return $this->clientId;
    }

    public function getSecretKey(): string
    {
        return $this->clientSecret;
    }

    public function getRedirectDomain(): string
    {
        return $this->redirectUri;
    }

}

class UserToken implements OAuthServiceInterface
{

    public $accessToken;
    public $baseDomain;

    public function __construct($accessToken, $baseDomain)
    {
        $this->baseDomain = $baseDomain;
        $this->accessToken = $accessToken;
    }

    public function saveOAuthToken(AccessTokenInterface $accessToken, string $baseDomain) :void
    {

        saveToken([
            'accessToken' => $accessToken->getToken(),
            'refreshToken' => $accessToken->getRefreshToken(),
            'expires' => $accessToken->getExpires(),
            'baseDomain' => $baseDomain,
        ]);

    }

}

функции get_token и save_token немного доработал под себя вот так

use League\OAuth2\Client\Token\AccessToken;

define('TOKEN_FILE', DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . 'token_info.json');

/**
 * @param array $accessToken
 */
function saveToken($accessToken)
{
    if (
        isset($accessToken)
        && isset($accessToken['accessToken'])
        && isset($accessToken['refreshToken'])
        && isset($accessToken['expires'])
        && isset($accessToken['baseDomain'])
    ) {

        $json = json_decode(file_get_contents(TOKEN_FILE), true);

        $json['tokens'][$accessToken['baseDomain']] = [
            'accessToken' => $accessToken['accessToken'],
            'expires' => $accessToken['expires'],
            'refreshToken' => $accessToken['refreshToken'],
            'baseDomain' => $accessToken['baseDomain'],
        ];

        file_put_contents(TOKEN_FILE, json_encode($json));
    } else {
        exit('Invalid access token ' . var_export($json, true));
    }
}

/**
 * @return AccessToken
 */
function getToken($domain)
{
    if (!file_exists(TOKEN_FILE)) {
        exit('Access token file not found');
    }

    $accessToken = json_decode(file_get_contents(TOKEN_FILE), true);

    if (isset($accessToken['tokens'])) {

        foreach ($accessToken['tokens'] as $token) {

            if (
                isset($token['accessToken'])
                && isset($token['refreshToken'])
                && isset($token['expires'])
                && isset($token['baseDomain'])
                && $token['baseDomain'] == $domain
            )  {

                return new AccessToken([
                    'access_token' => $token['accessToken'],
                    'refresh_token' => $token['refreshToken'],
                    'expires' => $token['expires'],
                    'baseDomain' => $token['baseDomain'],
                ]);

            }
        }
    } else {
        exit('Invalid access token ' . var_export($accessToken, true));
    }

}

bootstrap

$oAuthConfig = new UserAuth($clientId, $clientSecret, $redirectUri);

$accessToken = getToken($baseDomain);
$oAuthService = new UserToken($accessToken, $baseDomain);

$apiClientFactory = new \AmoCRM\AmoCRM\Client\AmoCRMApiClientFactory($oAuthConfig, $oAuthService);
$apiClient = $apiClientFactory->make();

$apiClient->setAccessToken($accessToken)
          ->setAccountBaseDomain($accessToken->getValues()['baseDomain']);

Вроде как работает токены обновляет по истечению срока. Может кому полезно будет.

eugene-borovov commented 3 years ago

При сохранении токена лучше использовать блокировку файла. Иначе одновременное обновление токенов с разных аккаунтов в разных процессах может вызвать потерю токенов.

$fp = fopen(TOKEN_FILE, "r+");

if (flock($fp, LOCK_EX)) {

    $json = json_decode(fread($fp, filesize(TOKEN_FILE)) ?: '{}', true);    
    if (!is_array($json['tokens'] ?? null)) {
        $json['tokens'] = [];
    }

    $json['tokens'][$accessToken['baseDomain']] = [
        'accessToken' => $accessToken['accessToken'],
        'expires' => $accessToken['expires'],
        'refreshToken' => $accessToken['refreshToken'],
        'baseDomain' => $accessToken['baseDomain'],
    ];

    ftruncate($fp, 0);
    fwrite($fp, json_encode($json));
    fflush($fp);
    flock($fp, LOCK_UN);
} else {
    exit('Could not lock token file');
}

fclose($fp);
AndresMachis commented 3 years ago

Добрый день. Уважаемый @ikulakov, могли бы вы подсказать как использовать ваше решение? Можем ли связаться в телегроам и ватсап?

Спасибо!

max-kut commented 3 years ago

С разными аккаунтами надо работать в БД, примерная структура будет такая (auth - это json объект с токенами)

create table amo_accounts
(
    id             bigint unsigned not null primary key,
    name           varchar(255)    not null,
    title          varchar(255)    null,
    auth           json            not null,
    created_at     timestamp       null,
    updated_at     timestamp       null,
    constraint amo_accounts_name_unique unique (name)
)
    collate = utf8mb4_unicode_ci;
Maximryzhkov commented 3 years ago

@bessudnov если выпустить ключ, и тут же его перевыпустить, через первичное получение ключа, оказывается, что работают оба ключ Это баг или итак и должно работать ? Было ожидание, что перевыпуск ключей полностью отключает старые ключи.

Maximryzhkov commented 3 years ago

@bessudnov у меня часто бывают задачи, когда нужен долгоиграющий скрипт, который работает больше суток. Параллельно работают короткие скрипты. Все на одном ключе.

Можете в офф библиотеку, перед рефрешем, сделать перечитывание ключа и уже если там тот же ключ, чтобы он переполучал его стандартным способом, а если есть более новый ключ- брал из хранилища? Или подскажите как это можно реализовать в вашей библиотеке без форков?

max-kut commented 3 years ago

@Maximryzhkov Не используй инстанс клиента долго. Пересоздавай по требованию

max-kut commented 3 years ago

Проблема может быть с параллельной обработкой. Представь два процесса используют клиент с одним аккаунтом. Пришло время обновить ключ и клиент в первом процессе его обновит, а второй будет пытаться стучаться в амо с устаревшим ключом. Он попытается обновить устаревший ключ устаревшим refresh_token и получит ошибку. Поэтому очень не рекомендуется держать инстанс параллельно в нескольких процессах.

Maximryzhkov commented 3 years ago

Проблема может быть с параллельной обработкой. Представь два процесса используют клиент с одним аккаунтом. Пришло время обновить ключ и клиент в первом процессе его обновит, а второй будет пытаться стучаться в амо с устаревшим ключом. Он попытается обновить устаревший ключ устаревшим refresh_token и получит ошибку. Поэтому очень не рекомендуется держать инстанс параллельно в нескольких процессах.

так я про это и пишу, и прошу совет \ предлагаю сделать механизм перечитывания ключа из хранилища.

max-kut commented 3 years ago

Есть интерфейс сохранения https://github.com/amocrm/amocrm-api-php/blob/master/src/AmoCRM/OAuth/OAuthServiceInterface.php данных аутентификации. Что мешает в реализации этого интерфейса добавить событие обноления ключа? Для одного процесса проблем нет слушать события. Для разных процессов - большой вопрос.

class OAuthService implements OAuthServiceInterface
{
    /**
     * @var \App\Models\Account
     */
    private Account $account;

    public function __construct(Account $account)
    {
        $this->account = $account;
    }

    public function saveOAuthToken(AccessTokenInterface $accessToken, string $baseDomain): void
    {
        $auth = $this->account->auth;
        $auth->accessToken = $accessToken->getToken();
        $auth->refreshToken = $accessToken->getRefreshToken();
        $auth->expires = Carbon::createFromTimestamp($accessToken->getExpires());

        $this->account->auth = $auth;
        $this->account->save();
        // здесь можно отправить событие
    }
}