自主系统通过iframe 嵌入 grafana 仪表板(dashboard)

使用iframe方式嵌入dashboard主要会遇到以下几个问题:

1、iframe方式嵌入dashboard跳转拒绝访问;

2、浏览器控制台报错:Refused to display 'http://{ip:port}/' in a frame because it set 'X-Frame-Options' to 'deny';

3、跳转之后依然需要登录;

......

以上有些问题,可以通过修改配置文件相关配置来解决(修改配置文件之后,记得重启grafana服务)

# 允许浏览器嵌入
allow_embedding = true
# 开启匿名访问,及以下组织和角色相关注释
enabled = true

但是针对跳转后依然需要登录的问题,如果只是用匿名访问的方式,这样就太被动了。对dashboard的维护以及对应操作控制都不够友好,针对以上问题有以下几种处理方式:

1、授权登录

grafana提供标准协议oauth2的接入方式,可以根据相关接入文档进行配置使用,但不可避免的需要做很多额外的学习以及操作。

2、自定义图表

grafana提供 api keys,可以通过调用相关接口进行自定义图表开发展示。

3、白名单配置

开启匿名登录,但是需要做好安全性配置,例如白名单,但同样存在局限性。

4、接口登录转发(同域条件下)

这是本文介绍的方式,核心流程就是提供前端访问后台接口,后台对接口进行权限校验(校验主系统登录权限),校验通过后访问grafana登录接口,成功后将得到的cookie添加至响应信息并重定向至grafna页面

5、更多…

官方接口文档:https://grafana.com/docs/grafana/latest/developers/http_api/

一、具体实现

1、grafana配置信息

配置nginx代理grafana实现域名访问参考《配置nginx代理grafana实现域名访问》。

以上3000端口为grafana服务的默认端口,根据占用情况决定是否修改,domain的ip地址为当前服务器的ip地址,即部署grafana服务器,root_url中的上下文/grafana根据实际情况自行修改,但是需要和nginx的代理配置保持一致。nginx的配置具体如下:

location  /grafana {
	proxy_buffering on;
	proxy_buffer_size 4k;
	proxy_buffers 8 4M;
	proxy_busy_buffers_size 4M;
	proxy_set_header Host $http_host;
	proxy_set_header X-Real-IP $remote_addr;
	proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	proxy_set_header X-Forwarded-Proto $scheme;
	proxy_pass http://10.130.9.30:3000;
}

加入以上配置之后记得重启nginx,配置之后的效果就是在业务系统域名之后跟/grafana就可以访问到grafana服务了,但是直接使用ip加端口的方式就无法访问了。比如说主系统的访问地址为:xxx.xxx.com,那grafana服务的访问地址就是xxx.xxx.com/grafana。

2、yaml文件中配置grafana相关信息

grafana:
  baseUrl: http://127.0.0.1:3000
  adminUser: admin
  adminPwd: admin
  viewUser: viewer
  viewPwd: viewer
  auth: glsa_wSWxeJgOo0WUKtP1mwONiwk9GakvhW7P_bd7a45d3
  kiosk: kiosk=full

2、GrafanaController.java

@RestController
@RequestMapping(value = "/grafana")
@Api(tags = {"Grafana"})
public class GrafanaController {
    
    @Autowired
    private GrafanaService grafanaService;

    @GetMapping(value = "/dashboard/redirect")
    @ApiOperation(value = "跳转仪表板", httpMethod = "GET")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "dashboardUrl", value = "仪表盘地址", dataType = "String", paramType = "query"),
            @ApiImplicitParam(name = "action", value = "行为(edit/view)", dataType = "String", paramType = "query")
    })
    public void redirect(@RequestParam(name = "dashboardUrl") String dashboardUrl,
                         @RequestParam(name = "action") String action) {
        grafanaService.redirect(dashboardUrl, action);
    }
}

3、GrafanaService.java

public interface GrafanaService {

    /**
     * 跳转grafana仪表盘
     *
     * @param dashboardUrl 仪表盘地址
     * @param action 行为(edit/view)
     */
    void redirect(String dashboardUrl, String action);    

}

