CAS 简介

1、CAS 单点登录分为两个部分,第一个是认证中心 Cas Server,第二个是 Cas Server。
我们使用 SpringBoot 集成 Cas 只需要集成Client。
2、CAS API官方文档
3、cas 简介博客

序言:

简单的说下我做完 cas 集成后的经验与技巧。
首先 cas 与 oauth2 认证方式不同,cas 需要集成:pom 依赖、配置类、配置文件。而 oauth2 则是接口方式,侵入性小。
其次认证方式不同,cas 是每次请求资源路径时,判断是否拥有票据,有票就验票,验票过了就跳转,没票带着你现在请求的资源路径为参数去跳单点登录的登录页,登录成功后,cas server 带着票跳你传的地址。比如:https://ehall.edu.cn/cas?sevice=请求资源的原始路径。oauth2 的登录流程这里就不赘述了。
我的配置还是比较灵活的,CAS 可根据配置文件动态开关,拦截路径可根据配置文件指定。
大体就这些,想起来我再补充。

与传统 SpringBoot 集成 CAS Client 略有不同,我这个集成不需要在 SpringBoot 启动类上加入启用 CAS Client 的 @EnableCasClient 注解。如果加了这个注解, CAS Client是不能实现根据配置文件开关的,每次想要关掉,必须修改代码,注释调注解才行。

在这里插入图片描述

SpringBoot 集成 CAS 步骤

1、引入 CAS Client pom 依赖。
2、配置类 CasConfig 配置 CAS Filter 拦截器(配置拦截规则)。
3、配置文件中配置 CAS 相关属性。
4、获取 CAS 用户Utils 类。

一、引入 POM 依赖

版本有很多,我用的这个版本比较稳定,兼容多个版本的 CAS Server。

		<dependency>
            <groupId>org.jasig.cas.client</groupId>
            <artifactId>cas-client-core</artifactId>
            <version>3.5.0</version>
        </dependency>

二、CasConfig 配置类

import lombok.extern.slf4j.Slf4j;
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;
import org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;

/**
 * @Author: 
 * @Date: 
 * @Description: CAS集成核心配置类
 */
@Configuration
@Slf4j
@ConditionalOnProperty(value = "cas.loginType", havingValue = "cas")
public class CasFilterConfig {

    /**
     * 需要走cas拦截的地址(/* 所有地址都拦截)
     */
    @Value("${cas.urlPattern:/casLogin}")
    private String filterUrl;

    /**
     * 默认的cas地址,防止通过 配置信息获取不到
     */
    @Value("${cas.server-url-prefix:http://cas.server.com:8443/cas}")
    private String casServerUrl;

    /**
     * 应用访问地址(这个地址需要在cas服务端进行配置)
     */
    @Value("${cas.authentication-url:http://localhost:8090}")
    private String authenticationUrl;

    /**
     * 应用访问地址(这个地址需要在cas服务端进行配置)
     */
    @Value("${cas.client-host-url:http://localhost:8090}")
    private String appServerUrl;

    @Bean
    public ServletListenerRegistrationBean servletListenerRegistrationBean() {
        log.info(" \n cas 单点登录配置 \n appServerUrl = " + appServerUrl + "\n casServerUrl = " + casServerUrl);
        log.info(" servletListenerRegistrationBean ");
        ServletListenerRegistrationBean listenerRegistrationBean = new ServletListenerRegistrationBean();
        listenerRegistrationBean.setListener(new SingleSignOutHttpSessionListener());
        listenerRegistrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return listenerRegistrationBean;
    }

