CAS单点登录介绍以及通过JAVA后台模拟单点登录

1,991 阅读8分钟

介绍

CAS(Center Authentication Service)是耶鲁大学研究的一款开源的单点登录项目,主要为web项目提供单点登录实现,属于Web SSO

CAS登录等系统分为CAS Server和CAS Client

CAS应用登录介绍

image.png

  1. 用户访问请求资源,浏览器如果存在TGC(Ticket Granted Cookie),则会携带TGC访问
  2. 客户端后台会判断会话中是否有_const_cas_assertion_,如果有,代表已经认证,直接返回认证通过,如果没有,就重定向到Cas Server
  3. CAS Server会对请求做认证,验证是否有TGC(Ticket Granted Cookie),如果没有则跳转到登录界面,用户输入用户名密码。认证通过后客户端会缓存一个TGC,并且后台会生成TGT,用于绑定访问信息。并且会生成唯一的票据ST(ST只能使用一次,使用完后就失效)
  4. 如果存在TGC,则服务端会去找对应的TGT,如果存在,则直接生成ST
  5. 重定向到跳转地址,在地址后面会跟上生成的ST
  6. 客户端收到访问请求以后,判断如果是存在ST票据,则重新向服务端请求,验证ST的有效性,如果有效则从请求中获取相关的用户信息,并记录到session._const_cas_assertion_中,就完成了登录过程

注意要点:

  • TGT(Ticket Granded Ticket),就是存储认证凭据的Cookie,有TGT说明已经通过认证
  • ST(Service Ticket),是由CAS认证中心生成的一个唯一的不可伪装的票据,用于认证的
  • 没登录过的或者TGT失效的,访问时候也跳转到认证中心,发现没有TGT,说明没有通过认证,直接重定向登录页面,输入账号密码后,再次重定向到认证中心,验证通过后,生成ST,返回客户端保存到TGC
  • 登录过的而且TGT没有失效的,直接带着去认证中心认证,认证中心发现有TGT,重定向到客户端,并且带上ST,客户端再带ST去认证中心验证

CAS应用登出介绍

image.png

  1. CAS服务端注销,清除TGT和TGC
  2. 获取TGT对应所有客户端URL
  3. 发送通知
  4. 客户端监听到服务端发出的注销请求,客户端执行注销过程,清理Session,系统会话结束

注意要点:

  • 注销请求一般都是访问 http://xxx/cas/logout 的时候就会触发
  • 特别要注意客户端的注销监听,服务器发送注销请求的时候要能接受到,不然会注销失败。

CAS-Client原理介绍

  客户端只要引入cas-client包就行,具体下面介绍的类就是client里面关于认证,票据认证相关的代码。

<dependency>
    <groupId>org.jasig.cas.client</groupId>
    <artifactId>cas-client-core</artifactId>
    <version>${cas.client.core.version}</version>
</dependency>

Bean处理方式

CAS配置类,用于配置CAS相关参数

@Configuration
@Conditional(CasCondition.class)
@Lazy(false)
public class CasConfig {

    static final Logger logger = LoggerFactory.getLogger(CasConfig.class);

    @Bean
    CustomCasFilter casFilter() {
        String adminPath = Global.getConfig(KeyConsts.ADMIN_PATH) == null ? DefaultValueConsts.DEFAULT_ADMIN_PATH : Global.getConfig(KeyConsts.ADMIN_PATH);
        CustomCasFilter customCasFilter = new CustomCasFilter();
        customCasFilter.setFailureUrl(adminPath + "/error");
        return customCasFilter;
    }

    //不能和ShiroConfig中的logoutFilter()同名,否则运行时会调用该方法,造成循环依赖
    @Bean
    LogoutFilter casLogoutFilter() {
        String casServerUrl = Global.getConfig(KeyConsts.CAS_SERVER_URL);
        String casProjectUrl = Global.getConfig(KeyConsts.CAS_PROJECT_URL);
        if (StringUtils.isEmpty(casServerUrl)) {
            String msg = "CAS初始化失败,请配置CAS服务端地址";
            logger.error(msg);
            throw new RuntimeException(msg);
        }
        if (StringUtils.isEmpty(casProjectUrl)) {
            String msg = "CAS初始化失败,请配置CAS客户端地址";
            logger.error(msg);
            throw new RuntimeException(msg);
        }
        CasLogoutFilter casLogoutFilter = new CasLogoutFilter();
        //修改LogoutFilter的登出路径,跳转到CAS SERVER完成退出登录
        casLogoutFilter.setRedirectUrl(casServerUrl + "/logout?service=" + casProjectUrl);
        return casLogoutFilter;
    }


    @Bean
    SystemAuthorizingCasRealm casRealm() {
        String casServerUrl = Global.getConfig(KeyConsts.CAS_SERVER_URL);
        String casProjectUrl = Global.getConfig(KeyConsts.CAS_PROJECT_URL);
        String adminPath = Global.getConfig(KeyConsts.ADMIN_PATH) == null ? DefaultValueConsts.DEFAULT_ADMIN_PATH : Global.getConfig(KeyConsts.ADMIN_PATH);
        if (StringUtils.isEmpty(casServerUrl)) {
            String msg = "SystemAuthorizingCasRealm初始化失败,请配置CAS服务端地址";
            logger.error(msg);
            throw new RuntimeException(msg);
        }
        if (StringUtils.isEmpty(casProjectUrl)) {
            String msg = "SystemAuthorizingCasRealm初始化失败,请配置CAS客户端地址";
            logger.error(msg);
            throw new RuntimeException(msg);
        }
        SystemAuthorizingCasRealm systemAuthorizingCasRealm = new SystemAuthorizingCasRealm();
        systemAuthorizingCasRealm.setCachingEnabled(true);
        systemAuthorizingCasRealm.setAuthenticationCachingEnabled(true);
        systemAuthorizingCasRealm.setAuthenticationCacheName("authenticationCache");
        systemAuthorizingCasRealm.setAuthorizationCachingEnabled(true);
        systemAuthorizingCasRealm.setAuthenticationCacheName("authorizationCache");
        systemAuthorizingCasRealm.setCasServerUrlPrefix(casServerUrl);
        systemAuthorizingCasRealm.setCasService(casProjectUrl + adminPath + "/cas-login");
        return systemAuthorizingCasRealm;
    }

    @Configuration
    @Conditional(CasCondition.class)
    @Lazy(false)
    static class AfterConfig{

        @Autowired
        CustomCasFilter casFilter;

        @Autowired
        LogoutFilter casLogoutFilter;

        @Autowired
        SystemAuthorizingCasRealm casRealm;

        @Autowired
        DefaultWebSecurityManager defaultWebSecurityManager;

