单点登录(Single Sign On , 简称 SSO )是目前比较流行的服务于企业业务整合的解决方案之一, SSO 使得在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
CAS(Central Authentication Service)是一款不错的针对 Web 应用的单点登录框架。
CAS的下载地址:http://www.jasig.org/cas/download 下载服务端cas-server-3.5.2-release.zip和客户端cas-client-3.2.1-release.zip
我们用3个web工程为例子演示CAS的使用。CASServer是CAS服务端,提供统一的登录页面。AA和BB分别是两个业务工程,在没登录之前,我们访问AA或BB都会跳到CASServer的登录页面。登录之后可以在AA和BB之间任意切换。退出之后必须得重新登录。
- 服务端配置
解压cas-server-3.5.2-release.zip后进入目录,可以看到cas-server-webapp,我们在这个工程的基础上新建我们的工程CASServer。这是个maven工程,配置文件和源码里面都有,jar包可以从cas-server-3.5.2\modules\cas-server-webapp-3.5.2.war里面拷过去。
取消https配置,这个工程默认是支持https,必须手工取消,否则logout不能通过http方式生效。取消的话改动两个地方
WEB-INF/deployerConfigContext.xml增加p:requireSecure="false"
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" p:requireSecure="false"/>
WEB-INF/spring-configuration/ticketGrantingTicketCookieGenerator.xml修改p:cookieSecure="false"
<bean id="ticketGrantingTicketCookieGenerator" class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator" p:cookieSecure="false"
整个工程的配置文件很多,其实我们只要关注/WEB-INF/deployerConfigContext.xml这一个主要配置文件
里面定义了一个id为authenticationManager的bean,负责基于提供的凭证信息进行用户认证。
里面有两个属性credentialsToPrincipalResolvers和authenticationHandlers
credentialsToPrincipalResolvers用来将传递进来的安全实体信息转换成完整的org.jasig.cas.authentication.principal.Principal
authenticationHandlers用于用户认证。
我们可以实现自己的authenticationManager和authenticationHandlers,并配置到deployerConfigContext.xml
编写MyAuthenticationHandler,提供了一个很简单的验证功能,只要输入用户名aa密码123就算验证通过
public class MyAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler { @Override protected boolean authenticateUsernamePasswordInternal( UsernamePasswordCredentials credentials) throws AuthenticationException { String username = credentials.getUsername(); String password = credentials.getPassword(); if(username.equals("aa")&&password.equals("123")){ return true; } return false; } }
编写MyAuthenticationManagerImpl,核心是SimplePrincipal principalTas = new SimplePrincipal("aaaaaaaaaaaaaa");这一行。封装一个SimplePrincipal 对象给客户端。这里是一个字符串,实际过程中可以是一个代表权限的json字符串。
public class MyAuthenticationManagerImpl extends AbstractAuthenticationManager { /** An array of authentication handlers. */ @NotNull @Size(min = 1) private List<AuthenticationHandler> authenticationHandlers; /** An array of CredentialsToPrincipalResolvers. */ @NotNull @Size(min = 1) private List<CredentialsToPrincipalResolver> credentialsToPrincipalResolvers; @Override protected Pair<AuthenticationHandler, Principal> authenticateAndObtainPrincipal( Credentials credentials) throws AuthenticationException { boolean foundSupported = false; boolean authenticated = false; AuthenticationHandler authenticatedClass = null; for (final AuthenticationHandler authenticationHandler : this.authenticationHandlers) { if (authenticationHandler.supports(credentials)) { foundSupported = true; if (!authenticationHandler.authenticate(credentials)) { if (log.isInfoEnabled()) { log.info("AuthenticationHandler: " + authenticationHandler.getClass().getName() + " failed to authenticate the user which provided the following credentials: " + credentials.toString()); } } else { if (log.isInfoEnabled()) { log.info("AuthenticationHandler: " + authenticationHandler.getClass().getName() + " successfully authenticated the user which provided the following credentials: " + credentials.toString()); } authenticatedClass = authenticationHandler; authenticated = true; break; } } } if (!authenticated) { if (foundSupported) { throw BadCredentialsAuthenticationException.ERROR; } throw UnsupportedCredentialsException.ERROR; } foundSupported = false; for (final CredentialsToPrincipalResolver credentialsToPrincipalResolver : this.credentialsToPrincipalResolvers) { if (credentialsToPrincipalResolver.supports(credentials)) { final Principal principal = credentialsToPrincipalResolver .resolvePrincipal(credentials); foundSupported = true; if (principal != null) { SimplePrincipal principalTas = new SimplePrincipal("aaaaaaaaaaaaaa"); return new Pair<AuthenticationHandler, Principal>( authenticatedClass, principalTas); } } } if (foundSupported) { if (log.isDebugEnabled()) { log.debug("CredentialsToPrincipalResolver found but no principal returned."); } throw BadCredentialsAuthenticationException.ERROR; } log.error("CredentialsToPrincipalResolver not found for " + credentials.getClass().getName()); throw UnsupportedCredentialsException.ERROR; } public List<AuthenticationHandler> getAuthenticationHandlers() { return authenticationHandlers; } public void setAuthenticationHandlers( List<AuthenticationHandler> authenticationHandlers) { this.authenticationHandlers = authenticationHandlers; } public List<CredentialsToPrincipalResolver> getCredentialsToPrincipalResolvers() { return credentialsToPrincipalResolvers; } public void setCredentialsToPrincipalResolvers( List<CredentialsToPrincipalResolver> credentialsToPrincipalResolvers) { this.credentialsToPrincipalResolvers = credentialsToPrincipalResolvers; } }
自此服务端配置完成,可以访问http://127.0.0.1/CASServer/login,用用户名aa密码123登陆成功说明一切正常。
- 客户端配置
新建web工程AA,可以引入上个工程里面所有的jar包。
修改web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5"> <display-name>AA</display-name> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:applicationContext*.xml </param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- CAS config begin --> <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>CAS Authentication Filter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetBeanName</param-name> <param-value>casAuthenticationFilter</param-value> </init-param> </filter> <filter-mapping> <filter-name>CAS Authentication Filter</filter-name> <url-pattern>*.jsp</url-pattern> </filter-mapping> <filter> <filter-name>CAS Validation Filter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetBeanName</param-name> <param-value>casValidationFilter</param-value> </init-param> </filter> <filter-mapping> <filter-name>CAS Validation Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>CAS HttpServletRequestWrapperFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetBeanName</param-name> <param-value>casHttpServletRequestWrapperFilter</param-value> </init-param> </filter> <filter-mapping> <filter-name>CAS HttpServletRequestWrapperFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <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> </web-app>
里面引入了spring的支持,spring的配置文件applicationContext-cas.xml。还有cas客户端相关的一些filter。
这些filter有org.jasig.cas.client.session.SingleSignOutFilter,org.jasig.cas.client.authentication.AuthenticationFilter,org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter,org.jasig.cas.client.util.HttpServletRequestWrapperFilter,org.jasig.cas.client.util.AssertionThreadLocalFilter。
关于各个filter的具体作用可以参考这篇文章http://blog.csdn.net/yuwenruli/article/details/6600032
applicationContext-cas.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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <bean id="casAuthenticationFilter" class="org.jasig.cas.client.authentication.AuthenticationFilter"> <property name="casServerLoginUrl" value="http://127.0.0.1/CASServer/login" /> <property name="serverName" value="127.0.0.1" /> </bean> <bean id="casValidationFilter" class="org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter"> <property name="ticketValidator"> <ref bean="Cas20ServiceTicketValidator" /> </property> <property name="useSession" value="true" /> <property name="serverName" value="127.0.0.1" /> <property name="redirectAfterValidation" value="true" /> </bean> <bean id="Cas20ServiceTicketValidator" class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator"> <constructor-arg index="0" value="http://127.0.0.1/CASServer" /> </bean> <bean id="casHttpServletRequestWrapperFilter" class="org.jasig.cas.client.util.HttpServletRequestWrapperFilter" /> </beans>
主要将casServerLoginUrl改成cas服务端的地址,serverName改成当前业务应用发布的地址。
新建测试主页面index.jsp,加入
<%=request.getUserPrincipal().getName() %> <a href="http://127.0.0.1/CASServer/logout">logout</a>
用同样的方法创建web工程BB。
- 测试
发布3个工程到本地tocmat。
第一次访问http://127.0.0.1/AA或http://127.0.0.1/BB 都会跳到CASServer的登陆页面
登陆成功后跳到相应工程的index页面,以后可以在AA和BB两个工程间自由切换。logout后必须重新登陆。