CAS 单用户登录(一个账号只能一个客户端登录)

                                         CAS 单用户登录

 该博客基于初次研究CAS单用户登录,网上资源较少,和https://blog.csdn.net/tian3559060/article/details/80166877博主探讨后进行完善CAS单用户登录过程中遇到的坑;

             一个系统中模块很多,并且每个模块相互独立,所以采用了SSO(单点登录),采用的是cas-server-webapp-5.2.4,但是在使用需求中,需要实现单用户登录(一个账号只能登录在一个客户端上);起初的因为不了解CAS,很天真的做法就是,每次登录是判断当前登录的用户信息在缓存中是否存在,如果存在就将缓存中的用户信息清除,并将缓存中对应的session信息注销掉,否则将当前用户信息缓存到服务端。但是后来做好之后,测试的时候,发现事情远没有想像中的那么简单,上边这种做法只是适用于单模块登录,但是集成CAS后会发现,cas登录过程中以及登录之后,每一次请求不在依赖session,而是ticket(票据);

CAS主要票据分为两种TGT(登录票据),ST(服务票据,后期完善CAS基础会写出);

        要实现单用户登录的需要解决的是:

        1,获取用户的id,TGT

        2,根据用户的id,认证方式,userName去找到关于该用的所有TGT

        3,  过滤掉非当前用户的TGT的所有TGT

        4,注销掉过滤的TGT(及只剩下当前用户的TGT)

      正文(注意:该博文只是基于单个CAS,不基于CAS和Shiro的集成)

        通过走查源码可以发现:
org.jasig.cas.CentralAuthenticationServiceImpl

       在这个类中首先先验证用户名,密码。然后进行创建TGT,ST,然后在缓存TGT,并且ST有效性的验证,以及销毁TGT都是在这个类中,所以这个类可以说是CAS的核心类了;

org.jasig.cas.ticket.registry.DefaultTicketRegistry  

      这个类主要是进行管理和存储TGT和ST,以及记录TGT和ST之间的关系,及通过tickeid查询ticket,添加ticket,删除ticked等

            了解了上边两个类,但是上边两个类都是在org.jasig.cas的jar中的,jar中的文件都是class文件无法进行修改,所以我们只能讲这两个类拿出来做单独操作,但是需要注意的是org.jasig.cas-server-webapp/WEB-INF/spirng-configuration/ticketRegistry.xml中 id为ticketRegistry 的class所指定的地址要和我们自定义的文件地址一致,否则我们定义的两个类是不会被引用的;改博文的配置是ticketRegistry.xml中 id为ticketRegistry 地址不变,定义的文件夹和地址一致;

    首先创建一个文件夹为org.jasig.cas放在java文件夹下,在该文件夹下在创建一个ticket.registry,文件夹中创建DefaultTicketRegistry;详情如下

/*
 * Licensed to Apereo under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Apereo 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 org.jasig.cas.ticket.registry;

import org.jasig.cas.ticket.ServiceTicket;
import org.jasig.cas.ticket.Ticket;
import org.jasig.cas.ticket.TicketGrantingTicket;
import org.jasig.cas.authentication.principal.Service;
import org.springframework.util.Assert;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Implementation of the TicketRegistry that is backed by a ConcurrentHashMap.
 *
 * @author Scott Battaglia
 * @since 3.0.0
 */
public final class DefaultTicketRegistry extends AbstractTicketRegistry  {

    /** A HashMap to contain the tickets. */
    private final Map<String, Ticket> cache;
    
    /** 保存用户名与票据ID对应关系 */
    private final Map<String, String> nameIdCache;

    /**
     * Instantiates a new default ticket registry.
     */
    public DefaultTicketRegistry() {
        this.cache = new ConcurrentHashMap<>();
        this.nameIdCache = new ConcurrentHashMap<>();
    }

    /**
     * Creates a new, empty registry with the specified initial capacity, load
     * factor, and concurrency level.
     *
     * @param initialCapacity - the initial capacity. The implementation
     * performs internal sizing to accommodate this many elements.
     * @param loadFactor - the load factor threshold, used to control resizing.
     * Resizing may be performed when the average number of elements per bin
     * exceeds this threshold.
     * @param concurrencyLevel - the estimated number of concurrently updating
     * threads. The implementation performs internal sizing to try to
     * accommodate this many threads.
     */
    public DefaultTicketRegistry(final int initialCapacity, final float loadFactor, final int concurrencyLevel) {
        this.cache = new ConcurrentHashMap<>(initialCapacity, loadFactor, concurrencyLevel);
        this.nameIdCache = new ConcurrentHashMap<>();
    }

    /**
     * {@inheritDoc}
     * @throws IllegalArgumentException if the Ticket is null.
     */
    @Override
    public void addTicket(final Ticket ticket) {
        Assert.notNull(ticket, "ticket cannot be null");

        logger.debug("Added ticket [{}] to registry.", ticket.getId());
        this.cache.put(ticket.getId(), ticket);
        if(ticket instanceof TicketGrantingTicket){
            String username = ((TicketGrantingTicket)ticket).getAuthentication().getPrincipal().toString().trim();
            this.nameIdCache.put(username, ticket.getId());
        }
    }

