Apereo CAS :自定义单点退出逻辑,增加自定义参数

Apereo CAS 与第三方单点退出成功后,默认返回的信息比较少或者可用的信息不多,使得第三方在编写接入的逻辑代码时,会比较不便。因此,我们需要改造 Apereo CAS 登录方法,追加 扩展信息

  • 1、修改 Apereo CAS 的登录方法,追加 扩展信息
  • 2、修改 Apereo CAS 的验票方法 /p3/validate,实时获取Redis缓存中的动态信息

1、引入 Maven 依赖

Apereo CAS 当前主要是采用 Overlay 的方式进行扩展。要想找到单点退出相关的对象,需要将相关的依赖项添加到项目的 Maven pom.xml 文件中:

<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-core-logout-api</artifactId>
    <version>${cas.version}</version>
</dependency>
<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-core-logout</artifactId>
    <version>${cas.version}</version>
</dependency>
<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-core-api-logout</artifactId>
    <version>${cas.version}</version>
</dependency>

2、分析源码

单点登出

根据官网文档 https://apereo.github.io/cas/7.0.x/installation/Logout-Single-Signout.html SLO 退出分为2种,Back Channel 和Front Channel,我这里使用的是Back Channel,其原理也很简单就是在每个service下配置一个回调地址,当触发退出时,如果这个服务验证过st,那么系统就会认为其已经保存了登录信息,那么在触发单点登出时,调用配置的退出地址。

配置service

我这里没有使用静态json配置service的方法,在resource/services 下添加你要接入cas的配置文件,文件名为
localhost-10000003.json,其中这里的localhost必须和下面配置文件的name对上,相应的10000003也必须和下面配置文件中的id对上,evaluationOrder为配置优先级,当一个接入的url同事满足多个配置文件时,值越小,优先级越高,还有一个配置单点登出的重要参数logoutUrl,这个就是触发登出时系统要去调用的子应用的回调地址,但实践当中我们的回调地址往往都需要带上一些凭证信息如用户id,所以用户id是动态生成的,CAS默认提供的逻辑只是静态的去调用配置的回调接口,我们要的是自定义登出逻辑,在配置的回调地址中加上参数,这里是加上用户id

{
 "@class" : "org.apereo.cas.services.RegexRegisteredService",
 "serviceId" : "http://127.0.0.1.*",
 "name" : "localhost",
 "id" : 10000003,
 "description" : "localhost",
 "evaluationOrder" : 1,
 "theme":"theme_default",
 "logoutUrl":"http://127.0.0.1:11202/logout"

}
定制单点登出回调逻辑

官网并没有对如何实现单点登出给出更多的文档,因为cas是通过webflow来实现的,通过跟踪具体的webflow配置信息就可以看出他的逻辑

DefaultLogoutWebflowConfigurer
这个类是单点退出的webflow配置入口,在这个类中,可以找到以下代码,其中terminateSessionAction就是处理单点退出所在的类

ActionState actionState = this.createActionState(flow, “terminateSession”, “terminateSessionAction”);

1

我们进入TerminateSessionAction 这个类 在terminate方法下有一行,根据方法名我们可以知道这里执行的是销毁TGT凭证的地方

List<LogoutRequest> logoutRequests = this.centralAuthenticationService.destroyTicketGrantingTicket(tgtId);

我们找到CentralAuthenticationService 这个接口,发现其默认只有一个实现类DefaultCentralAuthenticationService,在其的destroyTicketGrantingTicket方法中我们可以看到,如下代码,通过查看源码,发现this.logoutManager.performLogout(ticket)中执行的就是单点退出的逻辑。

public List<LogoutRequest> destroyTicketGrantingTicket(String ticketGrantingTicketId) {
       try {
           LOGGER.debug("Removing ticket [{}] from registry...", ticketGrantingTicketId);
           TicketGrantingTicket ticket = (TicketGrantingTicket)this.getTicket(ticketGrantingTicketId, TicketGrantingTicket.class);
           LOGGER.debug("Ticket found. Processing logout requests and then deleting the ticket...");
           AuthenticationCredentialsThreadLocalBinder.bindCurrent(ticket.getAuthentication());
           List<LogoutRequest> logoutRequests = this.logoutManager.performLogout(ticket);
           this.deleteTicket(ticketGrantingTicketId);
           this.doPublishEvent(new CasTicketGrantingTicketDestroyedEvent(this, ticket));
           return logoutRequests;
       } catch (InvalidTicketException var4) {
           LOGGER.debug("TicketGrantingTicket [{}] cannot be found in the ticket registry.", ticketGrantingTicketId);
           return new ArrayList(0);
       }
   }

我们进入其唯一实现类DefaultLogoutManager,找到 performLogout 方法,下面我做注释,注释的地方就是执行单点登出的地方