4、GrafanaServiceImpl.java

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.zdww.cloud.monitor.web.config.GrafanaProperties;
import com.zdww.cloud.monitor.web.constant.GrafanaConstants;
import com.zdww.cloud.monitor.web.service.GrafanaService;
import com.zdww.cloud.monitor.web.utils.RestTemplateUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.HttpCookie;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;

@Service
@Slf4j
public class GrafanaServiceImpl implements GrafanaService {

    @Autowired
    private GrafanaProperties grafanaProperties;
    @Autowired
    private RestTemplateUtil restTemplateUtil;
    @Resource
    private HttpServletResponse response;

    @Override
    public void redirect(String dashboardUrl, String action) {
        if (this.check(dashboardUrl)) {
            HttpCookie httpCookie = this.getLoginCookie(action);
            Cookie cookie = this.getRedirectCookie(httpCookie);
            response.addCookie(cookie);
        }
        try {
            response.sendRedirect(dashboardUrl);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取当前组织的id
     *
     */
    private String getCurrentOrgId() {
        String orgId = null;
        String result = restTemplateUtil.getStrData(grafanaProperties.getBaseUrl() + GrafanaConstants.GETCURRENTORG);
        if (null != JSON.parseObject(result).get("id")) {
            orgId = JSON.parseObject(result).get("id").toString();
        }
        return orgId;
    }

    /**
     * 获取重定向的Cookie信息
     *
     * @param httpCookie 登录用户的Cookie信息
     */
    private Cookie getRedirectCookie(HttpCookie httpCookie) {
        Cookie cookie = null;
        if (null != httpCookie) {
            cookie = new Cookie(httpCookie.getName(), httpCookie.getValue());
            // cookie.setDomain(httpCookie.getDomain());
            cookie.setPath(httpCookie.getPath());
            cookie.setVersion(httpCookie.getVersion());
            cookie.setComment(httpCookie.getComment());
            cookie.setHttpOnly(httpCookie.isHttpOnly());
            cookie.setMaxAge((int) httpCookie.getMaxAge());
            cookie.setSecure(httpCookie.getSecure());
        }
        return cookie;
    }

    /**
     * 获取不同权限登录用户的Cookie信息
     *
     * @param action 行为(查看/编辑)
     */
    private HttpCookie getLoginCookie(String action) {
        JSONObject params = new JSONObject();
        if (action.equals(GrafanaConstants.EDIT)) {
            params.put("user", grafanaProperties.getAdminUser());
            params.put("password", grafanaProperties.getAdminPwd());
        } else if (action.equals(GrafanaConstants.VIEW)) {
            params.put("user", grafanaProperties.getViewUser());
            params.put("password", grafanaProperties.getViewPwd());
        }
        ResponseEntity<String> result = restTemplateUtil.postForEntity(grafanaProperties.getBaseUrl() + GrafanaConstants.LOGIN, params.toJSONString());
        List<String> cookies = result.getHeaders().get(GrafanaConstants.COOKIE);
        if (CollUtil.isNotEmpty(cookies)) {
            for (String cookie : cookies) {
                List<HttpCookie> httpCookieList = HttpCookie.parse(cookie);
                if (CollUtil.isNotEmpty(httpCookieList)) {
                    for (HttpCookie httpCookie : httpCookieList) {
                        if (GrafanaConstants.GRAFANA_SESSION.equals(httpCookie.getName())) {
                            return httpCookie;
                        }
                    }
                } else {
                    log.error("登录Cookie为空!");
                }
            }
        } else {
            log.error("登录信息为空!");
        }
        return null;
    }

    /**
     * 检查dashboard地址中的组织id对应的组织是否存在
     *
     * @param dashboardUrl dashboard地址
     */
    private Boolean check(String dashboardUrl) {
        String orgId = this.getOrgId(dashboardUrl);
        JSONObject result = restTemplateUtil.getJsonData(grafanaProperties.getBaseUrl() + GrafanaConstants.GETORGBYID + orgId);
        if (null == result.get("name")) {
            log.error("该组织为无效组织!");
            return Boolean.FALSE;
        }
        return Boolean.TRUE;
    }

    /**
     * 获取dashboard地址中的组织id
     *
     * @param dashboardUrl dashboard地址
     */
    private String getOrgId(String dashboardUrl) {
        String orgId = null;
        // 使用URIBuilder来构建URI
        URI uri;
        try {
            // 构建的URI,特殊字符不被转义
            uri = new URI(dashboardUrl);
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
        String query = uri.getQuery();
        String[] params = query.split("&");
        for (String param : params) {
            if (param.contains("=")) {
                String key = param.substring(0, param.indexOf("="));
                if ("orgId".equals(key)) {
                    orgId = param.substring(param.indexOf("=") + 1);
                }
            }
        }
        return orgId;
    }

}

以上代码中用的配置类及工具类代码

1)GrafanaProperties.java

@Data
@Configuration
@ConfigurationProperties("grafana")
public class GrafanaProperties {
    /**
     * 访问地址
     */
    private String baseUrl;
    /**
     * 管理员账号
     */
    private String adminUser;
    /**
     * 管理员密码
     */
    private String adminPwd;
    /**
     * 查看人员账号
     */
    private String viewUser;
    /**
     * 查看人员密码
     */
    private String viewPwd;
    /**
     * Token
     */
    private String auth;
    /**
     * 设置dashboard隐藏左侧边栏和顶部菜单栏(kiosk=tv,隐藏左侧边栏;kiosk=full,隐藏左侧边栏且不会隐藏下拉框)
     */
    private String kiosk;
}

其中的auth和kiosk属性这里再说明一下,auth是调用grafana的api接口是需要的参数,具体的可以参考官方接口文档获取,kiosk是用来优化dashboard展示效果的。

2)RestTemplateUtil.java

@Component
public class RestTemplateUtil {

