CAS笔记: 部署与测试

CAS 简介

cas是YALE大学发起的一个开源项目, 旨在为web应用系统提供一种可靠的单点登录方法.

它分为server和client端, server端负责对用户的认证工作, client端则负责处理对客户端受保护的资源的访问请求.

CAS的原理,如图:

cas-01.jpg

CAS 名词

  • Service Ticket: 简称ST, ST是CAS为用户签发的访问某一service的票据。用户访问service时,service发现用户没有ST,则要求用户去CAS获取ST。用户向CAS发出获取ST的请求,如果用户的请求中包含cookie,则CAS会以此cookie值为key查询缓存中有无TGT,如果存在TGT,则用此TGT签发一个ST,返回给用户。用户凭借ST去访问service,service拿ST去CAS验证,验证通过后,允许用户访问资源。

  • Ticket granting ticket: 简称TGT. 是cas服务器为用户签发的登录票据.拥有了TGT,用户就可以证明自己在CAS成功登录过。TGT封装了Cookie值以及此Cookie值对应的用户信息。用户在CAS认证成功后,CAS生成cookie,写入浏览器,同时生成一个TGT对象,放入自己的缓存,TGT对象的ID就是cookie的值。当HTTP再次请求到来时,如果传过来的有CAS生成的cookie,则CAS以此cookie值为key查询缓存中有无TGT ,如果有的话,则说明用户之前登录过,如果没有,则用户需要重新登录。

  • Ticket granting cookie: 简称TGC. 这是一个cookie, 是cas服务器放到用户浏览器中用以标识用户身份的cookie.

CAS REST服务部署

stackoverflow参考资料

部署前的准备

  • 服务端创建证书
keytool -genkey -alias SomeName -keyalg RSA -keystore d:/your/dir/target.keystore

接着根据提示输入相关信息.在最后,提示输入密码时, 务必记住你输入的密码.

  • 服务端导出证书
keytool -export -file d:/your/dir/target.crt -alias SomeName -keystore d:/your/dir/target.keystore

导出时, 会提示你输入刚才创建keystore时的密码.
导出完成后, 生成的target.crt就可以分发给客户端的jdk使用了.

  • 客户端导入证书
keytool -import -keystore %JAVA_HOME%/jre/lib/security/cacerts -file d:/your/dir/target.crt -alias SomeName

提示输入密码. 如果出现keytool error: java.io.IOException: Keystore was tampered with, or password was incorrect错误, 则使用密码changeit.

  • 在服务端tomcat服务器上应用证书
<!-- 务必注意大小写 -->
<connector port="8443" protocol="HTTP/1.1" sslenabled="true"
    maxthreads="150" scheme="https" secure="true" 
    clientauth="false" sslprotocol="TLS" 
    keystoreFile="D:/your/dir/target.keystore" 
    keystorePass="yourPassWord">
</connector>
  • 启动tomcat服务器, 验证SSL是否启用

    访问地址https://localhost:8443/

生成支持rest的cas.war

新建目录, 编写pom.xml, 使用命令mvn clean package生成cas.war

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

  <parent>
    <groupId>org.jasig.cas</groupId>
    <artifactId>cas-server</artifactId>
    <version>3.4.12</version>
  </parent>

  <modelVersion>4.0.0</modelVersion>
  <groupId>h.usm.my</groupId>
  <artifactId>cas</artifactId>
  <packaging>war</packaging>
  <version>1.0</version>
  <name>HUSM CAS Web Application</name>

  <properties>
    <cas.version>3.4.12</cas.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.jasig.cas</groupId>
      <artifactId>cas-server-webapp</artifactId>
      <version>${cas.version}</version>
      <type>war</type>
      <scope>runtime</scope>
    </dependency>

    <dependency>
     <groupId>org.jasig.cas</groupId>
     <artifactId>cas-server-support-jdbc</artifactId>
     <version>${cas.version}</version>
   </dependency>

    <dependency>
      <groupId>org.jasig.cas</groupId>
      <artifactId>cas-server-integration-restlet</artifactId>
      <version>${cas.version}</version>
      <type>jar</type>
      <exclusions>
        <exclusion>
          <groupId>org.springframework</groupId>
          <artifactId>spring-web</artifactId>
        </exclusion>
      </exclusions>
    </dependency>

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>1.4.187</version>
    </dependency>

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>${hibernate.core.version}</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>3.6.0.Final</version>
    </dependency>
  </dependencies>

  <repositories>
    <repository>
      <id>ja-sig</id>
      <url>http://oss.sonatype.org/content/repositories/releases</url>
    </repository>
  </repositories>

  <build>
        <plugins>
            <plugin>
                 <artifactId>maven-war-plugin</artifactId>
                             <configuration>
                                 <warName>cas</warName>
                             </configuration>
                        </plugin>
        </plugins>
    </build>
</project>

修改cas.war的web.xml, 填写Rest Servlet

