Grafana基于API Key分享大盘

通过API Key鉴权方式生成免登录的可分享的大盘链接。

背景信息

原生的Grafana若需要直接访问大盘,要么使用Snapshot功能,要么开启匿名模式。前者对访问的数据做了镜像,随着时间变化无法查看后续更新的数据,后者若不配合IP白名单功能,则安全性较差。

可观测可视化 Grafana 版支持通过API Key鉴权方式生成免登录的可分享的大盘链接。

  • 您可以将其分享给其他用户。

  • 使用这个链接的用户将能够访问指定的大盘而无需登录,因为鉴权是通过API Key完成的。

步骤一:配置Grafana参数

  1. 登录可观测可视化 Grafana 版控制台,在左侧导航栏单击工作区管理

  2. 工作区管理页面,单击目标工作区ID。

  3. 在左侧导航栏单击参数设置

  4. 在左侧参数列表选择aliyun,然后单击修改参数

  5. 修改api_key_share参数的运行参数为true,然后单击保存并生效

    image

  6. 可选:若期望链接用于iFrame内嵌,则需要额外调整以下参数。

    • 跨域情况下

      域名需为HTTPS,并修改以下3个security参数。

      allow_embedding=true
      cookie_samesite=none
      cookie_secure=true

      跨域情况参数修改

    • 未跨域情况下

      修改security下的allow_embedding参数为true,开启iFrame内嵌即可。

步骤二:创建API Key

Grafana 9.0.x和Grafana 10.0.x版本创建API Key的步骤有所不同,请确认您的Grafana版本并选择对应的操作步骤。

如果您的Grafana是从9.0.x升级到10.0.x版本,那么在Administration > api keys页面依然可以看到Grafana 9.0.x中创建的API Key。此时,单击migrate to service account可以将原有的API Key迁移到Service Account,迁移完成后api keys页面将会被永久隐藏。此外,版本升级不会影响原有API Key的使用。

Grafana 9.0.x版本

  1. 登录可观测可视化 Grafana 版控制台,在左侧导航栏单击工作区管理

  2. 工作区管理页面,单击目标工作区右侧的访问地址URL链接进入Grafana。

    说明

    如果需要登录Grafana,可以使用Grafana的Admin账号和创建工作区时设置的密码登录Grafana,或单击Sign in with Alibaba Cloud直接使用当前购买工作区的阿里云账号登录Grafana。

  3. 单击Grafana首页左上角的image图标。

  4. 在Grafana左侧导航栏选择Configuration > API keys

    说明

    进入该菜单需要Admin权限。

  5. 单击New API keyAdd API key,然后设置以下参数。

    参数

    说明

    Key name

    API Key的名称,不可以和已有的名称重复。

    Role

    设置为Viewer。

    Time to live

    设置有效时间,例如60s(60秒)、10m(10分钟)、1d(1天)。

  6. 单击Add,在弹出的对话框中获取并保存API Key的值。

    重要

    对话框关闭后将无法再次查看API Key的值。

    API Key值

Grafana 10.0.x版本

  1. 登录可观测可视化 Grafana 版控制台,在左侧导航栏单击工作区管理

  2. 工作区管理页面,单击目标工作区右侧的访问地址URL链接进入Grafana。

    说明

    如果需要登录Grafana,可以使用Grafana的Admin账号和创建工作区时设置的密码登录Grafana,或单击Sign in with Alibaba Cloud直接使用当前购买工作区的阿里云账号登录Grafana。

  3. 单击Grafana首页左上角的image图标。

  4. 在Grafana左侧导航栏选择管理 > 服务账户

    重要
    • 进入该菜单需要Admin权限。

    • 服务账号会占用一个用户账号。

  5. 单击Add service account,然后设置以下参数,然后单击Create

    参数

    说明

    Display name

    Service Account的名称,不可以和已有的名称重复。

    Role

    设置为Viewer

  6. 单击页面右侧的Add service account token,然后设置以下参数。

    参数

    说明

    Display name

    API Key的名称,不可以和已有的名称重复。

    Expiration

    设置有效时间,

    • No Expiration:无终止日期

    • Set Expiration date:设置有效期

    Expiration date

    如果在Expiration中选择Set Expiration date,则您需要设置有效期的截止日。

  1. 单击Generate token,然后在弹出的对话框单击Copy to clipboard and close

