1. 程式人生 > >CAS 單點登出 loginout 解決方案 -- 最靠譜的方案,不是抄的--還是不靠譜大家不要抄了

CAS 單點登出 loginout 解決方案 -- 最靠譜的方案,不是抄的--還是不靠譜大家不要抄了

mmp,從11年開始用cas,但是總是在退出的時候掉鏈子,各種掉線子 從2.x版本開始用現在都4.2.7版本都沒解決這個退出掉鏈子的事情,於是自己看原始碼解決了此問題。

    cas 預設的基於 httpclient http 通知的,通知的時候,服務端給客戶端發一個xml 裡面有一個ticketid,客戶端用ticketid做退出。實記使用過程中,你會碰到各種退不出來的問題,關閉瀏覽器在重試,又TMD好了,為了解決這個問題,我使用mq做通知,不用httpclient做通知了。

方案:

        重寫服務端登出session的部分程式碼。

   

package org.jasig.cas;

import com.alibaba.fastjson.JSONObject;
import com.codahale.metrics.annotation.Counted;
import com.codahale.metrics.annotation.Metered;
import com.codahale.metrics.annotation.Timed;
import org.jasig.cas.authentication.Authentication;
import org.jasig.cas.authentication.AuthenticationBuilder;
import org.jasig.cas.authentication.AuthenticationContext;
import org.jasig.cas.authentication.AuthenticationException;
import org.jasig.cas.authentication.DefaultAuthenticationBuilder;
import org.jasig.cas.authentication.MixedPrincipalException;
import org.jasig.cas.authentication.principal.Principal;
import org.jasig.cas.authentication.principal.Service;
import org.jasig.cas.logout.LogoutManager;
import org.jasig.cas.logout.LogoutRequest;
import org.jasig.cas.services.RegisteredService;
import org.jasig.cas.services.RegisteredServiceAttributeReleasePolicy;
import org.jasig.cas.services.ServiceContext;
import org.jasig.cas.services.ServicesManager;
import org.jasig.cas.services.UnauthorizedProxyingException;
import org.jasig.cas.services.UnauthorizedServiceForPrincipalException;
import org.jasig.cas.services.UnauthorizedSsoServiceException;
import org.jasig.cas.support.events.CasProxyGrantingTicketCreatedEvent;
import org.jasig.cas.support.events.CasProxyTicketGrantedEvent;
import org.jasig.cas.support.events.CasServiceTicketGrantedEvent;
import org.jasig.cas.support.events.CasServiceTicketValidatedEvent;
import org.jasig.cas.support.events.CasTicketGrantingTicketCreatedEvent;
import org.jasig.cas.support.events.CasTicketGrantingTicketDestroyedEvent;
import org.jasig.cas.ticket.AbstractTicketException;
import org.jasig.cas.ticket.InvalidTicketException;
import org.jasig.cas.ticket.ServiceTicket;
import org.jasig.cas.ticket.ServiceTicketFactory;
import org.jasig.cas.ticket.TicketFactory;
import org.jasig.cas.ticket.TicketGrantingTicket;
import org.jasig.cas.ticket.TicketGrantingTicketFactory;
import org.jasig.cas.ticket.UnrecognizableServiceForServiceTicketValidationException;
import org.jasig.cas.ticket.proxy.ProxyGrantingTicket;
import org.jasig.cas.ticket.proxy.ProxyGrantingTicketFactory;
import org.jasig.cas.ticket.proxy.ProxyTicket;
import org.jasig.cas.ticket.proxy.ProxyTicketFactory;
import org.jasig.cas.ticket.registry.TicketRegistry;
import org.jasig.cas.validation.Assertion;
import org.jasig.cas.validation.ImmutableAssertion;
import org.jasig.inspektr.audit.annotation.Audit;
import org.jose4j.json.internal.json_simple.JSONArray;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * Concrete implementation of a {@link CentralAuthenticationService}, and also the
 * central, organizing component of CAS's internal implementation.
 * This class is threadsafe.
 *
 * @author William G. Thompson, Jr.
 * @author Scott Battaglia
 * @author Dmitry Kopylenko
 * @author Misagh Moayyed
 * @since 3.0.0
 */
