CAS 4.1.10 版本服务端源码解读

在工作中经常会对CAS进行二次改造适应不同的单点登录场景。这篇文章主要对CAS 4.1.10版本进行源码解读(主要是登录流程)。不同版本可以在github下载。

一、准备

下载下来的cas-overlay-template的依赖中默认只有


  org.jasig.cas
  cas-server-webapp
  ${cas.version}
  war
  runtime

为了跟踪相关的代码还需要添加下面的两个依赖


  org.jasig.cas
  cas-server-core
  ${cas.version}
  
  
    org.jasig.cas
    cas-server-webapp-support
    ${cas.version}

二、源码解读

2.1 访问CAS服务端进行认证

CAS 是使用SpringMVC+Spring WebFlow(工作流框架)控制登录,登出流程的。

一般情况下,我们在浏览器访问http://localhost:8080/cas,cas 服务端会默认访问index.jsp页面

<%@ page language="java"  session="false" %>
<%
final String queryString = request.getQueryString();
final String url = request.getContextPath() + "/login" + (queryString != null ? "?" + queryString : "");
response.sendRedirect(response.encodeURL(url));%>

从上面index.jsp页面的内容发现,它会从定向到http://localhost:8080/cas/login,该路径是由名为cas 的 servlet进行处理的


    cas
    
        org.springframework.web.servlet.DispatcherServlet
    
    
        contextConfigLocation
        /WEB-INF/cas-servlet.xml, /WEB-INF/cas-servlet-*.xml
    
    
        publishContext
        false
    
    1


    cas
    /login

这里将请求交给了SpringMVC进行处理。

2.2 SpringMVC与Spring WebFlow整合

在上面的servlet配置中会发现,其核心的配置文件是/WEB-INF/cas-servlet.xml, /WEB-INF/cas-servlet-*.xml。SpringMVC在初始化的时候会去自动加载cas-servlet.xmlcas-servlet-*.xml配置。在WEB-INF目录下我们找到了cas-servlet.xml这个文件,里面对SpringMVC和Spring WebFlow进行了整合配置。如果想深入了解可以参考整合细节






  
      
          
      
  



  




  
    
      
      
    
  



  
      
          cas_views
      
  

如果对SpringMVC的请求路径是login,那么SpringMVC会交给webflow进行处理。flow-builder-services节点中有个view-factory-creator属性,该属性定义了视图解析工厂。该视图解析工厂是由视图解析器组成的。这里只定义了一个视图解析器,就是viewResolvers。该视图解析器是springFramework中的ResourceBundleViewResolver的一个实例,该类可以通过basenames属性,找到value值对应的properties属性文件,该文件中式类似ke=values类型的内容,正是该文件将jsp文件映射成视图名称。

2.3 CAS 登录流程解析

由上面的分析知道,登录流程的配置文件是/WEB-INF/webflow/login目录下的login-webflow


定义UsernamePasswordCredential类型的变量,用于存放用户名和密码(默认),可进行扩展存放更多的属性。


    

这是流程开始的操作,要去执行initialFlowSetupAction这个bean,它定义在cas-servlet.xml里面


