CAS多点登陆之非主流配置方式 场景想要用到的场景:用户访问WEB服务,WEB访问非WEB服务1,服务1又再访问2、3,合并计算后,把数据返回给WEB及前端用户。想让访问链上的所有服务都能得到认证和鉴权,认为本次请求确实是来自用户的。所以想到用CAS,让用户在一点登录,所有服务都到此处认证和鉴权。 CAS小介绍下面是两张图,来自网上两个PPT(猛戳下载),其中一个还有动画演示,感谢原分享者。 用我的话解释下就是: 1、 用户先访问http://adm/index.html服务,因为没有登陆被重定向到CAS去输入用户名密码,这个好理解。注意重定向地址: 2、 登陆完成后,CAS会写一个COOKIE(CASTGC),它的作用是下次再认证时不用再输入密码。同时,CAS把用户重定向回原来访问地址: 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./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去登陆了。 罗里八嗦讲一大堆,用了好多歪门斜道,不值一提,只是为今后有个查阅的地方。 有兴趣可以访问下我的生活博客:qqmovie.qzone.com |
|