central authentication service,中央认证服务。耶鲁大学的一个开源项目,目的是为web应用系统提供一种可靠的单点登录方法。
cas包括两个部分,cas server和cas client。cas server负责对用户的认证;cas client负责处理对客户端受保护资源的访问请求,当登录时,重定向到cas server。
二、使用示例
从官网下载cas-server和cas-client的发行包。在cas-server包里的module目录下有cas-server.war的示例项目。网上搜索cas-client的示例项目mywebapp.war,不太好找。
示例war使用步骤
1. 配置tomcat虚拟主机
在tomcat中server.xml配置3个虚拟主机,两个客户端www.gougou.com和www.maomao.com,一个服务端www.server.com。
<Host name="www.server.com" appBase="server" autoDeploy="true" unpackWARs="true">
</Host>
<Host name="www.gougou.com" appBase="gougou" autoDeploy="true" unpackWARs="true">
</Host>
<Host name="www.maomao.com" appBase="maomao" autoDeploy="true" unpackWARs="true">
</Host>
在window下,将这3个域名和127.0.0.1做映射。在tomcat配置的虚拟主机的目录下,分别放置相应的war包,启动tomcat自动部署项目,然后关闭tomcat,将项目包的文件夹改名为ROOT,这样可以直接访问。在客户端的项目的lib目录下复制入cas-client-core的jar和commons-loggings的jar。启动tomcat,测试3个域名是否可以访问。可能出现java版本和cas-server版本的兼容问题,一般是java版本过高,考虑降低java版本。
2. 修改客户端的配置文件
修改客户端的web.xml
<filter>
<filter-name>CAS Authentication Filter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>http://www.server.com:8081/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://www.maomao.com:8081</param-value>
</init-param>
<init-param>
<param-name>renew</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>gateway</param-name>
<param-value>false</param-value>
</init-param>
</filter>
<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://www.server.com:8081</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://www.maomao.com:8081</param-value>
</init-param>
<!-- <init-param>
<param-name>proxyCallbackUrl</param-name>
<param-value>https://localhost:8443/mywebapp/proxyCallback</param-value>
</init-param>
<init-param>
<param-name>proxyReceptorUrl</param-name>
<param-value>/mywebapp/proxyCallback</param-value>
</init-param> -->
</filter>
3. 修改服务端配置文件,设置为单点登录
cas服务器使用spring配置文件,使用cookie技术。ticketGrantingTicketCookieGenerator.xml设置cookie相关的。
<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="false"
p:cookieMaxAge="3066"
p:cookieName="CASTGC"
p:cookiePath="/" />
修改cookie后,测试单点登录。
三、在项目开发中使用cas
使用eclipse开发,创建3个项目。一个casServer,和两个web客户端服务项目。分别配置两个客户端项目的web.xml中,的验证登录网址,以及服务端配置文件中cookie的配置,见上。
1. 在客户端获取登录用户名
4种方式
<dd>
方式1:
<%= request.getRemoteUser() %>
</dd>
<dd>
方式2:
<%
Principal p = request.getUserPrincipal();
out.print(p);
%>
</dd>
<dd>
方式3:
<%
Assertion assertion = AssertionHolder.getAssertion();
p = assertion.getPrincipal();
String user = p.getName();
out.print(user);
%>
</dd>
<dd>
方式4:
<%
assertion = (Assertion)session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
p = assertion.getPrincipal();
user = p.getName();
out.print(user);
%>
</dd>
2. 修改服务器登录验证规则
在服务器cas的deployerConfigContext.xml中,配置用户认证登录方式,可以配置多个,只要其中之一通过就通过。
3. 使用配置的用户名和密码
<property name="authenticationHandlers">
<list>
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
p:httpClient-ref="httpClient" />
<bean
class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" />
<bean class="org.jasig.cas.adaptors.generic.AcceptUsersAuthenticationHandler">
<property name="users">
<map>
<entry key="maozi" value="tom"></entry>
<entry key="gouzi" value="jim"></entry>
</map>
</property>
</bean>
</list>
</property>
4. 自定义验证规则
所有的认证类都继承自AuthenticationHandler。自定义的基于用户名和密码的验证类继承AbstractUsernamePasswordAuthenticationHandler。
编写MyUsernamePasswordAuthenticationHandler类
/**
* 自定义的用户名密码认证类
* 认证规则,密码是用户名的后两位,用户名长度需要大于3位
* @author Administrator
*
*/
public class MyUsernamePasswordAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler {
@Override
protected boolean authenticateUsernamePasswordInternal(UsernamePasswordCredentials credentials)
throws AuthenticationException {
// TODO Auto-generated method stub
//参数合法性判断
if(credentials == null){
return false;
}
//获取用户名和密码
final String username = credentials.getUsername();
final String password = credentials.getPassword();
//用户名长度合法性判断
if(username.length() < 3){
return false;
}
//密码是否与用户名后两位等同
if(password.length() > 0 && password.equals(username.substring(username.length() - 2))){
return true;
}
return false;
}
}
将自定的认证类配置到配置文件
<property name="authenticationHandlers">
<list>
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
p:httpClient-ref="httpClient" />
<bean class="com.cas.authentication.MyUsernamePasswordAuthenticationHandler"></bean>
</list>
</property>
5. 使用jdbc连接数据库认证登录
前提导入cas-server-support-jdbc-3.4.11.jar和mysql-connector的jar。配置数据库环境,创建数据库cas,添加表user,插入两个数据,密码采用md5加密。
在deployerConfigContext.xml的最后配置数据库连接dataSource。
<!-- 配置数据库连接 -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/cas?characterEncoding=UTF-8" />
<property name="username" value="root" />
<property name="password" value="xxxx" />
</bean>
可以使用两种方式查询数据库,需要配置在认证处理器里面
<property name="authenticationHandlers">
<list>
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
p:httpClient-ref="httpClient" />
<!-- <bean
class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" />
<bean class="org.jasig.cas.adaptors.generic.AcceptUsersAuthenticationHandler">
<property name="users">
<map>
<entry key="maozi" value="tom"></entry>
<entry key="gouzi" value="jim"></entry>
</map>
</property>
</bean> -->
<bean class="com.cas.authentication.MyUsernamePasswordAuthenticationHandler"></bean>
<!-- 数据库认证方式1,配置用户登录认证查询的表结构 -->
<!-- <bean class="org.jasig.cas.adaptors.jdbc.SearchModeSearchDatabaseAuthenticationHandler">
<property name="dataSource" ref="dataSource" />
<property name="tableUsers" value="user" />
<property name="fieldUser" value="username" />
<property name="fieldPassword" value="password" />
<property name="passwordEncoder"> 可选的加密
<bean class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
<constructor-arg value="MD5"/>
<property name="characterEncoding" value="UTF-8"></property>
</bean>
</property>
</bean> -->
<!-- 数据库认证方式2,使用sql语句查询数据库的方式 -->
<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
<property name="dataSource" ref="dataSource" />
<property name="sql" value="select password from user where username = ?" />
<property name="passwordEncoder">
<bean class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
<constructor-arg value="MD5"/>
<property name="characterEncoding" value="UTF-8"></property>
</bean>
</property>
</bean>
</list>
</property>
6. 获取其他用户信息
1)返回用户的id
原始的cas服务器中,通过deployerConfigContext.xml中的<property name="credentialsToPrincipalResolvers">中的bean来将服务器中的credentials转换为pricinpal返回给客户端,为了返回id,需要自定义一个MyCredentialsToPrincipalResolver实现CredentialsToPrincipalResolver接口,并配置到这个文件中。
/**
* 自定义的credentials转pricinpal的类
* @author Administrator
*
*/
public class MyCredentialsToPrincipalResolver implements CredentialsToPrincipalResolver {
private DataSource dataSource;
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Principal resolvePrincipal(Credentials credentials) {
// TODO Auto-generated method stub
//非空判断
if(credentials == null){
return null;
}
UsernamePasswordCredentials upCredentials = (UsernamePasswordCredentials)credentials;
final String username = upCredentials.getUsername();
final String password = upCredentials.getPassword();
String md5pwd = MD5Util.getPassword(password);
String sql = "select id from user where username = ? and password = ?";
int id;
JdbcTemplate jdbcTemplate = new JdbcTemplate(getDataSource());
id = jdbcTemplate.queryForInt(sql, username, md5pwd);
if(id > 0){
Principal principal = new SimplePrincipal(id+"");
return principal;
}
return null;
}
@Override
public boolean supports(Credentials credentials) {
// TODO Auto-generated method stub
return credentials != null
&& UsernamePasswordCredentials.class
.isAssignableFrom(credentials.getClass());
}
}
配置文件
<property name="credentialsToPrincipalResolvers">
<list>
<!-- <bean
class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver" /> -->
<bean class="com.cas.credentials.MyCredentialsToPrincipalResolver">
<property name="dataSource" ref="dataSource" />
</bean>
<bean
class="org.jasig.cas.authentication.principal.HttpBasedServiceCredentialsToPrincipalResolver" />
</list>
</property>
7. 获取更多用户信息
基本同上,在服务器返回自定义的principal时,将用户信息以map传入SimplePrincipal的第二个参数。
/**
* 自定义的credentials转pricinpal的类
* @author Administrator
*
*/
public class MyCredentialsToPrincipalResolver implements CredentialsToPrincipalResolver {
private DataSource dataSource;
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Principal resolvePrincipal(Credentials credentials) {
// TODO Auto-generated method stub
//非空判断
if(credentials == null){
return null;
}
UsernamePasswordCredentials upCredentials = (UsernamePasswordCredentials)credentials;
final String username = upCredentials.getUsername();
final String password = upCredentials.getPassword();
Map<String, Object> map = new HashMap<>();
map.put("username", username);
map.put("password", password);
String address = "上海";
map.put("address", address);
String md5pwd = MD5Util.getPassword(password);
String sql = "select id from user where username = ? and password = ?";
int id;
JdbcTemplate jdbcTemplate = new JdbcTemplate(getDataSource());
id = jdbcTemplate.queryForInt(sql, username, md5pwd);
if(id > 0){
map.put("id", id);
Principal principal = new SimplePrincipal(id+"", map);
return principal;
}
return null;
}
@Override
public boolean supports(Credentials credentials) {
// TODO Auto-generated method stub
return credentials != null
&& UsernamePasswordCredentials.class
.isAssignableFrom(credentials.getClass());
}
}
修改服务器的casServiceValidationSuccess.jsp文件
<%@ page session="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}</cas:user>
<c:if test="${not empty pgtIou}">
<cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>
</c:if>
<c:if test="${fn:length(assertion.chainedAuthentications) > 1}">
<cas:proxies>
<c:forEach var="proxy" items="${assertion.chainedAuthentications}" varStatus="loopStatus" begin="0" end="${fn:length(assertion.chainedAuthentications)-2}" step="1">
<cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>
</c:forEach>
</cas:proxies>
</c:if>
<cas:attributes>
<c:forEach items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}"
var="attr">
<cas:${attr.key}>${attr.value}</cas:${attr.key}>
</c:forEach>
</cas:attributes>
</cas:authenticationSuccess>
</cas:serviceResponse>
修改deployerConfigContext.xml文件,将最后一个配置InMemoryServiceRegistryDaoImpl这个bean的所有属性注释掉。
注意,如果出现中文乱码,可以使用URLEncoder和URLDecoder在服务端用utf-8编码,在客户端用utf-8解码。
8. 使用监听器将服务器返回的数据封装成自定义对象放到session中
服务器返回数据后,以_const_cas_assertion_为key将Assertion对象session中去,利用这个,可以通过监听session,将Assertion对象封装成自定义对象放入session返回。
在客户端创建一个User类
略。
在客户端创建一个监听器类
/**
* 自定义监听session信息的类
* @author Administrator
*
*/
public class MySessionListener implements HttpSessionAttributeListener {
@Override
public void attributeAdded(HttpSessionBindingEvent event) {
// TODO Auto-generated method stub
String key = event.getName();
if(key.equals(AbstractCasFilter.CONST_CAS_ASSERTION)){
//监听获取Assertion对象
Assertion assertion = (Assertion) event.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION);
//获取在服务器端配置的map
Map<String, Object> map = assertion.getPrincipal().getAttributes();
//遍历map将其中的值放入User对象
User user = new User();
user.setId(Integer.parseInt((String)map.get("id")));
user.setUsername((String)map.get("username"));
user.setPassword((String)map.get("password"));
user.setAddress((String)map.get("address"));
event.getSession().setAttribute("user", user);
}
}
@Override
public void attributeRemoved(HttpSessionBindingEvent event) {
// TODO Auto-generated method stub
}
@Override
public void attributeReplaced(HttpSessionBindingEvent event) {
// TODO Auto-generated method stub
}
}
在客户端的web.xml文件中配置这个监听器
<listener>
<listener-class>com.cas.client.listener.MySessionListener</listener-class>
</listener>
测试。
9. 单点注销
在cas的客户端,使用一个map维护所有登陆用户的session和tg凭据。cas服务器将依次将客户端发送请求,被cas客户端的注销过滤器拦截到,注销过滤器完成客户端的注销工作。实现单点注销,需要将客户端的单点注销过滤器打开。
<!--
<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>
-->
使用单点登录服务器的地址www.server.com/logout测试注销。