跳转至

应用程序和账户认证

cTrader Open API 认证流程基于OAuth 2.0 标准

该框架允许第三方应用程序代表资源所有者或通过协调资源所有者与服务之间的批准交互来获取对服务的有限访问权限,或者允许第三方应用程序代表自己获取访问权限。

认证流程

根据 OAuth 2.0 标准,账户认证需要授权码和访问令牌。

定义令牌

授权码是为单个 cTID 颁发的短期令牌。 其有效期为 1 分钟。 访问令牌是长期令牌,允许应用程序向 cTrader 后端发送和接收消息。 收到授权码后,必须迅速将其交换为访问令牌;之后,所有发送到 cTrader 后端的消息都必须使用收到的访问令牌进行签名以认证您的应用程序。 访问令牌的有效期为 2,628,000 秒(约 30 天)。 过期后,可以使用刷新令牌生成新的访问令牌。 刷新令牌没有有效期。

通常,新用户首次使用您的应用程序的过程如下:

1. 具有特定 cTID 的用户访问您的应用程序部署的位置;用户与操作按钮或等效项交互。

2. 应用程序使用嵌入式浏览器(或任何其他合适的方式)将用户重定向到特定 URI,用户将在该 URI 授予应用程序必要的权限。

获得信任

在将用户重定向到此 URI 之前,请确保用户信任您的应用程序,并了解他们需要提供额外权限的事实。

3. 用户被转移到预定义的重定向 URL。 此 URL 包含作为 code 查询参数值的访问令牌。

4. 用户自行或自动返回到应用程序。 应用程序被告知 code 参数的值。

5. 应用程序向特定端点发送请求,将授权码交换为访问令牌。

6. 应用程序收到包含访问令牌作为 accessToken 键值的响应。

7. 应用程序发送包含 clientIdclientSecret 字段的ProtoOAApplicationAuthReq 消息

8. 应用程序发送包含 accessToken 字段的ProtoOAGetAccountListByAccessTokenReq 消息。 响应(ProtoOAGetAccountListByAccessTokenRes)包含重复的 ctidTraderAccountId 字段。

9. 收到响应后,应用程序发送包含 ctidTraderAccountIdaccessToken 字段的ProtoOAAccountAuthReq 消息

之后,用户应在 ctidTraderAccountId 与步骤 8 中提供的 ctidTraderAccountId 匹配的账户下进行认证。

下图从您的 Open API 应用程序的角度解释了基本认证流程的工作原理。

graph TB
  A([将用户重定向到 <br/> 特定的URI]) ==> B([接收授权 <br/> 代码]);
  B ==> C([发送请求以获取 <br/> 访问代码]);
  C ==> D([发送 <br/> ProOAApplicationAuthReq <br/> 消息]);
  D ==> E([发送 <br/> ProtoOAAccountAuthReq <br/> 消息]);

在下面的子章节中,我们将详细解释认证流程的关键步骤。

添加重定向 URI

如前所述,您的应用程序需要在用户被发送到特定重定向 URI 后获取授权码。 此 URI 将包含作为 code 查询参数值的授权码。

要添加新的重定向 URI,请执行以下操作:

1. 在 Open API 门户中打开应用程序页面。

2. 选择要添加重定向 URL 的应用程序所在行中的编辑按钮。

3. 在新出现的屏幕中,向下滚动到重定向 URI部分。

4. 在可用的空文本输入字段中添加重定向 URL。 如果没有显示可用的字段,请选择添加重定向 URI按钮。

5. 选择页面底部的保存按钮以应用您的更改。

管理重定向 URI

第一个重定向 URI 用于 Playground 服务,该服务允许您在向普通用户提供访问权限之前测试您的应用程序。 它绝不能用于生产应用程序。 如果需要,您可以添加无限数量的重定向 URI。

获取授权码

要为特定 cTID 接收授权码,您的应用程序需要将用户发送到以下 URI。

https://id.ctrader.com/my/settings/openapi/grantingaccess/?client_id={clientId}&redirect_uri={your_redirectURI}&scope={scope}&product=web

注意

在此阶段,用户需要允许您的应用程序或服务访问其交易账户。 因此,您必须让用户充分了解使用您的应用程序或服务在权限、数据存储和数据处理方面的要求。

此 URI 具有以下查询参数。

参数 是否必需? 定义
client_id 您的 Open API 应用程序的唯一标识符。
redirect_uri 在应用程序设置中指定的有效重定向 URI。
scope 您希望授予应用程序的权限范围。 接受以下值:
accounts。 仅授予查看权限。 您的应用程序将能够访问账户信息和统计数据;执行交易操作将不可行。
trading. 授予账户信息和统计数据的完全访问权限以及所有允许的交易操作。
product 如果指定此参数并将其设置为 web,则授权屏幕将不显示页眉和页脚,使其在移动设备上看起来更好。 虽然此参数是可选的,但我们建议将其设置为 web

要查看您的 client_id,请转到应用程序页面并选择凭证列中的查看按钮。

