1、申 请 I D:HarryWan
2、个人邮箱:harrywan1993@163.com
3、原创技术文章:单点登陆CAS源码编译,添加自定义密码加解密校验(后端RSA解密,MD5+Salt加密)
环境准备:Cas Server=4.0.0
java version=1.7(官方申明cas4只支持jdk7及以下版本,高于1.7源码编译不通过)
maven=3.6
背景:企业ToB项目需要支持单点登陆,Cas自带的密码加解密校验方式不支持项目本身,需要进行源码的二次开发。
自定义加解密:前端RSA加密,到CAS Server先进行RSA解密,在对明文密码进行Md5+Salt加密,和后端DB里的密码(Md5+Salt)进行比对,从而完成SSO
文章包括:1.针对自定义加解密源码改动
2.CasServer部分源码解读
1.改动入口:cas-server-webapp下的deployerConfigContext.xml
[Asm] 纯文本查看 复制代码
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://1.1.1.1:8306/demo"></property>
<property name="username" value="xixi"></property>
<property name="password" value="haha"></property>
</bean>
<bean id="primaryAuthenticationHandler"
class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
<property name="dataSource" ref="dataSource" />
<property name="sql" value="select xx from xx where xx = ?" />
<property name="passwordEncoder" ref="Md5SaltPasswordEncoder" />
</bean>
<!-- <bean id="primaryAuthenticationHandler"
class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
<property name="users">
<map>
<entry key="casuser" value="Mellon"/>
</map>
</property>
</bean> -->
注:1.添加数据源
2.Cas不同的身份校验方式对应不同的Handler默认的校验方式为AcceptUsersAuthenticationHandler写死的用户名密码casuser/Mellon,这边注释,使用QueryDatabaseAuthenticationHandler,配置数据源,这里的Sql语句用来查询Db里对应的用户名密码进行身份判断,具体的密码加解密流程就在自定义的Md5SaltPasswordEncoder.java中
[XML] 纯文本查看 复制代码 <bean id="Md5SaltPasswordEncoder"
class="org.jasig.cas.authentication.handler.Md5SaltPasswordEncoder">
<constructor-arg index="0">
<value>MD5</value>
</constructor-arg>
<constructor-arg index="1">
<value>此处是Md5加密的盐值</value>
</constructor-arg>
<constructor-arg index="2">
<value>此处是Rsa的私钥,用于Rsa解密</value>
</constructor-arg>
<constructor-arg index="3">
<value>UTF-8</value>
</constructor-arg>
</bean>
注:这里通过构造器注入的方式,处理的业务逻辑为将前端的rsa密文解密,在进行MD5+Salt加密
[Java] 纯文本查看 复制代码 /*
* 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.authentication.handler;
import javax.validation.constraints.NotNull;
import org.apache.commons.lang.StringUtils;
import org.jasig.cas.util.MD5Utils;
import org.jasig.cas.util.RsaUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Md5SaltPasswordEncoder implements PasswordEncoder {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
@NotNull
private final String encodingAlgorithm;
@NotNull
private final String salt;
@NotNull
private final String privateKey;
@NotNull
private final String characterEncoding;
@Override
public String encode(String password) {
if (!StringUtils.isEmpty(password)) {
String rsaDecryptPwd = RsaUtils.decrypt(password, privateKey);
logger.debug("Md5SaltPasswordEncoder#encode,rsaDecryptPwd= {}", rsaDecryptPwd);
String md5SaltPwd = MD5Utils.digestWithSalt(rsaDecryptPwd, salt);
logger.debug("Md5SaltPasswordEncoder#encode,md5SaltPwd= {}", md5SaltPwd);
return md5SaltPwd;
}
return null;
}
public Md5SaltPasswordEncoder(String encodingAlgorithm, String salt, String privateKey, String characterEncoding) {
this.encodingAlgorithm = encodingAlgorithm;
this.salt = salt;
this.privateKey = privateKey;
this.characterEncoding = characterEncoding;
}
}
注:实现cas自带的PasswordEncoder,实现自定义encode方法,对前端的密码进行加解密,具体加解密代码分装在工具类中
2.部分源码解析(用户身份认证流程)
2.1 顶层接口AuthenticationHandler,通过authenticate(Credential credential)方法进行身份认证
2.2 抽象类AbstractUsernamePasswordAuthenticationHandler实现AuthenticationHandler接口,doAuthentication(final Credential credential)方法做认证前置的部分校验,
通过模板方法模式暴露authenticateUsernamePasswordInternal钩子方法,共自定义实现,不同的Handler实现对应不同的认证方式,包括Shiro,Ldap,QueryDatabase,以及默认的AcceptUserName方式,这边具体的 Handler在上面的xml中进行指定
[Java] 纯文本查看 复制代码 @Override
protected final HandlerResult doAuthentication(final Credential credential)
throws GeneralSecurityException, PreventedException {
final UsernamePasswordCredential userPass = (UsernamePasswordCredential) credential;
if (userPass.getUsername() == null) {
throw new AccountNotFoundException("Username is null.");
}
final String transformedUsername= this.principalNameTransformer.transform(userPass.getUsername());
if (transformedUsername == null) {
throw new AccountNotFoundException("Transformed username is null.");
}
userPass.setUsername(transformedUsername);
//钩子方法authenticateUsernamePasswordInternal,不同Handler实现类对应不同的认证方式,在deploy.xml配置指定
return authenticateUsernamePasswordInternal(userPass);
}
2.3,具体的认证流程在QueryDatabaseAuthenticationHandler类中,已在代码中进行了步骤注释
[Java] 纯文本查看 复制代码 @Override
protected final HandlerResult authenticateUsernamePasswordInternal(final UsernamePasswordCredential credential)
throws GeneralSecurityException, PreventedException {
final String username = credential.getUsername();
//1.获取指定的Md5SaltPasswordEncoder进行前端密码加解密
final String encryptedPassword = this.getPasswordEncoder().encode(credential.getPassword());
try {
//2.通过配置的数据源查询sql获取db中的密码
final String dbPassword = getJdbcTemplate().queryForObject(this.sql, String.class, username);
//3.这里进行密码比对,db里的密码和前端密码
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, new SimplePrincipal(username), null);
}
除此之外,在对Cas 源码进行编译时,也遇到了不少坑,比如绕过License的认证,部分jar包不存在需要手动指定仓库地址,以及如何绕过自带的code style校验!
tips:犯了低级错误,帖子没有备份保存下,发表的时候出错,内容回滚没有回滚全,又重新写了遍,后面申请的可以注意下!!!
|