通过Zuul来反向代理Kibana

项目中使用ELK来收集日志,问题是Kibana没有登陆功能,登陆功能被放在X-Pack中了,而X-Pack是收费的。可以通过Nginx来反向代理,添加基本的身份验证(Basic Auth),不过这里我选择使用Zuul来实现。

版本说明

  • Spring Boot 2.0.3.RELEASE
  • Spring Seesion 2.0.4.RELEASE
  • Shiro 1.4.0
  • Spring Cloud Netflix 2.0.0.RELEASE
  • Kibana 6.3.0

处理逻辑

由于已经有了一个管理系统,具有权限控制功能,通过Apache Shiro来实现,需要修改的就是将其Session保存到Redis中,使用Spring Session来实现。用户登陆管理系统后,会将其拥有的Function(这里的Function代表功能目录,拥有的功能会在页面显示)放在Session attribute中,一并保存到Redis中。

然后创建Gateway,使用Zuul来实现,过滤对Kibana的访问,此时根据用户提交的SessionID,从Redis中查询Session是否存在以及是否有效,并且较验是否有权限访问Kibana function。如果校验通过,就转发请求到Kibana,如果不通过则重定向到管理系统的登陆画面。

流程图如下:

image.png

Gateway

Gateway主要是两个过滤器,一个负责将从管理系统跳转到Gateway时,传递的SessionID写到Cookie里,一个负责校验Session。

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-data-redis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-spring</artifactId>
      <version>1.4.0</version>
    </dependency>
  </dependencies>

校验Session的有效性:


/**
 * Session filter, if the session in cookies is invalid, then go to mo login page
 *
 * @author Colin Feng
 */
@Component
public class CheckSessionPreFilter extends ZuulFilter {

    private Logger log = LoggerFactory.getLogger(this.getClass());

    @Value("${mo.url}")
    String loginUrl;

    @Autowired
    private FindByIndexNameSessionRepository repository;

    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER - 1;
    }

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest servletRequest = ctx.getRequest();

        String path = servletRequest.getRequestURI().toLowerCase();
        // For static resource, do not check session. If path contain /app/kibana, then check session
        if (!path.contains(DOT) && path.contains(KIBANA_PATH)) {
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Object run() {
        Session realSession = null;
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest servletRequest = ctx.getRequest();
        Cookie[] cookies = servletRequest.getCookies();

        if (cookies != null) {
            List<Cookie> moSession = Arrays.stream(cookies).filter(c -> MO_SESSION_ID_KEY.equals(c.getName())).collect(Collectors.toList());
            if (moSession != null && !moSession.isEmpty()) {
                String sid = moSession.get(0).getValue();
                // Get session from redis, if session is valid then continue else redirect to login.
                realSession = repository.findById(sid);
            }
        }

        if (realSession != null && !realSession.isExpired()) {
            // Get the function list, if not have the Kibana function(009001), redirect to MO login page
            List<TFunction> userFunctions = realSession.getAttribute("userFunction");
            if (userFunctions != null && !userFunctions.isEmpty()) {
                boolean hasKibanaFunction = userFunctions.stream().anyMatch(f -> f.getFunctionCode().equals("009001"));
                if (!hasKibanaFunction) {
                    redirect2MoLogin(ctx);
                }
            } else {
                redirect2MoLogin(ctx);
            }
        } else {
            redirect2MoLogin(ctx);
        }
        return null;
    }

    private void redirect2MoLogin(RequestContext ctx) {
        try {
            // redirect to login page
            ctx.setSendZuulResponse(false);
            ctx.put(FORWARD_TO_KEY, loginUrl);
            ctx.setResponseStatusCode(HttpStatus.SC_TEMPORARY_REDIRECT);
            ctx.getResponse().sendRedirect(loginUrl);
        } catch (IOException e) {
            log.error("Unable to send a redirect to the login page", e);
        }
    }
}

当从管理系统跳转过来时,会在URL的query param中添加SeesionID,该过滤器将负责将其写到Gateway域下的Cookie中。

/**
 * Cookie filter, insert session from query parameter into cookies
 *
 * @author Colin Feng
 */
@Component
public class UpdateCookiePostFilter extends ZuulFilter {

    private Logger log = LoggerFactory.getLogger(this.getClass());

    @Override
    public String filterType() {
        return POST_TYPE;
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext context = RequestContext.getCurrentContext();
        Map<String, List<String>> queryParams = context.getRequestQueryParams();

        // If query param contain session, then execute this filter
        if (queryParams != null && !queryParams.isEmpty() && !StringUtils.isEmpty(queryParams.get(SESSION_PARAM))) {
            List<String> sessionParams = queryParams.get(SESSION_PARAM);
            if (sessionParams != null && !sessionParams.isEmpty() && !StringUtils.isEmpty(sessionParams.get(0))) {
                return true;
            }
        }
        return false;
    }

    @Override
    public Object run() throws ZuulException {
        // Write session to cookies
        RequestContext context = RequestContext.getCurrentContext();
        Map<String, List<String>> queryParams = context.getRequestQueryParams();
        List<String> jsessionid = queryParams.get(SESSION_PARAM);

        HttpServletResponse response = context.getResponse();
        Cookie userCookie = new Cookie(MO_SESSION_ID_KEY, jsessionid.get(0));
        response.addCookie(userCookie);
        return null;
    }
}

常量

/**
 * Gateway utils
 *s
 * @author Colin Feng
 */
public class GatewayUtils {
    public static final String SESSION_PARAM = "SESSION";
    public static final String MO_SESSION_ID_KEY = "MOSESSIONID";
    public static final String DOT = ".";
    public static final String KIBANA_PATH = "/app/kibana";

    public static boolean isAjax(HttpServletRequest request) {
        String requestedWithHeader = request.getHeader("X-Requested-With");
        return "XMLHttpRequest".equals(requestedWithHeader);
    }
}

Application.yml

# server config
server:
  port: 8000
  servlet:
    context-path: /gateway

# Log config
logging:
  config: classpath:logback-spring.xml

spring:
  session:
    store-type: redis
    redis:
      flush-mode: on_save
      namespace: mo:ses

# zuul config
zuul:
  routes:
    kibana:
      id: kibana
      path: /kib-app/**
      url: http://127.0.0.1:5601/gateway/kib-app
  ssl-hostname-validation-enabled: false

---

spring:
  profiles: test
  redis:
    sentinel:
      master: mymaster
      nodes: 192.168.1.17:26379,192.168.1.17:26380,192.168.1.27:26379

mo:
  url: http://192.168.1.17:9999/mo/login

---

spring:
  profiles: prod
  redis:
    sentinel:
      master: mymaster
      nodes: 110.10.820.190:26379,110.10.740.120:26379,110.10.10.960:26379

mo:
  url: https://www.jpssb.com/mo/login

配置Kibana

修改kibana.yml

server.basePath: "/gateway/kib-app"
server.rewriteBasePath: true

JS跳转

在管理系统中,打开新的Tab,传递SessionID即可。

<script type="text/javascript" th:inline="javascript">
    /*<![CDATA[*/

    function openKibTab(url){
        var sid = [[${#session.id}]];
        // http://192.168.1.17:8000/gateway/kib-app/
        url = url + "?SESSION=" + sid;
        window.open(url, '_blank');
    }

    /*]]>*/
</script>

本文原创,转载请声明出处

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容