        @Autowired
        CustomShiroFilterFactoryBean customShiroFilterFactoryBean;

        @PostConstruct
        void addCasConfigs() {
            String casServerUrl = Global.getConfig(KeyConsts.CAS_SERVER_URL);
            String casProjectUrl = Global.getConfig(KeyConsts.CAS_PROJECT_URL);
            String adminPath = Global.getConfig(KeyConsts.ADMIN_PATH) == null ? DefaultValueConsts.DEFAULT_ADMIN_PATH : Global.getConfig(KeyConsts.ADMIN_PATH);
            if (StringUtils.isEmpty(casServerUrl)) {
                String msg = "CAS初始化失败,请配置CAS服务端地址";
                logger.error(msg);
                throw new RuntimeException(msg);
            }
            if (StringUtils.isEmpty(casProjectUrl)) {
                String msg = "CAS初始化失败,请配置CAS客户端地址";
                logger.error(msg);
                throw new RuntimeException(msg);
            }
            //SecurityManager配置
            //添加Realm
            ShiroSecurityUtils.addRealm2SecurityManager(casRealm,defaultWebSecurityManager);

            //ShiroFilterFactoryBean配置
            customShiroFilterFactoryBean.setLoginUrl(casServerUrl + "login?service=" + casProjectUrl + adminPath + "/cas-login");
            //添加过滤器CasFilter
            ShiroSecurityUtils.addPathAndFilterNameAndFilterEntry(adminPath + "/cas-login", "cas", casFilter,customShiroFilterFactoryBean);
            //添加CasLogoutFilter
            ShiroSecurityUtils.addPathAndFilterNameAndFilterEntry(adminPath +"/logout", "logout", casLogoutFilter,customShiroFilterFactoryBean);
        }
    }
}
public class CustomCasFilter extends CasFilter {
   private Logger logger = LoggerFactory.getLogger(CustomCasFilter.class);
   // the name of the parameter service ticket in url
    private static final String TICKET_PARAMETER = "ticket";

   /**
    * 登录成功,增加会话内容
    * @param token
    * @param subject
    * @param request
    * @param response
    * @return
    * @throws Exception
    */
   @Override
   protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request,
         ServletResponse response) throws Exception {
      CasToken casToken = (CasToken)token;
      if (casToken != null) {
         // 内网情况下,直接根据用户名去查询用户对应信息
         String userName = casToken.getPrincipal().toString();
         logger.info("customCasfilter类.......");
         logger.info("userName:{}",userName);
         User user = UserUtils.getUser(userName);
         if (user != null) {
            HttpServletRequest httpRequest = (HttpServletRequest)request;
            //会话保存已经认证的用户信息
            HttpSession session = httpRequest.getSession();
            session.setAttribute("userid_", user.getId());
            session.setAttribute("username_", userName);
            session.setAttribute("staffName_", user.getStaffName());
            session.setAttribute("password_", user.getPassWord()); 
            session.setAttribute("systemUser_", user.getSystemUser());
            session.setAttribute("fingerFlag", false);
            session.setAttribute("publicUser", false);
            logger.info("username:{}, password:{}",user.getUsername(),user.getPassWord());
         }else{
            logger.info("user is null");
         }
      }
      return super.onLoginSuccess(token, subject, request, response);
   }
   
   @Override
   public void setLoginUrl(String loginUrl) {
      // TODO Auto-generated method stub
      super.setLoginUrl(loginUrl);
   }
   
   
   private static final SingleSignOutHandler handler = new SingleSignOutHandler();

   @Override
   public void setFilterConfig(FilterConfig filterConfig) {

      super.setFilterConfig(filterConfig);
        handler.init();
   }

   @Override
   public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
         throws ServletException, IOException {

      super.doFilterInternal(request, response, chain);
      
      HttpServletRequest req = (HttpServletRequest) request;
      if (handler.isTokenRequest(req)) {
         handler.recordSession(req);
      } else if(handler.isLogoutRequest(req)) {
         String a = req.getContextPath();
         handler.destroySession(req);
            return;
      }
   }
}
public class SystemAuthorizingCasRealm extends CasRealm {
   
   private static final Logger loggger = LoggerFactory.getLogger(SystemAuthorizingCasRealm.class);

    //执行认证的逻辑,这里可以重写业务逻辑代码
   @SuppressWarnings({ "null" })
   @Override
   protected AuthenticationInfo doGetAuthenticationInfo(
         AuthenticationToken token) throws AuthenticationException {
      CasToken casToken = (CasToken) token;
        if (token == null) {
            return null;
        }
        
        String ticket = (String)casToken.getCredentials();
        if (!org.apache.shiro.util.StringUtils.hasText(ticket)) {
            return null;
        }

        //票据验证器,验证Service Ticket(ST)
        TicketValidator ticketValidator = ensureTicketValidator();

        try {
            //验证cas服务和ST
            // contact CAS server to validate service ticket
            Assertion casAssertion = ticketValidator.validate(ticket, getCasService());
            // get principal, user id and attributes
            AttributePrincipal casPrincipal = casAssertion.getPrincipal();
            if(loggger.isDebugEnabled()){
                loggger.debug("casPrincipal userName:{}",casPrincipal.getName());
            }
            //URLDecoder使用UTF8将字符串转成字符串
            String userName = URLDecoder.decode(casPrincipal.getName(),"utf-8");
            //todo 有些CAS服务器返回的字符串是utf-8编码的字节流,服务器使用gbk去生成字符串,就会导致乱码,在这种情况下,就需要先对字符串编码得到字节流,然后,使用utf-8编码得到正确的字符串
            // String userName=new String(casPrincipal.getName().getBytes("gbk"),"utf-8");
            if(loggger.isDebugEnabled()){
                loggger.debug("{} decoded userName:{}","utf8",userName);
            }
            loggger.debug("Validate ticket : {} in CAS server : {} to retrieve user : {}",
                    ticket, getCasServerUrlPrefix(), userName
            );

            // refresh authentication token (user id + remember me)
            casToken.setUserId(userName);
            String rememberMeAttributeName = getRememberMeAttributeName();
            Map<String, Object> attributes = casPrincipal.getAttributes();
            String rememberMeStringValue = (String)attributes.get(rememberMeAttributeName);
            boolean isRemembered = rememberMeStringValue != null && Boolean.parseBoolean(rememberMeStringValue);
            if (isRemembered) {
                casToken.setRememberMe(true);
            }
            
            User user = null;
          try {
                if(loggger.isDebugEnabled()){
                    loggger.debug("调用用户查询服务");
                }
             user = UserUtils.getUser(userName);
          } catch (Exception e) {
                loggger.error("查询用户失败:",e);
             e.printStackTrace();
          }

          if (user != null) {
             
             //byte[] salt = Encodes.decodeHex(user.getPassWord().substring(0, 16));
             return new SimpleAuthenticationInfo(new Principal(user), /*user.getPassWord().substring(16), ByteSource.Util.bytes(salt)*/
                   ticket,
                   getName());
          } else {
              String msg=String.format("查询不到用户:%s",userName);
              loggger.error(msg);
            if(StringUtils.isEmpty(userName)){
                throw new AuthenticationException(msg);
               // throw new AuthenticationException("msg:账号或密码不能为空!");
            }
            return null;
          }
            
            // create simple authentication info
//            List<Object> principals = CollectionUtils.asList(userId, attributes);
//            PrincipalCollection principalCollection = new SimplePrincipalCollection(principals, getName());
//            return new SimpleAuthenticationInfo(principalCollection, ticket);
        } catch (TicketValidationException e) {
            loggger.error("解析Ticket失败",e);
            throw new CasAuthenticationException("Unable to validate ticket [" + ticket + "]", e);
        } catch (UnsupportedEncodingException e) {
            loggger.error("未能正确解析用户名称:",e);
            throw new CasAuthenticationException(String.format("未能正确解析用户登录名称:%s",e.getMessage()));
        }
    }

