在之前的认证中,都是以CAS默认的用户名/密码 :casuser/Mellon 来登录,这样肯定是不能满足要求的,用户的信息一般都是会存在数据库中。所以,现在就开始探索一下CAS的JDBC认证方式。关于CAS的JDBC的认证方式,具体信息可以去 CAS官网上查看,依照官网上给出的几种handler及其配置进行选择,我这里选择是QueryAndEncodeDatabaseAuthenticationHandler,该handler可以进行密码的加密以及加盐,更加的安全。
根据官网的文档,这里先需要添加一个JDBC依赖,以及你需要连接的数据库的依赖,我这里是MYSQL。
org.jasig.cas
cas-server-support-jdbc
${cas.version}
mysql
mysql-connector-java
5.1.30
runtime
然后还需要将一个名为 dataSource的bean添加到Spring中,以及需要将认证处理器修改为指定的的 QueryAndEncodeDatabaseAuthenticationHandler 。这个配置是在 deployerConfigContext.xml 中。在该文件中可以看到这样一行配置
##
# Accepted Users Authentication
#
#accept.authn.users=casuser::Mellon
首先,将 deployerConfigContext.xml 拷贝到 工程中 WEB-INF 文件夹下 , 然后将 dataSource 的bean 添加进去 ,以及切换 handler。
接着按照官方文档将dataSource的这些属性都添加到 cas.properties 中。对应的 url,user,password都需配置成自己对应的。
# == Basic database connection pool configuration ==
database.driverClass=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf8
database.user=root
database.password=123456
database.pool.minSize=6
database.pool.maxSize=18
# Maximum amount of time to wait in ms for a connection to become
# available when the pool is exhausted
database.pool.maxWait=10000
# Amount of time in seconds after which idle connections
# in excess of minimum size are pruned.
database.pool.maxIdleTime=120
# Number of connections to obtain on pool exhaustion condition.
# The maximum pool size is always respected when acquiring
# new connections.
database.pool.acquireIncrement=6
# == Connection testing settings ==
# Period in s at which a health query will be issued on idle
# connections to determine connection liveliness.
database.pool.idleConnectionTestPeriod=30
# Query executed periodically to test health
database.pool.connectionHealthQuery=select 1
# == Database recovery settings ==
# Number of times to retry acquiring a _new_ connection
# when an error is encountered during acquisition.
database.pool.acquireRetryAttempts=5
# Amount of time in ms to wait between successive aquire retry attempts.
database.pool.acquireRetryDelay=2000
这样 , dataSource就配置好了,接下来需要配置对应的 QueryAndEncodeDatabaseAuthenticationHandler 的属性。首先看一下配置。
##
# JDBC Authentication
#
#cas.jdbc.authn.query.encode.sql=
#cas.jdbc.authn.query.encode.alg=
#cas.jdbc.authn.query.encode.salt.static=
#cas.jdbc.authn.query.encode.password=
#cas.jdbc.authn.query.encode.salt=
#cas.jdbc.authn.query.encode.iterations.field=
#cas.jdbc.authn.query.encode.iterations=
cas.jdbc.authn.query.encode.sql | 具体执行的sql语句 | 必填 |
cas.jdbc.authn.query.encode.alg | 加密的算法 | 可选,默认SHA-256 |
cas.jdbc.authn.query.encode.salt.static | 私盐 | 可选 |
cas.jdbc.authn.query.encode.password | 表中哪一个字段是密码 | 可选,默认passowrd |
cas.jdbc.authn.query.encode.salt | 表中哪一个字段是盐 | 必填 |
cas.jdbc.authn.query.encode.iterations.field | 表中哪一个字段是加密次数 | 可选 |
cas.jdbc.authn.query.encode.iterations | 需要加密的次数 | 可选,默认0 |
这里我需要提一下,在sql中需要将 盐的字段带出来,比如我使用的是用户名作为盐,所以sql中将 use_name 字段也带了出来。
##
# JDBC Authentication
#
cas.jdbc.authn.query.encode.sql=select password,user_name from sys_user where user_name = ?
cas.jdbc.authn.query.encode.alg=SHA-256
# cas.jdbc.authn.query.encode.salt.static=
cas.jdbc.authn.query.encode.password=password
cas.jdbc.authn.query.encode.salt=user_name
# cas.jdbc.authn.query.encode.iterations.field=
cas.jdbc.authn.query.encode.iterations=1
配置完成之后,需要将用户名和密码都存到数据中进行验证,所以可以先看一下 QueryAndEncodeDatabaseAuthenticationHandler 是怎么样将页面上输入的密码进行加密的。打开这个类看一下具体的实现,会发现这个方法。
@Override
protected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential transformedCredential)
throws GeneralSecurityException, PreventedException {
if (StringUtils.isBlank(this.sql) || StringUtils.isBlank(this.algorithmName) || getJdbcTemplate() == null) {
throw new GeneralSecurityException("Authentication handler is not configured correctly");
}
final String username = getPrincipalNameTransformer().transform(transformedCredential.getUsername());
final String encodedPsw = this.getPasswordEncoder().encode(transformedCredential.getPassword());
try {
final Map values = getJdbcTemplate().queryForMap(this.sql, username);
final String digestedPassword = digestEncodedPassword(encodedPsw, values);
if (!values.get(this.passwordFieldName).equals(digestedPassword)) {
throw new FailedLoginException("Password does not match value on record.");
}
return createHandlerResult(transformedCredential,
this.principalFactory.createPrincipal(username), null);
} 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);
}
}
可以看到 username 和 encodedPsw 就是 输入的用户名和密码 , 那么既然是 encoedPsw 就说明是经过修改了的密码,他是由 一个PasswordEncoder对象调用其encode方法得来的 ,PasswordEncoder是一个接口,有两个实现类 DefaultPasswordEncoder 和 PlainTextPasswordEncoder ,但是其实默认使用的是 PlainTextEncoder,顾名思义就是明文密码。然后通过执行sql获取一个map,将明文密码和map传入 digestEncodedPassword方法
/**
* 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();
if (StringUtils.isNotBlank(this.staticSalt)) {
hashService.setPrivateSalt(ByteSource.Util.bytes(this.staticSalt));
}
hashService.setHashAlgorithmName(this.algorithmName);
Long numOfIterations = this.numberOfIterations;
if (values.containsKey(this.numberOfIterationsFieldName)) {
final String longAsStr = values.get(this.numberOfIterationsFieldName).toString();
numOfIterations = Long.valueOf(longAsStr);
}
hashService.setHashIterations(numOfIterations.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();
}
从这里也可以看到,如果我们的sql中没有将盐查出来,是会抛出一个 运行时异常的,自然认证就是无法通过的了。但是这里主要关注的就是如何加密,很明显就是获取一个 HashRequest 对象,然后将该对象传给 DefaultHashService 的 computeHash方法,之后再调用 toHex 方法就得到了加密后的方法。HashRequest 和 DefaultHashService这两个对象都位于org.apache.shiro.crypto.hash 包下,CAS4.2.7版本使用的是 shiro 1.2.6版本 ,所以我们可以新建一个简单的java工程然后将 shiro-core-1.2.6 的 jar扔到 classpath下就可以使用了。
public static void main(String[] args) {
DefaultHashService hashService = new DefaultHashService();
hashService.setHashAlgorithmName("SHA-256");
hashService.setHashIterations(1);
String dynaSalt = "tadmin";
final HashRequest request = new HashRequest.Builder()
.setSalt(dynaSalt)
.setSource("admin")
.build();
System.out.println("salted pass : "+hashService.computeHash(request).toHex());
}
按照 我在 cas.properties 配置文件中的设置一样,采用SHA-256作为算法,迭代1次,盐是 tadmin,就是我的用户名,admin 是明文密码,将加密出来的密码同用户名 tadmin 一起 insert到 用户表中,重启CAS服务端。使用 tadmin / admin,作为用户名/密码登录 , 登录成功。