All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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