分享页Token参数签名校验
所有文档

          百度数据可视化 Sugar

          分享页Token参数签名校验

          下面介绍在发布大屏和报表时,使用 Token 参数签名校验的方法。通过 Token 参数签名校验功能,您可以对大屏和报表交互时传递的参数进行签名鉴权,来防止访问者通过修改页面传递的参数来访问其他未授权的页面数据,从而提高数据以及用户信息的安全性。

          前提条件

          在使用 Token 参数签名校验前,请确保:

          • 大屏或报表使用 Token 验证的方式进行公开分享,具体请参见「公开和加密分享」。
          • 大屏以 Get 方式在 URL 中传递参数(直接在 URL 后面加参数)。
          • 大屏 URL 中传递的参数要求不能被篡改。

          背景信息

          例如:

          某用户的系统嵌入了 Sugar 大屏,URL 通过 Token 计算,通过 Get 方式传递用户的工号给大屏展示相对应的数据,可以使用 https://sugar.baidubce.com/dashboard/1827981ec07ac66f937a88c9e65f****?\_sugar\_time=1612959542484&\_sugar\_signature=H09EX3nMls%2FE6IzhZKr6U6LfNq2Xl%2FJK%2BxYWoPv65D****&userNo=101 来访问大屏。

          其中 userNo(工号)为大屏传递的参数,存在被篡改的可能。比如工号为 101 的员工将 URL 改成 https://sugar.baidubce.com/dashboard/1827981ec07ac66f937a88c9e65f****?\_sugar\_time=1612959542484&\_sugar\_signature=H09EX3nMls%2FE6IzhZKr6U6LfNq2Xl%2FJK%2BxYWoPv65D****&userNo=102 ,就可以看到工号为 102 的员工的数据。因此需要对用户传递的参数进行签名鉴权,保证计算得到的 URL 的参数不能被更改,如果私自更改了传参,页面将无法访问。

          签名参数规则

          • 需要加入签名的参数,其参数名需以 sugar_sign_ 开头,后面可以带任何有效的参数名字符
          • 不符合此签名参数规则的参数,将不会进行参数签名校验,允许修改参数值
          • 签名参数按升序排序

          使用流程

          1. 确定需要签名计算的参数名(即不允许被篡改的参数)
          2. 在大屏或报表开发完成后,使用 Token 验证的方式发布大屏或报表
          3. 使用带签名参数的 URL 计算生成访问大屏或报表的 URL
          4. 使用上一步中计算得到的 URL 访问大屏或报表,在访问过程中,系统会自动进行参数签名校验
          5. 如果参数签名校验功能正常,当访问者修改了签名参数,再次访问此 URL 时,访问会被拒绝

          带签名参数的 URL 计算

          示例代码如下:

          Node.js

          const crypto = require('crypto');
          const querystring = require('querystring');
          const signedQueryParamReg = /^sugar_sign_.*/; // 符合此正则表达式的参数是需要签名的。
          let token = 'OAMf7CvniOGgoNijH9mFHEHSAf7****';
          let shareID = '1827981ec07ac66f937a88c9e65f****';
          const time = Date.now();
          
          const customeParams = {
            sugar_sign_no: 191866,
            name: 101
          };
          let signParamsStr = Object.keys(customeParams)
            .filter(paramName => customeParams[paramName] && signedQueryParamReg.test(paramName))
            .sort()
            .map(param => `${param}=${customeParams[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(customeParams).forEach(paramName => {
            queryParams[paramName] = customeParams[paramName];
          });
          
          let url = `https://sugar.baidubce.com/dashboard/${shareID}?${querystring.stringify(queryParams)}`;
          console.log(url);

          PHP

          <?php
            $token = "OAMf7CvniOGgoNijH9mFHEHSAf7****";
            $shareID = "1827981ec07ac66f937a88c9e65f****";
            $time = time()*1000;
            $customeParams = array(
              'sugar_sign_no'=>'191866',
              'name'=>'101'
            );
            $sign_array = preg_grep("/^sugar_sign_.*/", array_keys($customeParams));
            sort($sign_array);
            function toPlain($v)
            {
              global $customeParams;
              return "$v=$customeParams[$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($customeParams)));
            $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> customeParams = new HashMap<>();
              customeParams.put("sugar_sign_no", 191866);
              customeParams.put("name", 101);
              String signParamsStr =  customeParams.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));
            }
          }

          .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");   // 不需要签名。
                // 分享页前缀,大屏分享id、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> customeParams)
              {
                string pattern = @"^sugar_sign_.*";
                string timestamp = GetTimeStamp();
          
                // 参数排序
                Dictionary<string, string>.KeyCollection keyCol = customeParams.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 + "=" + customeParams[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 customeParams)
                {
                  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;
              }
            }
          }

          使用以上代码示例得到的 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 提供了单独的签名 URL 计算工具,您可以访问「签名 URL 计算工具」来生成带签名参数的 URL 示例。

          上一篇
          公开和加密分享
          下一篇
          多页面轮播