cas学习笔记

CAS

1. 简介

cas 是一套单点登录的具体实现,耶鲁大学开发, 目前最高版本为6.x (6.x使用gradle编译)  此处使用5.x版本(maven编译)

分为两部分,服务端和客户端

服务端就是统一的认证中心,客户端就是所有需要接入单点登录的系统

2.服务端

2.1 下载

[cas github地址]: https://github.com/apereo/cas    “源码地址”

cas不建议直接在源码上修改,提供了一个模板项目,在模板项目上做扩展

2.2 部署

项目拉下来之后,导入idea进行编译打包

或者直接使用

mvn clean package

打成war包 部署到tomcat下

启动Tomcat  cas服务端就部署好了

3.客户端

3.1简单接入
  1. 增加依赖

1. xml                               org.springframework.boot               spring-boot-starter-web                                            org.springframework.boot               spring-boot-test                                            net.unicon.cas               cas-client-autoconfig-support                

  1. 配置过滤器

1. java       package com.zhangyao.cas.config;              import org.apache.tomcat.util.net.openssl.ciphers.Authentication;       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.AssertionThreadLocalFilter;       import org.jasig.cas.client.util.HttpServletRequestWrapperFilter;       import org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;       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 java.util.ArrayList;       import java.util.HashMap;       import java.util.List;              /**        * @author: zhangyao        * @create:2020-05-08 22:52        **/       @Configuration       public class CasConfig {                  @Bean           public SingleSignOutHttpSessionListener singleSignOutHttpSessionListener(){               return  new SingleSignOutHttpSessionListener();           }                  /**            * 监听退出            * @return            */           @Bean           public ServletListenerRegistrationBean singleSignOutHttpSessionListenerServletListenerRegistrationBean(){               ServletListenerRegistrationBean listenerServletListenerRegistrationBean = new ServletListenerRegistrationBean<>();               listenerServletListenerRegistrationBean.setEnabled(true);               listenerServletListenerRegistrationBean.setListener(singleSignOutHttpSessionListener());               listenerServletListenerRegistrationBean.setOrder(1);               return listenerServletListenerRegistrationBean;           }                  /**            * 登出拦截器            * @return            */           @Bean           public FilterRegistrationBean singleSignOutFilterBean(){               FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();               filterRegistrationBean.setFilter(new SingleSignOutFilter());               filterRegistrationBean.setEnabled(true);               filterRegistrationBean.addUrlPatterns("/*");               filterRegistrationBean.setOrder(1);               HashMap map = new HashMap<>();               map.put("casServerUrlPrefix","http://127.0.0.1:8085/cas_overlay_template_war/");               filterRegistrationBean.setInitParameters(map);               return filterRegistrationBean;           }                  /**            * 授权            * @return            */           @Bean           public FilterRegistrationBean filterRegistrationBean(){               FilterRegistrationBean registrationBean = new FilterRegistrationBean();               registrationBean.setFilter(new AuthenticationFilter());                      registrationBean.addUrlPatterns("/*");               HashMap map = new HashMap<>();               map.put("casServerLoginUrl","http://127.0.0.1:8085/cas_overlay_template_war/login");               map.put("casServerUrlPrefix","http://127.0.0.1:8085/cas_overlay_template_war/");               map.put("serverName", "http://127.0.0.1:8086");               map.put("ignorePattern", "/index");               registrationBean.setInitParameters(map);                      registrationBean.setOrder(2);               return registrationBean;           }                  /**            * 验证票据            * @return            */           @Bean           public FilterRegistrationBean validationFilter(){               FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();               filterRegistrationBean.setFilter(new Cas30ProxyReceivingTicketValidationFilter());               HashMap map = new HashMap<>();               map.put("casServerUrlPrefix","http://127.0.0.1:8085/cas_overlay_template_war/");               map.put("serverName", "http://127.0.0.1:8086");               map.put("ignorePattern", "/cas_overlay_template_war/*,/index");               filterRegistrationBean.setInitParameters(map);               filterRegistrationBean.setOrder(1);               filterRegistrationBean.addUrlPatterns("/*");               return filterRegistrationBean;           }                  // 取用户信息           @Bean           public FilterRegistrationBean casHttpServletRequestWrapperFilter() {               FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();               authenticationFilter.setFilter(new HttpServletRequestWrapperFilter());               authenticationFilter.setOrder(1);               List urlPatterns = new ArrayList();               urlPatterns.add("/*");// 设置匹配的url               authenticationFilter.setUrlPatterns(urlPatterns);               return authenticationFilter;           }                  // 取用户信息           @Bean           public FilterRegistrationBean casAssertionThreadLocalFilter() {               FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();               authenticationFilter.setFilter(new AssertionThreadLocalFilter());               authenticationFilter.setOrder(1);               List urlPatterns = new ArrayList();               urlPatterns.add("/*");// 设置匹配的url               authenticationFilter.setUrlPatterns(urlPatterns);               return authenticationFilter;           }              }

  1. 开启注解

java    @SpringBootApplication    @EnableCasClient    public class TestApplication {            public static void main(String[] args) {            SpringApplication.run(TestApplication.class, args);        }    }

4.单点登出

​    实现的效果是: 每一个客户端登出的时候,cas服务端也需要登出,从而达到一端登出,多端登出的效果

​    实现方法: 客户端发送登出请求时,后台跳转cas服务端登出路径

package com.zhangyao.cas.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpSession;

/**
 * @author: zhangyao
 * @create:2020-05-08 22:58
 **/
@Controller
@RequestMapping("")
public class LoginController {

    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/loginOut")
    public String test(HttpSession session){
        session.invalidate();
//        String forObject = restTemplate.getForObject("http://127.0.0.1:8085/cas_overlay_template_war/logout", String.class);
        return "redirect:http://127.0.0.1:8085/cas_overlay_template_war/logout?service=http://127.0.0.1:8086/index";
    }


}

5.cas服务端overlays 打包方式

上文中说到下载后直接mvn clean package即可打包部署,但是当我们想要对服务端进行一些扩展,比如修改默认登录页,修改登录的验证方式等

有两种方法:

  1. 在package后的war包中直接修改对应的文件,部署后可直接生效  缺点是每次重新打包修改的文件都会被覆盖
  2. 使用cas官方提供的cas_overlay_template项目,也就是我们上文中下载的项目,使用maven 的overlay方式进行合并打包

具体分析overlays

pom.xml中的配置

<plugin>
    <groupId>org.apache.maven.pluginsgroupId>
    <artifactId>maven-war-pluginartifactId>
    <version>2.6version>
    <configuration>
        <warName>caswarName>
        <failOnMissingWebXml>falsefailOnMissingWebXml>
        <recompressZippedFiles>falserecompressZippedFiles>
        <archive>
            <compress>falsecompress>
            <manifestFile>${manifestFileToUse}manifestFile>
        archive>
        <overlays>
            <overlay>
                <groupId>org.apereo.casgroupId>
                <artifactId>cas-server-webapp${app.server}artifactId>
            overlay>
        overlays>
    configuration>
plugin>

这里的overlay配置的意思是使用我们主项目中的同名同路径文件覆盖cas-server-webapp${app.server}下的文件

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d2Ny4w8t-1591259677620)(.\images\image-20200512093750382.png)]

