通过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>

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

推荐阅读更多精彩内容