   @Override
   protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
      // TODO Auto-generated method stub
      //return super.doGetAuthorizationInfo(principals);
      
      // retrieve user information
        SimplePrincipalCollection principalCollection = (SimplePrincipalCollection) principals;
        List<Object> listPrincipals = principalCollection.asList(); 
        Principal principal = (Principal) listPrincipals.get(0);
        //Map<String, String> attributes = (Map<String, String>) listPrincipals.get(0);
       //create simple authorization info
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addStringPermission("user");
        List<String> list = UserUtils.getPrivileges(principal.getUsername());
        if (null !=    list) {
            simpleAuthorizationInfo.addStringPermissions(list);
        }
        return simpleAuthorizationInfo;
   }
   
   public void clearAuthorization() {
      //this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
      Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
      if (cache != null) {
         for (Object key : cache.keys()) {
            cache.remove(key);
         }
      }
   }

    @Override
    protected TicketValidator createTicketValidator() {
        String urlPrefix = getCasServerUrlPrefix();
        if ("saml".equalsIgnoreCase(getValidationProtocol())) {
            Saml11TicketValidator saml11TicketValidator=new Saml11TicketValidator(urlPrefix);
            //设置utf8编码
            saml11TicketValidator.setEncoding("utf8");
            return saml11TicketValidator;
        }
        Cas20ServiceTicketValidator cas20ServiceTicketValidator=new Cas20ServiceTicketValidator(urlPrefix);
        //设置utf8编码
        cas20ServiceTicketValidator.setEncoding("utf8");
        return cas20ServiceTicketValidator;
    }

}

XML配置方式(Web.xml)


	<listener>
		<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
	</listener>

	<filter>
		<filter-name>CAS Single Sign Out Filter</filter-name>
		<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
		<init-param>
			<param-name>casServerUrlPrefix</param-name>
			<param-value>http://192.168.5.200:9080/cas</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CAS Single Sign Out Filter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>



	<!-- 该过滤器负责用户的认证工作,必须启用它 -->
	<filter>
		<filter-name>CASFilter</filter-name>
            <!-- 自定义认证过滤器,可以继承AbstractCasFilter,重写initInternal逻辑 -->
		<filter-class>com.xxxxx.conf.RealEstateAuthenticationFilter</filter-class>
		<init-param>
			<param-name>casServerLoginUrl</param-name>
			<!--外网域名-->
			<param-value>http://192.168.5.200:9080/cas/login</param-value>
			<!-- 内网-->
			<!--<param-value>http://59.202.30.26:9080/cas/login</param-value>-->
			<!--这里的server是服务端的IP-->
		</init-param>
		<init-param>
			<param-name>serverName</param-name>
			<!--外网域名-->
			<!--<param-value>http://xxxxx:8080</param-value>-->
			<!-- 内网-->
			<!--<param-value>http://xxxxx:8080</param-value>-->
			<!--本地-->
			<param-value>http://192.168.5.200:8085</param-value>
			<!--外网测试-->
			<!--<param-value>http://59.202.30.122:8079</param-value>-->
			<!--内网测试-->
			<!--<param-value>http://59.202.30.122:8080</param-value>-->
		</init-param>
		<!--cas-client-core升级到3.3.3版本以上,支持排除部分url可以跳过单点验证路径分隔符采用 “|”-->
		<init-param>
			<param-name>ignorePattern</param-name>
			<param-value></param-value>
		</init-param>
		<init-param>
			<param-name>ignoreUrlPatternType</param-name>
			<param-value>REGEX</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CASFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>


	<!-- 该过滤器负责对Ticket的校验工作,必须启用它 -->
	<filter>
		<filter-name>CAS Validation Filter</filter-name>
		<filter-class>
			org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
		<init-param>
			<param-name>casServerUrlPrefix</param-name>
			<!--外网SLB-->
			<param-value>http://192.168.5.200:9080/cas</param-value>
			<!-- 内网-->
			<!--<param-value>http://59.202.30.26:9080/cas</param-value>-->
		</init-param>
		<init-param>
			<param-name>serverName</param-name>
			<!--外网SLB域名-->
			<!--<param-value>http://bdc.zjzwfw.gov.cn:8080</param-value>-->
			<!-- 内网-->
			<!--<param-value>http://govbdc.zjzwfw.gov.cn:8080</param-value>-->
			<!--本地-->
			<param-value>http://192.168.5.200:8085</param-value>
			<!--外网测试-->
			<!--<param-value>http://59.202.30.122:8079</param-value>-->
			<!--内网测试-->
			<!--<param-value>http://59.202.30.122:8080</param-value>-->
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CAS Validation Filter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>


	<!--
    该过滤器负责实现HttpServletRequest请求的包裹,
    比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。
    -->
	<filter>
		<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
		<filter-class>
			org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

AuthenticationFilter(可重写,认证核心类)

HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
if (this.isRequestUrlExcluded(request)) {
    this.logger.debug("Request is ignored.");
    filterChain.doFilter(request, response);
} else {
    HttpSession session = request.getSession(false);
    Assertion assertion = session != null ? (Assertion)session.getAttribute("_const_cas_assertion_") : null;

    //这里判断session是否存在,如果存在就直接跳出,也就是说第二次访问的时候,就不会再去验证cas了,这里也是一个隐藏的风险点
    //这里可以对判断逻辑进行重写,按照自己需要的方式并且_const_cas_assertion_里面包含了所有的用户信息,也可以做自定义处理
    if (assertion != null) {
        filterChain.doFilter(request, response);
    } else {
        String serviceUrl = this.constructServiceUrl(request, response);
        String ticket = this.retrieveTicketFromRequest(request);
        boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
        //如果ticket为空,则跳转到登录页面,并加上回调地址,http://cas/login?service=http://yewu.service
        if (!CommonUtils.isNotBlank(ticket) && !wasGatewayed) {
            this.logger.debug("no ticket and no assertion found");
            String modifiedServiceUrl;
            if (this.gateway) {
                this.logger.debug("setting gateway attribute in session");
                modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
            } else {
                modifiedServiceUrl = serviceUrl;
            }

            this.logger.debug("Constructed service url: {}", modifiedServiceUrl);
            String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, this.getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway);
            this.logger.debug("redirecting to \"{}\"", urlToRedirectTo);
            this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo);
        } else {
            filterChain.doFilter(request, response);
        }
    }
}

TicketValidator(票据验证)

  public final Assertion validate(String ticket, String service) throws TicketValidationException {
        String validationUrl = this.constructValidationUrl(ticket, service);
        this.logger.debug("Constructing validation url: {}", validationUrl);

        try {
            this.logger.debug("Retrieving response from server.");
            String serverResponse = this.retrieveResponseFromServer(new URL(validationUrl), ticket);
            if (serverResponse == null) {
                throw new TicketValidationException("The CAS server returned no response.");
            } else {
                this.logger.debug("Server response: {}", serverResponse);
                return this.parseResponseFromServer(serverResponse);
            }
        } catch (MalformedURLException var5) {
            throw new TicketValidationException(var5);
        }
    }

SingleSignOutHttpSessionListener(单点登出)

  public void sessionDestroyed(HttpSessionEvent event) {
        if (this.sessionMappingStorage == null) {
            this.sessionMappingStorage = getSessionMappingStorage();
        }

        HttpSession session = event.getSession();
        //移除session信息
        this.sessionMappingStorage.removeBySessionById(session.getId());
    }

CAS服务端相关原理介绍

整体结构介绍

login-webflow.xml

路径: classes\webflow\login\login-webflow.xml

系统登录相关配置,包括自定义的登录页面等 以及登录成功、失败等处理方法

一般来说,认证这块的逻辑每个业务系统都是不一样的,比如加入指纹登录,加密狗登录等,都需要基于CAS二次扩展,相对来说,CAS也提供了比较灵活的二次扩展手段

	<var name="credential"
		class="xxxxx.platform.security.cas.server.custom.UsernamePasswordCredentialExtendFingerAndSoftdog" />
	<on-start>
		<evaluate expression="initialFlowSetupAction" />
	</on-start>
    
    <!--这里定义的id都是 cas-servlet.xml定义的bean,指定用哪些方法处理-->
	<action-state id="ticketGrantingTicketCheck">
		<evaluate expression="ticketGrantingTicketCheckAction" />
		<transition on="notExists" to="gatewayRequestCheck" />
		<transition on="invalid" to="terminateSession" />
		<transition on="valid" to="hasServiceCheck" />
	</action-state>

public class UsernamePasswordCredentialExtendFingerAndSoftdog extends UsernamePasswordCredential {
   private static final long serialVersionUID = 3198836306626671289L;

   private String finger;
   private String fingerPassWord;
   private String softdog;
   private String rsaPassword;
   private String telCode;
   private String idCode;
   private Boolean warn = false;

   public String getTelCode() {
      return telCode;
   }

   public void setTelCode(String telCode) {
      this.telCode = telCode;
   }

   public String getFinger() {
      return finger;
   }

   public void setFinger(String finger) {
      this.finger = finger;
   }

   public String getFingerPassWord() {
      return fingerPassWord;
   }

   public void setFingerPassWord(String fingerPassWord) {
      this.fingerPassWord = fingerPassWord;
   }

   public String getSoftdog() {
      return softdog;
   }

   public void setSoftdog(String softdog) {
      this.softdog = softdog;
   }

   public String getRsaPassword() {
      return rsaPassword;
   }

   public void setRsaPassword(String rsaPassword) {
      this.rsaPassword = rsaPassword;
   }

   public String getIdCode() {
      return idCode;
   }

   public void setIdCode(String idCode) {
      this.idCode = idCode;
   }

   public Boolean getWarn() {
      return warn;
   }

   public void setWarn(Boolean warn) {
      this.warn = warn;
   }
}

logout-webflow.xml

路径: classes\webflow\logout\logout-webflow.xml

同上,一些注销事项的定义

cas-servlet.xml

核心的一些启动配置和相关自定义认证处理,都是在 cas-servlet.xml

例如启动类InitialFlowSetupAction

<bean id="initialFlowSetupAction" class="org.jasig.cas.web.flow.InitialFlowSetupAction"
    p:argumentExtractors-ref="argumentExtractors"
    p:warnCookieGenerator-ref="warnCookieGenerator"
    p:ticketGrantingTicketCookieGenerator-ref="ticketGrantingTicketCookieGenerator"
    p:servicesManager-ref="servicesManager"
    p:enableFlowOnAbsentServiceRequest="${create.sso.missing.service:true}"  />

deployerConfigContext.xml

一般是处理自定义验证方法,比如用户密码的验证,密码的加密算法等

  <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
     	<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
     	<property name="url" value="jdbc:mysql://ip:port/platform?characterEncoding=utf8"/>
     	<property name="username" value="username"/> 
     	<property name="password" value="password"/> 
     	<property name="initialSize" value="30"/> 
     	<property name="minIdle" value="30"/> 
     	<property name="maxActive" value="200"/> 
     	<property name="maxWait" value="60000"/> 
     	<property name="timeBetweenEvictionRunsMillis" value="60000"/> 
     	<property name="minEvictableIdleTimeMillis" value="300000"/>
     	<property name="validationQuery" value="select 'x' from dual"/>
     	<property name="testWhileIdle" value="true"/>
    </bean> 
  <!--自定义密码认证器, 一般业务系统都是会有的-->
     <bean id="passwordEncoder" class="xxxxx.platform.security.cas.server.custom.CustomPasswordEncoder"></bean>
public class CustomPasswordEncoder implements PasswordEncoder {

   private static final int HASH_INTERATIONS = 1024;
   byte[] salt;
   
   public String encode(String password) {

      byte[] hashPassword = Digests.sha1(password.getBytes(), salt, HASH_INTERATIONS);
      String encryptedPassword =  Hex.encodeToString(salt) + Hex.encodeToString(hashPassword);
      return encryptedPassword;
   }
   
