分享页Token参数签名校验
下面介绍在发布大屏和报表时,使用 Token 参数签名校验的方法。通过 Token 参数签名校验功能,您可以对大屏和报表交互时传递的参数进行签名鉴权,来防止访问者通过修改页面传递的参数来访问其他未授权的页面数据,从而提高数据以及用户信息的安全性。
前提条件
在使用 Token 参数签名校验前,请确保:
- 大屏或报表使用 Token 验证的方式进行公开分享,具体请参见「公开和加密分享」。
- 大屏在 URL 地址中传递参数(直接在 URL 后面加参数)。
- 大屏 URL 中传递的参数要求不能被篡改。
背景信息
例如:
某用户的系统嵌入了 Sugar BI 大屏,URL 地址通过 Token 计算出来,并且在 URL 地址中传递area(地区)
参数给大屏以展示相对应地区的数据,可以使用 https://sugar.baidubce.com/dashboard/1827981ec07ac66f937a88c9e65f****?_sugar_time=1612959542484&_sugar_signature=H09EX3nMls%2FE6IzhZKr6U6LfNq2Xl%2FJK%2BxYWoPv65D****&area=华东
来访问大屏。
其中 area
(地区)是为大屏传递的参数,存在被篡改的可能。比如华东
的员工将 URL 改成 https://sugar.baidubce.com/dashboard/1827981ec07ac66f937a88c9e65f****?_sugar_time=1612959542484&_sugar_signature=H09EX3nMls%2FE6IzhZKr6U6LfNq2Xl%2FJK%2BxYWoPv65D****&area=华北
,就可以看到华北
的数据。因此,为了保证数据的安全,需要对用户传递的参数进行签名鉴权,保证计算得到的 URL 参数不能被更改,如果私自更改了传参,页面将无法访问。
签名参数规则
- 需要加入签名的参数,其参数名需以
sugar_sign_
开头,后面可以带任何有效的参数名字符 - 不符合此签名参数规则的参数,将不会进行参数签名校验,允许修改参数值
- 签名参数按升序排序
使用流程
- 确定需要签名计算的参数名(即不允许被篡改的参数)
- 在大屏或报表开发完成后,使用 Token 验证的方式发布大屏或报表
- 使用带签名参数的 URL 计算生成访问大屏或报表的 URL
- 使用上一步中计算得到的 URL 访问大屏或报表,在访问过程中,系统会自动进行参数签名校验
- 如果参数签名校验功能正常,当访问者修改了签名参数,再次访问此 URL 时,访问会被拒绝
带签名参数的 URL 计算
shareID 说明
shareID 是大屏/报表在分享时自动生成的 url 中的部分,下面对大屏和报表中分别说明:
大屏分享的 url 地址如:https://sugar.aipage.com/dashboard/41510e632e1e1e4767b0a041030670ec
,41510e632e1e1e4767b0a041030670ec 就是 shareID
报表分享的 url 地址如:https://sugar.aipage.com/report/r_1013e-8xdmi3ud-k9wl5p/06e84b7f924ecc9c33857e832de04127
,06e84b7f924ecc9c33857e832de04127 就是 shareID
示例代码如下:
Node.js
const crypto = require('crypto');
const querystring = require('querystring');
const signedQueryParamReg = /^sugar_sign_.*/; // 符合此正则表达式的参数是需要签名的。
let token = 'OAMf7CvniOGgoNijH9mFHEHSAf7****';
let shareID = '1827981ec07ac66f937a88c9e65f****'; // shareID详见前面文档中的说明
const time = Date.now();
const customParams = {
sugar_sign_no: 191866,
name: 101
};
let signParamsStr = Object.keys(customParams)
.filter(paramName => customParams[paramName] && signedQueryParamReg.test(paramName))
.sort()
.map(param => `${param}=${customParams[param]}`)
.join('&');
let stringToSign = [shareID, time];
signParamsStr && stringToSign.push(signParamsStr);
stringToSign = stringToSign.join('|');
let signature = crypto.createHmac('sha256', token).update(stringToSign).digest().toString('base64');
let queryParams = {
_sugar_time: time,
_sugar_signature: signature
};
Object.keys(customParams).forEach(paramName => {
queryParams[paramName] = customParams[paramName];
});
let url = `https://sugar.baidubce.com/dashboard/${shareID}?${querystring.stringify(queryParams)}`;
console.log(url);
PHP
<?php
$token = "OAMf7CvniOGgoNijH9mFHEHSAf7****";
$shareID = "1827981ec07ac66f937a88c9e65f****"; // shareID详见前面文档中的说明
$time = time()*1000;
$customParams = array(
'sugar_sign_no'=>'191866',
'name'=>'101'
);
$sign_array = preg_grep("/^sugar_sign_.*/", array_keys($customParams));
sort($sign_array);
function toPlain($v)
{
global $customParams;
return "$v=$customParams[$v]";
};
$signParamsStr = join("&",array_map("toPlain",$sign_array));
$stringToSign = $shareID.'|'.$time.'|'.$signParamsStr;
$signature = urlencode(base64_encode(hash_hmac('sha256', $stringToSign, $token, true)));
$queryParams = join("&",array_map("toPlain",array_keys($customParams)));
$url = "https://sugar.baidubce.com/dashboard/".$shareID."?_sugar_time=".$time."&_sugar_signature=".$signature."&".$queryParams;
echo $url;
?>
<iframe width=100% height=100% src="<?=$url?>"/>
Java
package com.company;
import java.security.*;
import java.util.Date;
import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import java.net.URLEncoder;
public class TokenTest {
public static String getSignedUrl(String shareID, String token, String signParamsStr) {
Date date = new Date();
Long time = date.getTime();
String stringToSign = shareID + "|" + time;
if (!StringUtils.isEmpty(signParamsStr)) {
stringToSign = stringToSign + "|" + signParamsStr;
}
String signature = HMACSHA256(stringToSign.getBytes(), token.getBytes());
String url = "https://sugar.baidubce.com/dashboard/" + shareID + "?_sugar_time=" + time + "&_sugar_signature=" + signature + "&" + signParamsStr;
return url;
}
/**
* 使用java原生的摘要实现SHA256加密。
* @param str加密后的报文。
* @return
*/
public static String HMACSHA256(byte[] data, byte[] key) {
try {
SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(signingKey);
return URLEncoder.encode(byte2Base64(mac.doFinal(data)));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
return null;
}
private static String byte2Base64(byte[] bytes){
return Base64.encodeBase64String(bytes);
}
public static void main(String[] args) throws Exception {
String signedQueryParamReg = "^sugar_sign_.*";
Map<String, Integer> customParams = new HashMap<>();
customParams.put("sugar_sign_no", 191866);
customParams.put("name", 101);
String signParamsStr = customParams.entrySet().stream().filter(entry -> Pattern.matches(signedQueryParamReg, entry.getKey()))
.map(entry -> entry.getKey() + "=" + entry.getValue())
.sorted()
.collect(Collectors.joining("&"));
System.out.println(getSignedUrl("shareID", "token", signParamsStr)); // shareID详见前面文档中的说明
}
}
.NET
using System;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Text;
namespace sugarToken
{
class Program
{
static void Main(string[] args)
{
var dic = new Dictionary<string, string>(); // 自定义参数。
dic.Add("sugar_sign_no", "191866"); // sugar_sign_开头,需要签名。
dic.Add("sugar_sign_lo", "mm");
dic.Add("sugar_sign_mo", "aa");
dic.Add("name", "101"); // 不需要签名。
// 分享页前缀,大屏分享shareID、token,自定义参数字典。
Console.WriteLine(GenerateUrl("https://sugar.baidubce.com/dashboard/", "1827981ec07ac66f937a88c9e65f****", "OAMf7CvniOGgoNijH9mFHEHSAf7****", dic));
}
private static string GenerateUrl(string sugarBase, string shareID, string token, Dictionary<string, string> customParams)
{
string pattern = @"^sugar_sign_.*";
string timestamp = GetTimeStamp();
// 参数排序
Dictionary<string, string>.KeyCollection keyCol = customParams.Keys;
List<string> signKeys = new List<string>();
foreach (var item in keyCol.ToList())
{
if (Regex.IsMatch(item, pattern))
{
signKeys.Add(item);
}
}
// 按照key排序
signKeys = signKeys.OrderBy(k => k).ToList();
string paramsSignStr = signKeys.Aggregate("", (total, key) =>
{
if (total != "")
{
total += "&";
}
total += key + "=" + customParams[key];
return total;
});
string signStr = shareID + "|" + timestamp + "|" + paramsSignStr;
var encoding = new System.Text.ASCIIEncoding();
byte[] keyByte = encoding.GetBytes(token);
byte[] messageBytes = encoding.GetBytes(signStr);
string signature;
using (var hmacsha256 = new HMACSHA256(keyByte))
{
byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
signature = Convert.ToBase64String(hashmessage);
}
var paramDic = new Dictionary<string, string>();
paramDic.Add("_sugar_time", timestamp);
paramDic.Add("_sugar_signature", signature);
foreach (var item in customParams)
{
paramDic.Add(item.Key, item.Value);
}
return sugarBase + shareID + "?" + ParseToString(paramDic);
}
public static string GetTimeStamp()
{
TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalMilliseconds).ToString();
}
static public string ParseToString(IDictionary<string, string> parameters)
{
IDictionary<string, string> sortedParams = new SortedDictionary<string, string>(parameters);
IEnumerator<KeyValuePair<string, string>> dem = sortedParams.GetEnumerator();
StringBuilder query = new StringBuilder("");
while (dem.MoveNext())
{
string key = dem.Current.Key;
string value = dem.Current.Value;
if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(value))
{
query.Append(key).Append("=").Append(HttpUtility.UrlEncode(value)).Append("&");
}
}
string content = query.ToString().Substring(0, query.Length - 1);
return content;
}
}
}
Python
import time
import hmac
import base64
import urllib.parse
now = str(round(time.time() * 1000))
token = b'OAMf7CvniOGgoNijH9mFHEHSAf7****'
# shareID详见前面文档中的说明
share_id = '1827981ec07ac66f937a88c9e65f****'
# 自定义参数,其中sugar_sign_开头,需要签名
custome_params = {
"sugar_sign_no": 191866,
"name": 101
}
# 按照key排序
sorted_custome_params = {i: custome_params[i] for i in sorted(custome_params.keys())}
sign_params_str = '&'.join([f"{k}={v}" for k, v in sorted_custome_params.items() if k.startswith("sugar_sign_")])
string_to_sign = '|'.join([share_id, now, sign_params_str])
signature = base64.b64encode(hmac.digest(token, string_to_sign.encode('utf-8'), 'sha256')).decode('utf-8')
signature = urllib.parse.quote(signature)
# 生成最终链接
url = f"https://sugar.baidubce.com/dashboard/{share_id}?_sugar_time={now}&_sugar_signature={signature}&" \
+ urllib.parse.urlencode(sorted_custome_params)
print(url)
使用以上代码示例得到的 URL 为:https://sugar.baidubce.com/dashboard/1827981ec07ac66f937a88c9e65f****?_sugar_time=1612500688339&_sugar_signature=xM%2FNQv%2F5Je7o4j2046I0Gi%2BDmQegGn%2FEHXU%2BNskcEg****&sugar_sign_no=191866&name=101
,在 URL 的有效期内,如果修改了 sugar_sign_no
字段的值,链接将无法访问,如果修改了 name 字段的值,链接仍然可以访问,因为 sugar_sign_no
符合签名参数规则,参与了签名计算,而 name 不符合签名参数规则,不会进行签名计算。
带签名参数的 URL 计算工具
Sugar BI 提供了单独的签名 URL 计算工具,您可以访问「签名 URL 计算工具」来生成带签名参数的 URL 示例。