CAS 简介
cas是YALE大学发起的一个开源项目, 旨在为web应用系统提供一种可靠的单点登录方法.
它分为server和client端, server端负责对用户的认证工作, client端则负责处理对客户端受保护的资源的访问请求.
CAS的原理,如图:
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服务部署
部署前的准备
- 服务端创建证书
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" />