   protected void setSalt(byte[] salt) {
      this.salt = salt;
   }
}

相关原理介绍

页面打开的时候

如果浏览器中不存在TGC cookie 则直接跳转到登录页面

如果存在 cookie就,校验TGC是否合法(InitialFlowSetupAction.doExecute)

@Override
    protected Event doExecute(final RequestContext context) throws Exception {
        final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
        final String contextPath = context.getExternalContext().getContextPath();
        final String cookiePath = StringUtils.isNotBlank(contextPath) ? contextPath + '/' : "/";

        if (StringUtils.isBlank(warnCookieGenerator.getCookiePath())) {
            logger.info("Setting path for cookies for warn cookie generator to: {} ", cookiePath);
            this.warnCookieGenerator.setCookiePath(cookiePath);
        } else {
            logger.debug("Warning cookie path is set to {} and path {}", warnCookieGenerator.getCookieDomain(),
                    warnCookieGenerator.getCookiePath());
        }
        if (StringUtils.isBlank(ticketGrantingTicketCookieGenerator.getCookiePath())) {
            logger.info("Setting path for cookies for TGC cookie generator to: {} ", cookiePath);
            this.ticketGrantingTicketCookieGenerator.setCookiePath(cookiePath);
        } else {
            logger.debug("TGC cookie path is set to {} and path {}", ticketGrantingTicketCookieGenerator.getCookieDomain(),
                    ticketGrantingTicketCookieGenerator.getCookiePath());
        }

        WebUtils.putTicketGrantingTicketInScopes(context,
                this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request));

        WebUtils.putWarningCookie(context,
                Boolean.valueOf(this.warnCookieGenerator.retrieveCookieValue(request)));

        final Service service = WebUtils.getService(this.argumentExtractors, context);


        if (service != null) {
            logger.debug("Placing service in context scope: [{}]", service.getId());

            final RegisteredService registeredService = this.servicesManager.findServiceBy(service);
            if (registeredService != null && registeredService.getAccessStrategy().isServiceAccessAllowed()) {
                logger.debug("Placing registered service [{}] with id [{}] in context scope",
                        registeredService.getServiceId(),
                        registeredService.getId());
                WebUtils.putRegisteredService(context, registeredService);

                final RegisteredServiceAccessStrategy accessStrategy = registeredService.getAccessStrategy();
                if (accessStrategy.getUnauthorizedRedirectUrl() != null) {
                    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) {
            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 result("success");
    }

其中: WebUtils.putTicketGrantingTicketInScopes(context, this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request)) -->obtainCookieValue; 用于验证TGC

TGC生成是由 TGT + IP + UserAgent三个部分组成的

这时候校验TGC就是验证这三个部分有没有问题,其中IP保证是来着同一个域的,指的是CAS的服务端的域,UserAgent指的是浏览器的代理,这时候会发现,如果是浏览器的谷歌模式和IE模式,这个值就是不一样的,会照成验证不一致。

三段组成

登录过程

参考login-webflow, 以及cas-servlet

login-webflow

	<action-state id="realSubmit">
		<evaluate
			expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credential, messageContext)" />
		<transition on="warn" to="warn" />
		<!-- To enable AUP workflows, replace the 'success' transition with the 
			following: <transition on="success" to="acceptableUsagePolicyCheck" /> -->
		<transition on="success" to="sendTicketGrantingTicket" />
		<transition on="successWithWarnings" to="showMessages" />
		<transition on="authenticationFailure"
			to="handleAuthenticationFailure" />
		<transition on="error" to="generateLoginTicket" />
	</action-state>

cas-servlet

  <bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
        p:centralAuthenticationService-ref="centralAuthenticationService"
        p:warnCookieGenerator-ref="warnCookieGenerator"/>

最后可以定位到 AuthenticationViaFormAction


    /**
     * Handle the submission of credentials from the post.
     *
     * @param context the context
     * @param credential the credential
     * @param messageContext the message context
     * @return the event
     * @since 4.1.0
     */
    public final Event submit(final RequestContext context, final Credential credential,
                              final MessageContext messageContext)  {

        if (!checkLoginTicketIfExists(context)) {
            return returnInvalidLoginTicketEvent(context, messageContext);
        }

        if (isRequestAskingForServiceTicket(context)) {
            return grantServiceTicket(context, credential);
        }

        return createTicketGrantingTicket(context, credential, messageContext);
    }

其中 checkLoginTicketIfExists 验证登录携带的信息是否正确

当前版本登录需要携带 lt和execution两个属性信息 这两个属性在打开登录页的时候会自动生成,后续如果要模拟登录,这两个字段是重点

其中 isRequestAskingForServiceTicket 判断地址是都有renew参数 代表这个请求,服务端需要强制进行认证,针对一些敏感内容,可以要求强制重新认证 gateway 参数刚好相反,只要session存在就不用重新认证了

最后就是 createTicketGrantingTicket

 /**
     * Create ticket granting ticket for the given credentials.
     * Adds all warnings into the message context.
     *
     * @param context the context
     * @param credential the credential
     * @param messageContext the message context
     * @return the resulting event.
     * @since 4.1.0
     */
    protected Event createTicketGrantingTicket(final RequestContext context, final Credential credential,
                                               final MessageContext messageContext) {
        try {
            //认证并创建票据
            final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(credential);
            WebUtils.putTicketGrantingTicketInScopes(context, tgt);
            //写入缓存,也就是TGC的内容,重点
            putWarnCookieIfRequestParameterPresent(context);
            putPublicWorkstationToFlowIfRequestParameterPresent(context);
            if (addWarningMessagesToMessageContextIfNeeded(tgt, messageContext)) {
                return newEvent(SUCCESS_WITH_WARNINGS);
            }
            return newEvent(SUCCESS);
        } catch (final AuthenticationException e) {
            logger.debug(e.getMessage(), e);
            return newEvent(AUTHENTICATION_FAILURE, e);
        } catch (final Exception e) {
            logger.debug(e.getMessage(), e);
            return newEvent(ERROR, e);
        }
    }

生成ticket

    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);

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

            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));
    }

大致的类访问路径: login-webflow.xml cas-servlet.xml

graph TD
emperor(( ))-->AuthenticationViaFormAction.submit
AuthenticationViaFormAction.submit-->checkLoginTicketIfExists
AuthenticationViaFormAction.submit-->createTicketGrantingTicket
AuthenticationViaFormAction.submit-->isRequestAskingForServiceTicket
createTicketGrantingTicket--保存TGT-->WebUtils.putTicketGrantingTicketInScopes
createTicketGrantingTicket--生成TGT-->CentralAuthenticationService.createTicketGrantingTicket
createTicketGrantingTicket--生成Cookie,TGC-->putWarnCookieIfRequestParameterPresent