org.jasig.cas.web.flow.InitialFlowSetupAction继承自AbstractActionAbstractAction方法是org.springframework.webflow.action包中的类,是webflow中的基础类。该类中的doExecute方法是对应处理业务的方法。

  protected Event doExecute(RequestContext context) throws Exception {
    HttpServletRequest request = WebUtils.getHttpServletRequest(context);
    String contextPath = context.getExternalContext().getContextPath();
    String cookiePath = StringUtils.isNotBlank(contextPath) ? contextPath + '/' : "/";
    if (StringUtils.isBlank(this.warnCookieGenerator.getCookiePath())) {
      this.logger.info("Setting path for cookies for warn cookie generator to: {} ", cookiePath);
      this.warnCookieGenerator.setCookiePath(cookiePath);
    } else {
      this.logger.debug("Warning cookie path is set to {} and path {}", this.warnCookieGenerator.getCookieDomain(), this.warnCookieGenerator.getCookiePath());
    }

    if (StringUtils.isBlank(this.ticketGrantingTicketCookieGenerator.getCookiePath())) {
      this.logger.info("Setting path for cookies for TGC cookie generator to: {} ", cookiePath);
      this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
    } else {
      this.logger.debug("TGC cookie path is set to {} and path {}", this.ticketGrantingTicketCookieGenerator.getCookieDomain(), this.ticketGrantingTicketCookieGenerator.getCookiePath());
    }
    //将TGT放在RequestScope作用域中
    WebUtils.putTicketGrantingTicketInScopes(context, this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));
    //将warnCookieValue放在RequestScope作用域中
    WebUtils.putWarningCookie(context, Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));
    //获取service参数
    Service service = WebUtils.getService(this.argumentExtractors, context);
    if (service != null) {
      this.logger.debug("Placing service in context scope: [{}]", service.getId());
      //查找注册的service
      RegisteredService registeredService = this.servicesManager.findServiceBy(service);
      if (registeredService != null && registeredService.getAccessStrategy().isServiceAccessAllowed()) {
        this.logger.debug("Placing registered service [{}] with id [{}] in context scope", registeredService.getServiceId(), registeredService.getId());
        WebUtils.putRegisteredService(context, registeredService);
        RegisteredServiceAccessStrategy accessStrategy = registeredService.getAccessStrategy();
        if (accessStrategy.getUnauthorizedRedirectUrl() != null) {
          this.logger.debug("Placing registered service's unauthorized redirect url [{}] with id [{}] in context scope", accessStrategy.getUnauthorizedRedirectUrl(), registeredService.getServiceId());
          WebUtils.putUnauthorizedRedirectUrl(context, accessStrategy.getUnauthorizedRedirectUrl());
        }
      }
    } else if (!this.enableFlowOnAbsentServiceRequest) {
      this.logger.warn("No service authentication request is available at [{}]. CAS is configured to disable the flow.", WebUtils.getHttpServletRequest(context).getRequestURL());
      throw new NoSuchFlowExecutionException(context.getFlowExecutionContext().getKey(), new UnauthorizedServiceException("screen.service.required.message", "Service is required"));
    }

    WebUtils.putService(context, service);
    return this.result("success");
  }

该方法的参数是RequestContext对象,该参数是一个流程的容器。该方法从request中获取TGT,并且构建一个临时的service对象(不同域注册的service,详情见接入系统管理)。并且,将TGT,warnCookieValue和service放在RequestContext作用域中,以便在登录流程中的state中进行判断


    
    
    
    

初始化完成后,登录流程流转到第一个state(ticketGrantingTicketExistsCheck)


它会去执行ticketGrantingTicketCheckdoExecute方法检查requestContext中是否存在TGT,TGT是否有效

protected Event doExecute(RequestContext requestContext) throws Exception {
    String tgtId = WebUtils.getTicketGrantingTicketId(requestContext);
    if (!StringUtils.hasText(tgtId)) {
      return new Event(this, "notExists");
    } else {
      String eventId = "invalid";

      try {
        //验证TGT是否有效
        Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class);
        if (ticket != null && !ticket.isExpired()) {
          eventId = "valid";
        }
      } catch (TicketException var5) {
        this.logger.trace("Could not retrieve ticket id {} from registry.", var5);
      }

      return new Event(this, eventId);
    }
  }

第一次访问应用系统http://app1.example.com,此时应用系统会跳转到CAS单点登录的服务器端http://127.0.0.1:8081/cas-server/login?service=http%3a%2f%2fapp1.example.com,此时,request的cookies中不存在CASTGC(TGT),因此RequestContext作用域中的ticketGrantingTicketId为null,登录流程流转到第二个state(gatewayRequestCheck)


    

初始化时,把service保存在了RequestContext作用域中,但request中的参数gateway不存在,登录流程流转到第三个state(serviceAuthorizationCheck)



    
    


执行它的doExecute方法

protected Event doExecute(RequestContext context) throws Exception {
    Service service = WebUtils.getService(context);
    if (service == null) {
      return this.success();
    } else if (this.servicesManager.getAllServices().isEmpty()) {
      String msg = String.format("No service definitions are found in the service manager. Service [%s] will not be automatically authorized to request authentication.", service.getId());
      this.logger.warn(msg);
      throw new UnauthorizedServiceException("screen.service.empty.error.message");
    } else {
      RegisteredService registeredService = this.servicesManager.findServiceBy(service);
      String msg;
      if (registeredService == null) {
        msg = String.format("Service Management: Unauthorized Service Access. Service [%s] is not found in service registry.", service.getId());
        this.logger.warn(msg);
        throw new UnauthorizedServiceException("screen.service.error.message", msg);
      } else if (!registeredService.getAccessStrategy().isServiceAccessAllowed()) {
        msg = String.format("Service Management: Unauthorized Service Access. Service [%s] is not allowed access via the service registry.", service.getId());
        this.logger.warn(msg);
        WebUtils.putUnauthorizedRedirectUrlIntoFlowScope(context, registeredService.getAccessStrategy().getUnauthorizedRedirectUrl());
        throw new UnauthorizedServiceException("screen.service.error.message", msg);
      } else {
        return this.success();
      }
    }
  }