    @Override
    public Ticket getTicket(final String ticketId) {
        if (ticketId == null) {
            return null;
        }

        logger.debug("Attempting to retrieve ticket [{}]", ticketId);

        final Ticket ticket = this.cache.get(ticketId);

        if (ticket != null) {
            logger.debug("Ticket [{}] found in registry.", ticketId);
            return ticket;
        }

        final String tid = this.nameIdCache.get(ticketId);
        if(tid != null){
            final Ticket ticketU = this.cache.get(tid);

            if(ticketU != null) {
                logger.debug("Ticket [{}] found in registry.", ticketId);
                return ticketU;
            }
        }
        return null;
    }

    @Override
    public boolean deleteTicket(final String ticketId) {
        if (ticketId == null) {
            return false;
        }

        final Ticket ticket = getTicket(ticketId);
        if (ticket == null) {
            return false;
        }

        if (ticket instanceof TicketGrantingTicket) {
            logger.debug("Removing children of ticket [{}] from the registry.", ticket);
            deleteChildren((TicketGrantingTicket) ticket);
            // 删除用户名与票据ID关联
            String username = ((TicketGrantingTicket)ticket).getAuthentication().getPrincipal().toString().trim();
            this.nameIdCache.remove(username);
        }

        logger.debug("Removing ticket [{}] from the registry.", ticket);
        return (this.cache.remove(ticketId) != null);
    }

    /**
     * Delete TGT's service tickets.
     *
     * @param ticket the ticket
     */
    private void deleteChildren(final TicketGrantingTicket ticket) {
        // delete service tickets
        final Map<String, Service> services = ticket.getServices();
        if (services != null && !services.isEmpty()) {
            for (final Map.Entry<String, Service> entry : services.entrySet()) {
                if (this.cache.remove(entry.getKey()) != null) {
                    logger.trace("Removed service ticket [{}]", entry.getKey());
                } else {
                    logger.trace("Unable to remove service ticket [{}]", entry.getKey());
                }
            }
        }
    }

    public Collection<Ticket> getTickets() {
        return Collections.unmodifiableCollection(this.cache.values());
    }

    @Override
    public int sessionCount() {
        int count = 0;
        for (final Ticket t : this.cache.values()) {
            if (t instanceof TicketGrantingTicket) {
                count++;
            }
        }
        return count;
    }

    @Override
    public int serviceTicketCount() {
        int count = 0;
        for (final Ticket t : this.cache.values()) {
            if (t instanceof ServiceTicket) {
                count++;
            }
        }
        return count;
    }
}

上边的类主要修改的是在原来基础上添加上从如何缓存中获取到TGT,以及建立一个userName和TGT之间的关系;

注意,在org.jasig.cas目录下建立CentralAuthenticationServiceImpl,并实现CentralAuthenticationService接口,在CentralAuthenticationServiceImpl类中,主要是对createTicketGrantingTicket方法进行改写,改写如下

 
 
@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 Credential... credentials)
        throws AuthenticationException, TicketException {
    final Set<Credential> sanitizedCredentials = sanitizeCredentials(credentials);
    if (!sanitizedCredentials.isEmpty()) {
        final Authentication authentication = this.authenticationManager.authenticate(credentials);

        final TicketGrantingTicket ticketGrantingTicket = new TicketGrantingTicketImpl(
                this.ticketGrantingTicketUniqueTicketIdGenerator
                        .getNewTicketId(TicketGrantingTicket.PREFIX),
                authentication, this.ticketGrantingTicketExpirationPolicy);

        //找出用户id,并且不为当前tgt的,这里应当考虑数据性能,直接筛选用户再筛选tgt
        String id=ticketGrantingTicket.getAuthentication().getPrincipal().getId();
        String tgt=ticketGrantingTicket.getId();
        Collection<Ticket> tickets = this.ticketRegistry.getTickets();
        //进行循环比对tgt,筛选并注销当前username的非当前用户的TGT
        for (Ticket ticket : tickets) {
            if(ticket instanceof TicketGrantingTicket){
                TicketGrantingTicket t = ((TicketGrantingTicket)ticket).getRoot();
                 Authentication authentications=t.getAuthentication();
                if( t != null && authentication != null
                        && authentication.getPrincipal() != null && id.equals(authentications.getPrincipal().getId())
                        && !tgt.equals(t.getId())){
                    this.destroyTicketGrantingTicket(ticket.getId());
                };
            }
        }
        // 增加单用户登录限制,在使用过程中发现,因为jar包依赖版本问题,可能导致该方法无法获取到ticket,如果获取不到ticket,可以使用上面的方法进行获取并注销ticket
       /* Ticket ticket = this.ticketRegistry.getTicket(credentials[0].getId());
        if(ticket != null){
            this.destroyTicketGrantingTicket(ticket.getId());
        }*/
        // 增加当用户登录限制
        this.ticketRegistry.addTicket(ticketGrantingTicket);
        return ticketGrantingTicket;
    }
    final String msg = "No credentials were specified in the request for creating a new ticket-granting ticket";
    logger.warn(msg);
    throw new TicketCreationException(new IllegalArgumentException(msg));
}
这样,在新的TGT被加入缓存之前先进行一次验证,注销掉同一用户已有的TGT。由于CAS本身就对TGT和ST做了关联,所以TGT注销后通过它生成的ST会统统自动注销,完成对上一个登陆终端的踢出。


猜你喜欢

转载自blog.csdn.net/qq_34125349/article/details/80272826