CentralAuthenticationService.createTicketGrantingTicket-->PolicyBasedAuthenticationManager.authenticate
PolicyBasedAuthenticationManager.authenticate-->authenticateInternal
authenticateInternal-->AuthenticationHandler.authenticate
AuthenticationHandler.authenticate-->AbstractPreAndPostProcessingAuthenticationHandler.authenticate
AbstractPreAndPostProcessingAuthenticationHandler.authenticate-->doAuthentication
doAuthentication-->AbstractUsernamePasswordAuthenticationHandler.doAuthentication
AbstractUsernamePasswordAuthenticationHandler.doAuthentication-->authenticateUsernamePasswordInternal
authenticateUsernamePasswordInternal-->CustomAuthenticationHandler.authenticateUsernamePasswordInternal

注销过程

前面一样,通过logout-webflow.xml cas-servlet.xml找到入口方法

就是TerminateSessionAction.terminate

    public Event terminate(final RequestContext context) {
        String tgtId = WebUtils.getTicketGrantingTicketId(context);
        // 获取TGT,TGC等信息
        if (tgtId == null) {
            final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
            tgtId = this.ticketGrantingTicketCookieGenerator.retrieveCookieValue(request);
        }

        //如果存在TGT,则找到相关的请求信息
        //destroyTicketGrantingTicket 会调用业务系统的回调地址,去销毁业务系统的session或者登录信息 
        if (tgtId != null) {
            WebUtils.putLogoutRequests(context, this.centralAuthenticationService.destroyTicketGrantingTicket(tgtId));
        }
        final HttpServletResponse response = WebUtils.getHttpServletResponse(context);
        this.ticketGrantingTicketCookieGenerator.removeCookie(response);
        this.warnCookieGenerator.removeCookie(response);
        return this.eventFactorySupport.success(this);
    }

还有就是处理被单点的系统的退出 LogoutAction.doInternalExecute 例如有A,B,C系统登录了,这时候要分别调用A,B,C系统的注销工作,通过传递logRequest等参数,从而让客户端可以判断

protected Event doInternalExecute(final HttpServletRequest request, final HttpServletResponse response,
            final RequestContext context) throws Exception {

        boolean needFrontSlo = false;
        putLogoutIndex(context, 0);
        final List<LogoutRequest> logoutRequests = WebUtils.getLogoutRequests(context);
        if (logoutRequests != null) {
            for (final LogoutRequest logoutRequest : logoutRequests) {
                // if some logout request must still be attempted
                if (logoutRequest.getStatus() == LogoutRequestStatus.NOT_ATTEMPTED) {
                    needFrontSlo = true;
                    break;
                }
            }
        }

        final String service = request.getParameter("service");
        if (this.followServiceRedirects && service != null) {
            final Service webAppService = new SimpleWebApplicationServiceImpl(service);
            final RegisteredService rService = this.servicesManager.findServiceBy(webAppService);

            if (rService != null && rService.getAccessStrategy().isServiceAccessAllowed()) {
                context.getFlowScope().put("logoutRedirectUrl", service);
            }
        }

        // there are some front services to logout, perform front SLO
        if (needFrontSlo) {
            return new Event(this, FRONT_EVENT);
        } else {
            // otherwise, finish the logout process
            return new Event(this, FINISH_EVENT);
        }
    }

JAVA模拟CAS登录

准备工作

CAS服务端需要开启Rest服务支持

pom里面增加cas-server-support-rest,让后重新编译就行。

<dependency>
    <groupId>org.jasig.cas</groupId>
    <artifactId>cas-server-support-rest</artifactId>
    <version>4.1.10</version>
</dependency>

地址

    /**
     * cas根目录地址
     */
    public static final String SERVER_URL="http://192.168.20.238:8080/cas/";
    /**
     * 回调函数地址
     */
    public static final String TAGET_URL = "http://192.168.20.238:8089/testCas";

        /**
     * 登录页面地址,用于获取execution和lt
     */
    private static final String GET_EXECUTION_URL = SERVER_URL + "login?service="+ConstValue.TAGET_URL;
    /**
     * 获取TGT票据地址
     */
    private static final String GET_TOKEN_URL = SERVER_URL + "v1/tickets";
    /**
     * 登录接口地址
     */
    private static final String GET_TOKEN_URL_TGC = SERVER_URL + "login";
    /**
     * 系统注销地址
     */
    private static final String LOGOUT_URL = SERVER_URL + "logout";

根据CAS登录地址和回调地址获取属性

前面有讲到,登录过程中,需要用到lt和execution参数,所以,需要先模拟访问这个登录页面,然后获取到这两个参数

    /**
     * 获取 execution信息,用于后续的认证
     * @return
     * @throws IOException
     */
    public String[]  GetExecution() throws IOException {
        CloseableHttpClient client = HttpClientBuilder.create().build();
        HttpGet httpGet = new HttpGet(GET_EXECUTION_URL);
        HttpResponse response = client.execute(httpGet);
        String strResult = EntityUtils.toString(response.getEntity());

        Page page =new Page();
        page.setRawText(strResult);
        page.setRequest(new Request(GET_EXECUTION_URL));

        String execution = page.getHtml().xpath("//input[@name='execution']/@value").get();
        String lt = page.getHtml().xpath("//input[@name='lt']/@value").get();
        String[] arr = new String[2];
        arr[0] = execution;
        arr[1] = lt;
        return arr;
    }

模拟登录过程,并设置TGC Cookie信息

public  void putTGC(String username, String password, String execution,String lt, HttpServletResponse responses,HttpServletRequest request)
            throws ClientProtocolException, IOException {
        CloseableHttpClient httpClient = null;
        try {
            CookieStore cookieStore = new BasicCookieStore();
            httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
            HttpPost httpPost = new HttpPost(GET_TOKEN_URL_TGC);
            List<NameValuePair> nvps = new ArrayList<NameValuePair>();
            nvps.add(new BasicNameValuePair("username", username));
            nvps.add(new BasicNameValuePair("password", encode(password)));

            //lt和execution两个必须参数
            nvps.add(new BasicNameValuePair("execution", execution));
            nvps.add(new BasicNameValuePair("lt", lt));

            nvps.add(new BasicNameValuePair("_eventId", "submit"));

            HttpEntity reqEntity = new UrlEncodedFormEntity(nvps, Consts.UTF_8);
            String userAgent = request.getHeader("user-agent");
            httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
            httpPost.setHeader("user-agent",userAgent);
            httpPost.setEntity(reqEntity);

            CloseableHttpResponse response = httpClient.execute(httpPost);
            List<Cookie> cookies = cookieStore.getCookies();
            if (null != cookies && cookies.size() > 0) {
                cookies.forEach(p->{
                      //成功以后,可以获取到TGC信息,如果没有获取到,就是失败
                    //这块服务端要进行设置,服务端的异常都是内部处理掉了,不会返回,需要服务端增加异常处理机制
                    if(p.getName().toUpperCase(Locale.ROOT).equals("TGC")){
                        javax.servlet.http.Cookie cookie = new javax.servlet.http.Cookie(p.getName(),
                                p.getValue());
                        cookie.setPath(p.getPath());
                        //cookie.setHttpOnly(true);
                        //cookie.setSecure(true);
                        //cookie.setMaxAge(1800);
                        responses.addCookie(cookie);
                    }
                });

            }
        } finally {
            httpClient.close();
        }
    }