重要

对话框关闭后将无法再次查看API Key的值。

步骤三:生成分享链接

Grafana 9.0.x版本

  1. 在Grafana页面,进入需要分享的大盘页面。

  2. 单击分享图标图标,在Link页签获取大盘分享链接。

    Grafana大盘分享链接

  3. 在链接最后添加&aliyun_api_key=<API Key值>,API Key值为上文步骤二中获取的API Key。

    https://grafana-example.grafana.aliyuncs.com/d/TZWea****/test?orgId=1&from=167081684****&to=167083844****&aliyun_api_key=eyJr****WkIwNnN2c0RTSD******
  4. 使用带有API Key的分享链接即可免登录访问Grafana大盘。

Grafana 10.0.x版本

  1. 在Grafana页面,进入需要分享的大盘页面。

  2. 单击分享图标图标,在链接页签获取大盘分享链接。

    image

  3. 在链接最后添加&aliyun_api_key=<API Key值>,API Key值为上文步骤二中获取的API Key。

    https://grafana-example.grafana.aliyuncs.com/d/TZWea****/test?orgId=1&from=167081684****&to=167083844****&aliyun_api_key=eyJrIjoiWkIwNnN2c0RTSD******
  4. 使用带有API Key的分享链接即可免登录访问Grafana大盘。

步骤四:生成高安全性分享链接(可选)

步骤三:生成分享链接中生成的分享链接需要定期更换API Key,以避免API Key泄露造成的数据安全风险。本步骤分别介绍在9.0.x版本API Key和10.0.x版本Service Account Token场景下生成高安全性分享链接的方法。10.0.x版本后API Key称之为Service Account Token。

在进行本步骤前,您需要先修改api_key_share_version的运行参数为v2

  1. 登录可观测可视化 Grafana 版控制台,在左侧导航栏单击工作区管理

  2. 工作区管理页面,单击目标工作区ID,然后在左侧导航栏单击参数设置

  3. 修改api_key_share_version参数的运行参数为v2,然后单击保存并生效14.jpg