public List<LogoutRequest> performLogout(TicketGrantingTicket ticket) {
        LOGGER.info("Performing logout operations for [{}]", ticket.getId());
        if (this.singleLogoutCallbacksDisabled) {
            LOGGER.info("Single logout callbacks are disabled");
            return new ArrayList(0);
        } else {
            //执行单点退出逻辑
            List<LogoutRequest> logoutRequests = this.performLogoutForTicket(ticket);
            this.logoutExecutionPlan.getLogoutHandlers().forEach((h) -> {
                LOGGER.debug("Invoking logout handler [{}] to process ticket [{}]", h.getClass().getSimpleName(), ticket.getId());
                h.handle(ticket);
            });
            LOGGER.info("[{}] logout requests were processed", logoutRequests.size());
            return logoutRequests;
        }
    }

现在我们再进入performLogoutForTicket 方法,下面注释出已经标出了执行单点退出的地址

private List<LogoutRequest> performLogoutForTicket(TicketGrantingTicket ticketToBeLoggedOut) {
        Stream<Map<String, Service>> streamServices = Stream.concat(Stream.of(ticketToBeLoggedOut.getServices()), Stream.of(ticketToBeLoggedOut.getProxyGrantingTickets()));
        return (List)streamServices.map(Map::entrySet).flatMap(Collection::stream).filter((entry) -> {
            return entry.getValue() instanceof WebApplicationService;
        }).map((entry) -> {
            WebApplicationService service = (WebApplicationService)entry.getValue();
            LOGGER.debug("Handling single logout callback for [{}]", service);
            //具体执行单点退出的地方
            return this.singleLogoutServiceMessageHandler.handle(service, (String)entry.getKey());
        }).flatMap(Collection::stream).filter(Objects::nonNull).collect(Collectors.toList());
    }
增加用户参数

单点登出实践 重写 SingleLogoutServiceMessageHandler

public class LogoutHandler  implements SingleLogoutServiceMessageHandler {

    private HttpClient httpClient;

    private ServicesManager servicesManager;

    private TicketRegistry ticketRegistry;



    public LogoutHandler(HttpClient httpClient, ServicesManager servicesManager, TicketRegistry ticketRegistry) {
        this.httpClient = httpClient;
        this.servicesManager = servicesManager;
        this.ticketRegistry = ticketRegistry;

    }

    @Override
    public Collection<LogoutRequest> handle(WebApplicationService singleLogoutService, String ticketId) {
        //根据ticketId获取到ST
        ServiceTicket serviceTicket =(ServiceTicket)ticketRegistry.getTicket(ticketId);
        //根据ST获取到TGT
        TicketGrantingTicket ticketGrantingTicket=serviceTicket.getTicketGrantingTicket();
        //用TGT获取用户id
        String userId=ticketGrantingTicket.getAuthentication().getPrincipal().getId();
        Collection<LogoutRequest> logoutRequests=new ArrayList<>();
        //获取当前有多少服务使用TGT去生成ST,因为单点的统一凭证就是TGT,一次登录使用的都是同一个TGT,所以可以使用TGT发现有多少个自服务登录过系统
        for(Map.Entry<String, Service> entry: ticketGrantingTicket.getServices().entrySet()){
          //查询对应系统配置的信息
            RegisteredService registeredService = servicesManager.findServiceBy(entry.getValue());
            //获取到前面配置的service的回调地址并且加上userId作为参数
            String url=registeredService.getLogoutUrl()+"?userId="+userId;
            try {
              //发送消息
                HttpMessage message =httpClient.sendMessageToEndPoint(new URL(url));
                DefaultLogoutRequest defaultLogoutRequest=new DefaultLogoutRequest(ticketId,singleLogoutService,new URL(url));
                logoutRequests.add(defaultLogoutRequest);
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
        }

        return logoutRequests;
    }
}

在Srping的LogoutManager中注入的LogoutHandler


  @Autowired
   private LogoutMessageCreator logoutBuilder;

  @Autowired
  @Qualifier("noRedirectHttpClient")
  private HttpClient httpClient;

  @Autowired
  @Qualifier("servicesManager")
  private ServicesManager servicesManager;

  @Autowired
  @Qualifier("ticketRegistry")
  private ObjectProvider<TicketRegistry> ticketRegistry;


@Autowired
   @Bean
   public LogoutManager logoutManager(@Qualifier("logoutExecutionPlan") final LogoutExecutionPlan logoutExecutionPlan) {
       return new DefaultLogoutManager(logoutBuilder, new LogoutHandler(httpClient,servicesManager,ticketRegistry.getIfAvailable()),
             false  , logoutExecutionPlan);
   }
参考资料:
作者:Jeebiz  创建时间:2024-04-24 17:58
最后编辑:Jeebiz  更新时间:2024-05-06 16:13