    private static final String Authorization = "Authorization";

    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private GrafanaProperties grafanaProperties;

    public JSONObject getJsonData(String url) {
        HttpHeaders headers = new HttpHeaders();
        String auth = grafanaProperties.getAdminUser() + ":" + grafanaProperties.getAdminPwd();
        byte[] encodedAuth = Base64Utils.encode(auth.getBytes());
        headers.set(Authorization, "Basic " + new String(encodedAuth));
        HttpEntity<String> requestEntity = new HttpEntity<>(headers);
        return restTemplate.exchange(url, HttpMethod.GET, requestEntity, JSONObject.class).getBody();
    }

    public String getStrData(String url) {
        HttpHeaders headers = new HttpHeaders();
        headers.add(Authorization, "Bearer " + grafanaProperties.getAuth());
        HttpEntity<JSONObject> requestEntity = new HttpEntity<>(headers);
        return restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class).getBody();
    }

    public ResponseEntity<String> postForEntity(String url, String params) {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<String> requestEntity = new HttpEntity<>(params, headers);
        return restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
    }

}

3)GrafanaConstants.java

public class GrafanaConstants {
    /**
     * cookie
     */
    public static final String COOKIE = "Set-Cookie";
    /**
     * session
     */
    public static final String GRAFANA_SESSION = "grafana_session";
    /**
     * 编辑
     */
    public static final String EDIT = "edit";
    /**
     * 查看
     */
    public static final String VIEW = "view";
    /**
     * 登录(因为我在nginx中配置了/grafana上下文的域名代理,这里多个/grafana,正常应该是/login)
     */
    public static final String LOGIN = "/grafana/login";
    /**
     * 根据 ID 获取组织
     */
    public static final String GETORGBYID = "/api/orgs/";
    /**
     * 获取当前组织
     */
    public static final String GETCURRENTORG = "/api/org/";
}

dashboard跳转之后,为了提高用户体验,隐藏左侧菜单栏和顶部菜单栏,点击如下图所示的“小电脑”图标。

1、点击第一次,隐藏左侧边栏,相当于URL后面加上参数“&kiosk=tv”;

2、点击第二次,隐藏左侧边栏和顶部菜单栏(会隐藏下拉框),相当于URL后面加上参数“&kiosk”;

3、隐藏左侧边栏且不会隐藏下拉框,在URL后面加上参数“&kiosk=full”。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值