    /**
     * 单点登录退出
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean singleSignOutFilter() {
        log.info(" servletListenerRegistrationBean ");
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new SingleSignOutFilter());
        registrationBean.addUrlPatterns(filterUrl);
        registrationBean.addInitParameter("casServerUrlPrefix", casServerUrl);
        registrationBean.setName("CAS Single Sign Out Filter");
        registrationBean.setOrder(1);
        return registrationBean;
    }

    /**
     * 单点登录认证
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean AuthenticationFilter() {
        log.info(" AuthenticationFilter ");
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new AuthenticationFilter());
        registrationBean.addUrlPatterns(filterUrl);
        registrationBean.setName("CAS Filter");
        registrationBean.addInitParameter("casServerLoginUrl", casServerUrl);
        registrationBean.addInitParameter("serverName", appServerUrl);
        registrationBean.setOrder(1);
        return registrationBean;
    }

    /**
     * 单点登录校验
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean Cas30ProxyReceivingTicketValidationFilter() {
        log.info(" Cas30ProxyReceivingTicketValidationFilter ");
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
        registrationBean.addUrlPatterns(filterUrl);
        registrationBean.setName("CAS Validation Filter");
        registrationBean.addInitParameter("casServerUrlPrefix", authenticationUrl);
        registrationBean.addInitParameter("serverName", appServerUrl);
        registrationBean.setOrder(1);
        return registrationBean;
    }

    /**
     * 单点登录请求包装
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean httpServletRequestWrapperFilter() {
        log.info(" httpServletRequestWrapperFilter ");
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(new HttpServletRequestWrapperFilter());
        registrationBean.addUrlPatterns(filterUrl);
        registrationBean.setName("CAS HttpServletRequest Wrapper Filter");
        registrationBean.setOrder(1);
        return registrationBean;
    }

}

三、yml 配置文件

注:authentication-url 是认证服务器的地址,这个地址千万不要在末尾处追加 /login,否则验票的时候会出现认证服务器无响应,验票不通过的情况。
server-url-prefix 地址配置要加/login,认证服务器地址不要加/login。

cas:
  server-url-prefix: # 认证中心登录页面地址
  client-host-url: # 应用地址,也就是自己的系统地址。
  authentication-url: # 认证中心地址
  loginType: cas # 动态开启 cas 单点登录
  urlPattern: /* # cas  验票拦截路径
# 配置示例:
cas:
  server-url-prefix: https://ehall.ba.cn/cas/login
  client-host-url: http://59.88.12.56:8090/
  authentication-url:  https://ehall.ba.cn/cas/
  loginType: cas
  urlPattern: /api/loginByNameAndCardNo  

生产环境的配置文件:

在这里插入图片描述

四、获取 CAS 用户

CAS 认证通过后,回调我们接口的时候,一般数据结构基本一致。所以我写了一个获取用户的工具类,可参考,但不一定兼容所有 CAS Server。

1.CasUtils 工具类

package com.pty.charge.common.util;

import com.pty.charge.vo.CasUserInfo;
import org.jasig.cas.client.validation.Assertion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * @Author: 
 * @Date: 
 * @Description: 使用cas对接封装的cas返回的用户信息的对象
 */
public class CasUtil {

    private static final Logger LOGGER = LoggerFactory.getLogger(CasUtil.class);
    /**
     * cas client 默认的session key
     */
    public final static String CAS = "_const_cas_assertion_";

    /**
     * 封装CasUserInfo
     *
     * @param request
     * @return
     */
    public static CasUserInfo getCasUserInfoFromCas(HttpServletRequest request) {
        Object object = request.getSession().getAttribute(CAS);
        if (null == object) {
            return null;
        }
        Assertion assertion = (Assertion) object;
        return buildCasUserInfoByCas(assertion);
    }

    /**
     * 构建CasUserInfo
     *
     * @param assertion
     * @return
     */
    private static CasUserInfo buildCasUserInfoByCas(Assertion assertion) {
        if (null == assertion) {
            LOGGER.error(" Cas没有获取到用户 ");
            return null;
        }
        CasUserInfo casUserInfo = new CasUserInfo();
        String userName = assertion.getPrincipal().getName();
        LOGGER.info(" cas对接登录用户= " + userName);
        casUserInfo.setUserAccount(userName);
        //获取属性值
        Map<String, Object> attributes = assertion.getPrincipal().getAttributes();
        Object name = attributes.get("cn");
        casUserInfo.setUserName(name == null ? userName : name.toString());
        casUserInfo.setAttributes(attributes);
        return casUserInfo;
    }

}

2.CAS 用户 VO 类

package com.pty.charge.vo;

import lombok.Getter;
import lombok.Setter;

import java.util.Map;

/**
 * @Author: 
 * @Date: 
 * @Description: 返回的用户信息
 */
@Setter
@Getter
public class CasUserInfo {

