之前的项目一直是单节点,这次在生产系统中使用了负载均衡,一个应用部署了两个节点,负载均衡策略未知。这样在使用时发现了这么一个问题:在单点退出后,应用有时候可以退出,但有时还在登陆状态,这就很郁闷了。
我的cas版本是5.1.2。一点点排查,把每个节点的日志都打开,把日志级别设置成最小trace,功夫不负有心人,发现了问题:用户在浏览器访问的都是节点一,在cas单点退出后,cas服务端发送logoutRequest的post请求,结果负载均衡把请求发送到了节点二,节点二压根未访问过,logoutRequest请求当然不起作用。
问题找到了,看官可能觉得楼主有点low,首先,负载均衡具体什么策略,为什么会发错节点;第二,负载均衡多节点为什么不用session共享。下面我一一解答。
第一个,负载均衡不是我负责的,我只知道通过浏览器访问一个应用能保证每次都访问同个节点,这就保证了用户每次都携带登录信息。我以为采用的是nginx的ip的hash值,结果被告知不是,是通过硬件控制负载均衡的。
第二个,之前公司项目差不多都是单节点,几乎所有的应用都未使用session共享,况且现在是上线的关键节点,现改也来不及,最好是修改cas客户端的东西,大家都更新一下jar包就可以了。
logger.trace("Logout request:\n{}", logoutMessage); final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex"); if (CommonUtils.isNotBlank(token)) { final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token); if (session != null) { final String sessionID = session.getId(); logger.debug("Invalidating session [{}] for token [{}]", sessionID, token); try { session.invalidate(); } catch (final IllegalStateException e) { logger.debug("Error invalidating session.", e); } this.logoutStrategy.logout(request); }
了解cas的同学知道,cas服务端发送logoutRequest的post请求,里面携带token信息,客户端就凭借这个token找到session。图中标红部分,当未找到时不起作用,解决之道就在此,我们拿到了token信息,然后向携带着token向应用的各个节点发送一次退出请求,这样就能找到session,然后失效退出了。
就这样,问题找到了,就着手解决吧。
第一步:修改cas客户端,当session为空时,发送post请求,上代码。
logger.trace("Logout request:\n{}", logoutMessage); final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex"); if (CommonUtils.isNotBlank(token)) { final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token); if (session != null) { final String sessionID = session.getId(); logger.debug("Invalidating session [{}] for token [{}]", sessionID, token); try { session.invalidate(); } catch (final IllegalStateException e) { logger.debug("Error invalidating session.", e); } this.logoutStrategy.logout(request); }else{ if(CommonUtils.isNotBlank(this.balanceUrls)) { String[] urls = balanceUrls.split(","); for(String url : urls) { Mapmap = new HashMap (); map.put("token",token); try {
//发送post请求,不要在意我的工具类名字,复制来的 HttpClient.GET_JCMH_QX(map, url + "/logoutRequestOther"); }catch(Exception e) { logger.debug("Error HttpClient ", e); } } } }
比之前的代码多了else语句,这里的balanceUrls(在filter的initParam维护),是负载均衡的转发地址,多个用,隔开,然后遍历发送post请求,请求路径统一为“/logoutRequestOther“。
第二步在我们第一步的/logoutRequestOthe请求肯定是请求不到的,我们怎么处理呢?为了最小的改动,在cas客户端增加filter进行处理,增加LogoutForBalanceFilter类。
一定要跟SingleSignOutFilter同目录,原因是我们需要用到 private SessionMappingStorage sessionMappingStorage ,session信息就存储在这个类中。
package org.jasig.cas.client.session; import org.jasig.cas.client.util.CommonUtils; import org.jasig.cas.client.util.JsonUtil; import org.jasig.cas.client.util.XmlUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.InputStream; import java.util.Map; public class LogoutForBalanceFilter implements Filter { private final Logger logger = LoggerFactory.getLogger(LogoutForBalanceFilter.class); private SessionMappingStorage sessionMappingStorage; @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { if (servletRequest instanceof HttpServletRequest) { HttpServletRequest req = (HttpServletRequest) servletRequest; String uri = req.getRequestURI(); logger.debug("================== uri [{}]", uri); if (uri.contains("/logoutRequestOther")) { if (sessionMappingStorage == null) { sessionMappingStorage = getSessionMappingStorage(); }
//获取post请求的参数 Map,?> map = JsonUtil.jsonStrToMap(getParams(req)); if(map != null) { String token = (String)map.get("token"); logger.debug("================== token [{}]", token); if (CommonUtils.isNotBlank(token)) {
//通过token查找session final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token); if (session != null) { final String sessionID = session.getId(); logger.debug("Invalidating session [{}] for token [{}]", sessionID, token); try { session.invalidate(); } catch (final IllegalStateException e) { logger.debug("Error invalidating session.", e); } //this.logoutStrategy.logout(request); } } } return; } chain.doFilter(servletRequest, servletResponse); }else { chain.doFilter(servletRequest, servletResponse); } } @Override public void destroy() { } protected static SessionMappingStorage getSessionMappingStorage() { return SingleSignOutFilter.getSingleSignOutHandler().getSessionMappingStorage(); } protected String getParams(HttpServletRequest request){ String body = ""; StringBuilder stringBuilder = new StringBuilder(); BufferedReader bufferedReader = null; InputStream inputStream = null; try { inputStream = request.getInputStream(); if (inputStream != null) { bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); char[] charBuffer = new char[128]; int bytesRead = -1; while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { stringBuilder.append(charBuffer, 0, bytesRead); } } else { stringBuilder.append(""); } } catch (IOException ex) { ex.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (bufferedReader != null) { try { bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); } } } body = stringBuilder.toString(); return body; } }
至此客户端更改完毕。
这样,同事们只需要更改下web.xml,增加一个filter,添加负载均衡转发的地址参数列表。
Cas_Balance_Logout org.jasig.cas.client.session.LogoutForBalanceFilter Cas_Balance_Logout /logoutRequestOther CAS Single Sign Out Filter class>org.jasig.cas.client.session.SingleSignOutFilter class>casServerUrlPrefix http://zsptsso.jlsw.tax.cn/cas balanceUrls http://123.14.62.125:8002/zntx,http://123.14.62.126:8002/zntx CAS Single Sign Out Filter /* org.jasig.cas.client.session.SingleSignOutHttpSessionListener
图中红色部分就是同事们需要增加的,是不是很简单呢,完美!