目 录
1 CAS原理
2 cas-server配置
3 cas-client配置
4 客户端自定义登录界面

一、CAS 原理

  1. 访问服务: SSO 客户端发送请求访问应用系统提供的服务资源。

  2. 定向认证: SSO 客户端会重定向用户请求到 SSO 服务器。

  3. 用户认证:用户身份认证。

  4. 发放票据: SSO 服务器会产生一个随机的 Service Ticket 。

  5. 验证票据: SSO 服务器验证票据 Service Ticket 的合法性,验证通过后,允许客户端访问服务。

  6. 传输用户信息: SSO 服务器验证票据通过后,传输用户认证结果信息给客户端。

下面是 CAS 最基本的协议过程:

20161006img05

如上图: CAS Client 与受保护的客户端应用部署在一起,以Filter方式保护Web应用的受保护资源,过滤从客户端过来的每一个 Web 请求,同 时, CAS Client 会分析 HTTP 请求中是否包含请求 Service Ticket( ST 上图中的 Ticket) ,如果没有,则说明该用户是没有经过认证的;于是 CAS Client 会重定向用户请求到 CAS Server ( Step 2 ),并传递 Service (要访问的目的资源地址)。 Step 3 是用户认证过程,如果用户提供了正确的 Credentials , CAS Server 随机产生一个相当长度、唯一、不可伪造的 Service Ticket ,并缓存以待将来验证,并且重定向用户到 Service 所在地址(附带刚才产生的 Service Ticket ) , 并为客户端浏览器设置一个 Ticket Granted Cookie ( TGC ) ; CAS Client 在拿到 Service 和新产生的 Ticket 过后,在 Step 5 和 Step6 中与 CAS Server 进行身份核实,以确保 Service Ticket 的合法性。

在该协议中,所有与 CAS Server 的交互均采用 SSL 协议,以确保 ST 和 TGC 的安全性。协议工作过程中会有 2次重定向 的过程。但是 CAS Client 与 CAS Server 之间进行 Ticket 验证的过程对于用户是透明的(使用 HttpsURLConnection )。

CAS 请求认证时序图如下:

20161006img06

二、cas-server配置

本文所用测试地址说明:

cas-server地址:http://localhost:8080/cas

cas-client地址:http://localhost:7080/test_consumer

1.下载cas包

https://github.com/apereo/cas/releases/tag/v3.5.2

2.cas-server-3.5.2-release.zip解压,将modules下面的cas-server-webapp-3.5.2.war部署到tomcat服务器,重命名为cas.war并解压。####

3.导入modules中的cas-server-support-jdbc-3.5.2.jar包,导入数据库驱动mysql-connector-java-5.1.29.jar包

4.由于不采用https方式,需要修改配置文件

WEB-INF/DEPLOYERCONFIGCONTEXT.XML

增加参数 p:requireSecure="false" ,是否需要安全验证,即 HTTPS , false 为不采用 如下:
<bean class =  "org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref = "httpClient" p:requireSecure= "false"/>

WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml 修改

<bean id = "ticketGrantingTicketCookieGenerator" class = "org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
   p:cookieSecure = " false "
   p:cookieMaxAge = "-1"
   p:cookieName = "CASTGC"
   p:cookiePath = "/cas"/>

WEB-INF\spring-configuration\warnCookieGenerator.xml 修改

<bean id = "warnCookieGenerator" class = "org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
   p:cookieSecure = " false "
   p:cookieMaxAge = "-1"
   p:cookieName = "CASPRIVACY"
   p:cookiePath = "/cas"/>

5.自定义验证,将原来的测试用验证注释掉,然后添加查询数据库方式验证,对于数据库表tb_user

<!--<bean class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler"/>-->
			
<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
	<property name="dataSource" ref="dataSource"></property>
	<property name="sql" value="select password from tb_user where username=?"></property>
	<property name="passwordEncoder" ref="myPasswordEncoder"></property>
</bean>

6.自定义MD5验证

<bean id="myPasswordEncoder" class="org.jasig.cas.authentication.handler.MyPasswordEncoder"/>

自定义MD5 java代码如下:

package org.jasig.cas.authentication.handler;

import java.security.MessageDigest;

public class MyPasswordEncoder implements PasswordEncoder  
{

	public static void main(String[] args) {
		MyPasswordEncoder t = new MyPasswordEncoder();
		System.out.println(t.encode("123456"));
	}

	public String encode(String content) {
		return md5(md5(content) + "asiainfo");
	}
	
