CAS 集群环境下登录登出问题

CAS是一个开源的单点登录软件,这里主要讲它在集群下登出问题。

 

(一)CAS集群环境下登出问题处理

 因为CAS把tokenIDs和sessionId的映射放在了一个Map中,在集群环境下就会有问题,这里它又没有提供接口支持把这个映射放入缓存中所以变得有点麻烦。

 

 截图的版本是cas-client3.3.3,有两个方法解决这个问题。

(1)把HashMapBackedSessionMappingStorage重写,让各台机器使用同一个Map,可以结合redis实现。

(2)当注销失败的时候,通知全部的机器注销。

 

作者采用第二种方式。

当获取sessionId为null时销毁其他节点。

大致就是这样子,我把这个类的源码上传。 能做到各个节点在配置文件里面配置,这样就不需要每一个项目都去重新编译cas然后再打包。

 

 

  1 /** <a href="http://www.cpupk.com/decompiler">Eclipse Class Decompiler</a> plugin, Copyright (c) 2017 Chen Chao. */
  2 /*
  3  * Licensed to Jasig under one or more contributor license
  4  * agreements. See the NOTICE file distributed with this work
  5  * for additional information regarding copyright ownership.
  6  * Jasig licenses this file to you under the Apache License,
  7  * Version 2.0 (the "License"); you may not use this file
  8  * except in compliance with the License.  You may obtain a
  9  * copy of the License at the following location:
 10  *
 11  *   http://www.apache.org/licenses/LICENSE-2.0
 12  *
 13  * Unless required by applicable law or agreed to in writing,
 14  * software distributed under the License is distributed on an
 15  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 16  * KIND, either express or implied.  See the License for the
 17  * specific language governing permissions and limitations
 18  * under the License.
 19  */
 20 package org.jasig.cas.client.session;
 21 import java.util.ArrayList;
 22 import java.util.Arrays;
 23 import java.util.List;
 24 import java.util.zip.Inflater;
 25 
 26 import javax.servlet.ServletException;
 27 import javax.servlet.http.HttpServletRequest;
 28 import javax.servlet.http.HttpServletResponse;
 29 import javax.servlet.http.HttpSession;
 30 
 31 import org.apache.commons.codec.binary.Base64;
 32 import org.apache.commons.lang.StringUtils;
 33 import org.apache.http.NameValuePair;
 34 import org.apache.http.client.entity.UrlEncodedFormEntity;
 35 import org.apache.http.client.methods.HttpPost;
 36 import org.apache.http.client.utils.HttpClientUtils;
 37 import org.apache.http.impl.client.DefaultHttpClient;
 38 import org.apache.http.message.BasicNameValuePair;
 39 import org.jasig.cas.client.util.CommonUtils;
 40 import org.jasig.cas.client.util.XmlUtils;
 41 import org.slf4j.Logger;
 42 import org.slf4j.LoggerFactory;
 43 import org.springframework.beans.factory.annotation.Autowired;
 44 import org.springframework.context.annotation.PropertySource;
 45 import org.springframework.core.env.Environment;
 46 import org.springframework.web.context.ContextLoader;
 47 
 48 /**
 49  * Performs CAS single sign-out operations in an API-agnostic fashion.
 50  *
 51  * @author Marvin S. Addison
 52  * @version $Revision$ $Date$
 53  * @since 3.1.12
 54  *
 55  */
 56 @PropertySource("file:/home/webdata/csadmin/webroot/config/csadmin.properties")
 57 public final class SingleSignOutHandler {
 58 
 59     public final static String DEFAULT_ARTIFACT_PARAMETER_NAME = "ticket";
 60     public final static String DEFAULT_LOGOUT_PARAMETER_NAME = "logoutRequest";
 61     public final static String DEFAULT_FRONT_LOGOUT_PARAMETER_NAME = "SAMLRequest";
 62     public final static String DEFAULT_RELAY_STATE_PARAMETER_NAME = "RelayState";
 63     public final static String DEFAULT_LOGOUT_CLUSTER_NODES_PARAMETER_NAME = "logoutClusterNodesRequest";
 64 
 65     private final static int DECOMPRESSION_FACTOR = 10;
 66 
 67     /** Logger instance */
 68     private final Logger logger = LoggerFactory.getLogger(getClass());
 69 
 70     /** Mapping of token IDs and session IDs to HTTP sessions */
 71     private SessionMappingStorage sessionMappingStorage = new HashMapBackedSessionMappingStorage();
 72 
 73     /**
 74      * The name of the artifact parameter. This is used to capture the session
 75      * identifier.
 76      */
 77     private String artifactParameterName = DEFAULT_ARTIFACT_PARAMETER_NAME;
 78 
 79     /** Parameter name that stores logout request for back channel SLO */
 80     private String logoutParameterName = DEFAULT_LOGOUT_PARAMETER_NAME;
 81 
 82     private String logoutClusterNodesParameterName = DEFAULT_LOGOUT_CLUSTER_NODES_PARAMETER_NAME;
 83 
 84     /** Parameter name that stores logout request for front channel SLO */
 85     private String frontLogoutParameterName = DEFAULT_FRONT_LOGOUT_PARAMETER_NAME;
 86 
 87     /**
 88      * Parameter name that stores the state of the CAS server webflow for the
 89      * callback
 90      */
 91     private String relayStateParameterName = DEFAULT_RELAY_STATE_PARAMETER_NAME;
 92 
 93     /** The prefix url of the CAS server */
 94     private String casServerUrlPrefix = "";
 95 
 96     private boolean artifactParameterOverPost = false;
 97 
 98     private boolean eagerlyCreateSessions = true;
 99 
100     private List<String> safeParameters;
101 
102     @Autowired
103     private Environment env;
104 
105     private LogoutStrategy logoutStrategy = isServlet30() ? new Servlet30LogoutStrategy()
106             : new Servlet25LogoutStrategy();
107 
108     public void setSessionMappingStorage(final SessionMappingStorage storage) {
109         this.sessionMappingStorage = storage;
110     }
111 
112     public void setArtifactParameterOverPost(
113             final boolean artifactParameterOverPost) {
114         this.artifactParameterOverPost = artifactParameterOverPost;
115     }
116 
117     public SessionMappingStorage getSessionMappingStorage() {
118         return this.sessionMappingStorage;
119     }
120 
121     /**
122      * @param name
123      *            Name of the authentication token parameter.
124      */
125     public void setArtifactParameterName(final String name) {
126         this.artifactParameterName = name;
127     }
128 
129     /**
130      * @param name
131      *            Name of parameter containing CAS logout request message for
132      *            back channel SLO.
133      */
134     public void setLogoutParameterName(final String name) {
135         this.logoutParameterName = name;
136     }
137 
138     /**
139      * @param casServerUrlPrefix
140      *            The prefix url of the CAS server.
141      */
142     public void setCasServerUrlPrefix(final String casServerUrlPrefix) {
143         this.casServerUrlPrefix = casServerUrlPrefix;
144     }
145 
146     /**
147      * @param name
148      *            Name of parameter containing CAS logout request message for
149      *            front channel SLO.
150      */
151     public void setFrontLogoutParameterName(final String name) {
152         this.frontLogoutParameterName = name;
153     }
154 
155     /**
156      * @param name
157      *            Name of parameter containing the state of the CAS server
158      *            webflow.
159      */
160     public void setRelayStateParameterName(final String name) {
161         this.relayStateParameterName = name;
162     }
163 
164     public void setEagerlyCreateSessions(final boolean eagerlyCreateSessions) {
165         this.eagerlyCreateSessions = eagerlyCreateSessions;
166     }
167 
168     /**
169      * Initializes the component for use.
170      */
171     public synchronized void init() {
172         if (this.safeParameters == null) {
173             CommonUtils.assertNotNull(this.artifactParameterName,
174                     "artifactParameterName cannot be null.");
175             CommonUtils.assertNotNull(this.logoutParameterName,
176                     "logoutParameterName cannot be null.");
177             CommonUtils.assertNotNull(this.frontLogoutParameterName,
178                     "frontLogoutParameterName cannot be null.");
179             CommonUtils.assertNotNull(this.sessionMappingStorage,
180                     "sessionMappingStorage cannot be null.");
181             CommonUtils.assertNotNull(this.relayStateParameterName,
182                     "relayStateParameterName cannot be null.");
183             CommonUtils.assertNotNull(this.casServerUrlPrefix,
184                     "casServerUrlPrefix cannot be null.");
185 
186             if (CommonUtils.isBlank(this.casServerUrlPrefix)) {
187                 logger.warn("Front Channel single sign out redirects are disabled when the 'casServerUrlPrefix' value is not set.");
188             }
189 
190             if (this.artifactParameterOverPost) {
191                 this.safeParameters = Arrays.asList(this.logoutParameterName,
192                         this.artifactParameterName);
193             } else {
194                 this.safeParameters = Arrays.asList(this.logoutParameterName);
195             }
196         }
197     }
198 
199     /**
200      * Determines whether the given request contains an authentication token.
201      *
202      * @param request
203      *            HTTP reqest.
204      *
205      * @return True if request contains authentication token, false otherwise.
206      */
207     private boolean isTokenRequest(final HttpServletRequest request) {
208         return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request,
209                 this.artifactParameterName, this.safeParameters));
210     }
211 
212     /**
213      * Determines whether the given request is a CAS back channel logout
214      * request.
215      *
216      * @param request
217      *            HTTP request.
218      *
219      * @return True if request is logout request, false otherwise.
220      */
221     private boolean isBackChannelLogoutRequest(final HttpServletRequest request) {
222         return "POST".equals(request.getMethod())
223                 && !isMultipartRequest(request)
224                 && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request,
225                         this.logoutParameterName, this.safeParameters));
226     }
227 
228     /**
229      * Determines whether the given request is a CAS front channel logout
230      * request. Front Channel log out requests are only supported when the
231      * 'casServerUrlPrefix' value is set.
232      *
233      * @param request
234      *            HTTP request.
235      *
236      * @return True if request is logout request, false otherwise.
237      */
238     private boolean isFrontChannelLogoutRequest(final HttpServletRequest request) {
239         return "GET".equals(request.getMethod())
240                 && CommonUtils.isNotBlank(this.casServerUrlPrefix)
241                 && CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request,
242                         this.frontLogoutParameterName));
243     }
244 
245     private boolean isLogoutRequestFromClusterNode(
246             final HttpServletRequest request) {
247         return "POST".equals(request.getMethod())
248                 && CommonUtils.isNotBlank(this.casServerUrlPrefix)
249                 && CommonUtils.isNotBlank(request
250                         .getParameter(this.logoutClusterNodesParameterName));
251     }
252 
253     /**
254      * Process a request regarding the SLO process: record the session or
255      * destroy it.
256      *
257      * @param request
258      *            the incoming HTTP request.
259      * @param response
260      *            the HTTP response.
261      * @return if the request should continue to be processed.
262      */
263     public boolean process(final HttpServletRequest request,
264             final HttpServletResponse response) {
265         if (isTokenRequest(request)) {
266             logger.trace("Received a token request");
267             recordSession(request);
268             return true;
269 
270         } else if (isBackChannelLogoutRequest(request)) {
271             logger.trace("Received a back channel logout request");
272             destroySession(request);
273             return false;
274 
275         } else if (isFrontChannelLogoutRequest(request)) {
276             logger.trace("Received a front channel logout request");
277             destroySession(request);
278             // redirection url to the CAS server
279             final String redirectionUrl = computeRedirectionToServer(request);
280             if (redirectionUrl != null) {
281                 CommonUtils.sendRedirect(response, redirectionUrl);
282             }
283             return false;
284 
285         } else if (isLogoutRequestFromClusterNode(request)) {
286             destroySessionFromClusterNode(request);
287             return false;
288         } else {
289             logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
290             return true;
291         }
292     }
293 
294     /**
295      * Associates a token request with the current HTTP session by recording the
296      * mapping in the the configured {@link SessionMappingStorage} container.
297      * 
298      * @param request
299      *            HTTP request containing an authentication token.
300      */
301     private void recordSession(final HttpServletRequest request) {
302         final HttpSession session = request
303                 .getSession(this.eagerlyCreateSessions);
304 
305         if (session == null) {
306             logger.debug("No session currently exists (and none created).  Cannot record session information for single sign out.");
307             return;
308         }
309 
310         final String token = CommonUtils.safeGetParameter(request,
311                 this.artifactParameterName, this.safeParameters);
312         logger.debug("Recording session for token {}", token);
313 
314         try {
315             this.sessionMappingStorage.removeBySessionById(session.getId());
316         } catch (final Exception e) {
317             // ignore if the session is already marked as invalid. Nothing we
318             // can do!
319         }
320         sessionMappingStorage.addSessionById(token, session);
321     }
322 
323     /**
324      * Uncompress a logout message (base64 + deflate).
325      * 
326      * @param originalMessage
327      *            the original logout message.
328      * @return the uncompressed logout message.
329      */
330     private String uncompressLogoutMessage(final String originalMessage) {
331         final byte[] binaryMessage = Base64.decodeBase64(originalMessage);
332 
333         Inflater decompresser = null;
334         try {
335             // decompress the bytes
336             decompresser = new Inflater();
337             decompresser.setInput(binaryMessage);
338             final byte[] result = new byte[binaryMessage.length
339                     * DECOMPRESSION_FACTOR];
340 
341             final int resultLength = decompresser.inflate(result);
342 
343             // decode the bytes into a String
344             return new String(result, 0, resultLength, "UTF-8");
345         } catch (final Exception e) {
346             logger.error("Unable to decompress logout message", e);
347             throw new RuntimeException(e);
348         } finally {
349             if (decompresser != null) {
350                 decompresser.end();
351             }
352         }
353     }
354 
355     /**
356      * Destroys the current HTTP session for the given CAS logout request.
357      *
358      * @param request
359      *            HTTP request containing a CAS logout message.
360      */
361     private void destroySession(final HttpServletRequest request) {
362         final String logoutMessage;
363         // front channel logout -> the message needs to be base64 decoded +
364         // decompressed
365         if (isFrontChannelLogoutRequest(request)) {
366             logoutMessage = uncompressLogoutMessage(CommonUtils
367                     .safeGetParameter(request, this.frontLogoutParameterName));
368         } else {
369             logoutMessage = CommonUtils.safeGetParameter(request,
370                     this.logoutParameterName, this.safeParameters);
371         }
372         logger.trace("Logout request:\n{}", logoutMessage);
373 
374         final String token = XmlUtils.getTextForElement(logoutMessage,
375                 "SessionIndex");
376         if (CommonUtils.isNotBlank(token)) {
377             final HttpSession session = this.sessionMappingStorage
378                     .removeSessionByMappingId(token);
379 
380             if (session != null) {
381                 String sessionID = session.getId();
382 
383                 logger.debug("Invalidating session [{}] for token [{}]",
384                         sessionID, token);
385 
386                 try {
387                     session.invalidate();
388                 } catch (final IllegalStateException e) {
389                     logger.debug("Error invalidating session.", e);
390                 }
391                 this.logoutStrategy.logout(request);
392             } else {
393                 destroySessionFromClusterNodes(token);
394             }
395         }
396     }
397 
398     @SuppressWarnings("deprecation")
399     private void destroySessionFromClusterNodes(String token) {
400         String nodeUrls = "http://csadmin01.beta1.fn,http://csadmin02.beta1.fn";
401         env = ContextLoader.getCurrentWebApplicationContext().getEnvironment();
402         logger.info("destory ClusterNodes begin token : {} casClusterNodes: {} ", token , env.getProperty("casClusterNodes"));
403         List<String> clusterNodeUrls = Arrays.asList(nodeUrls.split(","));
404         if (clusterNodeUrls != null) {
405             for (String url : clusterNodeUrls) {
406                 org.apache.http.client.HttpClient httpClient = new DefaultHttpClient();
407                 HttpPost httpPost = new HttpPost(url);
408                 ArrayList<NameValuePair> paramList = new ArrayList<NameValuePair>();
409                 paramList.add(new BasicNameValuePair(
410                         this.logoutClusterNodesParameterName, "true"));
411                 paramList.add(new BasicNameValuePair(
412                         this.artifactParameterName, token));
413                 try {
414                     httpPost.setEntity(new UrlEncodedFormEntity(paramList));
415                     httpClient.execute(httpPost);
416                 } catch (Exception e) {
417                     logger.info("destory ClusterNodes error", e);
418                     logger.error(e.getMessage());
419                 } finally {
420                     HttpClientUtils.closeQuietly(httpClient);
421                 }
422             }
423         }
424     }
425 
426     private void destroySessionFromClusterNode(HttpServletRequest request) {
427         logger.info("destory ClusterNode begin token : {}",
428                 request.getParameter(this.artifactParameterName));
429         final String token = request.getParameter(this.artifactParameterName);
430         if (CommonUtils.isNotBlank(token)) {
431             final HttpSession session = this.sessionMappingStorage
432                     .removeSessionByMappingId(token);
433 
434             if (session != null) {
435                 String sessionID = session.getId();
436 
437                 logger.debug("Invalidating session [{}] for token [{}]",
438                         sessionID, token);
439 
440                 try {
441                     session.invalidate();
442                 } catch (final IllegalStateException e) {
443                     logger.debug("Error invalidating session.", e);
444                 }
445                 this.logoutStrategy.logout(request);
446             }
447         }
448     }
449 
450     /**
451      * Compute the redirection url to the CAS server when it's a front channel
452      * SLO (depending on the relay state parameter).
453      *
454      * @param request
455      *            The HTTP request.
456      * @return the redirection url to the CAS server.
457      */
458     private String computeRedirectionToServer(final HttpServletRequest request) {
459         final String relayStateValue = CommonUtils.safeGetParameter(request,
460                 this.relayStateParameterName);
461         // if we have a state value -> redirect to the CAS server to continue
462         // the logout process
463         if (StringUtils.isNotBlank(relayStateValue)) {
464             final StringBuilder buffer = new StringBuilder();
465             buffer.append(casServerUrlPrefix);
466             if (!this.casServerUrlPrefix.endsWith("/")) {
467                 buffer.append("/");
468             }
469             buffer.append("logout?_eventId=next&");
470             buffer.append(this.relayStateParameterName);
471             buffer.append("=");
472             buffer.append(CommonUtils.urlEncode(relayStateValue));
473             final String redirectUrl = buffer.toString();
474             logger.debug("Redirection url to the CAS server: {}", redirectUrl);
475             return redirectUrl;
476         }
477         return null;
478     }
479 
480     private boolean isMultipartRequest(final HttpServletRequest request) {
481         return request.getContentType() != null
482                 && request.getContentType().toLowerCase()
483                         .startsWith("multipart");
484     }
485 
486     private static boolean isServlet30() {
487         try {
488             return HttpServletRequest.class.getMethod("logout") != null;
489         } catch (final NoSuchMethodException e) {
490             return false;
491         }
492     }
493 
494     /**
495      * Abstracts the ways we can force logout with the Servlet spec.
496      */
497     private interface LogoutStrategy {
498 
499         void logout(HttpServletRequest request);
500     }
501 
502     private class Servlet25LogoutStrategy implements LogoutStrategy {
503 
504         public void logout(final HttpServletRequest request) {
505             // nothing additional to do here
506         }
507     }
508 
509     private class Servlet30LogoutStrategy implements LogoutStrategy {
510 
511         public void logout(final HttpServletRequest request) {
512             try {
513                 request.logout();
514             } catch (final ServletException e) {
515                 logger.debug("Error performing request.logout.");
516             }
517         }
518     }
519 }

 

转载于:https://www.cnblogs.com/tamguo/p/7217341.html

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值