@Component("centralAuthenticationService")
@Transactional(readOnly = false, transactionManager = "ticketTransactionManager")
public class CentralAuthenticationServiceImpl extends AbstractCentralAuthenticationService {

    private static final long serialVersionUID = -8943828074939533986L;


    /** 廣播交換機名稱 */
    private final static String EXCHANGE_NAME = "session-destroy-notify";


    @Autowired
    private RabbitTemplate template;


    /**
     * Instantiates a new Central authentication service impl.
     */
    public CentralAuthenticationServiceImpl() {
        super();
    }

    /**
     * Build the central authentication service implementation.
     *
     * @param ticketRegistry  the tickets registry.
     * @param ticketFactory   the ticket factory
     * @param servicesManager the services manager.
     * @param logoutManager   the logout manager.
     */
    public CentralAuthenticationServiceImpl(
            final TicketRegistry ticketRegistry,
            final TicketFactory ticketFactory,
            final ServicesManager servicesManager,
            final LogoutManager logoutManager) {

        super(ticketRegistry, ticketFactory, servicesManager, logoutManager);
    }

    /**
     * {@inheritDoc}
     * Destroy a TicketGrantingTicket and perform back channel logout. This has the effect of invalidating any
     * Ticket that was derived from the TicketGrantingTicket being destroyed. May throw an
     * {@link IllegalArgumentException} if the TicketGrantingTicket ID is null.
     *
     * @param ticketGrantingTicketId the id of the ticket we want to destroy
     * @return the logout requests.
     */
    @Audit(
            action = "TICKET_GRANTING_TICKET_DESTROYED",
            actionResolverName = "DESTROY_TICKET_GRANTING_TICKET_RESOLVER",
            resourceResolverName = "DESTROY_TICKET_GRANTING_TICKET_RESOURCE_RESOLVER")
    @Timed(name = "DESTROY_TICKET_GRANTING_TICKET_TIMER")
    @Metered(name = "DESTROY_TICKET_GRANTING_TICKET_METER")
    @Counted(name = "DESTROY_TICKET_GRANTING_TICKET_COUNTER", monotonic = true)
    @Override
    public List<LogoutRequest> destroyTicketGrantingTicket(@NotNull final String ticketGrantingTicketId) {
        try {
            logger.debug("Removing ticket [{}] from registry...", ticketGrantingTicketId);
            final TicketGrantingTicket ticket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
            logger.debug("Ticket found. Processing logout requests and then deleting the ticket...");
            final List<LogoutRequest> logoutRequests = logoutManager.performLogout(ticket);
            this.ticketRegistry.deleteTicket(ticketGrantingTicketId);


            /*------------start-------TODO-------增加向訊息佇列中傳送ticketId--------------------------*/
            JSONObject jsonObject = new JSONObject();
            JSONArray ticketIds = new JSONArray();
            jsonObject.put("ticketIds",ticketIds);
            for (LogoutRequest logoutRequest : logoutRequests) {
                ticketIds.add(logoutRequest.getTicketId());
            }
            template.convertAndSend(jsonObject.toJSONString());
            /*-------------end--------TODO-------增加向訊息佇列中傳送ticketId--------------------------*/


            doPublishEvent(new CasTicketGrantingTicketDestroyedEvent(this, ticket));
            return logoutRequests;
        } catch (final InvalidTicketException e) {
            logger.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId);
        }
        return Collections.emptyList();
    }

    @Audit(
            action = "SERVICE_TICKET",
            actionResolverName = "GRANT_SERVICE_TICKET_RESOLVER",
            resourceResolverName = "GRANT_SERVICE_TICKET_RESOURCE_RESOLVER")
    @Timed(name = "GRANT_SERVICE_TICKET_TIMER")
    @Metered(name = "GRANT_SERVICE_TICKET_METER")
    @Counted(name = "GRANT_SERVICE_TICKET_COUNTER", monotonic = true)
    @Override
    public ServiceTicket grantServiceTicket(
            final String ticketGrantingTicketId,
            final Service service, final AuthenticationContext context)
            throws AuthenticationException, AbstractTicketException {

        logger.debug("Attempting to get ticket id {} to create service ticket", ticketGrantingTicketId);
        final TicketGrantingTicket ticketGrantingTicket = getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
        final RegisteredService registeredService = this.servicesManager.findServiceBy(service);

        verifyRegisteredServiceProperties(registeredService, service);
        evaluatePossibilityOfMixedPrincipals(context, ticketGrantingTicket);

        if (ticketGrantingTicket.getCountOfUses() > 0 && !registeredService.getAccessStrategy().isServiceAccessAllowedForSso()) {
            logger.warn("Service [{}] is not allowed to use SSO.", service.getId());
            throw new UnauthorizedSsoServiceException();
        }

        evaluateProxiedServiceIfNeeded(service, ticketGrantingTicket, registeredService);

        // Perform security policy check by getting the authentication that satisfies the configured policy
        // This throws if no suitable policy is found
        logger.debug("Checking for authentication policy satisfaction...");
        getAuthenticationSatisfiedByPolicy(ticketGrantingTicket.getRoot(), new ServiceContext(service, registeredService));

        final List<Authentication> authentications = ticketGrantingTicket.getChainedAuthentications();
        final Principal principal = authentications.get(authentications.size() - 1).getPrincipal();

        logger.debug("Located principal {} for service ticket creation", principal);

        final RegisteredServiceAttributeReleasePolicy releasePolicy = registeredService.getAttributeReleasePolicy();
        final Map<String, Object> principalAttrs;
        if (releasePolicy != null) {
            principalAttrs = releasePolicy.getAttributes(principal);
        } else {
            principalAttrs = new HashMap<>();
        }

        if (!registeredService.getAccessStrategy().doPrincipalAttributesAllowServiceAccess(principal.getId(), principalAttrs)) {
            logger.warn("Cannot grant service ticket because Service [{}] is not authorized for use by [{}].",
                    service.getId(), principal);
            throw new UnauthorizedServiceForPrincipalException();
        }

        final ServiceTicketFactory factory = this.ticketFactory.get(ServiceTicket.class);
        final ServiceTicket serviceTicket = factory.create(ticketGrantingTicket, service,
                context != null && context.isCredentialProvided());
        logger.info("Granted ticket [{}] for service [{}] and principal [{}]",
                serviceTicket.getId(), service.getId(), principal.getId());

        this.ticketRegistry.addTicket(serviceTicket);
        logger.debug("Added service ticket {} to ticket registry", serviceTicket.getId());


        doPublishEvent(new CasServiceTicketGrantedEvent(this, ticketGrantingTicket, serviceTicket));

        return serviceTicket;
    }

    /**
     * Always keep track of a single authentication object,
     * as opposed to keeping a history of all. This helps with
     * memory consumption. Note that supplemental authentications
     * are to be removed.
     *
     * @param context              authentication context
     * @param ticketGrantingTicket the tgt
     * @return the processed authentication in the current context
     * @throws MixedPrincipalException in case there is a principal mismatch between TGT and the current authN.
     */
    private Authentication evaluatePossibilityOfMixedPrincipals(final AuthenticationContext context,
                                                                       final TicketGrantingTicket ticketGrantingTicket)
            throws MixedPrincipalException {
        Authentication currentAuthentication = null;
        if (context != null) {
            currentAuthentication = context.getAuthentication();
            if (currentAuthentication != null) {
                final Authentication original = ticketGrantingTicket.getAuthentication();
                if (!currentAuthentication.getPrincipal().equals(original.getPrincipal())) {
                    logger.debug("Principal associated with current authentication {} does not match "
                            + " the principal {} associated with the original authentication",
                            currentAuthentication.getPrincipal(), original.getPrincipal());
                    throw new MixedPrincipalException(
                            currentAuthentication, currentAuthentication.getPrincipal(), original.getPrincipal());
                }
                ticketGrantingTicket.getSupplementalAuthentications().clear();
                ticketGrantingTicket.getSupplementalAuthentications().add(currentAuthentication);
                logger.debug("Added authentication to the collection of supplemental authentications");
            }
        }
        return currentAuthentication;
    }

    @Audit(
            action = "PROXY_TICKET",
            actionResolverName = "GRANT_PROXY_TICKET_RESOLVER",
            resourceResolverName = "GRANT_PROXY_TICKET_RESOURCE_RESOLVER")
    @Timed(name = "GRANT_PROXY_TICKET_TIMER")
    @Metered(name = "GRANT_PROXY_TICKET_METER")
    @Counted(name = "GRANT_PROXY_TICKET_COUNTER", monotonic = true)
    @Override
    public ProxyTicket grantProxyTicket(final String proxyGrantingTicket, final Service service)
            throws AbstractTicketException {

        final ProxyGrantingTicket proxyGrantingTicketObject = getTicket(proxyGrantingTicket, ProxyGrantingTicket.class);
        final RegisteredService registeredService = this.servicesManager.findServiceBy(service);

        verifyRegisteredServiceProperties(registeredService, service);

        if (!registeredService.getAccessStrategy().isServiceAccessAllowedForSso()) {
            logger.warn("Service [{}] is not allowed to use SSO.", service.getId());
            throw new UnauthorizedSsoServiceException();
        }

        evaluateProxiedServiceIfNeeded(service, proxyGrantingTicketObject, registeredService);

        // Perform security policy check by getting the authentication that satisfies the configured policy
        // This throws if no suitable policy is found
        getAuthenticationSatisfiedByPolicy(proxyGrantingTicketObject.getRoot(), new ServiceContext(service, registeredService));

        final List<Authentication> authentications = proxyGrantingTicketObject.getChainedAuthentications();
        final Principal principal = authentications.get(authentications.size() - 1).getPrincipal();

        final RegisteredServiceAttributeReleasePolicy releasePolicy = registeredService.getAttributeReleasePolicy();
        final Map<String, Object> principalAttrs;
        if (releasePolicy != null) {
            principalAttrs = releasePolicy.getAttributes(principal);
        } else {
            principalAttrs = new HashMap<>();
        }

        if (!registeredService.getAccessStrategy().doPrincipalAttributesAllowServiceAccess(principal.getId(), principalAttrs)) {
            logger.warn("Cannot grant proxy ticket because Service [{}] is not authorized for use by [{}].",
                    service.getId(), principal);
            throw new UnauthorizedServiceForPrincipalException();
        }

        final ProxyTicketFactory factory = this.ticketFactory.get(ProxyTicket.class);
        final ProxyTicket proxyTicket = factory.create(proxyGrantingTicketObject, service);
        this.ticketRegistry.addTicket(proxyTicket);

        logger.info("Granted ticket [{}] for service [{}] for user [{}]",
                proxyTicket.getId(), service.getId(), principal.getId());

        doPublishEvent(new CasProxyTicketGrantedEvent(this, proxyGrantingTicketObject, proxyTicket));
        return proxyTicket;
    }

    @Audit(
            action = "PROXY_GRANTING_TICKET",
            actionResolverName = "CREATE_PROXY_GRANTING_TICKET_RESOLVER",
            resourceResolverName = "CREATE_PROXY_GRANTING_TICKET_RESOURCE_RESOLVER")
    @Timed(name = "CREATE_PROXY_GRANTING_TICKET_TIMER")
    @Metered(name = "CREATE_PROXY_GRANTING_TICKET_METER")
    @Counted(name = "CREATE_PROXY_GRANTING_TICKET_COUNTER", monotonic = true)
    @Override
    public ProxyGrantingTicket createProxyGrantingTicket(final String serviceTicketId, final AuthenticationContext context)
            throws AuthenticationException, AbstractTicketException {

        final ServiceTicket serviceTicket = this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class);

        if (serviceTicket == null || serviceTicket.isExpired()) {
            logger.debug("ServiceTicket [{}] has expired or cannot be found in the ticket registry", serviceTicketId);
            throw new InvalidTicketException(serviceTicketId);
        }

        final RegisteredService registeredService = this.servicesManager
                .findServiceBy(serviceTicket.getService());

        verifyRegisteredServiceProperties(registeredService, serviceTicket.getService());

        if (!registeredService.getProxyPolicy().isAllowedToProxy()) {
            logger.warn("ServiceManagement: Service [{}] attempted to proxy, but is not allowed.", serviceTicket.getService().getId());
            throw new UnauthorizedProxyingException();
        }

        final Authentication authentication = context.getAuthentication();
        final ProxyGrantingTicketFactory factory = this.ticketFactory.get(ProxyGrantingTicket.class);
        final ProxyGrantingTicket proxyGrantingTicket = factory.create(serviceTicket, authentication);

        logger.debug("Generated proxy granting ticket [{}] based off of [{}]", proxyGrantingTicket, serviceTicketId);
        this.ticketRegistry.addTicket(proxyGrantingTicket);

        doPublishEvent(new CasProxyGrantingTicketCreatedEvent(this, proxyGrantingTicket));

        return proxyGrantingTicket;

    }

    @Audit(
            action = "SERVICE_TICKET_VALIDATE",
            actionResolverName = "VALIDATE_SERVICE_TICKET_RESOLVER",
            resourceResolverName = "VALIDATE_SERVICE_TICKET_RESOURCE_RESOLVER")
    @Timed(name = "VALIDATE_SERVICE_TICKET_TIMER")
    @Metered(name = "VALIDATE_SERVICE_TICKET_METER")
    @Counted(name = "VALIDATE_SERVICE_TICKET_COUNTER", monotonic = true)
    @Override
    public Assertion validateServiceTicket(final String serviceTicketId, final Service service) throws AbstractTicketException {
        final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
        verifyRegisteredServiceProperties(registeredService, service);

        final ServiceTicket serviceTicket = this.ticketRegistry.getTicket(serviceTicketId, ServiceTicket.class);

        if (serviceTicket == null) {
            logger.info("Service ticket [{}] does not exist.", serviceTicketId);
            throw new InvalidTicketException(serviceTicketId);
        }

        try {
            synchronized (serviceTicket) {
                if (serviceTicket.isExpired()) {
                    logger.info("ServiceTicket [{}] has expired.", serviceTicketId);
                    throw new InvalidTicketException(serviceTicketId);
                }

                if (!serviceTicket.isValidFor(service)) {
                    logger.error("Service ticket [{}] with service [{}] does not match supplied service [{}]",
                            serviceTicketId, serviceTicket.getService().getId(), service);
                    throw new UnrecognizableServiceForServiceTicketValidationException(serviceTicket.getService());
                }
            }

            final TicketGrantingTicket root = serviceTicket.getGrantingTicket().getRoot();
            final Authentication authentication = getAuthenticationSatisfiedByPolicy(
                    root, new ServiceContext(serviceTicket.getService(), registeredService));
            final Principal principal = authentication.getPrincipal();

            final RegisteredServiceAttributeReleasePolicy attributePolicy = registeredService.getAttributeReleasePolicy();
            logger.debug("Attribute policy [{}] is associated with service [{}]", attributePolicy, registeredService);

            @SuppressWarnings("unchecked")
            final Map<String, Object> attributesToRelease = attributePolicy != null
                    ? attributePolicy.getAttributes(principal) : Collections.EMPTY_MAP;

            final String principalId = registeredService.getUsernameAttributeProvider().resolveUsername(principal, service);
            final Principal modifiedPrincipal = this.principalFactory.createPrincipal(principalId, attributesToRelease);
            final AuthenticationBuilder builder = DefaultAuthenticationBuilder.newInstance(authentication);
            builder.setPrincipal(modifiedPrincipal);

            final Assertion assertion = new ImmutableAssertion(
                    builder.build(),
                    serviceTicket.getGrantingTicket().getChainedAuthentications(),
                    serviceTicket.getService(),
                    serviceTicket.isFromNewLogin());

            doPublishEvent(new CasServiceTicketValidatedEvent(this, serviceTicket, assertion));

            return assertion;

        } finally {
            if (serviceTicket.isExpired()) {
                this.ticketRegistry.deleteTicket(serviceTicketId);
            }
        }
    }

    @Audit(
            action = "TICKET_GRANTING_TICKET",
            actionResolverName = "CREATE_TICKET_GRANTING_TICKET_RESOLVER",
            resourceResolverName = "CREATE_TICKET_GRANTING_TICKET_RESOURCE_RESOLVER")
    @Timed(name = "CREATE_TICKET_GRANTING_TICKET_TIMER")
    @Metered(name = "CREATE_TICKET_GRANTING_TICKET_METER")
    @Counted(name = "CREATE_TICKET_GRANTING_TICKET_COUNTER", monotonic = true)
    @Override
    public TicketGrantingTicket createTicketGrantingTicket(final AuthenticationContext context)
            throws AuthenticationException, AbstractTicketException {

        final Authentication authentication = context.getAuthentication();
        final TicketGrantingTicketFactory factory = this.ticketFactory.get(TicketGrantingTicket.class);
        final TicketGrantingTicket ticketGrantingTicket = factory.create(authentication);

        this.ticketRegistry.addTicket(ticketGrantingTicket);

        doPublishEvent(new CasTicketGrantingTicketCreatedEvent(this, ticketGrantingTicket));

        return ticketGrantingTicket;
    }

    /**
     * 測試rabbitMq
     */
    /*@PostConstruct
    public void aaa(){
        for (int i = 0; i < 10; i++) {
            JSONObject ticketObj = new JSONObject();
            ticketObj.put("ticketId",i);
            template.convertAndSend(ticketObj.toJSONString());
        }

    }*/
}r

    

    然後重寫客戶管的銷燬部分程式碼。