获取TGT信息

这个是用于后续获取ST票据的重要过程

/**
     * 获取TGT,服务端生成的票据信息
     * @param username
     * @param password
     * @return
     * @throws ClientProtocolException
     * @throws IOException
     */
    public String getTGT(String username, String password) throws ClientProtocolException, IOException {
        String tgt = "";
        CloseableHttpClient httpClient = null;
        try {
            CookieStore cookieStore = new BasicCookieStore();
            httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();

            HttpPost httpPost = new HttpPost(GET_TOKEN_URL);
            List<NameValuePair> nvps = new ArrayList<>();
            nvps.add(new BasicNameValuePair("username", username));
            nvps.add(new BasicNameValuePair("password", encode(password)));

            HttpEntity reqEntity = new UrlEncodedFormEntity(nvps, Consts.UTF_8);
            httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
            httpPost.setEntity(reqEntity);
            CloseableHttpResponse response = httpClient.execute(httpPost);
            String strResult = EntityUtils.toString(response.getEntity());
            Page page =new Page();
            page.setRawText(strResult);
            page.setRequest(new Request(GET_TOKEN_URL));

            String execution = page.getHtml().xpath("//form[@name='execution']/@value").get();

            try {

                Header[] tgtHead = response.getAllHeaders();
                if (tgtHead != null) {
                    for (int i = 0; i < tgtHead.length; i++) {
                        if (StringUtils.equals(tgtHead[i].getName(), "Location")) {
                            tgt = tgtHead[i].getValue().substring(tgtHead[i].getValue().lastIndexOf("/") + 1);
                        }
                    }
                }
                HttpEntity respEntity = response.getEntity();
                EntityUtils.consume(respEntity);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } finally {
                response.close();
            }
        } finally {
            httpClient.close();
        }
        return tgt;
    }

