以下账号跟域名脱敏处理。curl不通正常。
需求
在后台嵌入grafana的监控链接。假设我们后台域名是houtai.yanshinian.com ,grafana是 grafana.ysn.com。
三种方式
匿名登录、api key 免登陆、模拟登录种cookie,其中匿名登录不安全,那么使用api key免登陆但是在proxy header头中,强制加了Authorization 头之后,似乎又成了另一种匿名登录了。那么模拟登录种cookie似乎显的更安全了。
匿名登录
网上就属这个资料最多了。简单来说,配置下面的参数。启动服务。由于我用的是k8s部署,配置文件在挂载了一块存储卷,重启不会丢失,所以就直接重启pod了。
[auth.anonymous]
# enable anonymous access
enabled = true
org_name = 你想要的匿名登录的组织名,后台同样需要创建一个对应的组织
api key免登陆
1.首先要先创建一个api key,下面是我展示的实例
curl -X POST -H "Content-Type: application/json" -d '{"name":"test", "role": "Admin"}' 'http://admin:admin@grafana.ysn.com/grafana/api/auth/keys'
{"id":1,"name":"test","key":"eyJrIjoiaTM4dEdrSTEwVDY0bzM2N0JYNmpidWVhd2F1ECMgciLCJuIjoidGVzdCIsImlkIjoxfQ=="}
具体的文档在官网可以看到。这里要注意的是。创建前要先选组织防止创建到了其他组织下。然后role选择 viewer,因为是匿名免登陆,当然不能给写权限。
当我们创建了一个apikey ,把这 apikey 添加到网关,比如下面的示例配置。为什么用域名grafana-api-key.ysn.com 而不是 grafana.ysn.com 呢?因为你写死了auth头,就相当于限定了一个用户了。所以需要单独用一个域名。
server {
listen 80;
listen 443 ssl;
server_name grafana-api-key.ysn.com;
access_log /home/nginx/logs/grafana-api-key.ysn.com_access.log main;
include /home/openresty/nginx/conf/nconf/ysnssl.conf;
location / {
set $upstream grafana.ysn.com; # 这里是复用下upstream
proxy_pass http://$upstream;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Connection "";
proxy_set_header Authorization "Bearer eyJrIjoiaTM4dEdrSTEwVDYVCzM2N0JYNmpidWVhd2F1MW92cEMiLCJuIjoidGVzdCIsImlkIjoxfQ=="
proxy_redirect off;
client_max_body_size 500m;
client_body_buffer_size 128k;
proxy_ignore_client_abort on;
proxy_connect_timeout 60;
proxy_send_timeout 60;
proxy_read_timeout 60;
proxy_buffer_size 128k;
proxy_buffers 32 32k;
proxy_busy_buffers_size 128k;
proxy_temp_file_write_size 128k;
proxy_next_upstream off;
proxy_http_version 1.1;
add_header Xes-App $upstream_http_server;
}
}
重点是这行 proxy_set_header Authorization "Bearer eyJrIjoiaTM4dEdrSTEwVDYVCzM2N0JYNmpidWVhd2F1MW92cEMiLCJuIjoidGVzdCIsImlkIjoxfQ==";
种Cookie方式
首先先找到login 接口,我不知道官方文档有没有指明,我是这么找的。
上面是故意写错了账号,为了截图方便,下面复制一个成功登录的curl请求(Chrome可以复制出来curl)
curl 'https://grafana.ysn.com/grafana/login' \
-H 'authority: grafana.ysn.com' \
-H 'accept: application/json, text/plain, */*' \
-H 'accept-language: zh-CN,zh;q=0.9' \
-H 'cache-control: no-cache' \
-H 'content-type: application/json' \
-H 'origin: https://grafana.yanshinian.com' \
-H 'pragma: no-cache' \
-H 'referer: https://grafana.ysn.comm/grafana/login' \
-H 'sec-ch-ua: "Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104"' \
-H 'sec-ch-ua-mobile: ?0' \
-H 'sec-ch-ua-platform: "macOS"' \
-H 'sec-fetch-dest: empty' \
-H 'sec-fetch-mode: cors' \
-H 'sec-fetch-site: same-origin' \
-H 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36' \
--data-raw '{"user":"3333","password":"3333"}' \
--compressed
登录成功后的接口我们可以看到cookie信息 grafana_session=1bc3f73468da3633f2c18685094810ce; Path=/grafana; Max-Age=2592000; HttpOnly; SameSite=Lax。当然 curl 加上 -vvv 执行下也可以在命令行看到。
这里要注意下,种cookie 就必须是同域名或者同属一个根域名。所以需要把grafana的地址。再解析一个域名。于是我又弄了一个域名叫做 grafana.yanshinian.com 要跟 houtai.yanshinian.com 保证是一个根域名。
php代码示例(hyperf框架)
模拟登录
public function login()
{
if (empty(env('GRAFANA_HOST'))) {
return [];
}
$key = 'ysn:grafana_session_config';
$redis = ApplicationContext::getContainer()->get(Redis::class);
$grafanaSessionConfigResult = $redis->get($key);
if (!empty($grafanaSessionConfigResult)) {
return json_decode($grafanaSessionConfigResult, true);
}
$loginApi = 'http://'.env('GRAFANA_HOST').'/grafana/login';
$client = new Client([
'handler' => HandlerStack::create(new CoroutineHandler()),
'timeout' => 3,
'swoole' => [
'timeout' => 3,
'socket_buffer_size' => 1024 * 1024 * 2,
],
]);
$headers = [
'content-type' => 'application/json',
];
$user = env('GRAFANA_USER');
$password = env('GRAFANA_PASSWORD');
$response = $client->post($loginApi, [
'headers' => $headers,
'body' => json_encode([
'user' => $user,
'password' => $password,
]),
]);
if (200 === $response->getStatusCode()) {
$cookies = $response->getHeader('set-cookie');
$grafanaSessionConfig = [];
foreach ($cookies as $cookie) {
if (0 === strpos($cookie, 'grafana_session=')) {
$cookieSegments = explode(';', $cookie);
foreach ($cookieSegments as $segment) {
$segment = trim($segment);
$kv = explode('=', $segment);
if ('grafana_session' === $kv[0]) {
$grafanaSessionConfig['name'] = 'grafana_session';
$grafanaSessionConfig['value'] = $kv[1];
} elseif ('Max-Age' === $kv[0]) {
$grafanaSessionConfig['ttl'] = $kv[1];
} elseif ('Path' === $kv[0]) {
$grafanaSessionConfig['path'] = $kv[1];
}
}
break;
}
}
$redis->set($key, json_encode($grafanaSessionConfig), 10);
return $grafanaSessionConfig;
}
return [];
}
下发给浏览器
$cookie = new Cookie(
$grafanaCookieConfig['name'],
$grafanaCookieConfig['value'],
time() + $grafanaCookieConfig['ttl'],
$grafanaCookieConfig['path'],
'.yanshinian.com',
);
$response = Context::get(ResponseInterface::class);
$response = $response->withCookie($cookie);
Context::set(ResponseInterface::class, $response);
上面代码注意下cookie的作用范围是.yanshinian.com 而不是 grafana.yanshinian.com那是因为不是同一个域名,域名范围设置成 grafana.yanshinian.com就种不成功。
进一步提升体验
我们发下嵌入的页面有一个按钮是可以点的。
当我点了它之后会蹦出来
当我摁了ESC又会出来,侧边栏。
那么我们怎么去掉这个按钮呢?我们能想到的是
修改grafana的html页面,理论上当然可行,比如是不是可以用sed 命令匹配替换成成空字符串?
使用JavaScript代码操作iframe中的grafana页面,理论上也可行,但是会有跨域问题。
于是选择了用JavaScript操作iframe的方式。既然会有跨域问题。也就是houtai.yanshinian.com,无法操作grafana.yanshinian.com 的页面。那么我把grafana域名搞成houtai.yanshinian.com/grafana不就可以了?于是嵌入的页面的链接变成了houtai.yanshinian.com/grafana/xxxxxxxx。然后上面cookie的作用域改成
'houtai.yanshinian.com',,这样做是缩小范围,避免线上跟测试环境cookie冲突。因为houtai-test.yanshinian.com/grafana 跟 houtai.yanshinian.com/grafana 都是.yanshinian.com cookie会冲突。
下面是删除Cycle view mode按钮代码。
var iframe = document.getElementById('iframe');
var iwindow = iframe.contentWindow;
console.log(iwindow);
var idoc = iwindow.document;
console.log(idoc);
var btns = idoc.getElementsByClassName('toolbar-button');
for (var i = 0; i < btns.length; i++) {
var btn = btns[i];
var label = btn.getAttributeNode('aria-label');
console.log(label.value);
if (label.value === 'Cycle view mode') {
console.log(btn);
console.log('3333333')
btn.remove();
}
}
但是,我想在iframe加载后删除。而且,我由于我用表单给iframe链接设置参数,就会经常加载链接,那么使用onload事件?onload 函数中执行 btn.getAttributeNode('aria-label') 是有个问题的。浏览器控制台可以打印出btns内容。但是实际上btns.length为0。这是因为页面没有完全渲染,所以就无法拿到dom。那么怎么办?目前只有一个出路了。那就是用定时器,也只能这样了。
另外还想禁止用户点击下面的标题。
下面是代码。
function panelHeaderClickDefault (callback = function () {}) {
var iframe = document.getElementById('iframe')
var iwindow = iframe.contentWindow
var idoc = iwindow.document
var divs = idoc.getElementsByClassName('panel-header')
if (divs.length > 0) {
for (var i = 0; i < divs.length; i++) {
divs[i].onclick = function (event) { // 通过这样的绑定覆盖之前的点击事件,并阻止冒泡
event.stopPropagation()
}
}
callback()
}
}
// 清除 按钮
function clearCycleViewModeBtn (callBack = function () {}) {
var iframe = document.getElementById('iframe')
var iwindow = iframe.contentWindow
var idoc = iwindow.document
var btns = idoc.getElementsByClassName('toolbar-button')
for (var i = 0; i < btns.length; i++) {
var btn = btns[i]
var label = btn.getAttributeNode('aria-label')
if (label.value === 'Cycle view mode') {
btn.remove()
// 如果删除成功 执行回调,比如清除定时器
callBack()
}
}
}
document.getElementById('iframe').onload = function () {
clearCycleViewModeBtn()
var timer = null
timer = setInterval(clearCycleViewModeBtn, 1000, function () {
clearInterval(timer)
})
panelHeaderClickDefault()
var timer2 = null
timer2 = setInterval(panelHeaderClickDefault, 1000, function () {
clearInterval(timer2)
})
}
禁止ESC,下面代码摘自网上。
document.onkeydown = killesc;
function killesc()
{
if (window.event.keyCode == 27) {
console.log('esc 禁用');
window.event.keyCode = 0;
window.event.returnValue = false;
}
可能你会说,查看元素拿到iframe的链接,不就又可以各种操作了?是的,那样就没办法了。
实际上我并没有阻止esc键(尝试了好个博客的代码),不知道是代码问题还是?如果你知道怎么禁止的话。可以说下。
但是有个思路是。还是捕获onkeydown事件,通过隐藏导航条(也就是拿到dom,display none)的方式实现同样的目的,这样做就是费点点代码。
参考资料
- 《通过API Key免登录访问Grafana》blog.csdn.net/qq_16240085…