CAS 簡介
cas是YALE大學(xué)發(fā)起的一個開源項目, 旨在為web應(yīng)用系統(tǒng)提供一種可靠的單點登錄方法.
它分為server和client端, server端負(fù)責(zé)對用戶的認(rèn)證工作, client端則負(fù)責(zé)處理對客戶端受保護(hù)的資源的訪問請求.
CAS的原理,如圖:

CAS 名詞
Service Ticket: 簡稱ST, ST是CAS為用戶簽發(fā)的訪問某一service的票據(jù)。用戶訪問service時,service發(fā)現(xiàn)用戶沒有ST,則要求用戶去CAS獲取ST。用戶向CAS發(fā)出獲取ST的請求,如果用戶的請求中包含cookie,則CAS會以此cookie值為key查詢緩存中有無TGT,如果存在TGT,則用此TGT簽發(fā)一個ST,返回給用戶。用戶憑借ST去訪問service,service拿ST去CAS驗證,驗證通過后,允許用戶訪問資源。
Ticket granting ticket: 簡稱TGT. 是cas服務(wù)器為用戶簽發(fā)的登錄票據(jù).擁有了TGT,用戶就可以證明自己在CAS成功登錄過。TGT封裝了Cookie值以及此Cookie值對應(yīng)的用戶信息。用戶在CAS認(rèn)證成功后,CAS生成cookie,寫入瀏覽器,同時生成一個TGT對象,放入自己的緩存,TGT對象的ID就是cookie的值。當(dāng)HTTP再次請求到來時,如果傳過來的有CAS生成的cookie,則CAS以此cookie值為key查詢緩存中有無TGT ,如果有的話,則說明用戶之前登錄過,如果沒有,則用戶需要重新登錄。
Ticket granting cookie: 簡稱TGC. 這是一個cookie, 是cas服務(wù)器放到用戶瀏覽器中用以標(biāo)識用戶身份的cookie.
CAS REST服務(wù)部署
部署前的準(zhǔn)備
- 服務(wù)端創(chuàng)建證書
keytool -genkey -alias SomeName -keyalg RSA -keystore d:/your/dir/target.keystore
接著根據(jù)提示輸入相關(guān)信息.在最后,提示輸入密碼時, 務(wù)必記住你輸入的密碼.
- 服務(wù)端導(dǎo)出證書
keytool -export -file d:/your/dir/target.crt -alias SomeName -keystore d:/your/dir/target.keystore
導(dǎo)出時, 會提示你輸入剛才創(chuàng)建keystore時的密碼.
導(dǎo)出完成后, 生成的target.crt就可以分發(fā)給客戶端的jdk使用了.
- 客戶端導(dǎo)入證書
keytool -import -keystore %JAVA_HOME%/jre/lib/security/cacerts -file d:/your/dir/target.crt -alias SomeName
提示輸入密碼. 如果出現(xiàn)keytool error: java.io.IOException: Keystore was tampered with, or password was incorrect錯誤, 則使用密碼changeit.
- 在服務(wù)端tomcat服務(wù)器上應(yīng)用證書
<!-- 務(wù)必注意大小寫 -->
<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服務(wù)器, 驗證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, 修改用戶名密碼的驗證方式
注釋掉默認(rèn)的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>
這里使用了數(shù)據(jù)庫來存儲用戶的帳號與密碼.
驗證時使用sql進(jìn)行查詢,并對查詢獲得的password字段值,與使用MD5PasswordEncoder進(jìn)行加密后的輸入密碼, 進(jìn)行比對驗證.
相關(guān)的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接口可實現(xiàn)自定義加密類.
記得添加相應(yīng)的數(shù)據(jù)庫驅(qū)動jar包到lib目錄下.
驗證
訪問 https://localhost:8443/cas, 輸入賬密進(jìn)行網(wǎng)頁驗證.
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包及相關(guān)依賴
<dependency>
<groupid>org.jasig.cas.client</groupid>
<artifactid>cas-client-core</artifactid>
<version>3.1.12</version>
</dependency>
修改client端工程的web.xml, 添加cas的過濾器
<!-- 用于單點退出,該過濾器用于實現(xiàn)單點登出功能,可選配置-->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<!-- 該過濾器用于實現(xiàn)單點登出功能,可選配置。 -->
<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>
<!-- 該過濾器負(fù)責(zé)用戶的認(rèn)證工作,必須啟用它 -->
<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是服務(wù)端的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>
<!-- 該過濾器負(fù)責(zé)對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>
<!--
該過濾器負(fù)責(zé)實現(xiàn)HttpServletRequest請求的包裹,
比如允許開發(fā)者通過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>
<!--
該過濾器使得開發(fā)者可以通過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>
<!-- 自動根據(jù)單點登錄的結(jié)果設(shè)置本系統(tǒng)的用戶信息 -->
<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>
<!-- ======================== 單點登錄結(jié)束 ======================== -->
其中自定義的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;
/**
* 自動根據(jù)單點登錄系統(tǒng)的信息設(shè)置本系統(tǒng)的用戶信息
*
* @author 咖啡兔
* @site www.wsria.cn
*
*/
public class AutoSetUserAdapterFilter implements Filter {
/**
* Default constructor.
*/
public AutoSetUserAdapterFilter() {
}
/**
* @see Filter#destroy()
*/
public void destroy() {
}
/**
* 過濾邏輯:首先判斷單點登錄的賬戶是否已經(jīng)存在本系統(tǒng)中,
* 如果不存在使用用戶查詢接口查詢出用戶對象并設(shè)置在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標(biāo)志
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());
// 第一次登錄系統(tǒng)
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服務(wù)器界面
修改cas\WEB-INF\view\jsp\default\ui下相關(guān)的jsp文件
在服務(wù)端不使用SSL協(xié)議
- 修改
%CATALINA_HOME%\conf\server.xml文件, 關(guān)閉Tomcat服務(wù)器的SSL端口
<!-- 關(guān)閉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" />
-->
- 修改服務(wù)端
cas\WEB-INF\deployerConfigContext.xml文件
<!-- 添加非安全協(xié)議配置 -->
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
p:httpClient-ref="httpClient"
p:requireSecure="false" />
- 修改服務(wù)端的
cas\WEB-INF\spring-configuration\ticketGrantingTicketCookieGennerator.xml文件
<!-- 修改cookie非安全協(xié)議配置 -->
<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="false"
p:cookieMaxAge="600"
p:cookieName="CASTGC"
p:cookiePath="/cas" />