    /** 用户名 */
    private String userName;
    /** 用户 */
    private String userAccount;
    /** 用户信息 */
    private Map<String, Object> attributes;

}

五、单点登录示例

/**
   * cas 单点登录
   *
   * @param request 请求头(姓名+身份证号)
   * @param ticket cas 票据
   * @return
   */
  @GetMapping(value = "/api/loginByNameAndCardNo")
  @ApiOperation("cas单点登录")
  public String loginByNameAndCardNo(HttpServletRequest request) {
    CasUserInfo userInfo = CasUtil.getCasUserInfoFromCas(request);
    log.info("userInfo = " + JSONObject.toJSON(userInfo));
    String url = "main";
    MadStudent student = new MadStudent();
    student.setName(userInfo.getAttributes().get("Name").toString());
    student.setCardNo(userInfo.getAttributes().get("IdCard").toString());
  	// 登录用户校验 
  	// xxxxx
  	// 用户数据为 true
  	// 跳转页面
    return "url";
    } else {
      return "redirect:" + casUrl;
    }

  }

附上 CAS Server 本地搭建的博客地址:
CAS Server 本地搭建

六、补充

引言:
后期项目与多个第三方 CAS Server 认证中心做了单点登录,出现了一些问题,特此补充。
问题1:从认证中心认证通过,带票跳转失败,前端页面直接报错,后台报取不到票据带的用户信息,因为时间太久了,忘记报错信息是什么了,所以就不贴报错上来了。

解决办法:
经排查,发现是票据验证TicketValidationFilter版本的问题,部分认证中心使用的 CAS Server 的版本影响到了 Cas Client 的 TicketValidationFilter,导致不兼容,引起了冲突。
之前的文章里提到的TicketValidationFilter的版本是Cas30ProxyReceivingTicketValidationFilter,这里我灵活处理了一下,对于正在使用30版本的客户,还保留原来的配置。对于新对接的,且报错的,使用版本较低的配置,通过配置文件来动态决定使用哪个版本的验票过滤器。

代码如下(可对比第二步的 CasConfig 配置类中的代码参考):

	/**
     * 决定票据验证过滤器的版本,默认30,old是20版
     */
    @Value("${cas.filterVersion:new}")
    private String filterVersion;

	/**
     * 单点登录校验
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean Cas30ProxyReceivingTicketValidationFilter() {

        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        if (StringUtils.isNotBlank(filterVersion) && filterVersion.equals("old")) {
            log.info(" Cas20ProxyReceivingTicketValidationFilter ");
            registrationBean.setFilter(new Cas20ProxyReceivingTicketValidationFilter());
        } else {
            log.info(" Cas30ProxyReceivingTicketValidationFilter ");
            registrationBean.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
        }
        registrationBean.addUrlPatterns(filterUrl);
        registrationBean.setName("CAS Validation Filter");
        registrationBean.addInitParameter("casServerUrlPrefix", authenticationUrl);
        registrationBean.addInitParameter("serverName", appServerUrl);
        registrationBean.setOrder(1);
        return registrationBean;
    }

欢迎补充!

腾讯副总裁吴军博士说“成功的道路并不像想象得那么拥挤,因为在人生的马拉松长路上,绝大部分人跑不到一半就主动退下来了。到后来,剩下的少数人不是嫌竞争对手太多,而是发愁怎样找一个同伴陪自己一同跑下去。因此,教育是一辈子的事情,笑到最后的人是一辈子接受教育的人。回过头来看,一些过去比我们读书更优秀,在起跑线上抢到了更好位置的人,早已放弃了人生的马拉松,我们能够跑得更远,仅仅是因为我们还在跑,如此而已。”急功近利不好,悲观绝望也不好。


Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