根据TGT获取ST信息

 public  String getST(String tgt,String TAGET_URL) throws MalformedURLException {
        String serviceTicket = "";
        OutputStreamWriter out = null;
        BufferedWriter wirter = null;
        HttpURLConnection conn = null;

        URL url = new URL(GET_TOKEN_URL + "/" + tgt);
        try {
            conn = (HttpURLConnection) url.openConnection();
            conn.setDoOutput(true);
            String param = "service=" + URLEncoder.encode(TAGET_URL, "utf-8");
            out = new OutputStreamWriter(conn.getOutputStream());
            wirter = new BufferedWriter(out);
            wirter.write(param);
            wirter.flush();
            wirter.close();
            out.close();
            BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line = "";
            while ((line = in.readLine()) != null) {
                serviceTicket = line;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (conn != null) {
                    conn.disconnect();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return serviceTicket;
    }

最后地址回调

地址格式就是: 回调地址?ticket=前一步获取到的ST

if (StringUtils.isNotBlank(tgt)) {
            String ticket = getST(tgt,ConstValue.TAGET_URL);
            if(StringUtils.isNotBlank(ticket)){
                response.sendRedirect(ConstValue.TAGET_URL +"?ticket=" + ticket);
            }else{
                response.sendRedirect("/login2");
            }
        } else {
            response.sendRedirect("/login2");
        }

完整代码

package com.example.caslogin.service;

import com.example.caslogin.conf.ConstValue;
import com.sun.deploy.net.URLEncoder;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.*;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import sun.misc.BASE64Encoder;
import us.codecraft.webmagic.Page;
import us.codecraft.webmagic.Request;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

@Service
public class LoginService {




    private static final String GET_EXECUTION_URL = ConstValue.SERVER_URL + "login?service="+ConstValue.TAGET_URL;
    private static final String GET_TOKEN_URL = ConstValue.SERVER_URL + "v1/tickets";

    private static final String GET_TOKEN_URL_TGC = ConstValue.SERVER_URL + "login";
    private static final String LOGOUT_URL = ConstValue.SERVER_URL + "logout";
    public void logout(HttpServletRequest servletRequest,HttpServletResponse servletResponse) throws IOException {
        CookieStore cookieStore = new BasicCookieStore();

        javax.servlet.http.Cookie[] cookies = servletRequest.getCookies();
        for (javax.servlet.http.Cookie cookie :cookies){
            BasicClientCookie cookieNew =new BasicClientCookie(cookie.getName(),cookie.getValue());
            cookieNew.setDomain("192.168.20.238");
            cookieNew.setPath(cookie.getPath());
            cookieStore.addCookie(cookieNew);
        }
        CloseableHttpClient httpClient = HttpClients.custom()
                .setDefaultCookieStore(cookieStore)
                .build();
        HttpGet httpGet = new HttpGet(LOGOUT_URL);
        String userAgent = servletRequest.getHeader("user-agent");
        httpGet.setHeader("user-agent",userAgent);
        HttpResponse response = httpClient.execute(httpGet);
        String strResult = EntityUtils.toString(response.getEntity());
        System.out.println(strResult);
    }
    public void login(HttpServletRequest request,HttpServletResponse response) throws IOException {
        String account = "admin";
        String password = "admin";

        String[] result = GetExecution();
        String execution = result[0];
        String lt = result[1];
        putTGC(account, password, execution,lt, response,request);
        String tgt = getTGT(account, password);
        if (StringUtils.isNotBlank(tgt)) {
            String ticket = getST(tgt,ConstValue.TAGET_URL);
            if(StringUtils.isNotBlank(ticket)){
                response.sendRedirect(ConstValue.TAGET_URL +"?ticket=" + ticket);
            }else{
                response.sendRedirect("/login2");
            }
        } else {
            response.sendRedirect("/login2");
        }
    }
    /**
     * 获取 execution信息,用于后续的认证
     * @return
     * @throws IOException
     */
    public String[]  GetExecution() throws IOException {
        CloseableHttpClient client = HttpClientBuilder.create().build();
        HttpGet httpGet = new HttpGet(GET_EXECUTION_URL);
        HttpResponse response = client.execute(httpGet);
        String strResult = EntityUtils.toString(response.getEntity());

        Page page =new Page();
        page.setRawText(strResult);
        page.setRequest(new Request(GET_EXECUTION_URL));

        String execution = page.getHtml().xpath("//input[@name='execution']/@value").get();
        String lt = page.getHtml().xpath("//input[@name='lt']/@value").get();
        String[] arr = new String[2];
        arr[0] = execution;
        arr[1] = lt;
        return arr;
    }

    public static final String encode(String base){
        return encode(base.getBytes());
    }
    public static final String encode(byte[] baseBuff){
        return new BASE64Encoder().encode(baseBuff);
    }


    /**
     * 获取TGT,服务端生成的票据信息
     * @param username
     * @param password
     * @return
     * @throws ClientProtocolException
     * @throws IOException
     */
    public String getTGT(String username, String password) throws ClientProtocolException, IOException {
        String tgt = "";
        CloseableHttpClient httpClient = null;
        try {
            CookieStore cookieStore = new BasicCookieStore();
            httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();

            HttpPost httpPost = new HttpPost(GET_TOKEN_URL);
            List<NameValuePair> nvps = new ArrayList<>();
            nvps.add(new BasicNameValuePair("username", username));
            nvps.add(new BasicNameValuePair("password", encode(password)));

            HttpEntity reqEntity = new UrlEncodedFormEntity(nvps, Consts.UTF_8);
            httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
            httpPost.setEntity(reqEntity);
            CloseableHttpResponse response = httpClient.execute(httpPost);
            String strResult = EntityUtils.toString(response.getEntity());
            Page page =new Page();
            page.setRawText(strResult);
            page.setRequest(new Request(GET_TOKEN_URL));

            String execution = page.getHtml().xpath("//form[@name='execution']/@value").get();

            try {

                Header[] tgtHead = response.getAllHeaders();
                if (tgtHead != null) {
                    for (int i = 0; i < tgtHead.length; i++) {
                        if (StringUtils.equals(tgtHead[i].getName(), "Location")) {
                            tgt = tgtHead[i].getValue().substring(tgtHead[i].getValue().lastIndexOf("/") + 1);
                        }
                    }
                }
                HttpEntity respEntity = response.getEntity();
                EntityUtils.consume(respEntity);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } finally {
                response.close();
            }
        } finally {
            httpClient.close();
        }
        return tgt;
    }
    public  String getST(String tgt,String TAGET_URL) throws MalformedURLException {
        String serviceTicket = "";
        OutputStreamWriter out = null;
        BufferedWriter wirter = null;
        HttpURLConnection conn = null;

        URL url = new URL(GET_TOKEN_URL + "/" + tgt);
        try {
            conn = (HttpURLConnection) url.openConnection();
            conn.setDoOutput(true);
            String param = "service=" + URLEncoder.encode(TAGET_URL, "utf-8");
            out = new OutputStreamWriter(conn.getOutputStream());
            wirter = new BufferedWriter(out);
            wirter.write(param);
            wirter.flush();
            wirter.close();
            out.close();
            BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line = "";
            while ((line = in.readLine()) != null) {
                serviceTicket = line;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (conn != null) {
                    conn.disconnect();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return serviceTicket;
    }
    public  void putTGC(String username, String password, String execution,String lt, HttpServletResponse responses,HttpServletRequest request)
            throws ClientProtocolException, IOException {
        CloseableHttpClient httpClient = null;
        try {
            CookieStore cookieStore = new BasicCookieStore();
            httpClient = HttpClients.custom().setDefaultCookieStore(cookieStore).build();
            HttpPost httpPost = new HttpPost(GET_TOKEN_URL_TGC);
            List<NameValuePair> nvps = new ArrayList<NameValuePair>();
            nvps.add(new BasicNameValuePair("username", username));
            nvps.add(new BasicNameValuePair("password", encode(password)));

            //lt和execution两个必须参数
            nvps.add(new BasicNameValuePair("execution", execution));
            nvps.add(new BasicNameValuePair("lt", lt));

            nvps.add(new BasicNameValuePair("_eventId", "submit"));

            HttpEntity reqEntity = new UrlEncodedFormEntity(nvps, Consts.UTF_8);
            String userAgent = request.getHeader("user-agent");
            httpPost.setHeader("Content-type", "application/x-www-form-urlencoded");
            httpPost.setHeader("user-agent",userAgent);
            httpPost.setEntity(reqEntity);

            CloseableHttpResponse response = httpClient.execute(httpPost);
            List<Cookie> cookies = cookieStore.getCookies();
            if (null != cookies && cookies.size() > 0) {
                cookies.forEach(p->{
                    //成功以后,可以获取到TGC信息,如果没有获取到,就是失败
                    //这块服务端要进行设置,服务端的异常都是内部处理掉了,不会返回,需要服务端增加异常处理机制
                    if(p.getName().toUpperCase(Locale.ROOT).equals("TGC")){
                        javax.servlet.http.Cookie cookie = new javax.servlet.http.Cookie(p.getName(),
                                p.getValue());
                        cookie.setPath(p.getPath());
                        //cookie.setHttpOnly(true);
                        //cookie.setSecure(true);
                        //cookie.setMaxAge(1800);
                        responses.addCookie(cookie);
                    }
                });

            }
        } finally {
            httpClient.close();
        }
    }
}


JAVA模拟CAS注销

public void logout(HttpServletRequest servletRequest,HttpServletResponse servletResponse) throws IOException {
        CookieStore cookieStore = new BasicCookieStore();

        javax.servlet.http.Cookie[] cookies = servletRequest.getCookies();
        for (javax.servlet.http.Cookie cookie :cookies){
            BasicClientCookie cookieNew =new BasicClientCookie(cookie.getName(),cookie.getValue());
            cookieNew.setDomain("192.168.20.238");
            cookieNew.setPath(cookie.getPath());
            cookieStore.addCookie(cookieNew);
        }

        //获取Cookie,重要的是TGC这个,后续服务端要根据这个区获取TGT内容的
        CloseableHttpClient httpClient = HttpClients.custom()
                .setDefaultCookieStore(cookieStore)
                .build();
        HttpGet httpGet = new HttpGet(LOGOUT_URL);
        //和验证一样,需要统一user-ahent
        String userAgent = servletRequest.getHeader("user-agent");
        httpGet.setHeader("user-agent",userAgent);
        HttpResponse response = httpClient.execute(httpGet);
        String strResult = EntityUtils.toString(response.getEntity());
        System.out.println(strResult);
    }