	public String md5(String content){
		if(content == null){
			return null;
		}
		StringBuffer sbReturn = new StringBuffer();
		try {
			MessageDigest md = MessageDigest.getInstance("MD5");
			md.update((content).getBytes("utf-8"));
			for (byte b : md.digest()) {
				sbReturn.append(Integer.toString((b & 0xff) + 0x100, 16).substring(1));
			}
			return sbReturn.toString();
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}
}

测试cas-server环境

20161006img01

20161006img02

三、cas-client配置

web.xml配置

<!-- cas client begin -->
<!-- 用于单点退出,该过滤器用于实现单点登出功能,可选配置-->  
<listener>  
    <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>  
</listener>  
  
<!-- CAS Server 通知 CAS Client,删除session,注销登录信息  -->  
<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>  

    <!-- 将AuthenticationFilter代码复制一份自定义为ClientAuthenticationFilter.java文件 -->
    <filter-class>org.jasig.cas.client.authentication.ClientAuthenticationFilter</filter-class>    

    <init-param>  
        <param-name>casServerLoginUrl</param-name>  
        <param-value>http://localhost:8080/cas/login</param-value>  
    </init-param>

	<!-- 添加各客户端对应的登录界面 -->
    <init-param>  
        <param-name>login_from</param-name>  
        <param-value>http://localhost:7080/test_consumer/login.jsp</param-value>  
    </init-param>

    <init-param>  
        <!--这里的server是服务端的IP-->  
        <param-name>serverName</param-name>  
        <param-value>http://localhost:7080/</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>http://localhost:8080/cas</param-value>  
    </init-param>  
    <init-param>  
        <param-name>serverName</param-name>  
        <param-value>http://localhost:7080/</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>  
<!-- cas client end -->

自定义ClientAuthenticationFilter.java文件添加:

private String login_from;
......
//initInternal方法中添加:
setCasServerLoginUrl(getPropertyFromInitParams(filterConfig, "login_from", null));
log.trace("Loaded login_from parameter: " + this.login_from);

//doFilter方法中添加:
//如果是登录界面则跳过
String loginUrl = request.getRequestURL().toString();
if(this.casServerLoginUrl.equals(loginUrl)){
	filterChain.doFilter(request, response);
    return;
}

四、客户端自定义登录界面

1.自定义login.jsp登录界面

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>  
    <head> 
    <title>自定义登录界面</title>  
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">  
    </head>  
    <body>  
     <script type="text/javascript">    
        function getQueryStringByName(name) {    
             var result = location.search.match(new RegExp("[\?\&]" + name+ "=([^\&]+)","i"));    
             if(result == null || result.length < 1){    
             return "";    
         }    
         return result[1];    
         }  
        var info = getQueryStringByName('info');    
    if (info == "error")    
        alert("用户名密码错误!");    

	</script>
    <form method="GET" action="http://localhost:8080/cas/login">    
        <p>用户名 : <input type="text" name="username"/></p>    
        <p>密码 : <input type="password" name="password"/></p>
        <p><input type="submit" value="登录"/></p>    
        <input type="hidden" name="auto" value="true"/>    
        <input type="hidden" name="service" value="<%=request.getParameter("service") %>" />  
    </form>    
    </body>  
</html>

2.定义相同的路径包org.jasig.cas.web.flow

将cas-server-core-3.5.2中的AuthenticationViaFormAction.java复制到该包下就行改写,修改:
修改submit方法中
try {
        WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials));
        putWarnCookieIfRequestParameterPresent(context);
        return "success";
    } catch (final TicketException e) {
    	//添加验证失败后的跳转页面 begin
    	String login_from = context.getRequestParameters().get("login_from");  
        if (login_from != null && login_from.length() > 0) {  
            context.getRequestScope().put("redirectUrl", login_from + "?info=error");  
            return "customizedRedirect";  
        } 
        //添加验证失败后的跳转页面 end
    	
        populateErrorsInstance(e, messageContext);
        if (isCauseAuthenticationException(e))
            return getAuthenticationExceptionEventId(e);
        return "error";
    }
将生成的AuthenticationViaFormAction.class替换cas-server-core-3.5.2.jar中的class文件

3.修改客户端过滤规则,将login.jsp排除在外

<filter-mapping>  
    <filter-name>CASFilter</filter-name>  
    <url-pattern>/jsp/*</url-pattern>  
</filter-mapping>

4.修改cas的默认登录页面 WEB-INF/view/jsp/default/ui/casLoginView.jsp

20161006img07

5.cas-server-webapp工程中,修改WEB-INF/login-webflow.xml

<action-state id="realSubmit">  
    ...... 
    <transition on="error" to="generateLoginTicket"/>  
    <!--加入下面这句话该transition , 当验证失败之后转到自定义页面 -->    
    <transition on="customizedRedirect" to="customizedRedirectView"/>  
    ......   
</action-state>

6.添加客户端跳转页面

<end-state id="customizedRedirectView" view="externalRedirect:${requestScope.redirectUrl}"/>

测试自定义登录界面

在客户端浏览器中输入http://localhost:7080/test_consumer/jsp/home.jsp 回车,cas server验证失败 后重定向到客户端传过来的login_form地址

20161006img03

20161006img04