doExecute方法,要做的就是判断RequestContext作用域中是否存在service,如果service存在,查找service的注册信息。登录流程流转到第四个state(generateLoginTicket)


    
    


执行generate方法

public class GenerateLoginTicketAction {
  private static final String PREFIX = "LT";
  private final Logger logger = LoggerFactory.getLogger(this.getClass());
  @NotNull
  private UniqueTicketIdGenerator ticketIdGenerator;

  public GenerateLoginTicketAction() {
  }

  public final String generate(RequestContext context) {
    String loginTicket = this.ticketIdGenerator.getNewTicketId("LT");
    this.logger.debug("Generated login ticket {}", loginTicket);
    //放入RequestContext域
    WebUtils.putLoginTicket(context, loginTicket);
    return "generated";
  }

  public void setTicketIdGenerator(UniqueTicketIdGenerator generator) {
    this.ticketIdGenerator = generator;
  }
}

UniqueTicketIdGenerator要做的就是生成以LT作为前缀的loginTicket(例:LT-2-pfDmbEHfX2OkS0swLtDd7iDwmzlhsn),并且把loginTicket放到RequestContext作用域中(LT只作为登录时使用的票据)。登录流程流转到第五个state(viewLoginForm)


    
        
        
    
    
        

        
    
    

这样经过流程的流转登录界面展示在浏览器上,默认的用户界面是/WEB-INF/jsp/ui/default/ui/casLoginView.jsp,如果想要自定义登录界面可以参考这个。

默认的登录页面中有lt、execution和_eventId三个隐藏参数




lt参数值就是在GenerateLoginTicketAction的generate方法中生成的loginTicket

当用户输入了用户名和密码,点击登录按钮就会进入realSubmit这个state


    
    
    
    
    
    
    

执行authenticationViaFormActionsubmit方法

public class AuthenticationViaFormAction {
  public static final String SUCCESS = "success";
  public static final String SUCCESS_WITH_WARNINGS = "successWithWarnings";
  public static final String WARN = "warn";
  public static final String AUTHENTICATION_FAILURE = "authenticationFailure";
  public static final String ERROR = "error";
  public static final String PUBLIC_WORKSTATION_ATTRIBUTE = "publicWorkstation";
  protected final Logger logger = LoggerFactory.getLogger(this.getClass());
  @NotNull
  private CentralAuthenticationService centralAuthenticationService;
  @NotNull
  private CookieGenerator warnCookieGenerator;

  public AuthenticationViaFormAction() {
  }

  public final Event submit(RequestContext context, Credential credential, MessageContext messageContext) {
    if (!this.checkLoginTicketIfExists(context)) {
      return this.returnInvalidLoginTicketEvent(context, messageContext);
    } else {
      return this.isRequestAskingForServiceTicket(context) ? this.grantServiceTicket(context, credential) : this.createTicketGrantingTicket(context, credential, messageContext);
    }
  }

  protected boolean checkLoginTicketIfExists(RequestContext context) {
    String loginTicketFromFlowScope = WebUtils.getLoginTicketFromFlowScope(context);
    String loginTicketFromRequest = WebUtils.getLoginTicketFromRequest(context);
    this.logger.trace("Comparing login ticket in the flow scope [{}] with login ticket in the request [{}]", loginTicketFromFlowScope, loginTicketFromRequest);
    //判断FlowScope和request中的loginTicket是否相同
    return StringUtils.equals(loginTicketFromFlowScope, loginTicketFromRequest);
  }

  protected Event returnInvalidLoginTicketEvent(RequestContext context, MessageContext messageContext) {
    String loginTicketFromRequest = WebUtils.getLoginTicketFromRequest(context);
    this.logger.warn("Invalid login ticket [{}]", loginTicketFromRequest);
    messageContext.addMessage((new MessageBuilder()).error().code("error.invalid.loginticket").build());
    return this.newEvent("error");
  }