<!--add a servlet-->
    <servlet>
        <servlet-name>restlet</servlet-name>
        <servlet-class>com.noelios.restlet.ext.spring.RestletFrameworkServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>restlet</servlet-name>
        <url-pattern>/rest/*</url-pattern>
    </servlet-mapping>

修改cas.war的deployerConfigContext.xml, 修改用户名密码的验证方式

  • 注释掉默认的SimpleTestUsernamePasswordAuthenticationHandler

  • 添加新的AuthentitcationHandler

<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
    <property name="dataSource" ref="dataSource"></property>
    <property name="sql" value="select password from t_admin_user where login_name=?"></property>
    <property name="passwordEncoder" ref="MD5PasswordEncoder"></property>
</bean>

这里使用了数据库来存储用户的帐号与密码.
验证时使用sql进行查询,并对查询获得的password字段值,与使用MD5PasswordEncoder进行加密后的输入密码, 进行比对验证.

相关的dataSource与encoder配置如下:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
   <property name="driverClassName" value="com.mysql.jdbc.Driver" />
   <property name="url" value="jdbc:mysql:///wsriademo" />
   <property name="username" value="sa" />
   <property name="password" value="root" />
</bean>
 
<bean id="MD5PasswordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
    <constructor-arg index="0">
        <value>MD5</value>
    </constructor-arg>
</bean>

通过org.jasig.cas.authentication.handler.PasswordEncoder接口可实现自定义加密类.
记得添加相应的数据库驱动jar包到lib目录下.

验证

访问 https://localhost:8443/cas, 输入账密进行网页验证.

CAS Rest的java验证代码


import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;

import javax.net.ssl.HttpsURLConnection;

public class TestCasRest {
    
    /**
     * resolve exception:
     *      java.security.cert.CertificateException: No name matching localhost found
     */
    static {
        //for localhost testing only
        javax.net.ssl.HttpsURLConnection.setDefaultHostnameVerifier(
        new javax.net.ssl.HostnameVerifier(){
 
            public boolean verify(String hostname,
                    javax.net.ssl.SSLSession sslSession) {
                if (hostname.equals("localhost")) {
                    return true;
                }
                return false;
            }
        });
    }

    public static void main(String... args) throws Exception {
        String username = "alpha";
        String password = "123";
        validateFromCAS(username, password);
    }

    public static boolean validateFromCAS(String username, String password)
            throws Exception {

        String url = "https://localhost:8443/cas/rest/tickets";
        try {
            HttpsURLConnection hsu = (HttpsURLConnection) openConn(url);
            String s = URLEncoder.encode("username", "UTF-8") + "="
                    + URLEncoder.encode(username, "UTF-8");
            s += "&" + URLEncoder.encode("password", "UTF-8") + "="
                    + URLEncoder.encode(password, "UTF-8");

            System.out.println(s);
            OutputStreamWriter out = new OutputStreamWriter(
                    hsu.getOutputStream());
            BufferedWriter bwr = new BufferedWriter(out);
            bwr.write(s);
            bwr.flush();
            bwr.close();
            out.close();

            String tgt = hsu.getHeaderField("location");
            System.out.println("ResponseCode: " + hsu.getResponseCode());
            if (tgt != null && hsu.getResponseCode() == 201) {
                System.out.println(tgt);

                System.out.println("==> TGT is : "
                        + tgt.substring(tgt.lastIndexOf("/") + 1));
                tgt = tgt.substring(tgt.lastIndexOf("/") + 1);
                bwr.close();
                closeConn(hsu);

                String serviceURL = "http://localhost:8080/CasClient";
                String encodedServiceURL = URLEncoder
                        .encode("service", "utf-8")
                        + "="
                        + URLEncoder.encode(serviceURL, "utf-8");
                System.out.println("Service url is : " + encodedServiceURL);

                String myURL = url + "/" + tgt;
                System.out.println(myURL);
                hsu = (HttpsURLConnection) openConn(myURL);
                out = new OutputStreamWriter(hsu.getOutputStream());
                bwr = new BufferedWriter(out);
                bwr.write(encodedServiceURL);
                bwr.flush();
                bwr.close();
                out.close();

                System.out.println("Response code is:  "
                        + hsu.getResponseCode());

                BufferedReader isr = new BufferedReader(new InputStreamReader(
                        hsu.getInputStream()));
                String line;
                System.out.println(hsu.getResponseCode());
                while ((line = isr.readLine()) != null) {
                    System.out.println("==> ST is : " + line);
                }
                isr.close();
                hsu.disconnect();
                return true;

            } else {
                return false;
            }

        } catch (MalformedURLException mue) {
            mue.printStackTrace();
            throw mue;

        } catch (IOException ioe) {
            ioe.printStackTrace();
            throw ioe;
        }

    }

    static URLConnection openConn(String urlk) throws MalformedURLException,
            IOException {

        URL url = new URL(urlk);
        HttpsURLConnection hsu = (HttpsURLConnection) url.openConnection();
        hsu.setDoInput(true);
        hsu.setDoOutput(true);
        hsu.setRequestMethod("POST");
        return hsu;

    }

    static void closeConn(HttpsURLConnection c) {
        c.disconnect();
    }

}

Cas client端(非REST请求方式)的配置