当用户被发送到上述 URI 时(只要两个参数都有效),他们应该会看到一个登录/注册屏幕,提示他们在其 cTID 下授权。 随后,用户将看到一个对话框,他们可以在其中允许应用程序访问与其 cTID 关联的一个或多个账户。

当用户选择允许访问按钮时,他们将被重定向到您最初指定的 redirect_uri。 如下例所示,授权代码将作为查询参数添加到此 URI 中。

https://my-redirect-uri.com/?code=123ASDASffdg_as234_GCllkoq14adRB7_tvN

此时,您的应用程序需要访问并存储 code 参数的值。 它必须在授权代码过期之前快速与访问令牌交换。

附加账户

如果用户在完成上述操作后创建了附加账户,这些账户将不会在应用程序中授权。 相反,用户需要被重定向回上面提供的 URL,以便他们可以授予应用程序访问自上次登录以来创建的任何附加账户的权限。

通过 Google 进行身份验证

如果上述 URI 通过 WebView 打开,并且用户选择 Google 登录,Google 将显示访问被阻止页面,阻止用户继续操作。

为防止这种情况,请确保打开页面的设备的 user-agent 签名如下所示。

'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'

获取访问令牌

要获取访问令牌,请执行以下 REST API 调用。

相对 URL

请求 URL 相对于 https://openapi.ctrader.com

方法 URL
GET apps/token

此请求包括以下查询参数。

参数 是否必需? 定义
grant_type 发送到此端点的令牌类型。 接受以下值。
authorization_code. 将授权代码发送到端点以将其与访问令牌交换。
refresh_token. 将刷新令牌发送到端点以刷新现有的访问令牌。
code 发送到端点的令牌。 如果 grant_type=authorization_code,则此参数是必需的。
redirect_uri 在应用程序设置中指定的有效重定向 URI。 如果 grant_type=authorization_code,则此参数是必需的。
client_id 您的 Open API 应用程序的唯一标识符。
client_secret 分配给您的 Open API 应用程序的密钥。

请求示例如下所示。

curl -X GET 'https://openapi.ctrader.com/apps/token?grant_type=authorization_code&code=0ssdgds98as9_QSF56FVC_22dfdf&redirect_uri=https://spotware.com&client_id=5430012&client_secret=012sds23dlkjQsd' -H 'Accept: application/json' -H 'Content-Type: application/json'

要查看您的 client_secret,请返回应用程序页面并选择凭证列中的查看按钮。

请求成功后,您应收到包含以下键的响应。

值类型 定义
"accessToken" 字符串 您的应用程序将用于验证发送到 cTrader Open API 的消息的访问令牌。
"tokenType" 字符串 访问令牌的类型(bearer"" 应为该键的值,因为使用 REST 标准发出请求)。
"expiresIn" 整数 访问令牌过期之前必须经过的秒数(默认情况下,该键的值为 2628000)。
"refreshToken" 字符串 用于在访问令牌过期后更新访问令牌的刷新令牌。 刷新令牌本身没有过期时间。
"errorCode" string/null 描述请求失败原因的错误代码。 对于成功的请求,此键的值应为 null
"description" string/null 描述请求失败时发生的错误的详细信息。 对于成功的请求,此键的值应为 null

有关成功响应的示例,请参见下面的代码片段。

{
    "accessToken":"mos8Bw3D4EG0fRPd4Eqq0JxaFT4zjd8e4YijNezh_ag",
    "tokenType":"bearer",
    "expiresIn":2628000,
    "refreshToken":"VCuafFhy81AFZjsWkbuEzdOhhRj5YTWz8fWUwHam7KM",
    "errorCode":null,
    "description":null,
}

发送所需的 ProtoBuf 消息

在收到有效的访问代码后,您的应用程序需要发送 ProtoOAApplicationAuthReqProtoOAGetAccountListByAccessTokenReqProtoOAAccountAuthReq 消息。 可以按以下方式完成此操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private static RepeatedField<ProtoOACtidTraderAccount> _ctidTraderAccounts;

List<IDisposable> _disposables = new();
_disposables.Add(_client.OfType<ProtoOAGetAccountListByAccessTokenRes>().Subscribe(OnAccountIdReceived));

private static void OnAccountIdReceived(ProtoOAGetAccountListByAccessTokenRes response)
{

    Console.WriteLine($"\nMessage Received:\n{response}");

    _ctidTraderAccounts = response.CtidTraderAccount;

    var accountAuthReq = new ProtoOAAccountAuthReq
    {
        CtidTraderAccountId = (long)_ctidTraderAccounts[0].CtidTraderAccountId,
        AccessToken = _token.AccessToken,
    };

    _client.SendMessage(accountAuthReq);

}