  protected boolean isRequestAskingForServiceTicket(RequestContext context) {
    //requestScope和FlowScope中获取TGT
    String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
    ////FlowScope中获取service
    Service service = WebUtils.getService(context);
    return StringUtils.isNotBlank(context.getRequestParameters().get("renew")) && ticketGrantingTicketId != null && service != null;
  }

  protected Event grantServiceTicket(RequestContext context, Credential credential) {
    String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);

    try {
      Service service = WebUtils.getService(context);
      ServiceTicket serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId, service, new Credential[]{credential});
      WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
      this.putWarnCookieIfRequestParameterPresent(context);
      return this.newEvent("warn");
    } catch (AuthenticationException var6) {
      return this.newEvent("authenticationFailure", var6);
    } catch (TicketException var7) {
      if (var7 instanceof TicketCreationException) {
        this.logger.warn("Invalid attempt to access service using renew=true with different credential. Ending SSO session.");
        this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId);
      }

      return this.newEvent("error", var7);
    }
  }

  protected Event createTicketGrantingTicket(RequestContext context, Credential credential, MessageContext messageContext) {
    try {
      //根据用户凭证构造TGT,把TGT放到requestScope中,同时把TGT缓存到服务器的cache中
      TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(new Credential[]{credential});
      WebUtils.putTicketGrantingTicketInScopes(context, tgt);
      this.putWarnCookieIfRequestParameterPresent(context);
      this.putPublicWorkstationToFlowIfRequestParameterPresent(context);
      return this.addWarningMessagesToMessageContextIfNeeded(tgt, messageContext) ? this.newEvent("successWithWarnings") : this.newEvent("success");
    } catch (AuthenticationException var5) {
      this.logger.debug(var5.getMessage(), var5);
      return this.newEvent("authenticationFailure", var5);
    } catch (Exception var6) {
      this.logger.debug(var6.getMessage(), var6);
      return this.newEvent("error", var6);
    }
  }

  protected boolean addWarningMessagesToMessageContextIfNeeded(TicketGrantingTicket tgtId, MessageContext messageContext) {
    boolean foundAndAddedWarnings = false;
    Iterator i$ = tgtId.getAuthentication().getSuccesses().entrySet().iterator();

    while(i$.hasNext()) {
      Entry entry = (Entry)i$.next();

      for(Iterator i$ = ((HandlerResult)entry.getValue()).getWarnings().iterator(); i$.hasNext(); foundAndAddedWarnings = true) {
        MessageDescriptor message = (MessageDescriptor)i$.next();
        this.addWarningToContext(messageContext, message);
      }
    }

    return foundAndAddedWarnings;
  }

  private void putWarnCookieIfRequestParameterPresent(RequestContext context) {
    HttpServletResponse response = WebUtils.getHttpServletResponse(context);
    if (StringUtils.isNotBlank(context.getExternalContext().getRequestParameterMap().get("warn"))) {
      this.warnCookieGenerator.addCookie(response, "true");
    } else {
      this.warnCookieGenerator.removeCookie(response);
    }

  }

  private void putPublicWorkstationToFlowIfRequestParameterPresent(RequestContext context) {
    if (StringUtils.isNotBlank(context.getExternalContext().getRequestParameterMap().get("publicWorkstation"))) {
      context.getFlowScope().put("publicWorkstation", Boolean.TRUE);
    }

  }

  private Event newEvent(String id) {
    return new Event(this, id);
  }

  private Event newEvent(String id, Exception error) {
    return new Event(this, id, new LocalAttributeMap("error", error));
  }

  public final void setCentralAuthenticationService(CentralAuthenticationService centralAuthenticationService) {
    this.centralAuthenticationService = centralAuthenticationService;
  }

  public final void setWarnCookieGenerator(CookieGenerator warnCookieGenerator) {
    this.warnCookieGenerator = warnCookieGenerator;
  }

  /** @deprecated */
  @Deprecated
  public void setTicketRegistry(TicketRegistry ticketRegistry) {
    this.logger.warn("setTicketRegistry() has no effect and will be removed in future CAS versions.");
  }

  private void addWarningToContext(MessageContext context, MessageDescriptor warning) {
    MessageBuilder builder = (new MessageBuilder()).warning().code(warning.getCode()).defaultText(warning.getDefaultMessage()).args(warning.getParams());
    context.addMessage(builder.build());
  }
}

