网络上有很多关于CAS实现单点登录的帖子,但是大多数都是会以https协议作为认证的介绍。但是https需要各种认证证书的注册,所以操作起来会相当麻烦,而且一般的证书都是有有效时间的,超过了有效时间,证书就会失效,就需要重新进行证书生成操作(个人觉得比较麻烦,但https协议在网络中相对安全,https加入了ssl,每个系统也可以定制自己的认证证书)。在本文章中,只重点介绍基于http认证的配置及说明。(本文章只停留在应用层面上的技术(初入职场,技术有限,忘大神勿喷),如以下有什么错误的地方,请各位能够留言说明,以完善文档。或者有大神不吝啬赐教的话,本人不胜感激!)
首先,先简单的描述一下单点登录是个什么东东。接入了单点登录的应用可以采用统一的用户账号和密码登录业务应用系统,允许用户一次性进行认证之后,就访问系统中不同的应用;而不需要访问每个应用时,都重新输入密码。IBM对SSO有一个很形象的解释“单点登录,全网漫游”。
关于cas的介绍这里就不做过多的说明,网上对这部分内容很多都写得很详细,大家自己问问度娘吧,这里只简单说明一下cas的基本结构,如下:
CAS 包含两部分:
1、 CAS Server
CAS Server 负责完成对用户的认证工作, CAS Server 需要独立部署, 有不止一种 CAS Server 的实现, Yale CAS Server 和 ESUP CAS Server 都 是很不错的选择。
CAS Server 会处理用户名 / 密码等凭证 (Credentials) ,它可能会 到数据库检索一条用户帐号信息,也可能在 XML 文件中检索用户密码,对 这种方式, CAS 均提供一
种灵活但同一的接口 / 实现分离的方式, CAS 究 竟是用何种认证方式,跟 CAS 协议是分离的,也就是,这个认证的实现细 节可以自己定制和扩展。
2、CAS Client
CAS Client 负责部署在客户端(注意,我是指 Web 应用),原则上, CAS Client 的部署意味着,当有对本地 Web 应用的受保护资源的访问请求, 并且需要对请
求方进行身份认证, Web 应用不再接受任何的用户名密码等 类似的 Credentials ,而是重定向到 CAS Server 进行认证。
目前, CAS Client 支持(某些在完善中)非常多的客户端,包括 Java 、 .Net 、 ISAPI 、 Php 、 Perl 、 uPortal 、 Acegi 、 Ruby 、 VBScript 等客户端,几乎可
以这样说, CAS 协议能够适合任何语言编写的 客户端应用。
具体配置部署说明规范:
如上述所说,配置分为cas服务端与cas客户端(即需连接进sso的应用),具体配置如下:
服务端配置说明:
在cas官网下载服务端,地址:https://www.apereo.org/projects/cas,现在最新版本已经更新到了4.1.0,不同版本之间会有一些小的版本细节需要注意,这里不做深入研究,本文档选择cas-3.5.3版本,项目名为cas-server-webapp。
解压到jvm中,并修改部分配置信息然后部署到服务器上,修改如下:
,如下
修改之前:
<property name="authenticationHandlers">
<list>
…
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" />
…
</property>
p: requireSecure="false")
<property name="authenticationHandlers">
<list>
…
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServ iceCredentialsAuthenticationHandler"
p:httpClient-ref="httpClient"
p:requireSecure="false" />
…
</property>
2) ssoServer/webapp/WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml,如下:
修改前:
<bean id="ticketGrantingTicketCookieGenerator"
class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="true"
p:cookieMaxAge="-1"
p:cookieName="CASTGC"
p:cookiePath="/cas" />
cookieSecure为false):
<bean id="ticketGrantingTicketCookieGenerator"
class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="false"
p:cookieMaxAge="-1"
p:cookieName="CASTGC"
p:cookiePath="/cas" />
3) ssoServer/webapp/WEB-INF/spring-configuration/warnCookieGenerator.xml,如下:
修改前:
<bean id="warnCookieGenerator"
class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="true"
p:cookieMaxAge="-1"
p:cookieName="CASPRIVACY"
p:cookiePath="/cas" />
cookieSecure为false):
<bean id="warnCookieGenerator"
class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"
p:cookieSecure="false"
p:cookieMaxAge="-1"
p:cookieName="CASPRIVACY"
p:cookiePath="/cas" />
4) 认证服务器访问的数据源,这里是用户名密码验证以及用户信息返回的地方:ssoServer/webapp/WEB-INF/deployerConfigContext.xml
修改后如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:tx="http://www.springframework.org/schema/tx" xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<bean id="authenticationManager" class="org.jasig.cas.authentication.AuthenticationManagerImpl">
<property name="credentialsToPrincipalResolvers">
<list>
<bean
class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver">
<property name="attributeRepository" ref="attributeRepository" />
</bean>
<bean
class="org.jasig.cas.authentication.principal.HttpBasedServiceCredentialsToPrincipalResolver" />
</list>
</property>
<property name="authenticationHandlers">
<list>
<bean
class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
p:httpClient-ref="httpClient" p:requireSecure="false" />
<!-- 用户密码认证配置 -->
<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
<property name="dataSource" ref="dataSource"></property>
<property name="sql" value="SELECT pwd FROM user WHERE username = ? "></property>
<!--<property name="passwordEncoder" ref="MD5PasswordEncoder"></property> 用户密码加密认证,需要时开启-->
</bean>
</list>
</property>
</bean>
<!-- SSO数据库认证配置 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass"><value>oracle.jdbc.driver.OracleDriver</value></property>
<property name="jdbcUrl"><value>jdbc:oracle:thin:@127.0.0.1:1521:testuser</value></property>
<property name="user"><value>sys</value></property>
<property name="password"><value>sys</value></property>
</bean>
<!-- SSO密码加密配置 -->
<bean id="MD5PasswordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
<constructor-arg index="0">
<value>MD5</value>
</constructor-arg>
</bean>
<sec:user-service id="userDetailsService">
<sec:user name="@@THIS SHOULD BE REPLACED@@" password="notused"
authorities="ROLE_ADMIN" />
</sec:user-service>
<!-- 返回用户信息配置 start -->
<bean class="org.jasig.services.persondir.support.jdbc.SingleRowJdbcPersonAttributeDao" id="attributeRepository">
<constructor-arg index="0" ref="dataSource"/>
<constructor-arg index="1" value="select * from user where {0}"/>
<property name="queryAttributeMapping">
<map>
<entry key="username" value="username"/>
<!-- 这里的key需写username,value对应数据库用户名字段-->
</map>
</property>
<property name="resultAttributeMapping">
<map>
<!--key对应数据库字段,value对应客户端获取参数-->
<entry key="user_pwd" value="password"/>
<entry key="user_name" value="username"/>
<entry key="user_id" value="userid"/>
<entry key="user_desc" value="email"/>
</map>
</property>
</bean>
<bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">
<property name="registeredServices">
<list>
<bean class="org.jasig.cas.services.RegexRegisteredService">
<property name="id" value="0" />
<property name="name" value="HTTP and IMAP" />
<property name="description" value="Allows HTTP(S) and IMAP(S) protocols" />
<property name="serviceId" value="^(https?|imaps?)://.*" />
<property name="evaluationOrder" value="10000001" />
<property name="ignoreAttributes" value="true" />
<property name="allowedAttributes">
<!-- 客户端需要使用的对象的属性名称 -->
<list>
<value>password</value>
<value>username</value>
<value>userid</value>
</list>
</property>
</bean>
</list>
</property>
</bean>
<!-- 返回用户信息配置 end -->
<bean id="auditTrailManager"
class="com.github.inspektr.audit.support.Slf4jLoggingAuditTrailManager" />
<bean id="healthCheckMonitor" class="org.jasig.cas.monitor.HealthCheckMonitor">
<property name="monitors">
<list>
<bean class="org.jasig.cas.monitor.MemoryMonitor"
p:freeMemoryWarnThreshold="10" />
<bean class="org.jasig.cas.monitor.SessionMonitor"
p:ticketRegistry-ref="ticketRegistry"
p:serviceTicketCountWarnThreshold="5000"
p:sessionCountWarnThreshold="100000" />
</list>
</property>
</bean>
</beans>
5) /ssoServer/webapp/WEB-INF/view/jsp/protocol/2.0/casServiceValidationSuccess.jsp
修改后如下:加入增加部分
<%@ page session="false" %>
<%@ page pageEncoding="UTF-8"%>
<%@ 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>
<!-- 新增部分 start -->
<c:if test="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes) > 0}">
<cas:attributes>
<c:forEach var="attr" items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}">
<cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>
</c:forEach>
</cas:attributes>
</c:if>
<!-- 新增部分 end -->
<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:authenticationSuccess>
</cas:serviceResponse>
中加入 c3p0-0.9.1.2.jar包、ojdbc14.jar包和cas-server-support- jdbc-3.5.3.jar包。
做了以上操作,基本上sso服务端的配置就已经配置完备了。
客户端配置说明:
客户端我选择了3.2.1版本,具体配置如下:
Client工程WEB-INF/lib下添加cas-client-core-3.2.1.jar包。
CAS服务器返回的用户信息,进行OMS伪登录,进行原系统登录,代码如下:
import java.io.IOException;
import java.util.Map;
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 javax.servlet.http.HttpServletResponse;
import org.jasig.cas.client.validation.Assertion;
/**
* CAS单点登陆的过滤器功能类,该类用来自动生成子应用的登陆Session
*
*/
public class AutoSetUserAdapterFilter implements Filter {
/**
* Default constructor.
*/
public AutoSetUserAdapterFilter() {
}
/**
* @see Filter#destroy()
*/
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// _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();
Map<String, Object> map = assertion.getPrincipal().getAttributes();
String password = (String) map.get("password");
String username = (String) map.get("username");
<span style="color:#7f055;"><span style="color:#7f055;"><span style="color:#7f055;">//</span></span></span>这里可以根据应用系统的具体<span style="color:#7f055;">需求,做用户信息填充或者是验证处理等,逻辑<span style="color:#7f055;">可以自定义</span></span>
}
chain.doFilter(request, response);
}
/**
* @see Filter#init(FilterConfig)
*/
public void init(FilterConfig fConfig) throws ServletException {
}
}
3) 在客户端项目的web.xml配置过滤器,如下:将以下部分加入到应用项目的web.xml中
<!-- ======================== 单点登录/登出 ======================== -->
<!-- 该过滤器用于实现单点登出功能,可选配置。 -->
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<!-- 该过滤器负责用户的认证工作,必须启用它 -->
<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://127.0.0.1:8090/cas-server-webapp/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://127.0.0.1:7001</param-value>
</init-param>
</filter>
<!-- 该过滤器负责对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://127.0.0.1:8090/cas-server-webapp</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://127.0.0.1:7001</param-value>
</init-param>
<init-param>
<param-name>redirectAfterValidation</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<!-- 该过滤器负责实现HttpServletRequest请求的包裹, 比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。 -->
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<!-- 该过滤器使得开发者可以通过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 Single Sign Out Filter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<!-- 登录访问拦截 -->
<filter-mapping>
<filter-name>CAS Authentication Filter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<!-- ticket进行验证拦截 -->
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>CAS Assertion Thread Local Filter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<!-- 根据sso server 返回用户信息进行自定义操作入口,登录成功后,会首先执行到这个类中的doFilter 方法 -->
<filter>
<display-name>AutoSetUserAdapterFilter</display-name>
<filter-name>AutoSetUserAdapterFilter</filter-name>
<filter-class>WorkFlow.sso.AutoSetUserAdapterFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AutoSetUserAdapterFilter</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
<!-- ======================== 单点登录/登出结束 ======================== -->
配置到这里,客户端的配置基本上已经完成,启动客户端,启动服务端,就可以进行测试了,这里对测试不进行说明。操作了以上配置基本上是没问题的,笔者用了两个系统进行验证,都是可以进行单点登录。
如以下有什么纰漏的地方,希望大家能够不吝赐教,在下面留言,以便交流相互学习,谢谢!