var _client = new OpenClient(host, ApiInfo.Port, TimeSpan.FromSeconds(10), useWebSocket: false);
var applicationAuthReq = new ProtoOAApplicationAuthReq
{
    ClientId = {your_client_id}, // Add your client ID here
    ClientSecret = {your_app_secret}, // Add your app secret here
};
await _client.SendMessage(applicationAuthReq);
await Task.Delay(5000);
var getAccountListByAccessToken = new ProtoOAGetAccountListByAccessTokenReq 
{
    AccessToken = {access_token} // Add your access token here
}
var accountAuthReq = new ProtoOAAccountAuthReq 
{
    ctidTraderAccountId = {account_id}, // Add the ctidTraderAccountId of the account you want to authorize here
    AccessToken = {access_token} // Add your access token here
};
await _client.SendMessage(accountAuthReq);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
currentAccountId = None
client = Client(EndPoints.PROTOBUF_LIVE_HOST if hostType.lower() == "live" else EndPoints.PROTOBUF_DEMO_HOST, EndPoints.PROTOBUF_PORT, TcpProtocol)

def connected(client): # Callback for client connection
    print("\nConnected")
    request = ProtoOAApplicationAuthReq()
    request.clientId = appClientId
    request.clientSecret = appClientSecret
    deferred = client.send(request)



def disconnected(client, reason): # Callback for client disconnection
    print("\nDisconnected: ", reason)

def onMessageReceived(client, message): # Callback for receiving all messages
    global currentAccountId
    if message.payloadType in [ProtoOASubscribeSpotsRes().payloadType, ProtoOAAccountLogoutRes().payloadType, ProtoHeartbeatEvent().payloadType]:
        return
    elif message.payloadType == ProtoOAApplicationAuthRes().payloadType:
        print("API Application authorized\n")
        if currentAccountId is not None:
            sendProtoOAAccountAuthReq()
            return
        accountListReq = ProtoOAGetAccountListByAccessTokenReq()
        accountListReq.accessToken = accessToken
        deferred = client.send(accountListReq)
    elif message.payloadType == ProtoOAGetAccountListByAccessTokenRes().payloadType:
        protoOAGetAccountListByAccessTokenRes = Protobuf.extract(message)
        currentAccountId = int(protoOAGetAccountListByAccessTokenRes.ctidTraderAccount[0].ctidTraderAccountId)
        accountAuthReq = ProtoOAAccountAuthReq()
        accountAuthReq.ctidTraderAccountId = currentAccountId
        accountAuthReq.accessToken = accessToken
        deferred = client.send(accountAuthReq)
    elif message.payloadType == ProtoOAAccountAuthRes().payloadType:
        protoOAAccountAuthRes = Protobuf.extract(message)
        print(f"Account {protoOAAccountAuthRes.ctidTraderAccountId} has been authorized\n")
        print("This acccount will be used for all future requests\n")
    else:
        print("Message received: \n", Protobuf.extract(message))
    reactor.callLater(3, callable=executeUserCommand)

def onError(failure): # Call back for errors
    print("Message Error: ", failure)
    reactor.callLater(3, callable=executeUserCommand)

刷新访问令牌

在访问令牌过期后,您需要使用之前收到的 refreshToken 来更新它。 您可以通过两种不同的方法执行此操作。

发送 HTTP 请求

要获取新的访问令牌(以及随后的新刷新令牌),您可以执行与上面所述相同的 HTTP 请求。 如下例所示,您需要将 grant_type 查询参数设置为 refresh_token

curl -X POST 'https://openapi.ctrader.com/apps/token?grant_type=refresh_token&refresh_token=asdXCVCVAS_218kjashf_asdasd2ASD_223x&client_id=5430012&client_secret=012sds23dlkjQsd' -H 'Accept: application/json' -H 'Content-Type: application/json'

您将收到包含 accessTokenrefreshToken 键新值的响应。 您可以安全地使用这些新值,而相同键的旧值将自动失效。

发送 Protobuf 消息

或者,您可以发送 ProtoOARefreshTokenReq 并接收包含新有效令牌的 ProtoOARefreshTokenRes 消息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
_disposables.Add(_client.OfType<ProtoOARefreshTokenRes>().Subscribe(OnRefreshTokenResponse));
private static void OnRefreshTokenResponse(ProtoOARefreshTokenRes response) 
{
    _token = new Token 
    {
        AccessToken = response.AccessToken,
        RefreshToken = response.RefreshToken,
        ExpiresIn = DateTimeOffset.FromUnixTimeMilliseconds(response.ExpiresIn),
        TokenType = response.TokenType,
    }
}
var refreshTokenReq = new ProtoOARefreshTokenReq
    {
        refreshToken = {your_refresh_token}, // Add your refresh token here
    };
await _client.SendMessage(applicationAuthReq);
1
newToken = auth.refreshToken("refresh_Token")

使用 Playground

Playground 提供了一种快速获取您自己的 cTID 访问令牌的方法,使您能够在将应用程序分发给目标受众之前快速轻松地开发和测试您的应用程序。

要访问 Playground,请转到 Applications 页面并选择 Playground 按钮。 在新出现的窗口中,选择应用程序的范围并点击 Get token 按钮。

之后,您应该会看到以下页面。

您可以使用访问令牌和刷新令牌在通过您自己的 cTID 认证的情况下向 API 发送消息。