项目中使用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,如果不通过则重定向到管理系统的登陆画面。
流程图如下:
Gateway
Gateway主要是两个过滤器,一个负责将从管理系统跳转到Gateway时,传递的SessionID写到Cookie里,一个负责校验Session。
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.cloud
spring-cloud-starter-netflix-zuul
org.springframework.session
spring-session-data-redis
org.apache.shiro
shiro-spring
1.4.0
校验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 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 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> queryParams = context.getRequestQueryParams();
// If query param contain session, then execute this filter
if (queryParams != null && !queryParams.isEmpty() && !StringUtils.isEmpty(queryParams.get(SESSION_PARAM))) {
List 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> queryParams = context.getRequestQueryParams();
List 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即可。
本文原创,转载请声明出处