授权方式
锦浪云 API 提供两种授权方式,适用于不同的客户类型:
| 授权方式 | 认证方式 | 适用客户 |
|---|---|---|
| 面向云平台用户授权 | 自定义 HMAC-SHA1 签名 | 业主和安装商 |
| 第三方授权 | OAuth2.0 | 第三方服务商、第三方监控平台 |
API 基础域名:
- 面向云平台用户授权:
https://www.soliscloud.com- 第三方授权(OAuth2.0):
https://api-oauth2.soliscloud.com
注意
两种授权方式的接口相互独立,面向云平台用户授权接口不支持第三方授权调用,反之亦然。
关于接口路径中的 v1/v2 命名
文档中提到的 /v1/api/ 和 /v2/api/ 仅为接口路径命名约定,不代表 API 版本升级或替代关系。两种路径的接口均持续维护,请根据授权方式选择对应的接口路径。
面向云平台用户授权(HMAC 签名)
按用户分配系统访问权限,用户只能在自己被分配的资源范围内操作。适用于在锦浪云拥有可登录账号,且账号下有直接归属或共享电站的 业主和安装商。
接入流程
获取 API Key
- 登录锦浪云 WEB 端:
https://www.soliscloud.com - 点击 服务 → API 管理
- 点击 立即开通,验证身份后即可查看 KeyID 和 KeySecret
安全提醒
请妥善保管 KeySecret,避免数据泄露。开通 API 权限后,需要退出重新登录才能生效。
认证方式
接口使用自定义 HMAC-SHA1 签名认证。每次请求需要在 HTTP Header 中携带以下 4 个参数:
| Header 参数 | 说明 |
|---|---|
| Content-MD5 | Body 内容的 MD5 值,经 Base64 编码 |
| Content-Type | 固定值:application/json;charset=UTF-8 |
| Date | GMT 时间,格式:EEE, d MMM yyyy HH:mm:ss 'GMT' |
| Authorization | API {apiId}:{sign} |
Content-MD5 计算方式
- 对 Body 内容进行 MD5 加密
- 将加密结果转换为 128 位二进制数组
- 对二进制数组进行 Base64 编码
public static String getDigest(String body) {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(body.getBytes());
byte[] b = md.digest();
return Base64.encodeBytes(b);
}import hashlib
import base64
def get_digest(body: str) -> str:
md5_hash = hashlib.md5(body.encode('utf-8')).digest()
return base64.b64encode(md5_hash).decode('utf-8')const crypto = require('crypto');
function getDigest(body) {
return crypto.createHash('md5').update(body).digest('base64');
}public static String getDigest(String body) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(body.getBytes());
byte[] b = md.digest();
return Base64.encodeBytes(b);
}Date 格式要求
- 使用 GMT 时区时间
- 格式:
EEE, d MMM yyyy HH:mm:ss 'GMT' - 时间偏差不能超过当前时间正负 15 分钟,否则调用失败
SimpleDateFormat sdf = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));from datetime import datetime, timezone
date_str = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')const dateStr = new Date().toUTCString();SimpleDateFormat sdf = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
String dateStr = sdf.format(new Date());Authorization 签名计算
格式:"API " + apiId + ":" + Sign
Sign 计算公式:
Sign = Base64(HmacSHA1(KeySecret, POST + "\n" + Content-MD5 + "\n" + Content-Type + "\n" + Date + "\n" + CanonicalizedResource))CanonicalizedResource= 要访问的 API 接口路径,如/v1/api/inverterDetail\n表示换行符
完整签名示例:
import hashlib
import base64
import hmac
import json
from datetime import datetime, timezone
api_id = 'YOUR_API_ID'
api_secret = 'YOUR_API_SECRET'
body = json.dumps({})
canonicalized_resource = '/v1/api/userStationList'
# 1. Content-MD5
content_md5 = base64.b64encode(
hashlib.md5(body.encode('utf-8')).digest()
).decode('utf-8')
# 2. Date
date_str = datetime.now(timezone.utc).strftime('%a, %d %b %Y %H:%M:%S GMT')
# 3. Sign
sign_str = f'POST\n{content_md5}\napplication/json;charset=UTF-8\n{date_str}\n{canonicalized_resource}'
sign = base64.b64encode(
hmac.new(api_secret.encode('utf-8'), sign_str.encode('utf-8'), hashlib.sha1).digest()
).decode('utf-8')
authorization = f'API {api_id}:{sign}'const crypto = require('crypto');
const apiId = 'YOUR_API_ID';
const apiSecret = 'YOUR_API_SECRET';
const body = JSON.stringify({});
const canonicalizedResource = '/v1/api/userStationList';
// 1. Content-MD5
const contentMd5 = crypto.createHash('md5').update(body).digest('base64');
// 2. Date
const dateStr = new Date().toUTCString();
// 3. Sign
const signStr = `POST\n${contentMd5}\napplication/json;charset=UTF-8\n${dateStr}\n${canonicalizedResource}`;
const sign = crypto.createHmac('sha1', apiSecret).update(signStr).digest('base64');
const authorization = `API ${apiId}:${sign}`;import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;
String apiId = "YOUR_API_ID";
String apiSecret = "YOUR_API_SECRET";
String body = "{}";
String canonicalizedResource = "/v1/api/userStationList";
// 1. Content-MD5
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(body.getBytes());
String contentMd5 = Base64.getEncoder().encodeToString(md.digest());
// 2. Date
SimpleDateFormat sdf = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
String dateStr = sdf.format(new Date());
// 3. Sign
String signStr = "POST\n" + contentMd5 + "\napplication/json;charset=UTF-8\n" + dateStr + "\n" + canonicalizedResource;
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(apiSecret.getBytes(), "HmacSHA1"));
String sign = Base64.getEncoder().encodeToString(mac.doFinal(signStr.getBytes()));
String authorization = "API " + apiId + ":" + sign;请求示例
POST /v1/api/userStationList
Content-MD5: kxdxk7rbAsrzSIWgEwhH4w==
Content-Type: application/json;charset=UTF-8
Date: Fri, 26 Jul 2019 06:00:46 GMT
Authorization: API {apiId}:nBYQWeuzy3Y+gp67BN8zXTmvSDk=
Body: {"pageNo":1,"pageSize":10}签名校验工具
- 在线 HMAC 工具:https://dinochiesa.github.io/hmachash/index.html — 选择 HmacSHA1 算法,密钥填 KeySecret,输入签名串,输出做 Base64 即可得到
sign - Java 完整示例下载:Authorization.java
支持的 API 类型
第三方授权(OAuth2.0)
允许第三方应用通过令牌合法访问用户资源,无需用户的账号密码。适用于在锦浪云无电站访问权限的第三方应用,如 第三方服务商、第三方监控平台。
以下示例中
{API_OAUTH2_DOMAIN}请替换为实际域名:https://api-oauth2.soliscloud.com
接入流程总览
申请条件
第三方平台接入前,需联系 Solis 销售申请开通,提交 SolisCloud API 开通申请材料.xlsx,Solis 将为第三方平台分配 API Key(client_id) 和 API Secret(client_secret)。
授权流程详解
第一步:请求用户授权码
在浏览器中访问以下 URL,引导业主完成登录和授权:
GET https://{API_OAUTH2_DOMAIN}/oauth/authorize?response_type=code&client_id=YOUR_API_KEY&redirect_uri=YOUR_REDIRECT_URI&state=RANDOM_STATE| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| client_id | String | 是 | 第三方应用在锦浪注册的 AppKey |
| response_type | String | 是 | 固定值 code,OAuth 2.0 协议标准参数 |
| redirect_uri | String | 是 | 授权回调地址,需与申请时提供给锦浪的回调地址一致 |
| state | String | 否 | 状态值,用于防范 CSRF 攻击。业主授权后回调时会原样带回该字段,第三方应用可校验是否匹配。建议使用 UUID 或随机字符串生成,长度不超过 180 个字符 |
| scope | String | 否 | 请求的权限集,多个权限用空格分隔(URL 编码为 %20)。如 access_data control_device。不传时默认请求全部权限 |
redirect_uri 编码
当回调地址中包含 & 等特殊字符时,必须对 redirect_uri 进行 URL 编码后再拼接,否则可能导致参数解析异常。例如 https://www.example.com/cn/?a=b&c=d 应编码为 https%3A%2F%2Fwww.example.com%2Fcn%2F%3Fa%3Db%26c%3Dd。
第三方应用可将此链接集成到系统中,或直接发送给电站业主。业主访问后:
- 输入锦浪云账号密码登录
- 查看授权范围(数据获取 / 设备控制),点击同意
- 页面携带授权码重定向到回调地址,同时带回
state参数
# 授权成功(带回 state)
https://your-callback.com/response?code=AUTHORIZATION_CODE&state=RANDOM_STATE
# 业主拒绝授权
https://your-callback.com/response?error=access_denied&state=RANDOM_STATE第二步:通过授权码换取 Access Token
请求 URL:POST https://{API_OAUTH2_DOMAIN}/oauth/token
请求头(Authorization):使用 HTTP Basic 认证,值为 Basic Base64(client_id:client_secret)
| Header 参数 | 说明 |
|---|---|
| Content-Type | application/x-www-form-urlencoded |
| Authorization | Basic Base64(YOUR_API_KEY:YOUR_API_SECRET) |
请求体(Body):
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| grant_type | String | 是 | 固定为 authorization_code |
| code | String | 是 | 第一步获取的授权码 |
| redirect_uri | String | 是 | 与申请时填写的回调地址一致 |
client_id / client_secret
client_id 即锦浪分配的 AppKey,client_secret 即对应的 AppSecret。两者通过 Basic 认证方式放在请求头中,而非请求体中。
请求示例:
POST https://{API_OAUTH2_DOMAIN}/oauth/token
Authorization: Basic YOUR_BASE64_ENCODED_CREDENTIALS
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=YOUR_CODE&redirect_uri=https%3A%2F%2Fyour-callback.com响应示例:
{
"access_token": "eyJhbGci...",
"token_type": "bearer",
"refresh_token": "eyJhbGci...",
"expires_in": 86400,
"scope": "access_data control_device"
}| 响应字段 | 说明 |
|---|---|
| access_token | 调用 API 所需的令牌 |
| refresh_token | 用于续期的刷新令牌 |
| expires_in | Token 有效期(秒),默认 86400(1天) |
| scope | 授权范围:access_data 数据获取 / control_device 设备控制 |
重要提醒
- 授权码只能使用一次,使用后立即失效
- Access Token 有效期 1 天
- Refresh Token 有效期 30 天
- 授权码有效期 30 分钟
使用 Token 调用 API
获取 access_token 后,在每次 API 请求的 HTTP Header 中携带 Bearer Token:
curl -X POST "https://{API_OAUTH2_DOMAIN}/api/access_data/userStationList" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-d '{}'重要说明
- Bearer Token 必须放在 HTTP 请求头中,格式为
Authorization: Bearer {access_token} - 不能将 token 放在请求体中
- 所有 OAuth2.0 接口均使用此认证方式
刷新 Token
当 access_token 过期时,使用 refresh_token 续期,无需业主重新授权:
请求 URL:POST https://{API_OAUTH2_DOMAIN}/oauth/token
请求头(同第二步)
请求体(Body):
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| grant_type | String | 是 | 固定为 refresh_token |
| refresh_token | String | 是 | 上次返回的 refresh_token |
POST https://{API_OAUTH2_DOMAIN}/oauth/token
Authorization: Basic YOUR_BASE64_ENCODED_CREDENTIALS
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN刷新 Token 注意事项
- 建议在 access_token 过期前主动刷新
- 刷新后返回全新的 access_token 和 refresh_token,有效期重置
- 刷新后旧 Token 立即失效
- 若 refresh_token 也过期,需重新引导业主走完整授权流程
- 如无需频繁重新授权,可由后台定期调用刷新接口保持 Token 有效
撤销授权
业主主动撤销
电站业主可通过锦浪云 WEB 端或 App 撤销授权:账号与安全 > 授权管理
三方应用主动撤销
第三方应用也可通过 API 主动注销某业主的授权,使其无法再访问业主数据。适用场景包括:业主主动注销授权、检测到安全漏洞时紧急撤销等。
请求 URL:POST https://{API_OAUTH2_DOMAIN}/oauth/revoke
请求头(Authorization):使用 HTTP Basic 认证,值为 Basic Base64(client_id:client_secret)
| Header 参数 | 说明 |
|---|---|
| Content-Type | application/x-www-form-urlencoded |
| Authorization | Basic Base64(YOUR_API_KEY:YOUR_API_SECRET) |
请求体(Body):
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| token | String | 是 | 需要撤销的令牌,可以是 access_token 或 refresh_token |
请求示例:
POST /oauth/revoke HTTP/1.1
Host: api-oauth2.soliscloud.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic Base64(client_id:client_secret)
token=YOUR_ACCESS_TOKEN_OR_REFRESH_TOKEN返回示例:
撤销成功后,接口返回 HTTP 200 状态码。
注意
撤销后该令牌立即失效。如撤销的是 refresh_token,则关联的 access_token 也将同时失效。
