CAS多点登录

转自:http://www.blogjava.net/alwayscy/archive/2012/12/01/392322.html

 

场景

想要用到的场景:用户访问WEB服务,WEB访问非WEB服务1,服务1又再访问2、3,合并计算后,把数据返回给WEB及前端用户。想让访问链上的所有服务都能得到认证和鉴权,认为本次请求确实是来自用户的。所以想到用CAS,让用户在一点登录,所有服务都到此处认证和鉴权。

clip_image001[34]

CAS小介绍

下面是两张图,来自网上两个PPT(猛戳下载),其中一个还有动画演示,感谢原分享者。

clip_image003[28]

clip_image005[26]

用我的话解释下就是:

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后再去认证就可以了。

clip_image007[20]

开头场景遇到的问题

开始我们的场景如果全部照搬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的生命期和使用次数:

clip_image009[14]

2、 改为使用HTTP:

clip_image011[12]

clip_image013[12]

从JDBC认证和鉴权

1、 把默认的用户名密码相同即通过的认证方式注释掉:

clip_image015[8]

替换为下面这段从数据库读取:

<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、 把默认的鉴权信息获取方式注释掉:

clip_image017[6]

替换为:

这种是一个用户仅一个角色(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>

如果要用多行模式把相应的引用的类要变一下:

clip_image019[4]

4、 鉴权信息要能输出到前端,还要改下JSP:

clip_image021[4]

在上图所示位置加下:

<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、 加上数据源定义

clip_image023[4]

如下:

<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&amp;useUnicode=true&amp;characterEncoding=utf-8&amp;zeroDateTimeBehavior=convertToNull&amp;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函数来比对:

clip_image024[4]

有了上面这些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的相应行就好了:

clip_image025[4]

编译一下:

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就是权限信息:

clip_image026[4]

非WEB服务的认证和鉴权

我们开头场景中的WEB服务在访问后端时,把TICKET信息也带上,这样后端的非WEB服务也可以用刚才提到的函数去认证,并且获取到权限信息,做自己的鉴权。

服务1再访问服务2时,也一样带上TICKET,这样因为我们已经延长了TICKET有效次数和期限,它也不会过期。

但是,一旦用户LOGOUT了,这个TICKET也就失效,此时所有服务都将验证不通过,WEB自然又会把用户重定向到CAS去登陆了。

罗里八嗦讲一大堆,用了好多歪门斜道,不值一提,只是为今后有个查阅的地方。

posted @ 2017-03-27 20:20  adaandy  阅读(3173)  评论(0编辑  收藏  举报