• spring-oauth集群负载的cas单点登出问题


    原文及更多文章请见个人博客:http://heartlifes.com

    背景:

    前端有N台由spring-oauth,spring-cas搭建的提供oauth2服务的服务器,后端有单台cas搭建的sso单点登录服务器,通过nginx的iphash保证用户在同一会话工程中始终登录在固定的一台oauth2服务器上。

    现象:

    cas3.5默认不支持集群环境下的单点登出,导致当用户使用oauth服务时,出现单点故障,具体表现为:
    用户A在浏览器上完成整个oauth流程后,此时
    1.用户A在单点登录服务器上点击登出按钮
    2.系统提示用户登出成功
    3.用户B在同一个浏览器上访问oauth服务器,此时没有要求用户B登录,还是用户A的登录信息,并且后续oauth流程报错

    原因:

    假设oauth服务部署在A,B两台机器上,提供负载访问。SSO单点服务部署在C机器上。
    1.用户在C机上登出时,C机器上的SSO服务删除C服务器中的session,并且清空存在用户浏览器中的cookies
    2.C服务器中的sso服务通知A,B中部署的oauth服务,用户已经退出,请求oauth服务清空自己的session缓存。
    3.此时,由于A,B是负载设置,CAS通知的oauth登出服务,其实只是通知到了A或B中的一台。
    4.假设通知到的是A服务器,此时A服务器删除oauth中的session缓存,而B服务器中的oauth session缓存依旧存在
    5.用户再次使用oauth服务,此时,由于集群原因,用户可能正好使用到的是B服务器上的oauth服务,由于B服务器中session依旧存在,结果出现单点登出故障。

    解决方案:

    翻遍了google中所有的讨论,结果毫无进展。
    尝试了使用jedis来存储session,结果发现session中存储的AuthorizationRequest类,没有实现序列化接口,无法实体化到redis中,无奈之下,使用了一种监听广播的方式

    1.重写SingleSignOutFilter类中的doFilter方法

    if (handler.isTokenRequest(request)) {
    	handler.recordSession(request);
    } else if (handler.isLogoutRequest(request)) {
    	String from = request.getParameter("from");
    	if (StringUtils.isEmpty(from)) {
    	        multiCastToDestroy(request);
    	}
    	handler.destroySession(request);
    	// Do not continue up filter chain
    	return;
    } else {
    	log.trace("Ignoring URI " + request.getRequestURI());
    }
    

    增加multiCastToDestroy方法

    private void multiCastToDestroy(HttpServletRequest request) {
    	String logoutMessage = CommonUtils.safeGetParameter(request,"logoutRequest");
    	ExecutorService executors = Executors.newFixedThreadPool(100);
    	String[] tmps = multicastUrls.split(",");
    	for (String tmp : tmps) {
    		String[] urls = tmp.split("=");
    		String key = urls[0];
    		String url = urls[1];
    		executors.submit(new MessageSender(url, logoutMessage, ownKey,5000, 5000, true));
    	}
    }
    

    增加MessageSender内部类

    private static final class MessageSender implements Callable<Boolean> {
    	private String url;
    	private String message;
    	private String from;
    	private int readTimeout;
    	private int connectionTimeout;
    	private boolean followRedirects;
    
    	public MessageSender(final String url, final String message,final String from, final int readTimeout,final int connectionTimeout, final boolean followRedirects) {
    		this.url = url;
    		this.message = message;
    		this.from = from;
    		this.readTimeout = readTimeout;
    		this.connectionTimeout = connectionTimeout;
    		this.followRedirects = followRedirects;
    	}
    
    	public Boolean call() throws Exception {
    		HttpURLConnection connection = null;
    		BufferedReader in = null;
    		try {
    			System.out.println("Attempting to access " + url);
    			final URL logoutUrl = new URL(url);
    			final String output = "from=" + from + "&logoutRequest="+ URLEncoder.encode(message, "UTF-8");
    			connection = (HttpURLConnection) logoutUrl.openConnection();
    			connection.setDoInput(true);
    			connection.setDoOutput(true);
    			connection.setRequestMethod("POST");
    			connection.setReadTimeout(this.readTimeout);
    			connection.setConnectTimeout(this.connectionTimeout);
    			connection.setInstanceFollowRedirects(this.followRedirects);
    			connection.setRequestProperty("Content-Length",Integer.toString(output.getBytes().length));
    			connection.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
    			final DataOutputStream printout = new DataOutputStream(connection.getOutputStream());
    			printout.writeBytes(output);
    			printout.flush();
    			printout.close();
    			in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
            		while (in.readLine() != null) {
    				// nothing to do
    			}
    
    			System.out.println("Finished sending message to" + url);
    			return true;
    		} catch (final SocketTimeoutException e) {
    			e.printStackTrace();
    			return false;
    		} catch (final Exception e) {
    			e.printStackTrace();
    			return false;
    		} finally {
    			if (in != null) {
    				try {
    					in.close();
    				} catch (final IOException e) {
    					// can't do anything
    				}
    			}
    			if (connection != null) {
    				connection.disconnect();
    			}
    		}
    	}
    }
    

    修改oauth配置文件:
    增加以下配置:

    <bean id="singleLogoutFilter" class="com.xxx.xxx.cas.filter.SingleSignOutFilter">
    	<property name="multicastUrls"		value="127.0.0.1=http://127.0.0.1/api/j_spring_cas_security_check,localhost=http://localhost/api/j_spring_cas_security_check" />
    	<property name="ownKey" value="localhost" />
    </bean>
    

    这样,当CAS通知到A服务器去做登出操作时,A服务器会广播给其他几台服务器同步去做登出操作,通过广播的方式解决单点登出的故障

  • 相关阅读:
    window环境搭建zookeeper,kafka集群
    Spring Boot中使用Redis小结
    Spring Boot Mock单元测试学习总结
    Git快速入门进阶篇
    Git快速入门
    玩转Git入门篇
    Apache Kafka简介与安装(二)
    Apache Kafka简介与安装(一)
    Spring Boot中使用EhCache实现缓存支持
    java基础面试题(JVM篇)
  • 原文地址:https://www.cnblogs.com/heartlifes/p/6970992.html
Copyright © 2020-2023  润新知