zoukankan      html  css  js  c++  java
  • 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服务器会广播给其他几台服务器同步去做登出操作,通过广播的方式解决单点登出的故障

  • 相关阅读:
    手机号码正则表达式
    POJ 3233 Matrix Power Series 矩阵快速幂
    UVA 11468
    UVA 1449
    HDU 2896 病毒侵袭 AC自动机
    HDU 3065 病毒侵袭持续中 AC自动机
    HDU 2222 Keywords Search AC自动机
    POJ 3461 Oulipo KMP模板题
    POJ 1226 Substrings KMP
    UVA 1455 Kingdom 线段树+并查集
  • 原文地址:https://www.cnblogs.com/heartlifes/p/6970992.html
Copyright © 2011-2022 走看看