在client端工程添加cas-client-core.jar包及相关依赖

<dependency>
    <groupid>org.jasig.cas.client</groupid>
    <artifactid>cas-client-core</artifactid>
    <version>3.1.12</version>
</dependency>

修改client端工程的web.xml, 添加cas的过滤器

<!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置-->
<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>
</filter>
<filter-mapping>
    <filter-name>CAS Single Sign Out Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
 
<!-- 该过滤器负责用户的认证工作,必须启用它 -->
<filter>
    <filter-name>CASFilter</filter-name>
    <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
    <init-param>
        <param-name>casServerLoginUrl</param-name>
        <param-value>https://sso.wsria.com:8443/cas/login</param-value>
    </init-param>
    <init-param>
        <!--这里的server是服务端的IP-->
        <param-name>serverName</param-name>
        <param-value>http://localhost:10000</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>
        <param-value>https://sso.wsria.com:8443/cas</param-value>
    </init-param>
    <init-param>
        <param-name>serverName</param-name>
        <param-value>http://localhost:10000</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>
 
<!--
该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。
比如AssertionHolder.getAssertion().getPrincipal().getName()。
-->
<filter>
    <filter-name>CAS Assertion Thread Local Filter</filter-name>
    <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CAS Assertion Thread Local Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
 
<!-- 自动根据单点登录的结果设置本系统的用户信息 -->
<filter>
    <display-name>AutoSetUserAdapterFilter</display-name>
    <filter-name>AutoSetUserAdapterFilter</filter-name>
    <filter-class>com.wsria.demo.filter.AutoSetUserAdapterFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>AutoSetUserAdapterFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<!-- ======================== 单点登录结束 ======================== -->

其中自定义的AutoSetUserAdapterFilter的代码如下

package com.wsria.demo.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.jasig.cas.client.validation.Assertion;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import com.wsria.demo.entity.account.User;
import com.wsria.demo.service.account.UserManager;
import com.wsria.demo.util.UserUtil;


/**
 * 自动根据单点登录系统的信息设置本系统的用户信息
 *
 * @author 咖啡兔
 * @site www.wsria.cn
 *
 */
public class AutoSetUserAdapterFilter implements Filter {
        
        /**
         * Default constructor. 
         */
        public AutoSetUserAdapterFilter() {
        }

        /**
         * @see Filter#destroy()
         */
        public void destroy() {
        }

        /**
         * 过滤逻辑:首先判断单点登录的账户是否已经存在本系统中,
         * 如果不存在使用用户查询接口查询出用户对象并设置在Session中
         * @see Filter#doFilter(ServletRequest, ServletResponse, FilterChain)
         */
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
                        ServletException {
                HttpServletRequest httpRequest = (HttpServletRequest) request;
                
                // _const_cas_assertion_是CAS中存放登录用户名的session标志
                Object object = httpRequest.getSession().getAttribute("_const_cas_assertion_");
                
                if (object != null) {
                        Assertion assertion = (Assertion) object;
                        String loginName = assertion.getPrincipal().getName();
                        User user = UserUtil.getCurrentUser(httpRequest.getSession());
                        
                        // 第一次登录系统
                        if (user == null) {
                                WebApplicationContext wct = WebApplicationContextUtils.getWebApplicationContext(httpRequest
                                                .getSession().getServletContext());
                                UserManager userManager = (UserManager) wct.getBean("userManager");
                                user = userManager.findUserByLoginName(loginName);
                                // 保存用户信息到Session
                                UserUtil.saveUserToSession(httpRequest.getSession(), user);
                        }
                        
                }
                chain.doFilter(request, response);
        }

        /**
         * @see Filter#init(FilterConfig)
         */
        public void init(FilterConfig fConfig) throws ServletException {
        }

}

附注

单点退出

访问https://localhost:8443/cas/logout即可.

美化CAS服务器界面

修改cas\WEB-INF\view\jsp\default\ui下相关的jsp文件

在服务端不使用SSL协议

  • 修改%CATALINA_HOME%\conf\server.xml文件, 关闭Tomcat服务器的SSL端口
<!-- 关闭SSL端口
<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true"  
               maxThreads="150" scheme="https" secure="true"  
               clientAuth="false" sslProtocol="TLS"  
               keystoreFile="E:/sso/keys/dcssokey" keystorePass="dcfs00"  
               truststoreFile="D:/ProgramFiles/Java/jdk1.6.0_25/jre/lib/security/cacerts" />  
-->
  • 修改服务端cas\WEB-INF\deployerConfigContext.xml文件
<!-- 添加非安全协议配置 -->
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"  
    p:httpClient-ref="httpClient" 
    p:requireSecure="false" />  
  • 修改服务端的cas\WEB-INF\spring-configuration\ticketGrantingTicketCookieGennerator.xml文件
<!-- 修改cookie非安全协议配置 -->
<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"  
        p:cookieSecure="false"  
        p:cookieMaxAge="600"  
        p:cookieName="CASTGC"  
        p:cookiePath="/cas" />  
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344

推荐阅读更多精彩内容