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会统统自动注销,完成对上一个登陆终端的踢出。