9.0.x版本API Key

  1. 步骤二:创建API Key中获取的API Key进行Base64解密。

    Base64是网络上常见的用于传输8Bit字节码的编码方式之一,Base64是一种基于64个可打印字符来表示二进制数据的方法。

    • 搜索常用工具网站进行解密,例如base64

    • Java语言解密。

      package main
      
      import java.util.Base64;
      
      public class Base64Example{
          public static void main(String[] args) {
              String apiKey = "eyJr****REpzZGYzd2JIa0N3ekgyWjlWWmhrSTM5bWdGT2hGSmwiLCJuIjoidGVzdDEiLCJpZCI6MX0=";
              String decodeKey = new String(Base64.getDecoder().decode(apiKey));
              System.out.println(decodeKey);
          }
      }

      运行结果:

      {"k":"DJsd****HkCwzH2Z9VZhkI39mgFOhFJl","n":"test1","id":1}
    • Go语言解密。

      package main
      
      import "fmt"
      import "encoding/base64"
      
      func main() {
      	apiKey := "eyJr****REpzZGYzd2JIa0N3ekgyWjlWWmhrSTM5bWdGT2hGSmwiLCJuIjoidGVzdDEiLCJpZCI6MX0="
      	decodeKey, err := base64.StdEncoding.DecodeString(apiKey)
      	if err != nil {
      		fmt.Println(err.Error())
      		return
      	}
      	fmt.Println(string(decodeKey))
      }

      运行结果:

      {"k":"DJsd****HkCwzH2Z9VZhkI39mgFOhFJl","n":"test1","id":1}
  2. 将上述解密的API Key值中的k进行PBKDF2加密。

    PBKDF2(Password-Based Key Derivation Function 2)是一种基于密码的密钥导出函数,用于从用户提供的密码和一些其他参数(如盐值和迭代次数)安全地派生出加密密钥。它主要应用于需要存储用户密码的场景,旨在确保即使数据库被泄露,攻击者也难以直接获取到用户的明文密码或轻易破解派生出的密钥。

    加密有多种方式,以下是加密参数说明。

    参数

    说明

    盐salt

    设置为API Key的名称Name(即上面解密出来的n值) ,本示例中为test1。

    迭代次数iteration

    设置为:10000

    输出字节长度output len

    设置为:50

    加密密钥长度key size

    设置为:256

    输出类型

    设置为:Hex

    • 搜索常用网站工具加密,例如df2

    • Java语言加密。

      package main
      
      import javax.crypto.SecretKeyFactory;
      import java.security.GeneralSecurityException;
      import javax.crypto.spec.PBEKeySpec;
      import java.security.spec.KeySpec;
      
      public class PBKDFExapmle {
          public static void main(String[] args) {
              String password = "DJsd****HkCwzH2Z9VZhkI39mgFOhFJl";
              String salt = "test1";
              int iterationCount = 10000;
              int outputLength = 50 * 8;
      
              try {
                  KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), iterationCount, outputLength);
                  SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
                  byte[] keyBytes = skf.generateSecret(spec).getEncoded();
                  System.out.println(bytesToHex(keyBytes));
              } catch (GeneralSecurityException e) {
                  e.printStackTrace();
              }
          }
      
          private static String bytesToHex(byte[] bytes) {
              StringBuilder hexString = new StringBuilder();
              for (byte b : bytes) {
                  String hex = Integer.toHexString(0xff & b);
                  if (hex.length() == 1) {
                      hexString.append('0');
                  }
                  hexString.append(hex);
              }
              return hexString.toString();
          }
      }
      

      输出:PBKDF2 Password

      1e5b****80184e78832544aae4d2e031a3539c10b575b75d7c1d44af49fcf5a7de9c58a5f0035ce35fff0e5b0476e882****
    • Go语言加密。

      package main
      
      import "fmt"
      import "encoding/hex"
      import "crypto/sha256"
      import "golang.org/x/crypto/pbkdf2"
      
      func main() {
          password:="DJsd****HkCwzH2Z9VZhkI39mgFOhFJl"
          salt="test1"
          newPasswd := pbkdf2.Key([]byte(password), []byte(salt), 10000, 50, sha256.New)
      	encodePassword:= hex.EncodeToString(newPasswd)
      	fmt.Println(encodePassword)
      }

      输出:PBKDF2 Password

      1e5b****80184e78832544aae4d2e031a3539c10b575b75d7c1d44af49fcf5a7de9c58a5f0035ce35fff0e5b0476e882****
  3. 基于PBKDF2加密后的信息构造MD5摘要签名参数。

    MD5信息摘要算法(MD5 Message-Digest Algorithm),是一种被广泛使用的密码散列函数,可以产出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。

    加密内容为:<PBKDF2 Password>+"_"+<当前系统时间整数,单位:秒>

    例如:

    PBKDF2 Password为:1e5b****80184e78832544aae4d2e031a3539c10b575b75d7c1d44af49fcf5a7de9c58a5f0035ce35fff0e5b0476e882****

    当前系统时间为:2024-09-20 17:12:13 ,则精确到秒的时间整数为:1726823533

    所以需要MD5签名的整体信息为:1e5b****80184e78832544aae4d2e031a3539c10b575b75d7c1d44af49fcf5a7de9c58a5f0035ce35fff0e5b0476e882****_1726823533

    MD5签名有多种方式:

    • 搜索常用网站工具加密,例如MD5

    • Java代码进行MD5签名。

      package main;
      
      import java.security.MessageDigest;
      
      
      public class MD5 {
      
          public static void main(String[] args) {
              String pbkdfPassword = "1e5b****80184e78832544aae4d2e031a3539c10b575b75d7c1d44af49fcf5a7de9c58a5f0035ce35fff0e5b0476e882****";
              long timeSeconds=System.currentTimeMillis()/1000;
              String key=pbkdfPassword+"_"+timeSeconds;
              System.out.println(MD5.getMD5String(key,"UTF-8"));
          }
      
          
          public static String getMD5String(String str, String charset) {
              try {
                  MessageDigest messageDigest = MessageDigest.getInstance("MD5");
                  messageDigest.reset();
                  messageDigest.update(str.getBytes(charset));
                  byte[] byteArray = messageDigest.digest();
      
                  StringBuffer md5StrBuff = new StringBuffer();
      
                  for (int i = 0; i < byteArray.length; i++) {
                      if (Integer.toHexString(0xFF & byteArray[i]).length() == 1) {
                          md5StrBuff.append("0").append(
                              Integer.toHexString(0xFF & byteArray[i]));
                      } else {
                          md5StrBuff.append(Integer.toHexString(0xFF & byteArray[i]));
                      }
                  }
                  return md5StrBuff.toString().toLowerCase();
              } catch (Exception e) {
                  e.printStackTrace();
                  throw new RuntimeException("MD5 error:"+e.getMessage());
              }
          }
      }
      
    • Go代码进行MD5签名。

      package main
       
      import (
          "crypto/md5"
          "encoding/hex"
          "fmt"
          "io"
          "time"
          "strconv"
      )
       
      func main() {
          // 需要计算MD5的字符串
          pbkdfPassword := "1e5b****80184e78832544aae4d2e031a3539c10b575b75d7c1d44af49fcf5a7de9c58a5f0035ce35fff0e5b0476e882****"
          timeSeconds:= time.Now().Unix()
          key:=pbkdfPassword+"_"+strconv.FormatInt(timeSeconds, 10)
       
          // 使用MD5包计算字符串的MD5值
          hash := md5.New()
          io.WriteString(hash, key)
          md5Str := hash.Sum(nil)
       
          // 将二进制的MD5值转换成十六进制字符串
          md5StrHex := hex.EncodeToString(md5Str)
       
          fmt.Println("MD5 of", key, "is", md5StrHex)
      }
  4. 基于生成的MD5签名信息拼接链接参数。

    参数说明:

    参数名

    说明

    示例

    aliyun_api_key_sign

    MD5签名,随时间变化。

    例如:c3bf89b867cc88df72d507edc4d1****

    aliyun_api_key_timestamp

    签名时间,当签名时间和系统时间超过1分钟后,签名过期失效。

    例如:1726823533

    aliyun_api_key_name

    API Key的名称。

    例如:test1

    aliyun_api_key_org_id

    API Key所在的组织Org ID。

    例如:1

    aliyun_api_key_expire_seconds

    MD5签名登录后失效时间,单位:秒。

    默认:3600

    示例如下:

    https://grafana-example.grafana.aliyuncs.com/d/TZWea****/test?orgId=1&from=167081684****&to=167083844****&aliyun_api_key_sign=c3bf89b867cc88df72d507edc4d1****&aliyun_api_key_timestamp=1726823533&aliyun_api_key_name=test1&aliyun_api_key_org_id=1

    至此您可以基于程序代码每次动态生成更安全的免密登录Grafana的链接,同时可以避免API Key泄露问题。