package com.yzb.mq;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.jasig.cas.client.session.SingleSignOutHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpSession;

/**
 * @author xiaoh
 * @version [版本號, 2018/4/26 20:18]
 * @Description:
 * @versio 1.0
 * 西安優智泊物聯網技術服務有限公司
 * Copyright (c) 2017 All Rights Reserved.
 */
@Component
public class CasQueue implements InitializingBean {

    @Autowired
    private MqRadioService mqRadioService;

    protected final transient Logger logger = LoggerFactory.getLogger(this.getClass());

    private SingleSignOutHandler singleSignOutHandler;


    public void onAccessTokenDes(JSONObject message) {
        JSONArray ticketIds = message.getJSONArray("ticketIds");
        for (Object ticketId : ticketIds) {
            final HttpSession session = singleSignOutHandler.getSessionMappingStorage().removeSessionByMappingId((String) ticketId);
            if (session != null) {
                final String sessionID = session.getId();
                logger.debug("Invalidating session [{}] for token [{}]", sessionID, ticketId);
                try {
                    session.invalidate();
                } catch (final IllegalStateException e) {
                    logger.debug("Error invalidating session.", e);
                }
            }
        }
    }

    public void setSingleSignOutHandler(SingleSignOutHandler singleSignOutHandler) {
        this.singleSignOutHandler = singleSignOutHandler;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        mqRadioService.registerRadioListener(this::onAccessTokenDes, "cas_exchange");
    }
}