具体操作方式如上图

在根项目下新建 src/main/resources作为资源目录

src/main/java作为根目录

官网中也有介绍

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oZeUANHh-1591259677622)(.\images\image-20200512093919720.png)]

修改配置文件时从war包中沾出来再修改 比如修改cas服务端支持http访问

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mf15Se9N-1591259677623)(.images/image-20200512095229110.png)]

复制出这两个文件

并修改HTTPSandIMAPS-10000001.json

{
  "@class" : "org.apereo.cas.services.RegexRegisteredService",
  "serviceId" : "^(http|https|imaps)://.*", #增加http
  "name" : "HTTPS and IMAPS",
  "id" : 10000001,
  "description" : "This service definition authorizes all application urls that support HTTPS and IMAPS protocols.",
  "evaluationOrder" : 10000
}

修改application.properties

在最后加上

#支持http访问
cas.tgc.secure=false
cas.serviceRegistry.initFromJson=true

#支持退出自定义跳转页面
cas.logout.followServiceRedirects=true
cas.logout.redirectParameter=service
cas.logout.confirmLogout=false

6.源码原理分析

cas官网流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LJ6KoS7S-1591259677624)(.images/cas_flow_diagram.png)]

6.1客户端流程图解析:

ST:service-ticket cas服务端签发的票据,用于浏览器访问不同系统时携带,cas服务端使用此票据验证是否有效用户

TGC:全局session的key,存放于浏览器的cookie中,访问cas服务端时使用

TGT:全局session 存放于cas服务端中

  1. 第一次访问第一个客户端,客户端校验请求中是否含有ST(service_ticket,全局session标识),没有此标识就返回302重定向请求(重定向到cas服务端)给浏览器
  2. 浏览器访问cas服务端,服务端校验是否有TGC,如果没有,返回登录页给浏览器
  3. 浏览器展示cas服务端登录页,登录后发送post登录请求给cas服务端,服务端校验登录通过后,创建全局session TGT并签发ST,浏览器写入TGC 并返回302重定向请求(之前访问第一个客户端的路径)给浏览器
  4. 浏览器携带ST请求再次请求第一个客户端,客户端再次校验是否含有ST,并发送请求到cas服务端验证ST是否有效,如果有效,就创建局部session;至此浏览器与第一个客户端建立session会话
  5. 再次访问第一个客户端,这个时候客户端已经存在session会话,会直接验证会话的有效性,不再跳转登录页
  6. 此时浏览器再次访问第二个客户端,由于此时第二个客户端还没有与浏览器建立局部会话,所以会将请求重定向至cas服务端,但是浏览器访问cas服务端时会携带TGC,cas服务端校验通过后签发ST再302返回至浏览器,浏览器再次访问第二个客户端建立连接