10.0.x版本Service Account Token

  1. 步骤二:创建API Key中的API Key(10.0.x版本后称之为Service Account Token)切分后进行PBKDF2加密。

    PBKDF2(Password-Based Key Derivation Function 2)是一种基于密码的密钥导出函数,用于从用户提供的密码和一些其他参数(如盐值和迭代次数)安全地派生出加密密钥。它主要应用于需要存储用户密码的场景,旨在确保即使数据库被泄露,攻击者也难以直接获取到用户的明文密码或轻易破解派生出的密钥。

    将Service Account Token按“_”进行切分。

    #以下列Service Account Token为例:
    Token:glsa_yV9HAOVCjNKkvKoLMiypOc5T0Oov****_4f5ff3ce
    #切分后为:
    前缀:glsa
    Secret:yV9HAOVCjNKkvKoLMiypOc5T0Oov****
    Salt:4f5ff3ce

    加密有多种方式,以下是加密参数说明。

    参数

    说明

    盐salt

    设置为Token后缀,本示例中为4f5ff3ce。

    迭代次数iteration

    设置为:10000

    输出字节长度output len

    设置为:50

    加密密钥长度key size

    设置为:256

    输出类型

    设置为:Hex

    • 搜索常用工具网站进行解密,例如charsetpbkdf2

    • Java语言解密。

      package main
      
      import javax.crypto.SecretKeyFactory;
      import java.security.GeneralSecurityException;
      import javax.crypto.spec.PBEKeySpec;
      import java.security.spec.KeySpec;
      
      public class PBKDFExapmle {
          public static void main(String[] args) {
              String password = "yV9H****jNKkvKoLMiypOc5T0OovHXPV";
              String salt = "4f5ff3ce";
              int iterationCount = 10000;
              int outputLength = 50 * 8;
      
              try {
                  KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), iterationCount, outputLength);
                  SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
                  byte[] keyBytes = skf.generateSecret(spec).getEncoded();
                  System.out.println(bytesToHex(keyBytes));
              } catch (GeneralSecurityException e) {
                  e.printStackTrace();
              }
          }
      
          private static String bytesToHex(byte[] bytes) {
              StringBuilder hexString = new StringBuilder();
              for (byte b : bytes) {
                  String hex = Integer.toHexString(0xff & b);
                  if (hex.length() == 1) {
                      hexString.append('0');
                  }
                  hexString.append(hex);
              }
              return hexString.toString();
          }
      }
      

      输出:PBKDF2 Password

      c3cd****971bab928e4ecd6e7a00c74657696ea07d38c43f3bb5dc3190f2285cb80695cf7bf2f25c9b1f34fe1e0f9549****
    • Go语言加密。

      package main
      
      import "fmt"
      import "encoding/hex"
      import "crypto/sha256"
      import "golang.org/x/crypto/pbkdf2"
      
      func main() {
          password:="yV9H****jNKkvKoLMiypOc5T0OovHXPV"
          salt="4f5ff3ce"
          newPasswd := pbkdf2.Key([]byte(password), []byte(salt), 10000, 50, sha256.New)
      	encodePassword:= hex.EncodeToString(newPasswd)
      	fmt.Println(encodePassword)
      }

      输出:PBKDF2 Password

      c3cd****971bab928e4ecd6e7a00c74657696ea07d38c43f3bb5dc3190f2285cb80695cf7bf2f25c9b1f34fe1e0f9549****
  2. 基于PBKDF2加密后的信息构造MD5摘要签名参数。

    MD5信息摘要算法(MD5 Message-Digest Algorithm),是一种被广泛使用的密码散列函数,可以产出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。

    加密内容为:<PBKDF2 Password>+"_"+<当前系统时间整数,单位:秒>

    例如:

    PBKDF2 Password为:1e5b****80184e78832544aae4d2e031a3539c10b575b75d7c1d44af49fcf5a7de9c58a5f0035ce35fff0e5b0476e882****

    当前系统时间为2024-09-20 17:12:13 ,则精确到秒的时间整数为:1726823533

    所以需要MD5签名的整体信息为:1e5b****80184e78832544aae4d2e031a3539c10b575b75d7c1d44af49fcf5a7de9c58a5f0035ce35fff0e5b0476e882****_1726823533

    MD5签名有多种方式:

    • 搜索常用网站工具加密,例如MD5

    • Java代码进行MD5签名。

      package main;
      
      import java.security.MessageDigest;
      
      
      public class MD5 {
      
          public static void main(String[] args) {
              String pbkdfPassword = "1e5b****80184e78832544aae4d2e031a3539c10b575b75d7c1d44af49fcf5a7de9c58a5f0035ce35fff0e5b0476e882****";
              long timeSeconds=System.currentTimeMillis()/1000;
              String key=pbkdfPassword+"_"+timeSeconds;
              System.out.println(MD5.getMD5String(key,"UTF-8"));
          }
      
          
          public static String getMD5String(String str, String charset) {
              try {
                  MessageDigest messageDigest = MessageDigest.getInstance("MD5");
                  messageDigest.reset();
                  messageDigest.update(str.getBytes(charset));
                  byte[] byteArray = messageDigest.digest();
      
                  StringBuffer md5StrBuff = new StringBuffer();
      
                  for (int i = 0; i < byteArray.length; i++) {
                      if (Integer.toHexString(0xFF & byteArray[i]).length() == 1) {
                          md5StrBuff.append("0").append(
                              Integer.toHexString(0xFF & byteArray[i]));
                      } else {
                          md5StrBuff.append(Integer.toHexString(0xFF & byteArray[i]));
                      }
                  }
                  return md5StrBuff.toString().toLowerCase();
              } catch (Exception e) {
                  e.printStackTrace();
                  throw new RuntimeException("MD5 error:"+e.getMessage());
              }
          }
      }
      
    • Go代码进行MD5签名。

      package main
       
      import (
          "crypto/md5"
          "encoding/hex"
          "fmt"
          "io"
          "time"
          "strconv"
      )
       
      func main() {
          // 需要计算MD5的字符串
          pbkdfPassword := "1e5b****80184e78832544aae4d2e031a3539c10b575b75d7c1d44af49fcf5a7de9c58a5f0035ce35fff0e5b0476e882****"
          timeSeconds:= time.Now().Unix()
          key:=pbkdfPassword+"_"+strconv.FormatInt(timeSeconds, 10)
       
          // 使用MD5包计算字符串的MD5值
          hash := md5.New()
          io.WriteString(hash, key)
          md5Str := hash.Sum(nil)
       
          // 将二进制的MD5值转换成十六进制字符串
          md5StrHex := hex.EncodeToString(md5Str)
       
          fmt.Println("MD5 of", key, "is", md5StrHex)
      }
  3. 基于生成的MD5签名信息拼接链接参数。

    参数说明:

    参数名

    说明

    示例

    aliyun_api_key_sign

    MD5签名,随时间变化。

    例如:c3bf89b867cc88df72d507edc4d1****

    aliyun_api_key_timestamp

    签名时间,当签名时间和系统时间超过1分钟后,签名过期失效。

    例如:1726823533

    aliyun_api_key_name

    Service Account Token的名称。

    例如:test1

    aliyun_api_key_org_id

    Service Account Token所在的组织Org ID。

    例如:1

    aliyun_api_key_expire_seconds

    MD5签名登录后失效时间,单位:秒。

    默认:3600

    示例如下:

    https://grafana-example.grafana.aliyuncs.com/d/TZWea****/test?orgId=1&from=167081684****&to=167083844****&aliyun_api_key_sign=c3bf89b867cc88df72d507edc4d1****&aliyun_api_key_timestamp=1726823533&aliyun_api_key_name=test1&aliyun_api_key_org_id=1

    至此您可以基于程序代码每次动态生成更安全的免密登录Grafana的链接,同时可以避免Service Account Token泄露问题。

