CAS详解

CAS概念

CAS是Central Authentiction Service 的缩写,中央认证服务。CAS是Yale大学发起的开源项目,旨在为Web应用系统提供一种可靠的单点登录方法,CAS在2004年12月正式称为JA-SIG的一个项目。官网地址:https://www.apereo.org/projects/cas。特点如下:

  • 开源的企业级单点登录解决方案;
  • 支持多种协议(CAS,SAML, WS-Federation,OAuth2,OpenID,OpenID Connect,REST);
  • CAS Client 多语言支持包括Java, .Net, PHP, Perl, Apache等;
  • 支持三方授权登录(例如ADFS,Facebook,Twitter,SAML2 IdP等)的委派身份验证;
  • 支持多因素身份验证(Duo Security,FIDO U2F,YubiKey,Google Authenticator,Microsoft Azure,Authy等)。

CAS架构

image.png

如图所示,CAS分为:CAS Client和CAS Server。

  • CAS Client:需要接入单点登录的应用系统;
  • CAS Server:统一认证中心;

CAS协议

CAS协议,是CAS项目默认的协议,是一种简单而强大的基于票证的协议。涉及到几个核心概念如下:

  • TGT (Ticket Granting Ticket):俗称大令牌,存储到浏览器Cookie(Name=TGC, Value=“TGT的值”)上,可以通过TGT签发ST,具备时效性,默认2小时;
  • ST (Service Ticket):俗称小令牌,CAS Client通过ST校验是否登录成功,并获取用户信息。ST只有一次有效,有效期默认10秒。

CAS协议认证流程图

image.png

上图分为三种不同场景的访问方式:

  • 第一次访问某个接入CAS认证中心的应用系统(app1):

    1. 用户请求应用系统app1,app1判断用户未登录,跳转到CAS认证中心登录页面;
    2. 用户输入账号密码提交到CAS认证中心,如果认证通过则走第3步;
    3. CAS认证中心在它的域名下面的Cookie设置TGC,将签发的ST作为请求参数重定向到app1;
    4. app1通过ST校验登录是否合法,并获取用户信息,存储到本地session;
    5. app1跳转到开始访问的页面。
  • 第一次访问某个接入CAS认证中心的应用系统(app1):由于app1已经登录过了,之后访问app1都不需要再跟CAS认证中心打交道了。

  • 第一次访问其他接入CAS认证中心的应用系统(app2):

    1. 用户请求应用系统app2,app2判断用户未登录,跳转到CAS认证中心登录页面;
    2. CAS认证中心验证之前生成的TGT(从Cookie的TGC获取),如果该TGT有效,则签发ST重定向到app2;
    3. app2通过ST校验登录是否合法,并获取用户信息,存储到本地session;
    4. app2跳转到开始访问的页面。

CAS Server认证逻辑

image.png

上图是作者从源码梳理出来的逻辑,如有错误之处,欢迎指正~

实战手写CAS Client

根据前面学习到的CAS登录原理,我们来实战手写CAS Client端,巩固下刚刚学到的知识,详情见代码:

  1. 新建一个Spring Boot项目(自行实现);
  2. 引入包:
       
        
            org.jasig.cas.client
            cas-client-core
            3.6.1
        
       
        
            org.apache.httpcomponents
            httpclient
            4.3.3
        
  1. 配置文件
server.servlet.context-path=/client
server.port=8080
  1. 代码实现
    定义一个过滤器,用于登录、登出逻辑认证
@Configuration
public class AppConfig {
    @Bean
    public FilterRegistrationBean registerAuthFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new AccessFilter());
        registration.addUrlPatterns("/*");
        registration.setName("accessFilter");
        registration.setOrder(Integer.MIN_VALUE);
        return registration;
    }
}

public class AccessFilter implements Filter {
    private static final Logger LOGGER = LoggerFactory.getLogger(AccessFilter.class);
    public static final String USER_INFO = "USER_INFO";
    public static final String CAS_URL = "https://127.0.0.1:8443/cas";
    public static final String SERVICE_URL = "http://127.0.0.1:8080/client/";
    /** 记录登录的session,退出时,可以获取session并销毁 key-ST, value-session */
    private static final Map ST_SESSION_MAP = new ConcurrentHashMap<>();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                         FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String ticket = request.getParameter("ticket");
        // 校验ST
        if (!StringUtils.isEmpty(ticket) && "GET".equals(request.getMethod())) {
            this.validateTicket(ticket, request.getSession());
        }

        // 监听登出
        String logoutRequest = request.getParameter("logoutRequest");
        if (!StringUtils.isEmpty(logoutRequest) && "POST".equals(request.getMethod())) {
            LOGGER.info(logoutRequest);
            this.destroySession(logoutRequest);
            return;
        }