AuthenticationViaFormAction的submit要做的就是判断FlowScope和request中的loginTicket是否相同。如果不同跳转到错误页面,如果相同,则根据用户凭证生成TGT(登录成功票据),并放到requestScope作用域中,同时把TGT缓存到服务器的cache中。登录流程流转到下个state(sendTicketGrantingTicket)


    
    


执行 sendTicketGrantingTicketAction 的 doExecute方法

protected Event doExecute(RequestContext context) {
  //requestScope和FlowScope中获取TGT
  String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
  String ticketGrantingTicketValueFromCookie = (String)context.getFlowScope().get("ticketGrantingTicketId");
  if (ticketGrantingTicketId == null) {
    return this.success();
  } else {
    if (this.isAuthenticatingAtPublicWorkstation(context)) {
      LOGGER.info("Authentication is at a public workstation. SSO cookie will not be generated. Subsequent requests will be challenged for authentication.");
    } else if (!this.createSsoSessionCookieOnRenewAuthentications && this.isAuthenticationRenewed(context)) {
      LOGGER.info("Authentication session is renewed but CAS is not configured to create the SSO session. SSO cookie will not be generated. Subsequent requests will be challenged for authentication.");
    } else {
      //response中添加TGC
      this.ticketGrantingTicketCookieGenerator.addCookie(WebUtils.getHttpServletRequest(context), WebUtils.getHttpServletResponse(context), ticketGrantingTicketId);
    }

    if (ticketGrantingTicketValueFromCookie != null && !ticketGrantingTicketId.equals(ticketGrantingTicketValueFromCookie)) {
      this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketValueFromCookie);
    }

    return this.success();
  }
}

SendTicketGrantingTicketAction的doExecute要做的是获取TGT,并根据TGT生成cookie添加到response。登录流程流转到下一个state(serviceCheck)


    

由于此时FlowScope中存在service(http://127.0.0.1:8081/cas-server/login?service=http%3a%2f%2fapp1.example.com),登录流程流转到下一个state(generateServiceTicket)


    
    
    
    
    

执行generateServiceTicketActiondoExecute方法

protected Event doExecute(RequestContext context) {
  Service service = WebUtils.getService(context);
  String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context);

  try {
    //根据TGT和service生成service ticket(ST-2-97kwhcdrBW97ynpBbZH5-cas01.example.org)
    ServiceTicket serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicket, service);
    ////ST放到requestScope中
    WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
    return this.success();
  } catch (TicketException var5) {
    if (var5 instanceof InvalidTicketException) {
      this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicket);
    }

    return this.isGatewayPresent(context) ? this.result("gateway") : this.newEvent("error", var5);
  }
}

GenerateServiceTicketActiondoExecute要做的是获取service和TGT,并根据service和TGT生成以ST为前缀的serviceTicket(例:ST-2-97kwhcdrBW97ynpBbZH5-cas01.example.org),并把serviceTicket放到requestScope中。登录流程流转到下一个state(warn)


    

FlowScope中不存在warnCookieValue,登录流程流转到下一个state(redirect)


    
    

从requestScope中获取serviceTicket,构造response对象,并把response放到requestScope中。登录流程流转到下一个state(postRedirectDecision)


    

由于request请求是get类型,登录流程流转到下一个state(redirectView)


此时流程如下:

  1. 跳转到应用系统(http://app1.example.com/?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)
  2. 进入CAS客户端的AuthenticationFilter过滤器,由于session中获取名为_const_cas_assertion_的assertion对象不存在,但是request有ticket参数,所以进入到下一个过滤器
  3. TicketValidationFilter过滤器的validate方法通过httpClient访问CAS服务器端(http://127.0.0.1:8081/cas/serviceValidate?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org&service=http://app1.example.com)验证ticket是否正确,并返回assertion对象。
    Assertion对象格式类似于

    
        system

    

至此就完成了登录的整个流程
参考文档:(https://blog.csdn.net/dovejing/article/details/44523545 ,https://www.ibm.com/developerworks/cn/education/java/j-spring-webflow/index.html)

你可能感兴趣的:(CAS 4.1.10 版本服务端源码解读)