支持使用LDAPS的认证插件。
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

407 lines
16 KiB

package com.fr.plugin.decision.passport.ldaps;
import com.fr.config.Identifier;
import com.fr.config.holder.Conf;
import com.fr.config.holder.factory.Holders;
import com.fr.decision.authority.data.User;
import com.fr.decision.authorize.Passport;
import com.fr.decision.authorize.impl.AbstractPassport;
import com.fr.decision.authorize.impl.DefaultPassport;
import com.fr.decision.privilege.encrpt.PasswordValidator;
import com.fr.decision.webservice.utils.UserSourceFactory;
import com.fr.decision.webservice.v10.user.UserService;
import com.fr.general.ComparatorUtils;
import com.fr.io.utils.ResourceIOUtils;
import com.fr.log.FineLoggerFactory;
import com.fr.security.SecurityToolbox;
import com.fr.stable.AssistUtils;
import com.fr.stable.StringUtils;
import com.fr.third.javax.annotation.Nullable;
import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.Properties;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class LdapsPassport extends AbstractPassport {
private static final long serialVersionUID = -1848712022102261769L;
private static final String AUTH_SIMPLE = "simple";
private static final String REFERRAL_FOLLOW = "follow";
private static final String SUN_DEFAULT_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
private Control[] connCtls = null;
private ThreadLocal<String> threadLocal = new ThreadLocal<String>();
/**
* 缓存上次匹配的结果字段
*/
private String lastTimeMatchWord = StringUtils.EMPTY;
@Identifier("ldapsUrl")
private Conf<String> ldapUrl = Holders.simple(StringUtils.EMPTY);
@Identifier("ldapsSearchBase")
private Conf<String> ldapSearchBase = Holders.simple(StringUtils.EMPTY);
//是否使用检索位置作为baseDN,是的话就加到url中,否则加到search的参数中
@Identifier("ldapsRetrieveLocAsBaseDN")
private Conf<Boolean> retrieveLocAsBaseDN = Holders.simple(true);
//认证方式
@Identifier("ldapsAuthentication")
private Conf<String> authentication = Holders.simple(AUTH_SIMPLE);// none,simple,strong
//context初始化的Factory,一般用这个默认值
@Identifier("ldapsContextFactory")
private Conf<String> contextFactory = Holders.simple(SUN_DEFAULT_CONTEXT_FACTORY);
//指定将如何处理服务提供者遇到的引用的环境属性名称
@Identifier("ldapsReferral")
private Conf<String> referral = Holders.simple(REFERRAL_FOLLOW);// follow, ignore, throw
//ldap用户后缀,如@finereport.com
@Identifier("ldapsPprincipalSuffix")
private Conf<String> principalSuffix = Holders.simple(StringUtils.EMPTY);
//ldap系统的管理员姓名
@Identifier("ldapsSystemName")
private Conf<String> ldapSystemName = Holders.simple(StringUtils.EMPTY);
//ldap系统的管理员密码
@Identifier("ldapsSystemPassword")
private Conf<String> ldapSystemPassword = Holders.simple(StringUtils.EMPTY);
//ldaps证书路径
@Identifier("ldapsCertPath")
private Conf<String> certPath = Holders.simple(StringUtils.EMPTY);
public LdapsPassport() {
}
public String getLdapUrl() {
return ldapUrl.get();
}
public void setLdapUrl(String ldapUrl) {
this.ldapUrl.set(ldapUrl);
}
public String getLdapSearchBase() {
return ldapSearchBase.get();
}
public void setLdapSearchBase(String ldapSearchBase) {
this.ldapSearchBase.set(ldapSearchBase);
}
public String getAuthentication() {
return authentication.get();
}
public void setAuthentication(String authentication) {
this.authentication.set(authentication);
}
public String getContextFactory() {
return contextFactory.get();
}
public void setContextFactory(String contextFactory) {
this.contextFactory.set(contextFactory);
}
public String getReferral() {
return referral.get();
}
public void setReferral(String referral) {
this.referral.set(referral);
}
public String getPrincipalSuffix() {
return principalSuffix.get();
}
public void setPrincipalSuffix(String principalSuffix) {
this.principalSuffix.set(principalSuffix);
}
public String getLdapSystemName() {
return ldapSystemName.get();
}
public void setLdapSystemName(String ldapSystemName) {
this.ldapSystemName.set(ldapSystemName);
}
public String getLdapSystemPassword() {
return StringUtils.isEmpty(ldapSystemPassword.get()) ? null : SecurityToolbox.decrypt(ldapSystemPassword.get());
}
public void setLdapSystemPassword(String ldapSystemPassword) {
this.ldapSystemPassword.set(SecurityToolbox.encrypt(ldapSystemPassword));
}
public boolean isRetrieveLocAsBaseDN() {
return retrieveLocAsBaseDN.get();
}
public void setRetrieveLocAsBaseDN(boolean retrieveLocAsBaseDN) {
this.retrieveLocAsBaseDN.set(retrieveLocAsBaseDN);
}
public String getCertPath() {
return certPath.get();
}
public void setCertPath(String certPath) {
this.certPath.set(certPath);
}
@Override
public String markType() {
return LdapsPassportProvider.PASSPORT_TYPE;
}
@Override
public Object clone() throws CloneNotSupportedException {
LdapsPassport cloned = (LdapsPassport) super.clone();
return cloned;
}
@Override
public int hashCode() {
return AssistUtils.hashCode(ldapUrl.get(), ldapSearchBase.get(), ldapSystemName.get(), ldapSystemPassword.get(),
referral.get(), retrieveLocAsBaseDN.get(), authentication.get(), contextFactory.get(), certPath.get());
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof LdapsPassport)) {
return false;
}
LdapsPassport target = (LdapsPassport) obj;
return ComparatorUtils.equals(target.getPrincipalSuffix(), this.getPrincipalSuffix())
&& ComparatorUtils.equals(target.getReferral(), this.getReferral())
&& ComparatorUtils.equals(target.getLdapUrl(), this.getLdapUrl())
&& ComparatorUtils.equals(target.getContextFactory(), this.getContextFactory())
&& ComparatorUtils.equals(target.getAuthentication(), this.getAuthentication())
&& ComparatorUtils.equals(target.getLdapSystemName(), this.getLdapSystemName())
&& ComparatorUtils.equals(target.getLdapSearchBase(), this.getLdapSearchBase())
&& ComparatorUtils.equals(target.getLdapSystemPassword(), this.getLdapSystemPassword())
&& ComparatorUtils.equals(target.getCertPath(), this.getCertPath());
}
@Override
public boolean checkTicket(String username, String inputPassword, String savedPassword, String hashPassword) {
if (StringUtils.isNotEmpty(principalSuffix.get())) {
username += principalSuffix.get();
}
return connect(username, inputPassword);
}
public boolean connect(final String username, final String password) {
FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new Callable<Boolean>() {
@Override
public Boolean call() {
LdapContext ctx = null;
boolean isValid;
try {
// 封装到线程中
threadLocal.set(lastTimeMatchWord);
//初次连接,无管理员账号时是匿名登录
ctx = connectLdap();
//再次连接
isValid = authenticate(ctx, username, password);
} catch (Exception e) {
return false;
} finally {
closeContext(ctx);
threadLocal.remove();
}
return isValid;
}
});
futureTask.run();
try {
return futureTask.get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
return false;
}
return false;
}
@Nullable
private LdapContext connectLdap() {
Properties props = new Properties();
props.put(Context.INITIAL_CONTEXT_FACTORY, getContextFactory());
String url = getLdapUrl();
if (StringUtils.isNotBlank(url)) {
if (!url.endsWith("/")) {
url += "/";
}
}
if (isRetrieveLocAsBaseDN()) {
//检索位置作为BaseDN,那url中加上检索位置,search中则不用检索位置作为BaseDN
url += getLdapSearchBase();
}
props.put(Context.PROVIDER_URL, url);
props.put(Context.SECURITY_AUTHENTICATION, getAuthentication());
props.put(Context.REFERRAL, getReferral());
//-----------------LDAPS--------------------
try {
InputStream certStream = new FileInputStream(certPath.get());
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Certificate certificate = certificateFactory.generateCertificate(certStream);
KeyStore ks = KeyStore.getInstance("jks");
ks.load(null, null);
ks.setCertificateEntry("LDAP_ALIAS", certificate);
if (!ResourceIOUtils.exist("/plugins/ldaps.keystore")) {
ResourceIOUtils.createFile("/plugins/ldaps.keystore");
File keyStoreFile = new File(ResourceIOUtils.getRealPath("/plugins/ldaps.keystore"));
FileOutputStream fos = new FileOutputStream(keyStoreFile);
ks.store(fos, "LdapsKeyStorePwd".toCharArray());
}
} catch (Exception e) {
FineLoggerFactory.getLogger().error("LDAPS: Exception during reading key file and writing keyStore" + e.toString());
}
System.setProperty("javax.net.ssl.trustStore", ResourceIOUtils.getRealPath("/plugins/ldaps.keystore"));
System.setProperty("javax.net.ssl.keyStorePassword", "LdapsKeyStorePwd");
//java 1.8_181的安全性改动,开启了对于LDAP的端点标识,导致证书里的CN和ldaps服务器域名对不上就抛错
System.setProperty("com.sun.jndi.ldap.object.disableEndpointIdentification", "true");
//-----------------LDAPS--------------------
//有管理员账号时,是显示连接
if (StringUtils.isNotBlank(getLdapSystemName())
&& StringUtils.isNotBlank(getLdapSystemPassword())) {
props.put(Context.SECURITY_PRINCIPAL, getLdapSystemName());
props.put(Context.SECURITY_CREDENTIALS, getLdapSystemPassword());
}
try {
return new InitialLdapContext(props, connCtls);
} catch (javax.naming.AuthenticationException e) {
FineLoggerFactory.getLogger().error("LDAPS: AuthenticationException,Authentication failed: " + e.toString());
} catch (Exception e) {
FineLoggerFactory.getLogger().error("LDAPS: Exception,Something wrong while authenticating: " + e.toString());
}
return null;
}
private boolean authenticate(LdapContext ctx, String username, String password) {
boolean isValid = true;
String userDN = recurseGetUserDN(ctx, username);
if (StringUtils.isEmpty(userDN)) {
//未正确获取到DN,代表域上没有这个用户而平台有,客户需求:转而用平台登录
FineLoggerFactory.getLogger().debug("LDAPS: No such user on ldap server, authenticating with platform passport");
try {
User user = UserService.getInstance().getUserByUserName(username);
if (user == null) {
return false;
}
PasswordValidator passwordValidator = UserSourceFactory.getInstance().getUserSource(user).getPasswordValidator();
String hashPassword = passwordValidator.encode(user.getUserName(), password);
Passport passport = new DefaultPassport();
return passport.checkTicket(user.getUserName(), password, user.getPassword(), hashPassword);
} catch (Exception e) {
FineLoggerFactory.getLogger().error("LDAPS: Exception during logging with platform default passport");
return false;
}
}
try {
ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDN);
ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
ctx.reconnect(connCtls);
} catch (AuthenticationException e) {
FineLoggerFactory.getLogger().error(e.getMessage(), e);
isValid = false;
} catch (NamingException e) {
FineLoggerFactory.getLogger().error(e.getMessage(), e);
isValid = false;
}
return isValid;
}
private String recurseGetUserDN(LdapContext ctx, String name) {
String lastTimeMatchWordShadow = threadLocal.get();
// 前一次匹配成功的字段作为优先匹配的字段
if (StringUtils.isNotEmpty(lastTimeMatchWordShadow)) {
FineLoggerFactory.getLogger().debug("LDAPS: Using cached word " + lastTimeMatchWordShadow);
String userDN = getUserDN(ctx, name, lastTimeMatchWordShadow);
if (StringUtils.isNotBlank(userDN)) {
return userDN;
}
}
String[] fWords = {"sAMAccountName", "cn", "userPrincipalName", "uid", "displayName", "name", "sn",};
for (String fWord : fWords) {
if (StringUtils.isNotEmpty(lastTimeMatchWordShadow) && ComparatorUtils.equals(lastTimeMatchWordShadow, fWord)) {
continue;
}
String userDN = getUserDN(ctx, name, fWord);
if (StringUtils.isNotBlank(userDN)) {
// 缓存匹配字段
FineLoggerFactory.getLogger().debug("LDAPS: Cached word updated to " + fWord);
lastTimeMatchWord = fWord;
return userDN;
}
}
return StringUtils.EMPTY;
}
private String getUserDN(LdapContext ctx, String name, String word) {
String userDN = StringUtils.EMPTY;
try {
SearchControls constraints = new SearchControls();
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
String filter = "(&(" + word + "=" + name + "))";
String baseDN = StringUtils.EMPTY;
if (!isRetrieveLocAsBaseDN()) {
//检索位置不作为BaseDN,那url中不加上检索位置,search中则用检索位置作为BaseDN
baseDN = getLdapSearchBase();
}
NamingEnumeration en = ctx.search(baseDN, filter, constraints);
while (en != null && en.hasMoreElements()) {
if (en.hasMoreElements()) {
SearchResult sr = (SearchResult) en.nextElement();
userDN = sr.getNameInNamespace();
if (StringUtils.isNotBlank(userDN) && userDN.contains(name)) {
break;
}
}
}
} catch (Exception e) {
FineLoggerFactory.getLogger().error("LDAPS: error during getUserDN ", e);
}
FineLoggerFactory.getLogger().debug("LDAPS: Using word " + word + " name "+ name +" and found userDN " + userDN);
return userDN;
}
private void closeContext(LdapContext ctx) {
try {
if (ctx != null) {
ctx.close();
}
} catch (NamingException e) {
FineLoggerFactory.getLogger().error(e.getMessage(), e);
}
}
}