        // 未登录?则跳转CAS登录页面
        if (request.getSession().getAttribute(USER_INFO) == null) {
            response.sendRedirect(CAS_URL + "/login?service=" + SERVICE_URL);
            return;
        }
        filterChain.doFilter(request, response);
    }

    /**
     * 校验ST
     * @param ticket
     * @param session
     */
    private void validateTicket(String ticket, HttpSession session) {
        String result = null;
        try {
            // 向CAS发起ST校验,并获取用户信息
            result = HttpClientUtils.doGet(CAS_URL + "/serviceValidate?service=" + SERVICE_URL
                    + "&ticket=" + ticket);
            LOGGER.info(result);
        } catch (Exception e) {
            throw new RuntimeException("serviceValidate请求失败:" + e.getMessage(), e);
        }
        // 校验成功可以解析到user信息(XML格式)
        String user = XmlUtils.getTextForElement(result, "user");
        // 记录登录信息
        if (!StringUtils.isEmpty(user)) {
            session.setAttribute(USER_INFO, user);
            ST_SESSION_MAP.put(ticket, session);
        } else {
            throw new RuntimeException("校验ST失败");
        }
    }

    /**
     * 销毁session
     * @param logoutRequest
     */
    private void destroySession(String logoutRequest) {
        final String ticket = XmlUtils.getTextForElement(logoutRequest, "SessionIndex");
        if (CommonUtils.isBlank(ticket)) {
            return;
        }
        final HttpSession session = ST_SESSION_MAP.get(ticket);
        if (session != null) {
            session.invalidate();
        }
    }

}

相关工具类

public abstract class HttpClientUtils {
    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientUtils.class);
    public static final String ENCODING = "UTF-8";
    private static final int CONNECT_TIMEOUT = 3000;
    private static final int SOCKET_TIMEOUT = 10000;
    private static final int CONNECTION_REQUEST_TIMEOUT = 3000;



    private static CloseableHttpClient httpClient;

    static {
        httpClient = createHttpClient();
    }

    private static CloseableHttpClient createHttpClient() {
        if (httpClient != null) {
            return httpClient;
        }
        try {
            SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
                public boolean isTrusted(X509Certificate[] chain,
                                         String authType) throws CertificateException {
                    return true;
                }
            }).build();
            SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext,SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
            HttpClientBuilder httpClientBuilder = HttpClients.custom();
            httpClientBuilder.setSSLSocketFactory(sslsf);
            httpClientBuilder.setMaxConnPerRoute(50);
            httpClientBuilder.setMaxConnTotal(150);
            RequestConfig.Builder configBuilder = RequestConfig.custom();
            configBuilder.setConnectTimeout(CONNECT_TIMEOUT);
            configBuilder.setSocketTimeout(SOCKET_TIMEOUT);
            configBuilder.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT);
            configBuilder.setStaleConnectionCheckEnabled(true);
            httpClientBuilder.setDefaultRequestConfig(configBuilder.build());
            httpClient = httpClientBuilder.build();
        } catch (Exception ex) {
            LOGGER.error("create https client support fail:"+ ex.getMessage(), ex);
        }
        return httpClient;
    }

    public static String doGet(String url) throws Exception {
        URIBuilder uriBuilder = new URIBuilder(url);
        HttpGet httpGet = new HttpGet(uriBuilder.build());
        return getHttpClientResult(httpGet);
    }

    public static String getHttpClientResult(HttpRequestBase httpMethod) throws IOException{
        CloseableHttpResponse httpResponse = null;
        try {
            httpResponse = httpClient.execute(httpMethod);
            String content = "";
            if (httpResponse != null && httpResponse.getStatusLine() != null && httpResponse.getEntity() != null) {
                content = EntityUtils.toString(httpResponse.getEntity(), ENCODING);
            }
            return content;
        } finally {
            if (httpResponse != null) {
                httpResponse.close();
            }
        }
    }
}

控制层代码

@Controller
public class BaseController {

    @RequestMapping("/")
    public String home() {
        return "redirect:/index";
    }

    @RequestMapping("/index")
    @ResponseBody
    public String index(HttpServletRequest request) {
        return "登录用户:" + request.getSession().getAttribute(AccessFilter.USER_INFO);
    }

    @RequestMapping("/logout")
    public void logout(HttpServletResponse response) throws IOException {
        response.sendRedirect(AccessFilter.CAS_URL + "/logout?service=" + AccessFilter.SERVICE_URL);
    }
}

你可能感兴趣的:(CAS详解)