@zh350229319
2017-10-26T01:26:12.000000Z
字数 12896
阅读 660
CAS
因公司要求使用cas服务器,公司原来有一个使用ajax方式进行单点登录的cas服务器,在项目上线后,发现问题太多,改为使用正常方式的cas服务器。
客户端项目用到了两种权限管理框架,分别是shiro和spring security。
整个过程分四部分记录修改过程,第一部分记录cas服务器连接mysql配置;第二部分记录cas服务器增加登录验证码;第三部分记录shiro的配置;第四部分记录spring security的配置。
cas服务器端下载地址:http://developer.jasig.org/cas/
cas客户端下载地址:http://developer.jasig.org/cas-clients/
本文使用版本cas-server-4.0.0-release
试过了最新的4.1.3,使用jetty:run和tomcat都没有本法正常启动,所以使用了此版本。本版本在修改默认语言为中文时发现了一个bug,参考cas-server-4.1.3进行了修复。
cas-server包含很多项目,基础只需要cas-server-core、cas-server-webapp、cas-server-webapp-support三个项目。因为需要连接数据库验证用户名和密码,保留了cas-server-support-jdbc项目。
因为cas使用了log4j,项目运行路径时包含中文路径会报错,但是不影响使用。
可以使用eclipse的run on server启动cas,要修改Server工程中server.xml中Context节点,保持docBase和path一致,即可启动成功。
<Context docBase="cas-server-webapp" path="/cas-server-webapp" reloadable="true"source="org.eclipse.jst.j2ee.server:cas-server-webapp"/>
引入工程后,子项目的pom文件会出现警告,可以在cas-server的pom文件中,把警告提到的插件移动到pluginManagement的plugins节点下。并去掉com.mycila.maven-license-plugin插件。
deployerConfigContext.xml:找到id为proxyAuthenticationHandler的bean,增加p:requireSecure="false"
<bean id="proxyAuthenticationHandler"class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"p:httpClient-ref="httpClient" p:requireSecure="false" />
ticketGrantingTicketCookieGenerator.xml:
warnCookieGenerator.xml:p:cookieSecure="true"改为p:cookieSecure="false"
<bean id="ticketGrantingTicketCookieGenerator"class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"p:cookieSecure="false"p:cookieMaxAge="-1"p:cookieName="CASTGC"p:cookiePath="/cas" />
<bean id="warnCookieGenerator"class="org.jasig.cas.web.support.CookieRetrievingCookieGenerator"p:cookieSecure="false"p:cookieMaxAge="-1"p:cookieName="CASPRIVACY"p:cookiePath="/cas" />
cas-server-4.0.0中有一个bug,需要修改org.jasig.cas.web.viewCasReloadableMessageBundle类中的getMessageInternal()方法
final String filename = this.basenames[i] + "_" + locale.getLanguage();
改为
final String filename = this.basenames[i] + "_" + locale;
messages_zh_CN.properties中缺少了一些中文提示,因此从cas-server-4.1.3中复制了一个覆盖原文件。
cas-server-4.1.3下载地址:https://github.com/Jasig/cas/tree/v4.1.3
现在cas-server可以部署到tomcat中并正常启动,在deployerConfigContext.xml的id为deployerConfigContext.xml的bean中保存了默认的用户名和密码。
<bean id="primaryAuthenticationHandler"class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler"><property name="users"><map><entry key="casuser" value="Mellon"/></map></property></bean>
因为客户端项目在登出后需要跳转到登录页面,需要开启cas登出后跳转页面的功能。
cas-servlet.xml:修改id为logoutAction的bean,p:followServiceRedirects="${cas.logout.followServiceRedirects:false}"改为true。
<bean id="logoutAction" class="org.jasig.cas.web.flow.LogoutAction"p:servicesManager-ref="servicesManager"p:followServiceRedirects="${cas.logout.followServiceRedirects:true}"/>
修改cas-server工程的pom文件,注释掉没有用到的modules。本文连接的数据库是mysql,使用druid连接池。pom增加如下依赖:
<dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${version.mysql}</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>${version.druid}</version></dependency>
properties节点
<version.druid>1.0.16</version.druid><version.mysql>5.1.34</version.mysql>
修改cas-server-support-jdbc工程的pom文件,增加依赖:
<dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId></dependency><!-- 可选,项目需要使用salt加盐算法计算密码才引用 --><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId></dependency>
修改cas-server-webapp工程的pom文件,增加依赖:
<dependency><groupId>org.jasig.cas</groupId><artifactId>cas-server-support-jdbc</artifactId><version>${project.version}</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId></dependency>
cas-server-support-jdbc工程中自带4个验证处理类,可以通过配置的方法来使用。这四个不满足本项目的需求,因此新增了一个自定义验证处理类。
deployerConfigContext.xml:去掉默认的primaryAuthenticationHandler,增加自定义处理类,并增加druid连接池。
<!-- 自定义验证处理类 --><bean id="primaryAuthenticationHandler"class="org.jasig.cas.adaptors.jdbc.QueryAndSaltDatabaseAuthenticationHandler"><constructor-arg name="dataSource" ref="dataSource" /><constructor-arg name="sql" value="${jdbc.selectSQL}" /><!-- 加密算法 --><constructor-arg name="algorithmName" value="${algorithmName}"/><property name="numberOfIterations" value="${numberOfIterations}" /><!-- 数据库中密码字段 --><property name="passwordFieldName" value="${passwordFieldName}" /><!-- 数据库中salt字段 --><property name="saltFieldName" value="${saltFieldName}" /></bean><!-- 数据源配置, 使用druid连接池 --><bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"init-method="init" destroy-method="close"><property name="driverClassName" value="${jdbc.driver}" /><property name="url" value="${jdbc.url}" /><property name="username" value="${jdbc.username}" /><property name="password" value="${jdbc.password}" /><!-- 配置监控统计拦截的filters --><property name="filters" value="${druid.filters}" /><property name="connectionProperties" value="${druid.connectionProperties}" /><!-- 配置初始化大小、最小、最大 --><property name="initialSize" value="${druid.initialSize}"/><property name="minIdle" value="${druid.minIdle}"/><property name="maxActive" value="${druid.maxActive}"/><!-- 配置获取连接等待超时的时间 --><property name="maxWait" value="${druid.maxWait}"/><!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 --><property name="timeBetweenEvictionRunsMillis" value="${druid.timeBetweenEvictionRunsMillis}" /><!-- 配置一个连接在池中最小生存的时间,单位是毫秒 --><property name="minEvictableIdleTimeMillis" value="${druid.minEvictableIdleTimeMillis}" /><property name="validationQuery" value="${druid.validationQuery}" /><property name="testWhileIdle" value="${druid.testWhileIdle}" /><property name="testOnBorrow" value="${druid.testOnBorrow}" /><property name="testOnReturn" value="${druid.testOnReturn}" /><!-- 打开PSCache,并且指定每个连接上PSCache的大小 如果用Oracle,则把poolPreparedStatements配置为true,mysql可以配置为false。--><property name="poolPreparedStatements" value="${druid.poolPreparedStatements}" /><property name="maxPoolPreparedStatementPerConnectionSize" value="${druid.maxPoolPreparedStatementPerConnectionSize}" /></bean>
参考cas-server-4.1.3中QueryAndEncodeDatabaseAuthenticationHandler类进行编写,修改authenticateUsernamePasswordInternal()和digestEncodedPassword()。
/** Licensed to Jasig under one or more contributor license* agreements. See the NOTICE file distributed with this work* for additional information regarding copyright ownership.* Jasig licenses this file to you under the Apache License,* Version 2.0 (the "License"); you may not use this file* except in compliance with the License. You may obtain a* copy of the License at the following location:** http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing,* software distributed under the License is distributed on an* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY* KIND, either express or implied. See the License for the* specific language governing permissions and limitations* under the License.*/package org.jasig.cas.adaptors.jdbc;import java.security.GeneralSecurityException;import java.util.Date;import java.util.Map;import javax.security.auth.login.AccountNotFoundException;import javax.security.auth.login.FailedLoginException;import javax.sql.DataSource;import javax.validation.constraints.NotNull;import org.apache.shiro.crypto.hash.ConfigurableHashService;import org.apache.shiro.crypto.hash.DefaultHashService;import org.apache.shiro.crypto.hash.HashRequest;import org.jasig.cas.authentication.AccountDisabledException;import org.jasig.cas.authentication.HandlerResult;import org.jasig.cas.authentication.PreventedException;import org.jasig.cas.authentication.UsernamePasswordCredential;import org.jasig.cas.authentication.principal.SimplePrincipal;import org.springframework.dao.DataAccessException;import org.springframework.dao.IncorrectResultSizeDataAccessException;/*** Class that if provided a query that returns a password (parameter of query* must be username) will compare that password to a translated version of the* password provided by the user. If they match, then authentication succeeds.* Default password translator is plaintext translator.** @author Scott Battaglia* @author Dmitriy Kopylenko* @author Marvin S. Addison** @since 3.0*/public class QueryAndSaltDatabaseAuthenticationHandler extends AbstractJdbcUsernamePasswordAuthenticationHandler {private static final String DEFAULT_PASSWORD_FIELD = "password";private static final String DEFAULT_SALT_FIELD = "salt";private static final long DEFAULT_ITERATIONS = 1024;/*** The Algorithm name.*/@NotNullprotected final String algorithmName;/*** The Sql statement to execute.*/@NotNullprotected final String sql;/*** The Sql statement to execute.*/@NotNullprotected final String updateSql;/*** The Password field name.*/@NotNullprotected String passwordFieldName = DEFAULT_PASSWORD_FIELD;/*** The Salt field name.*/@NotNullprotected String saltFieldName = DEFAULT_SALT_FIELD;/*** The number of iterations. Defaults to 0.*/protected Long numberOfIterations = DEFAULT_ITERATIONS;public QueryAndSaltDatabaseAuthenticationHandler(final DataSource dataSource,final String sql,final String updateSql,final String algorithmName) {super();setDataSource(dataSource);this.sql = sql;this.updateSql = updateSql;this.algorithmName = algorithmName;}/** {@inheritDoc} */@Overrideprotected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential)throws GeneralSecurityException, PreventedException {final String username = credential.getUsername();final String encodedPsw = this.getPasswordEncoder().encode(credential.getPassword());try {final Map<String, Object> values = getJdbcTemplate().queryForMap(this.sql, username);//检查用户状态if(!values.containsKey("status")){throw new AccountDisabledException();}final String status = values.get("status").toString();if ("00".equals(status)) {//禁用throw new AccountDisabledException();}else if("02".equals(status)){//删除throw new AccountNotFoundException();}//效验密码final String digestedPassword = digestEncodedPassword(encodedPsw, values);if (!values.get(this.passwordFieldName).equals(digestedPassword)) {throw new FailedLoginException("Password does not match value on record.");}int flag = getJdbcTemplate().update(updateSql, new Date(), username);logger.debug("更新{}最后登录时间:{}", username, flag);} 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, new SimplePrincipal(username), null);}/*** Digest encoded password.** @param encodedPassword the encoded password* @param values the values retrieved from database* @return the digested password*/protected String digestEncodedPassword(final String encodedPassword, final Map<String, Object> values) {final ConfigurableHashService hashService = new DefaultHashService();//配置hashService.setHashAlgorithmName(this.algorithmName);hashService.setHashIterations(numberOfIterations.intValue());if (!values.containsKey(this.saltFieldName)) {throw new RuntimeException("Specified field name for salt does not exist in the results");}final String dynaSalt = values.get(this.saltFieldName).toString();final HashRequest request = new HashRequest.Builder().setSalt(dynaSalt).setSource(encodedPassword).build();return hashService.computeHash(request).toHex();}/*** Sets password field name. Default is {@link #DEFAULT_PASSWORD_FIELD}.** @param passwordFieldName the password field name*/public final void setPasswordFieldName(final String passwordFieldName) {this.passwordFieldName = passwordFieldName;}/*** Sets salt field name. Default is {@link #DEFAULT_SALT_FIELD}.** @param saltFieldName the password field name*/public final void setSaltFieldName(final String saltFieldName) {this.saltFieldName = saltFieldName;}/*** Sets number of iterations. Default is 0.** @param numberOfIterations the number of iterations*/public final void setNumberOfIterations(final Long numberOfIterations) {this.numberOfIterations = numberOfIterations;}}
在propertyFileConfigurer.xml修改property-placeholder节点。
<context:property-placeholder location="/WEB-INF/*.properties"/>
在WEB-INF目录下新增jdbc.properties
jdbc.driver=com.mysql.jdbc.Driverjdbc.url=jdbc:mysql://192.168.5.127:3306/ucenter?useUnicode=true&characterEncoding=utf-8jdbc.username=rootjdbc.password=XlYr6hqtsD3YulwHugnpy/FgYywQT2lzuekedA8TtP8kj62MHqa6txQIMIygqD5DVjTv+q5i7V6+yCfqfrE6LA==druid.filters=configdruid.connectionProperties=config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKRTQGQ4hl08ZckgQKLLvt3oEFfwrl3Jdt23j4Qc9ZMVooK7sgZ/c7GK4fEXI/vTXFVPjV/utcvPbKpF2LhTJCcCAwEAAQ==#druid connection pool settingsdruid.initialSize=10druid.minIdle=10druid.maxActive=50druid.maxWait=60000druid.timeBetweenEvictionRunsMillis=60000druid.minEvictableIdleTimeMillis=300000druid.validationQuery=SELECT 'x'druid.testWhileIdle=truedruid.testOnBorrow=falsedruid.testOnReturn=falsedruid.poolPreparedStatements=truedruid.maxPoolPreparedStatementPerConnectionSize=20jdbc.selectSQL=SELECT `password`,`salt`,`status` FROM ucs_user WHERE username = ?algorithmName=SHA-1numberOfIterations=1024passwordFieldName=passwordsaltFieldName=salt