通过API Key鉴权方式生成免登录的可分享的大盘链接。
背景信息
-
原生的Grafana若需要直接访问大盘,要么使用Snapshot功能,要么开启匿名模式。前者对访问的数据做了镜像,随着时间变化无法查看后续更新的数据,后者若不配合IP白名单功能,则安全性较差。可观测可视化 Grafana 版支持通过API Key鉴权方式生成免登录的可分享的大盘链接。
-
您可以将其分享给其他用户。
-
使用这个链接的用户将能够访问指定的大盘而无需登录,因为鉴权是通过API Key完成的。
-
-
Grafana 10.0.x版本中,API Key功能已被Service Account Token取代,但API Key仍可使用。Grafana 11.x版本中,API Key已被完全移除,API Keys管理页面返回404,只能使用Service Account Token进行鉴权。更多信息,请参见Service accounts。
步骤一:配置Grafana参数
登录ARMS控制台,在左侧导航栏选择。
在工作区管理页面,单击目标工作区ID。
-
在左侧导航栏单击参数设置。
-
在左侧参数列表选择aliyun,然后单击修改参数。
-
修改api_key_share参数的运行参数为true,然后单击保存并生效。
在左侧导航栏选择 aliyun 配置分类,在右侧参数列表中找到 api_key_share 参数行。
-
可选:若期望链接用于iFrame内嵌,则需要额外调整以下参数。
-
跨域情况下
域名需为HTTPS,并修改以下3个security参数。
allow_embedding=true cookie_samesite=none cookie_secure=true -
未跨域情况下
修改security下的allow_embedding参数为true,开启iFrame内嵌即可。
-
如果iFrame嵌入到另外一个Grafana中,请在对应Grafana中调整panels参数。
enable_alpha = true disable_sanitize_html = true
-
步骤二:创建API Key
Grafana 9.0.x 和 10.0.x 创建 API Key 的步骤有所不同,请确认您的 Grafana 版本并选择对应操作。
Grafana 11.x版本已完全移除API Key功能,无需执行Grafana 9.0.x的API Key创建步骤,直接按照Grafana 10.0.x版本的Service Account Token流程操作即可。
如果您从 9.0.x 升级到 10.0.x,在Administration > api keys页面仍可看到Grafana 9.0.x中创建的API Key。您可以单击migrate to service account将其迁移到Service Account,迁移后api keys页面将被隐藏。原有API Key的使用不受影响。
Grafana 9.0.x版本
登录ARMS控制台,在左侧导航栏选择。
在工作区管理页面,单击目标工作区右侧的访问地址URL链接进入Grafana。
说明如果需要登录Grafana,可以使用Grafana的Admin账号和创建工作区时设置的密码登录Grafana,或单击Sign in with Alibaba Cloud直接使用当前购买工作区的阿里云账号登录Grafana。
-
单击Grafana首页左上角的
图标。 -
在Grafana左侧导航栏选择Configuration > API keys。
说明进入该菜单需要Admin权限。
-
单击New API key或Add API key,然后设置以下参数。
参数
说明
Key name
API Key的名称,不可以和已有的名称重复。
Role
设置为Viewer。
Time to live
设置有效时间,例如60s(60秒)、10m(10分钟)、1d(1天)。
-
单击Add,在弹出的对话框中获取并保存API Key的值。
重要对话框关闭后将无法再次查看API Key的值。
在 API Key Created 弹窗中,Key 字段右侧提供 Copy 按钮,请立即复制 API Key。弹窗下方展示了使用
AuthorizationHTTP header 认证请求的 curl 命令示例:curl -H "Authorization: Bearer eyJrIjoi..." https://grafana-xxx.grafana.aliyuncs.com/api/dashboards/home。
Grafana 10.0.x版本
登录ARMS控制台,在左侧导航栏选择。
在工作区管理页面,单击目标工作区右侧的访问地址URL链接进入Grafana。
说明如果需要登录Grafana,可以使用Grafana的Admin账号和创建工作区时设置的密码登录Grafana,或单击Sign in with Alibaba Cloud直接使用当前购买工作区的阿里云账号登录Grafana。
-
单击Grafana首页左上角的
图标。 -
在Grafana左侧导航栏选择。
重要-
进入该菜单需要Admin权限。
-
服务账号会占用一个用户账号。
-
-
单击Add service account,然后设置以下参数,然后单击Create。
参数
说明
Display name
Service Account的名称,不可以和已有的名称重复。
Role
设置为Viewer。
-
单击页面右侧的Add service account token,然后设置以下参数。
参数
说明
Display name
API Key的名称,不可以和已有的名称重复。
Expiration
设置有效时间,
-
No Expiration:无终止日期
-
Set Expiration date:设置有效期
Expiration date
如果在Expiration中选择Set Expiration date,则您需要设置有效期的截止日。
-
-
单击Generate token,然后在弹出的对话框单击Copy to clipboard and close。
对话框关闭后将无法再次查看API Key的值。
步骤三:生成分享链接
Grafana 9.0.x版本
-
在Grafana页面,进入需要分享的大盘页面。
-
单击
图标,在Link页签获取大盘分享链接。 -
在链接最后添加
&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****** -
使用带有API Key的分享链接即可免登录访问Grafana大盘。
Grafana 10.0.x版本
-
在Grafana页面,进入需要分享的大盘页面。
-
单击
图标,在链接页签获取大盘分享链接。对话框中可配置锁定时间范围(将相对时间转换为绝对时间)、主题(当前、深色、浅色)和缩短网址等选项,在底部链接网址区域单击复制按钮获取链接。
-
在链接最后添加
&aliyun_api_key=<API Key值>,URL参数名aliyun_api_key保持不变,将值替换为上文步骤二中获取的Service Account Token即可,无需额外改动。https://grafana-example.grafana.aliyuncs.com/d/TZWea****/test?orgId=1&from=167081684****&to=167083844****&aliyun_api_key=eyJrIjoiWkIwNnN2c0RTSD****** -
使用带有Service Account Token的分享链接即可免登录访问Grafana大盘。
步骤四:生成高安全性分享链接(可选)
步骤三:生成分享链接中生成的分享链接需要定期更换API Key,以避免API Key泄露造成的数据安全风险。本步骤分别介绍在9.0.x版本API Key和10.0.x版本Service Account Token场景下生成高安全性分享链接的方法。
在进行本步骤前,您需要先修改api_key_share_version的运行参数为v2。
登录ARMS控制台,在左侧导航栏选择。
-
在工作区管理页面,单击目标工作区ID,然后在左侧导航栏单击参数设置。
-
修改api_key_share_version参数的运行参数为v2,然后单击保存并生效。
9.0.x版本API Key
-
将步骤二:创建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}
-
-
将上述解密的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**
-
-
基于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) }
-
-
基于生成的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
-
将步骤二:创建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****
-
-
基于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) }
-
-
基于生成的MD5签名信息拼接链接参数。
参数说明:
参数名
说明
示例
aliyun_api_key_sign
MD5签名,随时间变化。
例如:c3bf89b867cc88df72d507edc4d1****
aliyun_api_key_timestamp
签名时间,当签名时间和系统时间超过1分钟后,签名过期失效。
例如:1726823533
aliyun_api_key_name
Service Account Token的名称。
例如:sa-1-******-*****-75c7-41ae-94da-*******
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泄露问题。
常见问题
-
通过共享连接访问大盘时页面报错如下:访问内嵌大盘页面时,浏览器提示拒绝了我们的连接请求错误,无法正常加载页面。
可能原因:内嵌大盘情况下,allow_embedding参数未设置。配置allow_embedding参数的操作,请参见上文步骤一。
-
无法显示大盘数据。页面显示 Not found 提示,并弹出 Unauthorized 黄色警告通知,大盘主体区域无任何图表或数据。
内嵌大盘情况下,Cookie无法写入导致,可能原因如下:
-
跨域,即根域名不同时,默认配置无法写入Cookie。
-
cookie_samesite参数设置为none,但是cookie_secure参数设置为false。
-
域名为HTTP。由于cookie_secure参数无法在HTTP下生效,因此域名不支持HTTP。
解决方案:参考上文步骤一重新配置Grafana参数。
-
-
通过共享连接访问大盘时页面报错如下:Grafana 报错 If you're seeing this Grafana has failed to load its application files,页面列出反向代理配置、子路径部署(需检查
root_url或设置serve_from_sub_path = true)、本地开发构建、重启 grafana-server 及浏览器兼容性等排查建议。可能原因:
-
浏览器版本过低。
-
内嵌大盘的情况下浏览器配置导致。
解决方案:
-
查看Cookie使用的配置,允许使用Cookie。
-
若使用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,然后删除。不会自动删除。在 API keys 管理页面中,开启 Include expired keys 开关后,可查看已过期的 API Key 记录(包括名称、角色和过期时间)。如需移除,可单击对应记录行末的删除按钮手动删除。