因公司要求使用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>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${version.mysql}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>${version.druid}version>
dependency>
properties节点
<version.druid>1.0.16version.druid>
<version.mysql>5.1.34version.mysql>
修改cas-server-support-jdbc工程的pom文件,增加依赖:
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-coreartifactId>
dependency>
修改cas-server-webapp工程的pom文件,增加依赖:
<dependency>
<groupId>org.jasig.casgroupId>
<artifactId>cas-server-support-jdbcartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
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}" />
<property name="saltFieldName" value="${saltFieldName}" />
bean>
<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}" />
<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}" />
<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.
*/
@NotNull
protected final String algorithmName;
/**
* The Sql statement to execute.
*/
@NotNull
protected final String sql;
/**
* The Sql statement to execute.
*/
@NotNull
protected final String updateSql;
/**
* The Password field name.
*/
@NotNull
protected String passwordFieldName = DEFAULT_PASSWORD_FIELD;
/**
* The Salt field name.
*/
@NotNull
protected 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} */
@Override
protected 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 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 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.Driver
jdbc.url=jdbc:mysql://192.168.5.127:3306/ucenter?useUnicode=true&characterEncoding=utf-8
jdbc.username=root
jdbc.password=XlYr6hqtsD3YulwHugnpy/FgYywQT2lzuekedA8TtP8kj62MHqa6txQIMIygqD5DVjTv+q5i7V6+yCfqfrE6LA==
druid.filters=config
druid.connectionProperties=config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKRTQGQ4hl08ZckgQKLLvt3oEFfwrl3Jdt23j4Qc9ZMVooK7sgZ/c7GK4fEXI/vTXFVPjV/utcvPbKpF2LhTJCcCAwEAAQ==
#druid connection pool settings
druid.initialSize=10
druid.minIdle=10
druid.maxActive=50
druid.maxWait=60000
druid.timeBetweenEvictionRunsMillis=60000
druid.minEvictableIdleTimeMillis=300000
druid.validationQuery=SELECT 'x'
druid.testWhileIdle=true
druid.testOnBorrow=false
druid.testOnReturn=false
druid.poolPreparedStatements=true
druid.maxPoolPreparedStatementPerConnectionSize=20
jdbc.selectSQL=SELECT `password`,`salt`,`status` FROM ucs_user WHERE username = ?
algorithmName=SHA-1
numberOfIterations=1024
passwordFieldName=password
saltFieldName=salt