6.2客户端源码分析:
6.2.1 授权过滤器

​    验证是否授权的过滤器,也即上文中配置的授权过滤器

​     org.jasig.cas.client.authentication.AuthenticationFilter

进入doFilter

public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        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;
            if (assertion != null) {
                filterChain.doFilter(request, response);
            } else {
                //如果既不是不拦截请求,也没有session,就需要重定向至cas服务端登录
                String serviceUrl = this.constructServiceUrl(request, response);
                String ticket = this.retrieveTicketFromRequest(request);
                boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
                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);
                }
            }
        }
    }
6.2.2 验证ticket过滤器

org.jasig.cas.client.validation.AbstractTicketValidationFilter

进入doFilter

public final void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //preFilter 执行过滤器前可以进行一些操作 源码中直接返回true了
        if (this.preFilter(servletRequest, servletResponse, filterChain)) {
            HttpServletRequest request = (HttpServletRequest)servletRequest;
            HttpServletResponse response = (HttpServletResponse)servletResponse;
            //获取ticket
            String ticket = this.retrieveTicketFromRequest(request);
            if (CommonUtils.isNotBlank(ticket)) {
                this.logger.debug("Attempting to validate ticket: {}", ticket);

                try {
                    //发送请求到cas服务端验证ticket是否有效
                    Assertion assertion = this.ticketValidator.validate(ticket, this.constructServiceUrl(request, response));
                    this.logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());
                    //验证通过后设置属性
                    request.setAttribute("_const_cas_assertion_", assertion);
                    //如果是已经登录过有会话,更新session中的属性
                    if (this.useSession) {
                        request.getSession().setAttribute("_const_cas_assertion_", assertion);
                    }
    
                    //验证成功进行一些操作,源码中为空
                    this.onSuccessfulValidation(request, response, assertion);
                    //redirectAfterValidation 源码中定义为true
                    if (this.redirectAfterValidation) {
                        this.logger.debug("Redirecting after successful ticket validation.");
                        //重定向至之前的访问请求
                        response.sendRedirect(this.constructServiceUrl(request, response));
                        return;
                    }
                } catch (TicketValidationException var8) {//如果有异常 则返回错误403
                    this.logger.debug(var8.getMessage(), var8);
                    this.onFailedValidation(request, response);
                    if (this.exceptionOnValidationFailure) {
                        throw new ServletException(var8);
                    }

                    response.sendError(403, var8.getMessage());
                    return;
                }
            }

            filterChain.doFilter(request, response);
        }
    }
6.2.3 获取用户信息过滤器

org.jasig.cas.client.util.HttpServletRequestWrapperFilter

 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
     //从session/request中获取Principal用户信息
     AttributePrincipal principal = this.retrievePrincipalFromSessionOrRequest(servletRequest);
     //CasHttpServletRequestWrapper是此类中定义的内部类,继承了HttpServletRequestWrapperFilter 增加了获取用户的方法 之后获取用户信息可以直接通过 request.getUserPrincipal()获取用户信息
     filterChain.doFilter(new HttpServletRequestWrapperFilter.CasHttpServletRequestWrapper((HttpServletRequest)servletRequest, principal), servletResponse);
 }
6.3 服务端源码分析

7.自定义操作

7.1 数据库查询用户登录
  1. 修改application.properties,增加如下配置

properties    #查询用户密码的sql    cas.authn.jdbc.query[0].sql=SELECT * FROM cof_user WHERE name=?    #数据库连接    cas.authn.jdbc.query[0].url=jdbc:mysql://122.51.97.53:3306/coframe?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false    #数据库用户    cas.authn.jdbc.query[0].user=root    #数据库密码    cas.authn.jdbc.query[0].password=tlqaz1234    #数据库驱动    cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver    #标识sql查出来的哪个字段是password    cas.authn.jdbc.query[0].fieldPassword=PASSWORD

  1. pom.xml增加jar包

xml            org.apereo.cas        cas-server-support-jdbc        ${cas.version}                org.apereo.cas        cas-server-support-jdbc-drivers        ${cas.version}                mysql        mysql-connector-java        6.0.6                org.apereo.cas        cas-server-support-jdbc-authentication        5.2.6        pom    

##### 7.2 自定义加密类

properties    #自定义加密类    cas.authn.jdbc.query[0].passwordEncoder.type=com.zhangyao.cas.CustomPasswordEncoder

定义加密类 并实现import org.springframework.security.crypto.password.PasswordEncoder

此处我采用的是 org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder spring security默认的加密类

你可能感兴趣的:(java,cas)