常见问题

  • 通过共享连接访问大盘时页面报错如下:访问大盘报错1

    可能原因:内嵌大盘情况下,allow_embedding参数未设置。配置allow_embedding参数的操作,请参见上文步骤一

  • 无法显示大盘数据。无法显示大盘内容

    内嵌大盘情况下,Cookie无法写入导致,可能原因如下:

    • 跨域,即根域名不同时,默认配置无法写入Cookie。

    • cookie_samesite参数设置为none,但是cookie_secure参数设置为false

    • 域名为HTTP。由于cookie_secure参数无法在HTTP下生效,因此域名不支持HTTP。

    解决方案:参考上文步骤一重新配置Grafana参数。

  • 通过共享连接访问大盘时页面报错如下:页面报错2

    可能原因:

    • 浏览器版本过低。

    • 内嵌大盘的情况下浏览器配置导致。

      解决方案:

      1. 查看Cookie使用的配置,允许使用Cookie。

      2. 若使用Chrome浏览器,在无痕模式下需要配置允许所有Cookie。

  • 如果用于大盘内嵌,API Key建议是设置较短的有效时间使用一次就更换,还是配置一个很长时间的免登Key?

    您可以根据安全需要做配置,建议3个月换一次,若Key泄露可以通过删除让Key失效。

  • API Key是否有数量上限?

    Key的创建官方源码里并没有做限制,由于页面查询时最多展示100条,建议不要超过100个。

  • API Key配置的有效时间到期了,这个API Key会自动删除吗?

    Key失效后,数据仍然存在,如果担心占用数量您可以手动删除Key。Grafana页面上失效的Key默认不展示,您可以单击Include expired keys显示已失效Key,然后删除。显示失效key