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 }