
com.digitalchina.platform.security.logout.SingleSignOutHandler Maven / Gradle / Ivy
The newest version!
/*
* Licensed to Jasig under one or more contributor license
* agreements. See the NOTICE file distributed with this work
* for additional information regarding copyright ownership.
* Jasig licenses this file to you under the Apache License,
* Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a
* copy of the License at the following location:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.digitalchina.platform.security.logout;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.HttpClientUtils;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.jasig.cas.client.session.HashMapBackedSessionMappingStorage;
import org.jasig.cas.client.session.SessionMappingStorage;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.XmlUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.zip.Inflater;
/**
* Created with IntelliJ IDEA.
* User: 胡本强
* Date: 2017-4-11:11:11
*/
public final class SingleSignOutHandler {
public final static String DEFAULT_ARTIFACT_PARAMETER_NAME = "ticket";
public final static String DEFAULT_LOGOUT_PARAMETER_NAME = "logoutRequest";
public final static String DEFAULT_FRONT_LOGOUT_PARAMETER_NAME = "SAMLRequest";
public final static String DEFAULT_RELAY_STATE_PARAMETER_NAME = "RelayState";
private final static String DEFAULT_CLUSTER_NOTE_LOGOUT_PARAMETER = "_cluster_notes_logout_";
private final static String CLUSTER_NOTES_PARAM_TOKEN = "token";
private final static String GATEWAY_HEADERS_APIKEY = "apiKey";
private final static String GATEWAY_PARAMS_APPNAME = "appName";
private final static String GATEWAY_RESULT_CODE_SUCC = "000000";
private final static String GATEWAY_RESULT_STATUS_SUCC = "OK";
private boolean cluterLogoutPower;
private String gatewayNotesUrl;
private String appName;
private String apiKey;
private final static int DECOMPRESSION_FACTOR = 10;
/**
* Logger instance
*/
private final Logger logger = LoggerFactory.getLogger(getClass());
/**
* Mapping of token IDs and session IDs to HTTP sessions
*/
private SessionMappingStorage sessionMappingStorage = new HashMapBackedSessionMappingStorage();
/**
* The name of the artifact parameter. This is used to capture the session identifier.
*/
private String artifactParameterName = DEFAULT_ARTIFACT_PARAMETER_NAME;
/**
* Parameter name that stores logout request for back channel SLO
*/
private String logoutParameterName = DEFAULT_LOGOUT_PARAMETER_NAME;
/**
* Parameter name that stores logout request for front channel SLO
*/
private String frontLogoutParameterName = DEFAULT_FRONT_LOGOUT_PARAMETER_NAME;
/**
* Parameter name that stores the state of the CAS server webflow for the callback
*/
private String relayStateParameterName = DEFAULT_RELAY_STATE_PARAMETER_NAME;
/**
* The prefix url of the CAS server
*/
private String casServerUrlPrefix = "";
private boolean artifactParameterOverPost = false;
private boolean eagerlyCreateSessions = true;
private List safeParameters;
private LogoutStrategy logoutStrategy = isServlet30() ? new Servlet30LogoutStrategy() : new Servlet25LogoutStrategy();
public void setSessionMappingStorage(final SessionMappingStorage storage) {
this.sessionMappingStorage = storage;
}
public void setArtifactParameterOverPost(final boolean artifactParameterOverPost) {
this.artifactParameterOverPost = artifactParameterOverPost;
}
public SessionMappingStorage getSessionMappingStorage() {
return this.sessionMappingStorage;
}
/**
* @param name Name of the authentication token parameter.
*/
public void setArtifactParameterName(final String name) {
this.artifactParameterName = name;
}
/**
* @param name Name of parameter containing CAS logout request message for back channel SLO.
*/
public void setLogoutParameterName(final String name) {
this.logoutParameterName = name;
}
/**
* @param casServerUrlPrefix The prefix url of the CAS server.
*/
public void setCasServerUrlPrefix(final String casServerUrlPrefix) {
this.casServerUrlPrefix = casServerUrlPrefix;
}
/**
* @param name Name of parameter containing CAS logout request message for front channel SLO.
*/
public void setFrontLogoutParameterName(final String name) {
this.frontLogoutParameterName = name;
}
/**
* @param name Name of parameter containing the state of the CAS server webflow.
*/
public void setRelayStateParameterName(final String name) {
this.relayStateParameterName = name;
}
public void setEagerlyCreateSessions(final boolean eagerlyCreateSessions) {
this.eagerlyCreateSessions = eagerlyCreateSessions;
}
public void setGatewayNotesUrl(String gatewayNotesUrl) {
this.gatewayNotesUrl = gatewayNotesUrl;
}
public void setCluterLogoutPower(boolean cluterLogoutPower) {
this.cluterLogoutPower = cluterLogoutPower;
}
public void setAppName(String appName) {
this.appName = appName;
}
public void setApiKey(String apiKey) {
this.apiKey = apiKey;
}
/**
* Initializes the component for use.
*/
public synchronized void init() {
if (this.safeParameters == null) {
CommonUtils.assertNotNull(this.artifactParameterName, "artifactParameterName cannot be null.");
CommonUtils.assertNotNull(this.logoutParameterName, "logoutParameterName cannot be null.");
CommonUtils.assertNotNull(this.frontLogoutParameterName, "frontLogoutParameterName cannot be null.");
CommonUtils.assertNotNull(this.sessionMappingStorage, "sessionMappingStorage cannot be null.");
CommonUtils.assertNotNull(this.relayStateParameterName, "relayStateParameterName cannot be null.");
CommonUtils.assertNotNull(this.casServerUrlPrefix, "casServerUrlPrefix cannot be null.");
if (CommonUtils.isBlank(this.casServerUrlPrefix)) {
logger.warn("Front Channel single sign out redirects are disabled when the 'casServerUrlPrefix' value is not set.");
}
if (this.artifactParameterOverPost) {
this.safeParameters = Arrays.asList(this.logoutParameterName, this.artifactParameterName);
} else {
this.safeParameters = Arrays.asList(this.logoutParameterName);
}
}
}
/**
* Determines whether the given request contains an authentication token.
*
* @param request HTTP reqest.
* @return True if request contains authentication token, false otherwise.
*/
private boolean isTokenRequest(final HttpServletRequest request) {
return CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.artifactParameterName,
this.safeParameters));
}
/**
* Determines whether the given request is a CAS back channel logout request.
*
* @param request HTTP request.
* @return True if request is logout request, false otherwise.
*/
private boolean isBackChannelLogoutRequest(final HttpServletRequest request) {
return "POST".equals(request.getMethod())
&& !isMultipartRequest(request)
&& CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.logoutParameterName,
this.safeParameters));
}
/**
* Determines whether the given request is a CAS front channel logout request. Front Channel log out requests are only supported
* when the 'casServerUrlPrefix' value is set.
*
* @param request HTTP request.
* @return True if request is logout request, false otherwise.
*/
private boolean isFrontChannelLogoutRequest(final HttpServletRequest request) {
return "GET".equals(request.getMethod()) && CommonUtils.isNotBlank(this.casServerUrlPrefix)
&& CommonUtils.isNotBlank(CommonUtils.safeGetParameter(request, this.frontLogoutParameterName));
}
private boolean isClusterNodeLogoutRequest(final HttpServletRequest request) {
return "POST".equals(request.getMethod())
&& !isMultipartRequest(request)
&& (request.getParameter(DEFAULT_CLUSTER_NOTE_LOGOUT_PARAMETER) != null)
&& (request.getParameter(DEFAULT_CLUSTER_NOTE_LOGOUT_PARAMETER).equals(DEFAULT_CLUSTER_NOTE_LOGOUT_PARAMETER));
}
/**
* Process a request regarding the SLO process: record the session or destroy it.
*
* @param request the incoming HTTP request.
* @param response the HTTP response.
* @return if the request should continue to be processed.
*/
public boolean process(final HttpServletRequest request, final HttpServletResponse response) {
if (isTokenRequest(request)) {
logger.trace("Received a token request");
recordSession(request);
return true;
} else if (isBackChannelLogoutRequest(request)) {
logger.trace("Received a back channel logout request");
destroySession(request);
return false;
} else if (isFrontChannelLogoutRequest(request)) {
logger.trace("Received a front channel logout request");
destroySession(request);
// redirection url to the CAS server
final String redirectionUrl = computeRedirectionToServer(request);
if (redirectionUrl != null) {
CommonUtils.sendRedirect(response, redirectionUrl);
}
return false;
} else if (isClusterNodeLogoutRequest(request)) {
logger.trace("Received a node logout request");
destorySessionFromClusterNode(request);
return false;
} else {
logger.trace("Ignoring URI for logout: {}", request.getRequestURI());
return true;
}
}
/**
* Associates a token request with the current HTTP session by recording the mapping
* in the the configured {@link SessionMappingStorage} container.
*
* @param request HTTP request containing an authentication token.
*/
private void recordSession(final HttpServletRequest request) {
final HttpSession session = request.getSession(this.eagerlyCreateSessions);
if (session == null) {
logger.debug("No session currently exists (and none created). Cannot record session information for single sign out.");
return;
}
final String token = CommonUtils.safeGetParameter(request, this.artifactParameterName, this.safeParameters);
logger.debug("Recording session for token {}", token);
try {
this.sessionMappingStorage.removeBySessionById(session.getId());
} catch (final Exception e) {
// ignore if the session is already marked as invalid. Nothing we can do!
}
sessionMappingStorage.addSessionById(token, session);
}
/**
* Uncompress a logout message (base64 + deflate).
*
* @param originalMessage the original logout message.
* @return the uncompressed logout message.
*/
private String uncompressLogoutMessage(final String originalMessage) {
final byte[] binaryMessage = Base64.decodeBase64(originalMessage);
Inflater decompresser = null;
try {
// decompress the bytes
decompresser = new Inflater();
decompresser.setInput(binaryMessage);
final byte[] result = new byte[binaryMessage.length * DECOMPRESSION_FACTOR];
final int resultLength = decompresser.inflate(result);
// decode the bytes into a String
return new String(result, 0, resultLength, "UTF-8");
} catch (final Exception e) {
logger.error("Unable to decompress logout message", e);
throw new RuntimeException(e);
} finally {
if (decompresser != null) {
decompresser.end();
}
}
}
/**
* Destroys the current HTTP session for the given CAS logout request.
*
* @param request HTTP request containing a CAS logout message.
*/
private void destroySession(final HttpServletRequest request) {
final String logoutMessage;
// front channel logout -> the message needs to be base64 decoded + decompressed
if (isFrontChannelLogoutRequest(request)) {
logoutMessage = uncompressLogoutMessage(CommonUtils.safeGetParameter(request,
this.frontLogoutParameterName));
} else {
logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
}
logger.info("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) {
logger.debug("session in this node,invalidate the session");
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 {
logger.debug("Session not in this node");
logger.debug("The clusterLogoutPower is: " + this.cluterLogoutPower);
// 判断是否开启了集群登出开关
if (this.cluterLogoutPower) {
logger.debug("Call node logout requests.");
destorySessionOfClusterNodes(token);
}
}
}
}
private void destorySessionOfClusterNodes(String token) {
List clusterNodeUrls = this.getClusterNoteList(this.appName);
if (CollectionUtils.isNotEmpty(clusterNodeUrls)) {
for (String clusterNodeUrl : clusterNodeUrls) {
logger.debug(clusterNodeUrl);
HttpClient client = new DefaultHttpClient();
HttpPost post = new HttpPost(clusterNodeUrl);
List params = Lists.newArrayList();
params.add(new BasicNameValuePair(CLUSTER_NOTES_PARAM_TOKEN, token));
params.add(new BasicNameValuePair(DEFAULT_CLUSTER_NOTE_LOGOUT_PARAMETER, DEFAULT_CLUSTER_NOTE_LOGOUT_PARAMETER));
try {
logger.debug("Call url [{}] for invalidate session", clusterNodeUrl);
post.setEntity(new UrlEncodedFormEntity(params));
client.execute(post);
} catch (Exception e) {
logger.debug("Error destorySessionOfClusterNodes", e);
} finally {
HttpClientUtils.closeQuietly(client);
}
}
}
}
private List getClusterNoteList(String appName) {
logger.debug("Call gateway for note urls");
List list = null;
HttpClient client = new DefaultHttpClient();
String url = gatewayNotesUrl + "?" + GATEWAY_PARAMS_APPNAME + "=" + appName;
HttpGet httpGet = new HttpGet(url);
httpGet.setHeader(GATEWAY_HEADERS_APIKEY, apiKey);
List params = Lists.newArrayList();
params.add(new BasicNameValuePair(GATEWAY_PARAMS_APPNAME, this.appName));
try {
String response = EntityUtils.toString(client.execute(httpGet).getEntity());
list = this.getClusterNotesUrls(response);
} catch (Exception e) {
logger.debug("Error to load Cluster nodes by appName [{}] ", appName);
} finally {
HttpClientUtils.closeQuietly(client);
}
return list;
}
private List getClusterNotesUrls(String responseMgr) {
List lists = Lists.newArrayList();
if (responseMgr != null && responseMgr.trim().length() != 0) {
try {
JSONObject responseMsg = (JSONObject) JSONObject.parse(responseMgr);
String code = responseMsg.getString("code");
String status = responseMsg.getString("status");
JSONArray result = responseMsg.getJSONArray("result");
if (GATEWAY_RESULT_CODE_SUCC.equals(code) && GATEWAY_RESULT_STATUS_SUCC.equals(status)) {//调用成功
Iterator iterator = result.iterator();
while (iterator.hasNext()) {
JSONObject obj = (JSONObject) iterator.next();
StringBuffer url = new StringBuffer("http://");
url.append(obj.getString("ip"));
String port = obj.getString("port");
if (StringUtils.isNotEmpty(port)) {
url.append(":" + port);
}
lists.add(url.toString());
}
}
} catch (Exception e) {
logger.debug("Error to Parsing Cluster note list.");
}
}
return lists;
}
private void destorySessionFromClusterNode(final HttpServletRequest request) {
String token = request.getParameter(CLUSTER_NOTES_PARAM_TOKEN);
if (StringUtils.isNotEmpty(token)) {
final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);
if (session != null) {
String sessionID = session.getId();
logger.debug("Invalidating node session [{}] for token [{}]", sessionID, token);
try {
session.invalidate();
} catch (final IllegalStateException e) {
logger.debug("Error invalidating session.", e);
}
}
}
}
/**
* Compute the redirection url to the CAS server when it's a front channel SLO
* (depending on the relay state parameter).
*
* @param request The HTTP request.
* @return the redirection url to the CAS server.
*/
private String computeRedirectionToServer(final HttpServletRequest request) {
final String relayStateValue = CommonUtils.safeGetParameter(request, this.relayStateParameterName);
// if we have a state value -> redirect to the CAS server to continue the logout process
if (StringUtils.isNotBlank(relayStateValue)) {
final StringBuilder buffer = new StringBuilder();
buffer.append(casServerUrlPrefix);
if (!this.casServerUrlPrefix.endsWith("/")) {
buffer.append("/");
}
buffer.append("logout?_eventId=next&");
buffer.append(this.relayStateParameterName);
buffer.append("=");
buffer.append(CommonUtils.urlEncode(relayStateValue));
final String redirectUrl = buffer.toString();
logger.debug("Redirection url to the CAS server: {}", redirectUrl);
return redirectUrl;
}
return null;
}
private boolean isMultipartRequest(final HttpServletRequest request) {
return request.getContentType() != null && request.getContentType().toLowerCase().startsWith("multipart");
}
private static boolean isServlet30() {
try {
return HttpServletRequest.class.getMethod("logout") != null;
} catch (final NoSuchMethodException e) {
return false;
}
}
/**
* Abstracts the ways we can force logout with the Servlet spec.
*/
private interface LogoutStrategy {
void logout(HttpServletRequest request);
}
private class Servlet25LogoutStrategy implements LogoutStrategy {
public void logout(final HttpServletRequest request) {
// nothing additional to do here
}
}
private class Servlet30LogoutStrategy implements LogoutStrategy {
public void logout(final HttpServletRequest request) {
try {
request.logout();
} catch (final ServletException e) {
logger.debug("Error performing request.logout.");
}
}
}
public static void main(String[] args) {
String s = "{\n" +
" \"code\": \"000000\", \n" +
" \"message\": null, \n" +
" \"result\": [\n" +
" {\n" +
" \"ip\": \"192.168.1.1\", \n" +
" \"port\": \"1111\"\n" +
" }, \n" +
" {\n" +
" \"ip\": \"192.168.1.2\", \n" +
" \"port\": \"1111\"\n" +
" }, \n" +
" {\n" +
" \"ip\": \"192.168.1.3\", \n" +
" \"port\": \"1111\"\n" +
" }, \n" +
" {\n" +
" \"ip\": \"192.168.1.4\", \n" +
" \"port\": \"1111\"\n" +
" }\n" +
" ], \n" +
" \"status\": \"OK\"\n" +
"}";
List list = new SingleSignOutHandler().getClusterNotesUrls(s);
for (String url : list) {
System.out.println(url);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy