【原创】apereo cas 4.2从0开始
转载请声明来自:http://www.cnblogs.com/chixinzei/p/7504272.html 谢谢!
目前已实现功能
数据库验证,druid数据源,页面定制,restful登录,shibboleth idp saml2.0整合,根据账号自动登录服务器后跳转子系统链接(webUtils修改的有问题,暂时不要参照此功能)
cas相关参考文档
http://www.cnblogs.com/xwdreamer/archive/2011/11/10/2296939.html spring webflow
https://apereo.github.io/cas/4.2.x/ CAS官方文档
http://docs.spring.io/spring-webflow/docs/2.4.5.RELEASE/reference/html Spring webflow 官方文档
与Shibboleth IDP端 SAML2.0整合文档:
https://apereo.github.io/cas/4.2.x/integration/Shibboleth.html 官网cas和Shibboleth IDP 整合
http://wwwcomy.iteye.com/blog/2236016 Shibboleth IDP安装中文博客
https://wiki.shibboleth.net/confluence/display/SHIB2/IdPInstall Shibboleth IDP安装
工作环境准备
运行基础:jdk8.0.131,cas-server:4.2.1(基于gradle),cas-client:3.3.3(基于maven),win7 64,tomcat8.5,mysql 5.6.21,idea-2017.2
ssl配置相关准备
1.http访问配置
取消cas https验证:http://www.cnblogs.com/xiaojf/p/6617693.html
2.https访问配置
准备本地tomcat证书生成与配置:文档参考:http://www.bug315.com/article/412.htm
若要在本地使用SSL访问cas,则只需要修改cas-server-webapp\src\main\resources\services\HTTPSandIMAPS-10000001.json的属性tgc.secure=false即可,意思是cas的cookie是否只在ssl下生成(如果为true,则登陆后cookie也不会存放登录信息和票据,依然重定向到登录界面)
默认jdk密匙库口令:changeit
1)cmd 移动到tomcat目录下,生成server key :
keytool -genkey -alias tomcat -keyalg RSA -storepass changeit -keystore server.keystore -validity 3600
解释:
-alias 表示证书的别名,一个keystore文件中可以存放多个alias。
-keyalg RSA 表示密钥算法的名称为RSA算法
-keypass changeit 表示密钥的口令是changeit
-storepass changeit 表示密钥库(生成的keystore文件)的密钥是changeit
-keystore server.keystore 表示指定密匙库的名称
-validity 3600 表示证书有效期3600天,也就是大概10年
删除证书命令:
先移动到jdk的密匙总库目录:
如:D:\Program Files\Java\jdk1.8.0_131\jre\lib\security
执行删除jdk密匙库证书:
keytool -delete -alias tomcat -keystore cacerts
删除后可以再次将证书导入jdk密匙库
2)输入参数:
名字和姓氏:149p874e84.51mypc.cn (域名)
组织单位名称:149p874e84.51mypc.cn
组织名称:149p874e84.51mypc.cn
所在城市:jinjiang
所在省:fujian
国家/地区代码:zh
3)证书导入的JDK的证书信任库中
keytool -export -trustcacerts -alias tomcat -file server.cer -keystore server.keystore -storepass changeit keytool -import -trustcacerts -alias tomcat -file server.cer -keystore "jdk目录/jre/lib/security/cacerts" -storepass changeit
4)修改tomcat server.xml增加https支持,不需要删除原来的http连接
<!--开启https连接,如果是非外部ide启动,则keystoreFile配置为server.keystore即可!--> <Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true" maxThreads="150" scheme="https" secure="true" clientAuth="false" sslProtocol="TLS" keystoreFile="D:/Program Files (x86)/apache-tomcat-8.5.16(8086)/server.keystore" keystorePass="changeit"/>
3.支持配置多个验证拦截器。
简单用户名和密码配置修改:/cas-server-support-x509/src/test/resources/deployerConfigContext.xml
casuser,Mellon。
另外此用户名密码也可以配置在cas.properties中:
两者都存在时优先使用properties的配置
xml存在,而properties不存在时,则读取xml。
CAS服务端配置修改
⒈jdbc简单查询校验:
1.1mysql建表:
CREATE TABLE `t_user` ( `id` bigint(15) NOT NULL COMMENT'主键', `account` varchar(30) DEFAULT NULL COMMENT'账号', `password` varchar(255) DEFAULT NULL COMMENT'密码', `valid` tinyint(1) DEFAULT NULL COMMENT '是否有效', PRIMARY KEY(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; --插入1条数据: INSERT INTO `t_user` (`id`, `account`, `password`, `valid`) VALUES (1, 'admin', 'md5加密后的密码', 1);
1.2 cas-server-webapp\build.gradle 引入jdbc支持包依赖:
compile project(':cas-server-support-jdbc') compile group: 'mysql', name: 'mysql-connector-java', version: mysqlConnectorJavaVersion
1.3 deployerConfigContext.xml
关闭简单用户密码校验,改为jdbc校验,密码MD5加密,配合druid
1 <!--修改登录方式为jdbc验证,注意,需要注释掉QueryAndEncodeDatabaseAuthenticationHandler的注解注入,有冲突--> 2 <!--<alias name="acceptUsersAuthenticationHandler" alias="primaryAuthenticationHandler" />--> 3 <alias name="queryDatabaseAuthenticationHandler" alias="primaryAuthenticationHandler"/> 4 <bean id="queryDatabaseAuthenticationHandler" 5 class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler"> 6 <!--<property name="dataSource" ref="queryDatabaseDataSource"></property>--> 7 <!--<property name="sql" value="select password from t_user where account=?"></property>--> 8 <property name="passwordEncoder" ref="MD5PasswordEncoder"></property> 9 </bean> 10 <!-- 添加MD5密码加密功能 --> 11 <bean id="MD5PasswordEncoder" class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder"> 12 <constructor-arg index="0"> 13 <value>MD5</value> 14 </constructor-arg> 15 </bean> 16 17 <!-- 数据源配置 --> 18 <alias name="dataSource" alias="queryDatabaseDataSource"/> 19 <!-- 阿里 druid 数据库连接池 --> 20 <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> 21 <!-- 数据库基本信息配置 --> 22 <property name="url" value="${cas.druid.database.url}"/> 23 <property name="username" value="${cas.druid.database.username}"/> 24 <property name="password" value="${cas.druid.database.password}"/> 25 <property name="driverClassName" value="${cas.druid.database.driverClassName}"/> 26 <property name="filters" value="${cas.druid.database.filters}"/> 27 <!-- 最大并发连接数 --> 28 <property name="maxActive" value="${cas.druid.database.maxActive}"/> 29 <!-- 初始化连接数量 --> 30 <property name="initialSize" value="${cas.druid.database.initialSize}"/> 31 <!-- 配置获取连接等待超时的时间 --> 32 <property name="maxWait" value="${cas.druid.database.maxWait}"/> 33 <!-- 最小空闲连接数 --> 34 <property name="minIdle" value="${cas.druid.database.minIdle}"/> 35 <!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --> 36 <property name="timeBetweenEvictionRunsMillis" value="${cas.druid.database.timeBetweenEvictionRunsMillis}"/> 37 <!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --> 38 <property name="minEvictableIdleTimeMillis" value="${cas.druid.database.minEvictableIdleTimeMillis}"/> 39 <property name="validationQuery" value="${cas.druid.database.validationQuery}"/> 40 <property name="testWhileIdle" value="${cas.druid.database.testWhileIdle}"/> 41 <property name="testOnBorrow" value="${cas.druid.database.testOnBorrow}"/> 42 <property name="testOnReturn" value="${cas.druid.database.testOnReturn}"/> 43 <property name="maxOpenPreparedStatements" value="${cas.druid.database.maxOpenPreparedStatements}"/> 44 <!-- 打开 removeAbandoned 功能 --> 45 <property name="removeAbandoned" value="${cas.druid.database.removeAbandoned}"/> 46 <!-- 1800 秒,也就是 30 分钟 --> 47 <property name="removeAbandonedTimeout" value="${cas.druid.database.removeAbandonedTimeout}"/> 48 <!-- 关闭 abanded 连接时输出错误日志 --> 49 <property name="logAbandoned" value="${cas.druid.database.logAbandoned}"/> 50 <property name="proxyFilters"> 51 <list> 52 <ref bean="log-filter"/> 53 </list> 54 </property> 55 </bean> 56 <bean id="log-filter" class="com.alibaba.druid.filter.logging.Slf4jLogFilter"> 57 <property name="connectionLogEnabled" value="${cas.druid.database.connectionLogEnabled}"/> 58 <property name="statementLogEnabled" value="${cas.druid.database.statementLogEnabled}"/> 59 <property name="resultSetLogEnabled" value="${cas.druid.database.resultSetLogEnabled}"/> 60 <property name="statementExecutableSqlLogEnable" value="${cas.druid.database.statementExecutableSqlLogEnable}"/> 61 </bean>
1.4 cas.propertie中新增配置:
1 #sql通过用户名查询密码 2 cas.jdbc.authn.query.sql=select password from t_user where account=? and valid=true 3 4 #druid config by xbwu 5 cas.druid.database.url=jdbc:mysql://localhost:3306/xbwudb?characterEncoding=utf8 6 cas.druid.database.username=root 7 cas.druid.database.password=root 8 cas.druid.database.driverClassName=com.mysql.jdbc.Driver 9 cas.druid.database.filters=stat,wall 10 cas.druid.database.maxActive=20 11 cas.druid.database.initialSize=1 12 cas.druid.database.maxWait=60000 13 cas.druid.database.minIdle=10 14 cas.druid.database.timeBetweenEvictionRunsMillis=60000 15 cas.druid.database.minEvictableIdleTimeMillis=300000 16 cas.druid.database.validationQuery=SELECT 'x' 17 cas.druid.database.testWhileIdle= true 18 cas.druid.database.testOnBorrow=false 19 cas.druid.database.testOnReturn=false 20 cas.druid.database.maxOpenPreparedStatements=20 21 cas.druid.database.removeAbandoned=true 22 cas.druid.database.removeAbandonedTimeout=1800 23 cas.druid.database.logAbandoned=true 24 25 cas.druid.database.connectionLogEnabled=false 26 cas.druid.database.statementLogEnabled=false 27 cas.druid.database.resultSetLogEnabled=true 28 cas.druid.database.statementExecutableSqlLogEnable=true
1.5 web.xml新增druid控制台:
1 <!-- 连接池 启用 Web 监控统计功能 start--> 2 <filter> 3 <filter-name> DruidWebStatFilter </filter-name> 4 <filter-class> com.alibaba.druid.support.http.WebStatFilter </filter-class> 5 <init-param > 6 <param-name> exclusions </param-name> 7 <param-value> *. js ,*. gif ,*. jpg ,*. png ,*. css ,*. ico ,/ druid /* </param-value> 8 </init-param> 9 </filter > 10 <filter-mapping> 11 <filter-name> DruidWebStatFilter </filter-name> 12 <url-pattern>/*</url-pattern> 13 </filter-mapping> 14 <servlet > 15 <servlet-name> DruidStatView </servlet-name> 16 <servlet-class> com.alibaba.druid.support.http.StatViewServlet </servlet-class> 17 </servlet > 18 <servlet-mapping> 19 <servlet-name> DruidStatView </servlet-name> 20 <url-pattern>/druid/console/*</url-pattern> 21 </servlet-mapping> 22 <!-- 连接池 启用 Web 监控统计功能 end—>
1.6 cas-server-webapp\src\main\resources\log4j2.xml新增druid日志配置
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <!-- Specify the refresh internal in seconds. --> 3 <!-- OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --> 4 <Configuration monitorInterval="60"> 5 <Appenders> 6 <Console name="console" target="SYSTEM_OUT"> 7 <PatternLayout pattern="%d %p [%c] - %n<%m>%n"/> 8 </Console> 9 <RollingFile name="file" fileName="../caslog/cas.log" append="true" 10 filePattern="../caslog/cas-%d{yyyy-MM-dd-HH}.log"> 11 <PatternLayout pattern="%d %p [%c] - %n<%m>%n"/> 12 <Policies> 13 <OnStartupTriggeringPolicy /> 14 <SizeBasedTriggeringPolicy size="10 MB"/> 15 <TimeBasedTriggeringPolicy /> 16 </Policies> 17 </RollingFile> 18 <RollingFile name="auditlogfile" fileName="../caslog/cas_audit.log" append="true" 19 filePattern="../caslog/cas_audit-%d{yyyy-MM-dd-HH}.log"> 20 <PatternLayout pattern="%d %p [%c] - %m%n"/> 21 <Policies> 22 <OnStartupTriggeringPolicy /> 23 <SizeBasedTriggeringPolicy size="10 MB"/> 24 <TimeBasedTriggeringPolicy /> 25 </Policies> 26 </RollingFile> 27 <RollingFile name="perfFileAppender" fileName="../caslog/perfStats.log" append="true" 28 filePattern="../caslog/perfStats-%d{yyyy-MM-dd-HH}.log"> 29 <PatternLayout pattern="%m%n"/> 30 <Policies> 31 <OnStartupTriggeringPolicy /> 32 <SizeBasedTriggeringPolicy size="10 MB"/> 33 <TimeBasedTriggeringPolicy /> 34 </Policies> 35 </RollingFile> 36 </Appenders> 37 <Loggers> 38 <AsyncLogger name="org.jasig" level="debug" additivity="false" includeLocation="true"> 39 <AppenderRef ref="console"/> 40 <AppenderRef ref="file"/> 41 </AsyncLogger> 42 <!--不配置append默认使用AsyncRoot的append--> 43 <AsyncLogger name="org.springframework" level="warn" /> 44 <AsyncLogger name="org.springframework.webflow" level="warn" /> 45 <AsyncLogger name="org.springframework.web" level="warn" /> 46 <AsyncLogger name="org.pac4j" level="warn" /> 47 <!-- 48 <AsyncLogger name="org.opensaml" level="debug" additivity="false"> 49 <AppenderRef ref="console"/> 50 <AppenderRef ref="file"/> 51 </AsyncLogger> 52 <AsyncLogger name="org.ldaptive" level="debug" additivity="false"> 53 <AppenderRef ref="console"/> 54 <AppenderRef ref="file"/> 55 </AsyncLogger> 56 <AsyncLogger name="com.hazelcast" level="debug" additivity="false"> 57 <AppenderRef ref="console"/> 58 <AppenderRef ref="file"/> 59 </AsyncLogger> 60 --> 61 62 <AsyncLogger name="perfStatsLogger" level="info" additivity="false" includeLocation="true"> 63 <AppenderRef ref="perfFileAppender"/> 64 </AsyncLogger> 65 66 <AsyncLogger name="org.jasig.cas.web.flow" level="info" additivity="true" includeLocation="true"> 67 <AppenderRef ref="file"/> 68 </AsyncLogger> 69 <AsyncLogger name="org.jasig.inspektr.audit.support" level="info" includeLocation="true"> 70 <AppenderRef ref="console"/> 71 <AppenderRef ref="auditlogfile"/> 72 <AppenderRef ref="file"/> 73 </AsyncLogger> 74 <!--druid连接池信息打印--> 75 <AsyncLogger name="druid.sql.Statement" level="debug" additivity="false" includeLocation="true"> 76 <AppenderRef ref="console"/> 77 <AppenderRef ref="file"/> 78 </AsyncLogger> 79 <!--<AsyncLogger name="druid.sql.DataSource" level="debug" additivity="false" includeLocation="true"> 80 <AppenderRef ref="console"/> 81 </AsyncLogger> 82 <AsyncLogger name="druid.sql.Connection" level="debug" additivity="false" includeLocation="true"> 83 <AppenderRef ref="console"/> 84 </AsyncLogger>--> 85 <!--<AsyncLogger name="druid.sql.ResultSet" level="debug" additivity="false" includeLocation="true"> 86 <AppenderRef ref="console"/> 87 </AsyncLogger>--> 88 <AsyncLogger name="druid.sql" level="error" additivity="false" includeLocation="true"> 89 <AppenderRef ref="console"/> 90 </AsyncLogger> 91 <!--根输出配置,输出等级--> 92 <!-- OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --> 93 <!--如果上面的日志输出配置了appender,并且输出等级跟根配置有重叠--> 94 <!--比如上面配置debug,则两者的info级别以上日志都会在相同的输出appender中(console)输出,也就是说控制台会输出2次--> 95 <!--所以尽量根输出配置高一点,比如error等级--> 96 <!--如果我们确实有这种需求(不想遵循父类的Appender),可以在自定义配置中加上additivity="false"参数--> 97 <AsyncRoot level="info"> 98 <AppenderRef ref="console"/> 99 <AppenderRef ref="file"/> 100 </AsyncRoot> 101 </Loggers> 102 </Configuration>
1.7 页面样式定制化修改配置:
1.7.1 复制cas-server-webapp\src\main\webapp\WEB-INF下的view文件夹一份,命名为:view_custom
1.7.2 修改cas-server-webapp\src\main\webapp\WEB-INF\cas.properties内容:
1.7.3 样式文件-修改cas-server-webapp\src\main\resources\cas-theme-default.properties的内容:
4.复制css和js各一份出来,重命名为上面的名字:
2.服务器支持restfull调用登录
cas-server-webapp\build.gradle 增加依赖:compile project(':cas-server-support-rest')
cas-server-webapp\src\main\webapp\WEB-INF\cas.properties 加长一次性票据有效期:st.timeToKillInSeconds=20 20秒有效
cas单点登录restfull请求登录范例:
参考官方文档:https://apereo.github.io/cas/5.1.x/protocol/REST-Protocol.html
2.1 请求获取TGT
请求方式:POST
head设置:Content-Type: application/x-www-form-urlencoded
范例:{cas服务器地址}/cas/v1/tickets?username=battags&password=password&additionalParam1=paramvalue
demo: http://localhost:8080/cas/v1/tickets?username=admin&password=1234qwer
正确返回:
返回头:
HTTP/1.1 201
Cache-Control: no-store
Location: http://localhost:8080/cas/v1/tickets/TGT-1-fHlelcwDXNYOiFpiiGwnTtyArXD4rjNdtelyBaBaBWWNHHHvav-www.casServer.com
Content-Type: text/html;charset=UTF-8
Content-Length: 376
Date: Mon, 07 Aug 2017 01:42:35 GMT
2.2 拿到TGT,请求一次性票据ST
请求方式:POST
head设置:Content-Type: application/x-www-form-urlencoded
范例:{cas服务器地址}/cas/v1/tickets/{TGT id}?service={form encoded parameter for the service url}
demo:http://localhost:8080/cas/v1/tickets/TGT-1-fHlelcwDXNYOiFpiiGwnTtyArXD4rjNdtelyBaBaBWWNHHHvav-www.casServer.com?service=http%3A%2F%2FcasTest02.com%3A8082%2F
正确返回:
返回头:
HTTP/1.1 200
Cache-Control: no-store
Content-Disposition: inline;filename=f.txt
Content-Type: application/x-msdownload;charset=UTF-8
Content-Length: 43
Date: Mon, 07 Aug 2017 01:45:56 GMT
返回内容:
ST-1-0wjyNMlwOK3kevi5Fa6w-www.casServer.com
2.3 拿着一次性票据,请求服务器验证
请求方式:GET
范例:{cas服务器地址}/cas/p3/serviceValidate?service={service url}&ticket={service ticket}
demo:http://localhost:8080/cas/p3/serviceValidate?ticket=ST-1-0wjyNMlwOK3kevi5Fa6w-www.casServer.com&service=http%3A%2F%2FcasTest02.com%3A8082%2F
正确返回:
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>admin</cas:user>
<cas:attributes>
<cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed>
<cas:isFromNewLogin>true</cas:isFromNewLogin>
<cas:authenticationDate>2017-08-07T10:09:02.521+08:00</cas:authenticationDate>
</cas:attributes>
</cas:authenticationSuccess>
</cas:serviceResponse>
一次性票据过期返回:
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationFailure code='INVALID_TICKET'>
未能够识别出目标 'ST-1-0wjyNMlwOK3kevi5Fa6w-www.casServer.com'票根
</cas:authenticationFailure>
</cas:serviceResponse>
CAS客户端
1.单点登录拦截器配置:web.xml
<!-- ========================单点登录开始 ======================== --> <!--用于单点退出,该过滤器用于实现单点登出功能,可选配置 --> <listener> <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class> </listener> <!--该过滤器用于实现单点登出功能,可选配置。 --> <filter> <filter-name>CASSingle Sign OutFilter</filter-name> <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class> </filter> <filter-mapping> <filter-name>CASSingle Sign OutFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>CASFilter</filter-name> <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class> <init-param> <param-name>casServerLoginUrl</param-name> <param-value>http://localhost:8080/cas/login</param-value> </init-param> <init-param> <!--此地址是用于给cas服务器进行回调的域名和端口,注意,如果在这个回调地址后面配置了具体的路径和参数,而非简单的域名端口,比如:--> <!--http://casTest01.com:8081/aaa/bb.jsp?AA=123--> <!--1.参数AA=123在访问任何客户端地址时都会cas服务器被带回来--> <!--2.具体路径:/aaa/bb.jsp 只有在访问客户端简单域名加端口时,cas服务器才会将完整路径返回:http://casTest01.com:8081/aaa/bb--> <param-name>serverName</param-name> <param-value>http://casTest01.com:8081</param-value> <!--此地址是用于给cas服务器进行绝对回调的地址,只要是被cas拦截,只回调地址必然是service所对应参数--> <!--<param-name>service</param-name>--> <!--<param-value>http://casTest01.com:8081/loginSuccess.do</param-value>--> </init-param> </filter> <filter-mapping> <filter-name>CASFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--该过滤器负责对Ticket的校验工作,必须启用它 --> <filter> <filter-name>CASValidationFilter</filter-name> <filter-class> org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter </filter-class> <init-param> <param-name>casServerUrlPrefix</param-name> <param-value>http://localhost:8080/cas</param-value> </init-param> <init-param> <param-name>serverName</param-name> <param-value>http://casTest01.com:8081</param-value> </init-param> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CASValidationFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 该过滤器负责实现HttpServletRequest请求的包裹, 比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。 --> <filter> <filter-name>CASHttpServletRequest WrapperFilter</filter-name> <filter-class> org.jasig.cas.client.util.HttpServletRequestWrapperFilter </filter-class> </filter> <filter-mapping> <filter-name>CASHttpServletRequest WrapperFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。 比如AssertionHolder.getAssertion().getPrincipal().getName()。 --> <filter> <filter-name>CASAssertion Thread LocalFilter</filter-name> <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class> </filter> <filter-mapping> <filter-name>CASAssertion Thread LocalFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- ========================单点登录结束 ======================== -->
番外篇!--与shibboleth idp saml2.0 联合配置
1.工作环境准备
1.1 运行基础
jdk8.0.131 tomcat8.5.16 cas-4.2.1(基于gradle) ,shibboleth-identityprovider-2.4.5,win7 64,mysql 5.6.21,花生壳免费域名映射(否则自己需要再下一个shibboleth的sp服务器用于测试)
1.2 ssl配置
tomcat必须配置ssl连接,此tomcat用于启动cas-server和shib-idp(我一个是用idea启动,ssl访问端口设置不同而已),参考上面的《本地tomcat证书生成与配置》
2. 安装shibboleth-identityprovider-2.4.5
参考文档:https://wiki.shibboleth.net/confluence/display/SHIB2/IdPInstall
2.1 添加cas client 包支持
把cas的客户端jar包(cas-client-core-3.4.1.jar)丢到idp解压的lib目录下,例如:D:\Program Files (x86)\shibboleth-identityprovider-2.4.5\lib,这样安装好的idp就会有这个jar包
2.2 执行install.bat安装idp
2.3 修改idp的元数据文件
D:\Program Files (x86)\shibboleth-idp-server\metadata\idp-metadata.xml
这里面的几个idp接口访问端口要改一下,因为我用的花生壳所以端口号是指定好了的,把文件里面的:8443全部替换成空串即可。
注意,由于默认该元数据文件生成的sope是二级域名scope,需要改成完整的三级域名sope,否则testshib验证不通过!
例如:
我的花生壳idp地址:149p874e84.51mypc.cn:11898
原始生成 idp-metadata.xml的scope:<shibmd:Scope regexp="false">51mypc.cn:11898</shibmd:Scope>
需要修改成:<shibmd:Scope regexp="false">149p874e84.51mypc.cn:11898</shibmd:Scope>
3.cas-server所需配合部分
上面先告一段落,开始修改cas-server的整合部分:
参考:https://apereo.github.io/cas/4.2.x/integration/Shibboleth.html
3.1 修改idp安装目录配置文件
idp安装目录/conf/handler.xml
把里面的ph:LoginHandler xsi:type="ph:RemoteUser"节点替换成以下内容:
<!-- Remote User handler for CAS support --> <ph:LoginHandler xsi:type="ph:RemoteUser"> <ph:AuthenticationMethod> urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified </ph:AuthenticationMethod> <ph:AuthenticationMethod> urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport </ph:AuthenticationMethod> </ph:LoginHandler>
3.2 修改idp.war包的web.xml文件
增加以下内容:
1 <!-- For CAS client support --> 2 <context-param> 3 <param-name>serverName</param-name> 4 <param-value>${idp域名+端口}</param-value> 5 </context-param> 6 <!-- CAS client filters --> 7 <filter> 8 <filter-name>CAS Authentication Filter</filter-name> 9 <filter-class> 10 org.jasig.cas.client.authentication.AuthenticationFilter 11 </filter-class> 12 <init-param> 13 <param-name>casServerLoginUrl</param-name> 14 <param-value>${cas服务器域名加端口加项目名}/login</param-value> 15 </init-param> 16 </filter> 17 <filter-mapping> 18 <filter-name>CAS Authentication Filter</filter-name> 19 <url-pattern>/Authn/RemoteUser</url-pattern> 20 </filter-mapping> 21 <filter> 22 <filter-name>CAS Validation Filter</filter-name> 23 <filter-class> 24 org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter 25 </filter-class> 26 <init-param> 27 <param-name>casServerUrlPrefix</param-name> 28 <param-value>${cas服务器域名加端口加项目名}</param-value> 29 </init-param> 30 <init-param> 31 <param-name>redirectAfterValidation</param-name> 32 <param-value>true</param-value> 33 </init-param> 34 </filter> 35 <filter-mapping> 36 <filter-name>CAS Validation Filter</filter-name> 37 <url-pattern>/Authn/RemoteUser</url-pattern> 38 </filter-mapping> 39 <filter> 40 <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name> 41 <filter-class> 42 org.jasig.cas.client.util.HttpServletRequestWrapperFilter 43 </filter-class> 44 </filter> 45 <filter-mapping> 46 <filter-name>CAS HttpServletRequest Wrapper Filter</filter-name> 47 <url-pattern>/Authn/RemoteUser</url-pattern> 48 </filter-mapping>
3.3 cas-server增加saml2.0支持包 cas-server-support-saml-mdui
3.4 cas-server-webapp的deployerConfigContext.xml添加
1 <!--saml2.0支持--> 2 <bean id="samlMetadataUIParserAction" 3 class="org.jasig.cas.support.saml.web.flow.mdui.SamlMetadataUIParserAction" 4 c:entityIdParameterName="entityId" 5 c:metadataAdapter-ref="metadataAdapter"/> 6 <!--元数据配置--> 7 <bean id="metadataAdapter" 8 class="org.jasig.cas.support.saml.web.flow.mdui.StaticMetadataResolverAdapter" 9 c:metadataResources-ref="metadataResources" 10 p:refreshIntervalInMinutes="300" 11 p:requireValidMetadata="true" /> 12 <!--元数据源配置,动态获取idp上的元数据--> 13 <util:map id="metadataResources"> 14 <entry key="${完整idp域名https访问地址包括项目名}/entities/"> 15 <bean class="org.opensaml.saml.metadata.resolver.filter.impl.MetadataFilterChain"> 16 <property name="filters"> 17 <list /> 18 </property> 19 </bean> 20 </entry> 21 </util:map> 22 <!--元数据过滤器配置--> 23 <bean id="metadataFilters" 24 class="org.opensaml.saml.metadata.resolver.filter.impl.MetadataFilterChain"> 25 <property name="filters"> 26 <list> 27 <!-- 28 <bean class="org.opensaml.saml.metadata.resolver.filter.impl.RequiredValidUntilFilter" 29 c:maxValidity="0" /> 30 --> 31 <bean class="org.opensaml.saml.metadata.resolver.filter.impl.SignatureValidationFilter" 32 c:engine-ref="trustEngine" p:requireSignature="false" /> 33 </list> 34 </property> 35 </bean> 36 <bean id="trustEngine" 37 class="org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine" 38 c:keyInfoResolver-ref="keyInfoResolver" 39 c:resolver-ref="credentialResolver" /> 40 <bean id="keyInfoResolver" 41 class="org.opensaml.xmlsec.keyinfo.impl.BasicProviderKeyInfoCredentialResolver"> 42 <constructor-arg name="keyInfoProviders"> 43 <list> 44 <bean class="org.opensaml.xmlsec.keyinfo.impl.provider.RSAKeyValueProvider" /> 45 <bean class="org.opensaml.xmlsec.keyinfo.impl.provider.DSAKeyValueProvider" /> 46 <bean class="org.opensaml.xmlsec.keyinfo.impl.provider.DEREncodedKeyValueProvider" /> 47 <bean class="org.opensaml.xmlsec.keyinfo.impl.provider.InlineX509DataProvider" /> 48 </list> 49 </constructor-arg> 50 </bean> 51 <bean id="credentialResolver" 52 class="org.opensaml.security.credential.impl.StaticCredentialResolver" 53 c:credential-ref="credentialFactoryBean" /> 54 <bean id="credentialFactoryBean" 55 class="net.shibboleth.idp.profile.spring.relyingparty.security.credential.BasicResourceCredentialFactoryBean" 56 p:publicKeyInfo="classpath:inc-md-pub.pem" ></bean>
3.5 修改cas-server-webapp的login-webflow.xml内容
在view-state id="viewLoginForm" 节点提交动作新增saml2.0校验,修改大致如下:
<view-state id="viewLoginForm" view="casLoginView" model="credential"> <binder> <binding property="username" required="true"/> <binding property="password" required="true"/> <!-- <binding property="rememberMe" /> --> </binder> <on-entry> <set name="viewScope.commandName" value="'credential'"/> <!--进入该视图节点时,进行saml2.0元数据校验--> <evaluate expression="samlMetadataUIParserAction" /> </on-entry> <transition on="submit" bind="true" validate="true" to="realSubmit"/> </view-state>
4.使用testshib作为sp端免费测试
参考教程:https://www.testshib.org/register.html
4.1 将我们的cas-server和idp.war都部署到开启了ssl的tomcat上进行启动吧!然后开启我们的花生壳内网映射!
4.2 开始上传idp端元数据文件
4.3 修改idp端的元数据提供者配置
在idp安装目录下找到D:\Program Files (x86)\shibboleth-idp-server\conf\relying-party.xml
在节点<metadata:MetadataProvider id="ShibbolethMetadata" xsi:type="metadata:ChainingMetadataProvider">内插入testshib的idp元数据提供,插入后是这样子:
<metadata:MetadataProvider id="ShibbolethMetadata" xsi:type="metadata:ChainingMetadataProvider"> <!-- Load the IdP's own metadata. This is necessary for artifact support. --> <metadata:MetadataProvider id="IdPMD" xsi:type="metadata:FilesystemMetadataProvider" metadataFile="D:\Program Files (x86)\shibboleth-idp-server/metadata/idp-metadata.xml" maxRefreshDelay="P1D" /> <!--test shib--> <metadata:MetadataProvider id="HTTPMetadataTESTSHIB" xsi:type="metadata:FileBackedHTTPMetadataProvider" backingFile="D:\Program Files (x86)\shibboleth-idp-server/metadata/testshib-providers.xml" metadataURL="http://www.testshib.org/metadata/testshib-providers.xml"/>
4.4 开始测试
访问测试sp地址:https://sp.testshib.org/,在修改下方的idp访问地址,go!
账号自动登录后跳转子系统(webUtils修改的有问题,暂时不要参照此功能)
注意:修改全在cas服务器端修改
修改涉及文件
1.UsernamePasswordCredential
添加用于区别自动登录和正常登录的账号属性
内容如下:
package org.jasig.cas.authentication; import org.apache.commons.lang3.builder.HashCodeBuilder; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import java.io.Serializable; /** * Credential for authenticating with a username and password. * * @author Scott Battaglia * @author Marvin S. Addison * @since 3.0.0 */ public class UsernamePasswordCredential implements Credential, Serializable { /** Authentication attribute name for password. **/ public static final String AUTHENTICATION_ATTRIBUTE_PASSWORD = "credential"; private static final long serialVersionUID = -700605081472810939L; @NotNull @Size(min=1, message = "required.username") private String username; /** * 用于区别自动登录用户名 */ private String autoAccount; @NotNull @Size(min=1, message = "required.password") private String password; /** Default constructor. */ public UsernamePasswordCredential() {} /** * Creates a new instance with the given username and password. * * @param userName Non-null user name. * @param password Non-null password. */ public UsernamePasswordCredential(final String userName, final String password) { this.username = userName; this.password = password; } public final String getPassword() { return this.password; } public final void setPassword(final String password) { this.password = password; } public final String getUsername() { return this.username; } public final void setUsername(final String userName) { this.username = userName; } public final String getAutoAccount() { return autoAccount; } public final void setAutoAccount(String autoAccount) { this.autoAccount = autoAccount; } @Override public String getId() { return this.username; } @Override public String toString() { return this.username; } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final UsernamePasswordCredential that = (UsernamePasswordCredential) o; if (password != null ? !password.equals(that.password) : that.password != null) { return false; } if (username != null ? !username.equals(that.username) : that.username != null) { return false; } return true; } @Override public int hashCode() { return new HashCodeBuilder() .append(username) .append(password) .toHashCode(); } }
2.WebUtils的getTicketGrantingTicketId方法
添加获取在自动登录后的cookie参数,避免获取不到导致无法识别自动登录
/** * Gets the ticket granting ticket id from the request and flow scopes. * * @param context the context * @return the ticket granting ticket id */ public static String getTicketGrantingTicketId( @NotNull final RequestContext context) { final String tgtFromRequest = (String) context.getRequestScope().get("ticketGrantingTicketId"); final String tgtFromFlow = (String) context.getFlowScope().get("ticketGrantingTicketId"); String tgtFromCookie=null; Cookie[] cookies=getHttpServletRequest(context).getCookies();g if(cookies!=null){ for(Cookie cookie : cookies){ if(cookie.getName().equals("TGC")){ tgtFromCookie= cookie.getValue(); } } } if(tgtFromRequest!=null){ return tgtFromRequest; }else if(tgtFromFlow!=null){ return tgtFromFlow; }else if(tgtFromCookie!=null){ return tgtFromCookie; }else{ return null; } // return tgtFromRequest != null ? tgtFromRequest : tgtFromFlow; }
3.QueryDatabaseAuthenticationHandler(jdbc验证类)的authenticateUsernamePasswordInternal方法
添加根据自动登录参数验证的操作
@Override protected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential) throws GeneralSecurityException, PreventedException { if (StringUtils.isBlank(this.sql) || getJdbcTemplate() == null ) { throw new GeneralSecurityException("Authentication handler is not configured correctly"); } if(credential.getAutoAccount()!=null){ final String username = credential.getAutoAccount(); try { final String encryptedPassword = getJdbcTemplate().queryForObject(this.sql, String.class, username); } catch (final IncorrectResultSizeDataAccessException e) { if (e.getActualSize() == 0) { throw new AccountNotFoundException(username + " not found with SQL query"); } else { throw new FailedLoginException("Multiple records found for " + username); } } catch (final DataAccessException e) { throw new PreventedException("SQL exception while executing query for " + username, e); } return createHandlerResult(credential, this.principalFactory.createPrincipal(username), null); }else { final String username = credential.getUsername(); final String encryptedPassword = this.getPasswordEncoder().encode(credential.getPassword()); try { final String dbPassword = getJdbcTemplate().queryForObject(this.sql, String.class, username); if (!dbPassword.equals(encryptedPassword)) { throw new FailedLoginException("Password does not match value on record."); } } catch (final IncorrectResultSizeDataAccessException e) { if (e.getActualSize() == 0) { throw new AccountNotFoundException(username + " not found with SQL query"); } else { throw new FailedLoginException("Multiple records found for " + username); } } catch (final DataAccessException e) { throw new PreventedException("SQL exception while executing query for " + username, e); } return createHandlerResult(credential, this.principalFactory.createPrincipal(username), null); } }
4.TicketGrantingTicketCheckAction(tgt验证类)的doExecute方法
添加根据自动登录后把cookie放回 login流程的requestScope操作,否则默认流程login会提示没有登录
@Override protected Event doExecute(final RequestContext requestContext) throws Exception { final String tgtId = WebUtils.getTicketGrantingTicketId(requestContext); if (!StringUtils.hasText(tgtId)) { return new Event(this, NOT_EXISTS); } String eventId = INVALID; try { final Ticket ticket = this.centralAuthenticationService.getTicket(tgtId, Ticket.class); if (ticket != null && !ticket.isExpired()) { eventId = VALID; //有效则放入 WebUtils.putTicketGrantingTicketInScopes(requestContext, tgtId); } } catch (final AbstractTicketException e) { logger.trace("Could not retrieve ticket id {} from registry.", e); } return new Event(this, eventId); }
5.AutoLoginController自动登录入口
package com.sbs.cas.controller;/** * @description * @autor xbwu on 2017/9/9. */ import org.apache.commons.lang3.StringUtils; import org.jasig.cas.CentralAuthenticationService; import org.jasig.cas.authentication.*; import org.jasig.cas.ticket.TicketGrantingTicket; import org.jasig.cas.web.support.WebUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.util.CookieGenerator; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.NotNull; /** * 自动登录接口 * @author xbwu * @create 2017-09-09 **/ @Controller(value = "/autoLogin") public class AutoLoginController { protected final transient Logger logger = LoggerFactory.getLogger(getClass()); @Autowired @Qualifier(value = "centralAuthenticationService") private CentralAuthenticationService centralAuthenticationService; @Autowired @Qualifier(value = "ticketGrantingTicketCookieGenerator") private CookieGenerator ticketGrantingTicketCookieGenerator; @NotNull @Autowired(required=false) @Qualifier("defaultAuthenticationSystemSupport") private AuthenticationSystemSupport authenticationSystemSupport; @RequestMapping(value = "/checkLogin") public ModelAndView autoLogin(HttpServletRequest request,HttpServletResponse response,Model model) throws Exception{ String systemUrl=request.getParameter("systemUrl"); String autoAccount=request.getParameter("autoAccount"); if(StringUtils.isBlank(autoAccount)){ throw new Exception("参数错误"); } bindTicketGrantingTicket(autoAccount,request,response); response.sendRedirect(systemUrl); return null; } protected void bindTicketGrantingTicket(String autoAccount,HttpServletRequest request, HttpServletResponse response){ try { UsernamePasswordCredential credential = new UsernamePasswordCredential(); credential.setAutoAccount(autoAccount); credential.setUsername(autoAccount); final AuthenticationContextBuilder builder = new DefaultAuthenticationContextBuilder( this.authenticationSystemSupport.getPrincipalElectionStrategy()); final AuthenticationTransaction transaction = AuthenticationTransaction.wrap(credential); this.authenticationSystemSupport.getAuthenticationTransactionManager().handle(transaction, builder); final AuthenticationContext authenticationContext = builder.build(null); final TicketGrantingTicket tgt = this.centralAuthenticationService.createTicketGrantingTicket(authenticationContext); ticketGrantingTicketCookieGenerator.addCookie(response, tgt.getId()); } catch (Exception e){ e.printStackTrace(); logger.error("bindTicketGrantingTicket has exception.", e); } } }
6.web.xml配置自动登录路径通过springmvc请求
<servlet-mapping> <servlet-name>cas</servlet-name> <url-pattern>/autoLogin/*</url-pattern> </servlet-mapping>
完工!我的本地测试路径:http://localhost:8080/cas/autoLogin/checkLogin?systemUrl=http://casTest01.com:8081&autoAccount=admin
demo下载:https://code.aliyun.com/cas/cas-overlay