啊。。。。。。it's quite a long time。
好久没更新博客了,有一年之久了,一直在忙于公司的一些项目。2014年到2015年工作太忙,我也接触到了新的领域,认识了新的同事。
对于一些经常跟我博客的读者们深深说一声抱歉。
同时,也在新的一年将到之际,祝各位新春愉快,羊年洋洋洋。
好了,废话少说,开始我们架构师之路的新的历程,SSO-单点登录。
另外想以此SSO系列教程献给我那身体不太好的同事-小白同志,祝小白同志身体一直健康
——叫你废话少说你还再啰嗦,你还说,好了,关掉,开始正经话题。
什么是单点登录?什么是SSO?
SSO就是单点登录!!!
SSO即Single Sign On。
可是为什么我们要单点登录呢?为什么不能把所有的系统做成一个war包里呢?
道理很简单啊,如果这个银行这个企业全是你一家公司里的一个项目组包下来了,然后是从头开始开发,你当然可以把所有的功能模块做到一个WAR工程中啊。
可是很多时侯我们是做一个工程或者是两个工程然后需要和另一个工程去做结合,每个工程都有一个入口地址,对吧?那么我们从用户的角度出发来说:
如果一个企业的工作人员需要操作4,5个不同的系统如:HR系统、报销系统、进销存系统还有企业内的eLearning系统和SAP系统,那么他要登录5次对不对?要登录5次?
那么有人说了,为什么这么麻烦呢?我们不能把所有的系统除了一个主系统,然后让用户只要登录一次,再把其它系统的链接以菜单的形式挂在主系统的菜单点,然后去除登录功能,这样用户不就可以只需要登录一次了呢?
答案当然是:你可以这样做。
可是安全性呢?
因此我们引入了单点登录的概念,我们可以想一下,用户面对10几个系统,这10几个系统只有一个“表皮”(主页),在这个主页中有许多的外挂连接,连向10几个子系统,这10几个子系统每个都需要根据用户名和密码来判断用户是否对于本系统有无操作权限?但是用户只需要在这个“表皮”上登录一次,当他点每一个菜单链接时,系统会自动把用户名和密码做一次匹配然后根据权限系统来判断该用户是否具有相应的权限,进而使得用户可以通过主页再链入不同的子系统中,这就是单点登录。
或许是中文翻译的问题,我们就从Single Sign On这3个字来理解,只需登录一记,啊。。。这就对了,多个系统我只需要登录一次。
我们进一步设想一下,其实用户还是登录了两次,为什么呢?
对于正规的企业来说都有一个“域登录”,即WINDOWS域我们又称之为Active Directory,简称AD域,用户打开WIN7,WIN8, WINDOWS SERVER。。。bla...bla...bla...what every啦。
此时他需要输入域帐号,登录一次。
然后他进入WINDOWS桌面后,打开IE,点开公司内部OA系统,做报销或者是做请假,又登录一次。
其实是两次登录。
因此做的好的SSO,因该是怎么样啊?
即让用户登录一次,就是用户一旦登录了公司的AD域后,他在IE里打开公司内部的一些网址都无需再做登录了,因为用户的AD域帐户就是它的系统帐户。
如:公司内每个员工的邮箱帐号,它打开OUTLOOK后就自动连上了相关的MAIL服务器,然后该员工的AD域帐号加上@xxx.xxx这样的格式,这不就是他公司的邮件地址了,对吧?
对不对?这就是一个SSO的活生生的例子。
那么很多时候,确实,为了安全,我们可以让员工在登录了AD域后再打开IE,然后输入公司内的OA网址时再登录一次,这样做其实也是可以,而公司内的OA一般不仅仅只是一个系统,它一定一定是连接着多个子系统。
比如说我们一个公司的OA系统是一个EAR,它下挂有10多个WAR。
甚至一些公司还会外接如:SalesForce, SAP等其它系统,因此我们把员工打开的这层公司OA系统的最外面的这层“皮”,称之为PORTAL。
对的,PORTAL里一定含有SSO,在此先提一下。
下面是一些著名的调查公司显示的统计数据:
- 用户每天平均16分钟花在身份验证任务上 - 资料来源:IDS
- 频繁的IT用户平均有21个密码 - 资料来源:NTA Monitor Password Survey
- 49%的人写下了其密码,而67%的人很少改变它们
- 每79秒出现一起身份被窃事件 - 资料来源:National Small Business Travel Assoc
- 全球欺骗损失每年约12B - 资料来源:Comm Fraud Control Assoc
使用“单点登录”整合后,只需要登录一次就可以进入多个系统,而不需要重新登录,这不仅仅带来了更好的用户体验,更重要的是降低了安全的风险和管理的消耗。
请看下面的统计数据:
- 提高IT效率:对于每1000个受管用户,每用户可节省$70K
- 帮助台呼叫减少至少1/3,对于10K员工的公司,每年可以节省每用户$75,或者合计$648K
- 生产力提高:每个新员工可节省$1K,每个老员工可节省$350 - 资料来源:Giga
- ROI回报:7.5到13个月 - 资料来源:Gartner
另外,使用“单点登录”还是SOA时代的需求之一。在面向服务的架构中,服务和服务之间,程序和程序之间的通讯大量存在,服务之间的安全认证是SOA应用的难点之一,应此建立“单点登录”的系统体系能够大大简化SOA的安全问题,提高服务之间的合作效率。
以下是一个标准的企业内通过SSO来集成各个系统间的认证与权限的模型图
好了,以上基本知识普及完毕开始我们的SSO实现。
CAS SSO
SSO实现有很多产品,我们今天选用的这个是耶鲁大学发明的CAS SSO服务器。这个CAS SSO是目前我看到过的功能较全的,使用也是最简单的配置式SSO服务器,它基于SPRING的原理,因此这个配置文件我们看起来因当是相当的熟悉的。
它分为Server版和Client版2个模块,同时它对于一些其它功能特性如:数据库、LDAP协议(就是WINDOWS AD域使用的协议)、安全等还提供了一系列的插件。因此,在本例中,我使用的是:
Server端:
Cas Server 3.5.2
Client端:
Cas Client 3.2.1
注:
请严格按照我的版本号进行试验。
什么叫Server端 ,什么叫Client端?
看上图,假设我们有3个War包。
- 一个叫cas-server.war,它放在tomcat里;
- 一个叫cas-sample-site1.war,它放在jboss里;
- 一个叫cas-sample-site2.war,它放在jboss里;
那么位于tomcat里的cas-server.war,它就是我们的CAS的Server端,位于jboss里的两个war就是我们CAS的client端。
SSO中的Server端与Client端概念搞清后,我们现在就开始布署吧。
布署cas-server
先说一下我们的环境:
版本号 | web端口 | |
tomcat | 6 | 9090 |
jboss | 7 | 8080 |
我们把Server端解压得到以下这样的一个文件夹
它里面含了一堆的东西,关键在于以下这个文件夹
进入该文件夹,找到这样一个war包。
把这个war包解压后重命名成cas-server.war,放于tomcat的webapp目录中去,启动tomcat一切无误后即可。
然后我们打开一个ie,输入http://localhost:9090/cas-server会得到以下这个界面,那就说明你的cas sso已经安装成功了。
配置CAS SERVER
添加依赖包
在本例中我们将使用Oracle数据库中自建一个用户表来管理我们的用户名和密码,因此:
- 将oracle的ojdbc6.jar放入tomcat的lib目录内D:\tomcat\lib
- 将cas-server-3.5.2-release\cas-server-3.5.2\modules下的这几个文件拷入tomcat\webapp\cas-server\web-inf\lib目录内
修改配置文件
CAS SSO的好处在于它的配置文件是完全spring的,你只要懂spring就可以非常容易的在里面去添加修改自己的一些功能,我们在第一天的教程中为了尽量简单,我们只需要改动一个文件,它就是:
tomcat\webapps\cas-server\WEB-INF目录下的
deployerConfigContext.xml文件
我们用纯文件编辑器打开它,找到下面这行:
<bean class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" />
把它注释掉
<!-- <bean class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" /> -->
然后再在它下面添加如下内容
<!-- <bean class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" /> -->
<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"> <property name="dataSource" ref="dataSource" ></property> <property name="sql" value="select password from sys_user where user_id=?" ></property> </bean>
好,这边我们看到了一个dataSource对吧,EASY,来。。。
<!-- <bean class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" /> --> <bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"> <property name="dataSource" ref="dataSource" ></property> <property name="sql" value="select password from sys_user where user_id=?" ></property> </bean> </list> </property> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="oracle.jdbc.OracleDriver" /> <property name="url" value="jdbc:oracle:thin:@localhost:1521:orcl" /> <property name="username" value="ymk" /> <property name="password" value="password_1" /> </bean>
注意我加的这个dataSource的bean和刚才那段配置代码之间的位置对应关系哦,别胡乱搞一个回车就乱加一行哦。
全加完了,怎么样啦?完成了吗?
还没,CAS SSO严格意义上来说需要J2EE APP SERVER里实现HTTPS即SSL的双向认证模式才能正常使用,但是我们因为这个是教程,因此不想搞了太麻烦,我们可以在“不使用HTTPS认证”的情况下也可以使用CAS SSO。
为此,我们要关闭CAS SSO的HTTPS认证模式,编辑:
tomcat\webapps\cas-server\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" /> </beans>
把这边的p:cookieSecure从true改为false即可。
然后我们在oracle中建一个用户表吧,建表语句如下:
CREATE TABLE SYS_USER ( "USER_ID" VARCHAR2(16), "PASSWORD" VARCHAR2(8), CONSTRAINT "PK_SYS_USER" PRIMARY KEY ("USER_ID") );
该表中含有一条记录:
这就是我们的用于测试的单点登录的用户名和密码了,很简单吧?
全部保存后,重启tomcat,一切无误,然后我们打开一个ie,输入http://localhost:9090/cas-server会得到以下这个界面,那就说明你的cas sso已经安装成功了。
我们在用户名中输入sso, 在密码一栏中输入aaaaaa,然后看到下面这个界面,即代表我们的cas server和我们的数据库已经完全连上了。
如果我们不按照sys_user表中的用户名和密码就随意输入用户名和密码,那我们便会得到这样的结果:
将不同的工程连接上cas server以实现单点登录
按照这个图,我们将会有2个不同的war包
- 一个叫cas-sample-site1.war,它放在jboss里;
- 一个叫cas-sample-site2.war,它放在jboss里;
我们在我们的eclipse里创建两个这样的war工程即可,这是非常简单的事,这2个工程都含有一个index.jsp文件。
cas-sample-site1
在它的index.jsp文件中含有如下内容:
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>cas sample site1</title> </head> <body> <h1>cas sample site1</h1> <a href="http://localhost:8080/cas-sample-site2/index.jsp">cas-sample-site2</a> </br> <a href="http://localhost:9090/cas-server/logout">退出</a> </body> </html>
cas-sample-site2
在它的index.jsp文件中含有如下内容:
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>cas sample site2</title> </head> <body> <h1>cas sample site2</h1> <a href="http://localhost:8080/cas-sample-site1/index.jsp">cas-sample-site1</a> </br> <a href="http://localhost:9090/cas-server/logout">退出</a> </body> </html>
这两个war工程都有一个lib目录,确保它们的lib目录里都有这样几个jar
look, 注意要有cas-client-core-3.2.1.jar哦,它来自于:cas-client-3.2.1-release\cas-client-3.2.1\modules即cas-client-3.2.1-release.zip解压出来的内容。
这两个工程的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>cas-sample-site2</display-name> <welcome-file-list> <welcome-file>index.jsp</welcome-file> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>default.html</welcome-file> <welcome-file>default.htm</welcome-file> <welcome-file>default.jsp</welcome-file> </welcome-file-list> <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 Validation Filter</filter-name> <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class> <init-param> <param-name>casServerUrlPrefix</param-name> <param-value>http://localhost:9090/cas-server</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://localhost:8080</param-value> </init-param> <init-param> <param-name>useSession</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>redirectAfterValidation</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CAS Validation Filter</filter-name> <url-pattern>*</url-pattern> </filter-mapping> <filter> <filter-name>CAS Filter</filter-name> <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class> <init-param> <param-name>casServerLoginUrl</param-name> <param-value>http://localhost:9090/cas-server/login</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://localhost:8080</param-value> </init-param> </filter> <filter-mapping> <filter-name>CAS Filter</filter-name> <url-pattern>*</url-pattern> </filter-mapping> <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> </web-app>
看到了没有,有这么一堆的listener和filter,而且它们一个不能漏,并且它们的顺序也是绝对不能够错的,一定要按照下面这个从上至下的顺序:
- org.jasig.cas.client.session.SingleSignOutHttpSessionListener
- org.jasig.cas.client.session.SingleSignOutFilter
- org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
- org.jasig.cas.client.authentication.AuthenticationFilter
- org.jasig.cas.client.util.HttpServletRequestWrapperFilter
漏了一个,或者顺序错了,你将会发生下列情况:
- 可以正常登录,无法统一注销,即然是单点登录,那么我在任意一个站点上点“注销“是不是也因该是统一注销啊?
- 可以登录,可以统一注销,但是拿不到cas-server上登录的用户的user session,如果我们是两个系统,那么这两个系统是不是都有web sesssion?较常用的就是user session,那么如果你的顺序配错了,或者是你漏配了一个,你是得不到cas-server上传过来的用户的一些登录信息的,这个很糟糕,这将会为我们后面的编程开发带来烦恼
- 不能登录
至于为什么会得到这样的一些结果?
嘿嘿!
我们在后面的课程中会来分析cas 单点登录的源码(这个过程一点不变态,很简单的,一说就通,跟着我的教程一步步走不难的),在深入源码中后你就可以看出为什么这几个东西它们有严格意义上的顺序的关系了。
在这个web.xml文件里我们可以看到有两处出现了下面的这样的东西:
<init-param> <param-name>casServerUrlPrefix</param-name> <param-value>http://localhost:9090/cas-server</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://localhost:8080</param-value> </init-param>
记住,上面的那行代表我们的cas server的服务器所在的地址,当用户用上面的cas-server的登录界面登录成功后,cas server 会自动跳回用户在ie地址里输入的子系统地址的首页。
如:我们先输入http://localhost:8080/cas-sample-site1,此时系统会先跳到http://localhost:9090/cas-server/login的画面要求用户先去做一次登录。
那么cas server它是怎么知道子系统地址的首页位于哪个地址(哪台服务器上)的呢,那么你要”注册“这个地址给cas server。
因此,第二行就是我们的具体的子系统的首页所在的地址。
这样的地方在我们的web.xml文件中一共出现了两处:
- 一处位于org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
- 一处位于org.jasig.cas.client.authentication.AuthenticationFilter
这2处如果没配好,你会遇到下列问题:
- 登录后无法正常跳回子系统的首页
- 无法正常退出
- 无法做系统间切换时的跳转
因此一定要注意啦!!!
我们把两个工程通过ECLIPSE布署在JBOSS7上,然后运行起来吧。
别忘了启动我们的Tomcat里的cas server哦。
全部启动完毕后我们在IE浏览器里输入:http://localhost:8080/cas-sample-site1
此时浏览器显示如下画面
我们在cas server的登录画面输入我们数据库表sys_user中的相应的用户名与密码后,再来看此时的浏览器它会跑到哪儿去?
点击cas-sample-site2这个链接呢?
然后点击“退出”这个链接
此时我们再在浏览器里输入:http://localhost:8080/cas-sample-site2--有时浏览器有缓存,它还是会显示cas-sample-site2的首页,这时你可以点一下F5或者是刷新按钮(这个可以通过代码来避免jsp或者是html页中使用缓存来做到)
look!
由于是统一注销,因此一旦注销,两个WEB都无法访问了,必须要求“统一登录一下”,于是我们再次输入用户名和密码
对不对。
好了,第一天不讲太多,从明天开始我们会依次开始讲:
1. 如何使用LDAP即模拟WINDOWS AD域的登录模式
2. 如何在不同的web工程间获取“统一登录”时的用户信息即userprincipal
3. 如何改造CAS SSO自带的登录界面
4. 如何使得我们的CAS SSO可以支持类似于淘宝这种“多租户”的概念
差不多所有内容预计在5-6课左右,敬请期待。