CAS多点登录
转自:http://www.blogjava.net/alwayscy/archive/2012/12/01/392322.html
场景
想要用到的场景:用户访问WEB服务,WEB访问非WEB服务1,服务1又再访问2、3,合并计算后,把数据返回给WEB及前端用户。想让访问链上的所有服务都能得到认证和鉴权,认为本次请求确实是来自用户的。所以想到用CAS,让用户在一点登录,所有服务都到此处认证和鉴权。
CAS小介绍
下面是两张图,来自网上两个PPT(猛戳下载),其中一个还有动画演示,感谢原分享者。
用我的话解释下就是:
1、 用户先访问http://adm/index.html服务,因为没有登陆被重定向到CAS去输入用户名密码,这个好理解。注意重定向地址:
https://cas.company.com/login?service=http://adm/index.html
问号前是CAS服务地址,后面跟了原来要访问的服务,方便CAS把你再重定向回来。
2、 登陆完成后,CAS会写一个COOKIE(CASTGC),它的作用是下次再认证时不用再输入密码。同时,CAS把用户重定向回原来访问地址:
http://adm/index.html?ticket= ST-1-qRPh34B1xhe4dquzz
注意后面多了个ticket
3、 这时候,ADM这个WEB服务,再用ticket去CAS做认证,CAS报告OK,它即认为用户登陆了。
4、 如:用户再访问下一个AMS的WEB服务时,因为带有CASTGC这个COOKIE,被重定向到CAS后,它就会用这个COOKIE直接生成一个ticket(就没有让用户登陆的过程了哦!),AMS拿到ticket后再去认证就可以了。
开头场景遇到的问题
开始我们的场景如果全部照搬CAS的应用,会存在如下问题:
1、 和CAS的交互全走HTTPS,要在JRE中生成和导入证书(网上搜配置tomcat的https一大把),用户认证时会被提示证书不可信。如果是一个直接交付给终端的产品,谁来配置这些东东?让用户看到这种提示又情何以堪?
2、 每当访问一个新服务都要和CAS产生两次交互,申请签发TICKET,再去认证TICKET
3、 默认的ticket有效时间很短,重定向回来后,要马上去认证,并且一个ticket只能去CAS认证一次就失效了
4、 Ticket是和原始的URL绑定的,两者都要提供给CAS才能认证通过,即你不能用AMS服务签发的ticket,去用在ADM服务的认证上
5、 如果是非WEB的服务要认证,需要用到CAS的代理模式,过程比较繁复
结论是开头场景要用CAS是很艰难的。
变通后的方案
这是我想到的一些改动来满足开头场景:
1、 改为HTTP验证方式
2、 由WEB服务去CAS签发一次TICKET,后继的非WEB服务全部用这一个TICKET到CAS做认证,它和用户登陆后有效期一致,也没有使用次数限制
3、 提供一个FILTER来为WEB层所有页面统一提供认证服务
4、 用户名、密码、鉴权信息(用户角色)存到数据库
下面就介绍这种非主流的改法,可能已经安全性大大降低,但至少能RUN啦。。。
下载安装
下载并解压CAS安装包:(不要问为啥下载JASIG的,因为网上全是它。。)
http://www.jasig.org/cas_server_3_5_1_release
解压后带源码,后面步骤还会用到。
把其中modules目录下的这个WAR包布到tomcat的webapp目录,重启下就算安装好了:
cas-server-3.5.1/modules/cas-server-webapp-3.5.1.war
改配置解决HTTP和TICKET生命期
1、 加长TICKET的生命期和使用次数:
2、 改为使用HTTP:
从JDBC认证和鉴权
1、 把默认的用户名密码相同即通过的认证方式注释掉:
替换为下面这段从数据库读取:
<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
<property name="sql" value="select passwd from t_admin where nickname=?"/>
<property name="dataSource" ref="dataSource"/>
</bean>
2、 把默认的鉴权信息获取方式注释掉:
替换为:
这种是一个用户仅一个角色(SingleRow):
<bean id="attributeRepository" class="org.jasig.services.persondir.support.jdbc.SingleRowJdbcPersonAttributeDao">
<constructor-arg index="0" ref="dataSource"/>
<constructor-arg index="1" value="SELECT g.id,g.groupname,role.role FROM t_group AS g LEFT OUTER JOIN t_group_role AS grouprole ON (g.id = grouprole.groupid) LEFT OUTER JOIN t_role AS role ON (role.id = grouprole.roleid) LEFT OUTER JOIN t_group_user AS groupuser on (g.id = groupuser.groupid) LEFT OUTER JOIN t_admin ON (t_admin.id = groupuser.userid) WHERE t_admin.nickname = ?"/>
<!--这里的key需写username,value对应数据库用户名字段 -->
<property name="queryAttributeMapping">
<map>
<entry key="username" value="nickname"/>
</map>
</property>
<!--key对应数据库字段,value对应客户端获取参数 -->
<property name="resultAttributeMapping">
<map>
<entry key="role" value="authorities"/>
</map>
</property>
</bean>
3、 多行模式(和上面的单行模式二选一)
这种是一个用户对应多个角色(MultiRow):(这里这个attr_name绕了我半天。。。这里有点解释)
<bean id="attributeRepositoryMulti" class="org.jasig.services.persondir.support.jdbc.MultiRowJdbcPersonAttributeDao">
<constructor-arg index="0" ref="dataSource"/>
<constructor-arg index="1" value="SELECT g.id,g.groupname,'authorities' as attr_name,role.role FROM t_group AS g LEFT OUTER JOIN t_group_role AS grouprole ON (g.id = grouprole.groupid) LEFT OUTER JOIN t_role AS role ON (role.id = grouprole.roleid) LEFT OUTER JOIN t_group_user AS groupuser on (g.id = groupuser.groupid) LEFT OUTER JOIN t_admin ON (t_admin.id = groupuser.userid) WHERE t_admin.nickname = ?"/>
<!--这里的key需写username,value对应数据库用户名字段 -->
<property name="queryAttributeMapping">
<map>
<entry key="username" value="nickname"/>
</map>
</property>
<property name="nameValueColumnMappings">
<map>
<entry key="attr_name" value="role" />
</map>
</property>
</bean>
如果要用多行模式把相应的引用的类要变一下:
4、 鉴权信息要能输出到前端,还要改下JSP:
在上图所示位置加下:
<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}"
varStatus="loopStatus"
begin="0"
end="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes)-1}"
step="1">
<cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>
</c:forEach>
</cas:attributes>
</c:if>
5、 加上数据源定义
如下:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://127.0.0.1:3306/uu?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true"></property>
<property name="username" value="root"></property>
<property name="password" value="xxxxx"></property>
</bean>
6、 建表:(表结构来自此处)
SET FOREIGN_KEY_CHECKS=0;
------------------------------
-- 创建管理员帐号表t_admin
-- ----------------------------
CREATE TABLE `t_admin` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`passwd` varchar(12) NOT NULL DEFAULT '' COMMENT '用户密码',
`nickname` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名字',
`phoneno` varchar(32) NOT NULL DEFAULT '' COMMENT '电话号码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
-- ----------------------------
-- 添加3个管理帐号
-- ----------------------------
INSERT INTO `t_admin` VALUES ('1', 'admin', 'admin', '');
INSERT INTO `t_admin` VALUES ('4', '123456', 'test', '');
INSERT INTO `t_admin` VALUES ('5', '111111', '111111', '');
-- ----------------------------
-- 创建权限表t_role
-- ----------------------------
CREATE TABLE `t_role` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`role` varchar(40) NOT NULL DEFAULT '',
`descpt` varchar(40) NOT NULL DEFAULT '' COMMENT '角色描述',
`category` varchar(40) NOT NULL DEFAULT '' COMMENT '分类',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=60 DEFAULT CHARSET=utf8;
-- ----------------------------
-- 加入4个操作权限
-- ----------------------------
INSERT INTO `t_role` VALUES ('1', 'ROLE_ADMIN', '系统管理员', '系统管理员');
INSERT INTO `t_role` VALUES ('2', 'ROLE_UPDATE_FILM', '修改', '影片管理');
INSERT INTO `t_role` VALUES ('3', 'ROLE_DELETE_FILM', '删除', '影片管理');
INSERT INTO `t_role` VALUES ('4', 'ROLE_ADD_FILM', '添加', '影片管理');
-- ----------------------------
-- 创建权限组表
-- ----------------------------
CREATE TABLE `t_group` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`groupname` varchar(50) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
-- ----------------------------
-- 添加2个权限组
-- ----------------------------
INSERT INTO `t_group` VALUES ('1', 'Administrator');
INSERT INTO `t_group` VALUES ('2', '影片维护');
-- ----------------------------
-- 创建权限组对应权限表t_group_role
-- ----------------------------
CREATE TABLE `t_group_role` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`groupid` bigint(20) unsigned NOT NULL,
`roleid` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `groupid2` (`groupid`,`roleid`),
KEY `roleid` (`roleid`),
CONSTRAINT `t_group_role_ibfk_1` FOREIGN KEY (`groupid`) REFERENCES `t_group` (`id`),
CONSTRAINT `t_group_role_ibfk_2` FOREIGN KEY (`roleid`) REFERENCES `t_role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=83 DEFAULT CHARSET=utf8;
-- ----------------------------
-- 加入权限组与权限的对应关系
-- ----------------------------
INSERT INTO `t_group_role` VALUES ('1', '1', '1');
INSERT INTO `t_group_role` VALUES ('2', '2', '2');
INSERT INTO `t_group_role` VALUES ('4', '2', '4');
-- ----------------------------
-- 创建管理员所属权限组表t_group_user
-- ----------------------------
CREATE TABLE `t_group_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`userid` bigint(20) unsigned NOT NULL,
`groupid` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `userid` (`userid`),
KEY `groupid` (`groupid`),
CONSTRAINT `t_group_user_ibfk_2` FOREIGN KEY (`groupid`) REFERENCES `t_group` (`id`),
CONSTRAINT `t_group_user_ibfk_3` FOREIGN KEY (`userid`) REFERENCES `t_admin` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;
-- ----------------------------
-- 将管理员加入权限组
-- ----------------------------
INSERT INTO `t_group_user` VALUES ('1', '1', '1');
INSERT INTO `t_group_user` VALUES ('2', '4', '2');
-- ----------------------------
-- 创建管理员对应权限表t_user_role
-- 设置该表可跳过权限组,为管理员直接分配权限
-- ----------------------------
CREATE TABLE `t_user_role` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`userid` bigint(20) unsigned NOT NULL,
`roleid` bigint(20) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `userid` (`userid`),
KEY `roleid` (`roleid`),
CONSTRAINT `t_user_role_ibfk_1` FOREIGN KEY (`userid`) REFERENCES `t_admin` (`id`),
CONSTRAINT `t_user_role_ibfk_2` FOREIGN KEY (`roleid`) REFERENCES `t_role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
与spring-security结合使用
我们是自己开发filter,但顺便把spring-security的配置方法带一下:
1、 Web.xml加上spring-security的filter:
<!-- spring 配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:/applicationContext-security-ns.xml</param-value>
</context-param>
<!-- spring 默认侦听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>
<!-- Filter 定义 -->
<!-- spring security filter -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
2、 application参考配置以及修改的vote文件(这里)
在单行模式下,权限信息可以正常解析到detail变量中,但在多行的时候,CAS传过来的多个角色是这种格式:[ROLE_1,ROLE_2],带有中括号,原有VOTE是精确比对,附件里改了个index函数来比对:
有了上面这些Spring-security就可以正常运作,但是场景里要使用非WEB服务多次验证,所以其实不能用spring-security的filter,我们还是要自己写的。
Go on…
解除TICKET与SERVICE的绑定,要改代码哦!
Cas服务器ticket和service验证时,要和来签发时的service url一致才行,否则就报下面的错误:
org.jasig.cas.client.validation.TicketValidationException:
ticket 'ST-14-SYa99tdAMhI31ZehfSTW-cas01.example.org' does not match supplied service
为了我们多个不同服务可以重复使用一个ticket,CAS的源码上做个小小改动即可:
去到之前解压的CAS目录,搜出这句话的java文件:
grep -R "does not match supplied service" .
修改这个文件:
vi ./cas-server-core/src/main/java/org/jasig/cas/CentralAuthenticationServiceImpl.java
注释掉验证服务URL的相应行就好了:
编译一下:
mvn compile
mvn -DskipTests=true package
把编好的:
cas-server-3.5.1/cas-server-core/target/cas-server-core-3.5.1.jar
拷到下列位置替换原来的:
/usr/local/apache-tomcat-7.0.32/webapps/cas/WEB-INF/lib/
重启下TOM的小猫就可以了。
自制的FILTER
自己制作的filter要达到目标是:
1、 在没认证时可以重定向
2、 重定向回来的时候,要去认证并把ticket写COOKIE
3、 有COOKIE时,取出来直接去做认证
Filter的一个比较清晰易懂的基础介绍
一个讲COOKIE的文章
最后是完整代码:点我
Filter的代码是CasFilter.java,相当简单,下面这段就是用CAS提供的客户端去验证TICKET,因为service不验了,所以validate的第二个参数已经不重要。
Attributes就是权限信息:
非WEB服务的认证和鉴权
我们开头场景中的WEB服务在访问后端时,把TICKET信息也带上,这样后端的非WEB服务也可以用刚才提到的函数去认证,并且获取到权限信息,做自己的鉴权。
服务1再访问服务2时,也一样带上TICKET,这样因为我们已经延长了TICKET有效次数和期限,它也不会过期。
但是,一旦用户LOGOUT了,这个TICKET也就失效,此时所有服务都将验证不通过,WEB自然又会把用户重定向到CAS去登陆了。
罗里八嗦讲一大堆,用了好多歪门斜道,不值一提,只是为今后有个查阅的地方。