fix: LDAP支持邮箱登录

This commit is contained in:
shiziyuan9527 2020-07-22 11:35:19 +08:00
parent 5d2844c4d3
commit ffd4ae427d
13 changed files with 256 additions and 220 deletions

View File

@ -7,12 +7,12 @@ import io.metersphere.commons.exception.MSException;
import io.metersphere.controller.ResultHolder;
import io.metersphere.controller.request.LoginRequest;
import io.metersphere.i18n.Translator;
import io.metersphere.ldap.domain.Person;
import io.metersphere.ldap.service.LdapService;
import io.metersphere.service.SystemParameterService;
import io.metersphere.service.UserService;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@ -36,29 +36,37 @@ public class LdapController {
MSException.throwException(Translator.get("ldap_authentication_not_enabled"));
}
Person person = ldapService.authenticate(request);
DirContextOperations dirContext = ldapService.authenticate(request);
String email = ldapService.getMappingAttr("email", dirContext);
String userId = ldapService.getMappingAttr("username", dirContext);
SecurityUtils.getSubject().getSession().setAttribute("authenticate", UserSource.LDAP.name());
SecurityUtils.getSubject().getSession().setAttribute("email", email);
String username = request.getUsername();
String email = person.getEmail();
if (StringUtils.isBlank(email)) {
MSException.throwException(Translator.get("login_fail_email_null"));
}
User u = userService.selectUser(request.getUsername());
// userId email 有一个相同即为存在本地用户
User u = userService.selectUser(userId, email);
if (u == null) {
// 新建用户 获取LDAP映射属性
String name = ldapService.getMappingAttr("name", dirContext);
User user = new User();
user.setId(username);
user.setName(username);
user.setId(userId);
user.setName(name);
user.setEmail(email);
user.setSource(UserSource.LDAP.name());
userService.addLdapUser(user);
}
return userService.login(request);
// 执行 ShiroDBRealm LDAP 登录逻辑
LoginRequest loginRequest = new LoginRequest();
loginRequest.setUsername(userId);
return userService.login(loginRequest);
}
@PostMapping("/test/connect")

View File

@ -1,9 +0,0 @@
package io.metersphere.ldap.dao;
import io.metersphere.ldap.domain.Person;
public interface PersonRepo {
Person getDnForUser(String name);
}

View File

@ -1,177 +0,0 @@
package io.metersphere.ldap.dao;
import io.metersphere.commons.constants.ParamConstants;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.EncryptUtils;
import io.metersphere.i18n.Translator;
import io.metersphere.ldap.domain.Person;
import io.metersphere.service.SystemParameterService;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.realm.ldap.LdapUtils;
import org.springframework.ldap.AuthenticationException;
import org.springframework.ldap.InvalidNameException;
import org.springframework.ldap.InvalidSearchFilterException;
import org.springframework.ldap.NameNotFoundException;
import org.springframework.ldap.core.*;
import org.springframework.ldap.core.support.AbstractContextMapper;
import org.springframework.ldap.core.support.DefaultDirObjectFactory;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.query.SearchScope;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.naming.directory.DirContext;
import javax.naming.ldap.LdapContext;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
@Service
public class PersonRepoImpl implements PersonRepo {
@Resource
private SystemParameterService service;
public boolean authenticate(String dn, String credentials) {
LdapTemplate ldapTemplate = getConnection();
return authenticate(dn, credentials, ldapTemplate);
}
private boolean authenticate(String dn, String credentials, LdapTemplate ldapTemplate) throws AuthenticationException {
DirContext ctx = null;
try {
ctx = ldapTemplate.getContextSource().getContext(dn, credentials);
return true;
} finally {
// It is imperative that the created DirContext instance is always closed
LdapUtils.closeContext((LdapContext) ctx);
}
}
@Override
public Person getDnForUser(String username) {
LdapTemplate ldapTemplate = getConnection();
String filter = getUserFilter();
String[] arr = getUserOu();
List<Person> result = null;
for (String ou : arr) {
try {
result = ldapTemplate.search(query().base(ou.trim()).filter(filter, username), getContextMapper());
if (result.size() == 1) {
return result.get(0);
}
} catch (NameNotFoundException e) {
MSException.throwException(Translator.get("login_fail_ou_error"));
} catch (InvalidNameException e) {
MSException.throwException(Translator.get("login_fail_ou_error"));
} catch (InvalidSearchFilterException e) {
MSException.throwException(Translator.get("login_fail_filter_error"));
}
}
if (result.size() != 1) {
MSException.throwException(Translator.get("user_not_found_or_not_unique"));
}
return result.get(0);
}
private String getUserFilter() {
String filter = service.getValue(ParamConstants.LDAP.FILTER.getValue());
if (StringUtils.isBlank(filter)) {
MSException.throwException(Translator.get("ldap_user_filter_is_null"));
}
return filter;
}
private String[] getUserOu() {
String ou = service.getValue(ParamConstants.LDAP.OU.getValue());
if (StringUtils.isBlank(ou)) {
MSException.throwException(Translator.get("ldap_ou_is_null"));
}
String[] arr = ou.split("\\|");
return arr;
}
protected ContextMapper getContextMapper() {
return new PersonContextMapper();
}
private static class PersonContextMapper extends AbstractContextMapper<Person> {
@Override
public Person doMapFromContext(DirContextOperations context) {
Person person = new Person();
person.setDn(context.getNameInNamespace());
person.setUid(context.getStringAttribute("uid"));
person.setCommonName(context.getStringAttribute("cn"));
person.setSurName(context.getStringAttribute("sn"));
person.setUsername(context.getStringAttribute("sAMAccountName"));
person.setEmail(context.getStringAttribute("mail"));
return person;
}
}
public LdapTemplate getConnection() {
String url = service.getValue(ParamConstants.LDAP.URL.getValue());
String dn = service.getValue(ParamConstants.LDAP.DN.getValue());
String password = service.getValue(ParamConstants.LDAP.PASSWORD.getValue());
preConnect(url, dn, password);
String credentials = EncryptUtils.aesDecrypt(password).toString();
LdapContextSource sourceLdapCtx = new LdapContextSource();
sourceLdapCtx.setUrl(url);
sourceLdapCtx.setUserDn(dn);
sourceLdapCtx.setPassword(credentials);
sourceLdapCtx.setDirObjectFactory(DefaultDirObjectFactory.class);
sourceLdapCtx.afterPropertiesSet();
LdapTemplate ldapTemplate = new LdapTemplate(sourceLdapCtx);
ldapTemplate.setIgnorePartialResultException(true);
Map<String, Object> baseEnv = new Hashtable<>();
baseEnv.put("com.sun.jndi.ldap.connect.timeout", "3000");
baseEnv.put("com.sun.jndi.ldap.read.timeout", "3000");
sourceLdapCtx.setBaseEnvironmentProperties(baseEnv);
ldapTemplate.setDefaultSearchScope(SearchScope.SUBTREE.getId());
// ldapTemplate 是否可用
try {
authenticate(dn, credentials, ldapTemplate);
} catch (AuthenticationException e) {
MSException.throwException(Translator.get("ldap_connect_fail_user"));
} catch (Exception e) {
MSException.throwException(Translator.get("ldap_connect_fail"));
}
return ldapTemplate;
}
private void preConnect(String url, String dn, String password) {
if (StringUtils.isBlank(url)) {
MSException.throwException(Translator.get("ldap_url_is_null"));
}
if (StringUtils.isBlank(dn)) {
MSException.throwException(Translator.get("ldap_dn_is_null"));
}
if (StringUtils.isBlank(password)) {
MSException.throwException(Translator.get("ldap_password_is_null"));
}
}
}

View File

@ -1,39 +1,221 @@
package io.metersphere.ldap.service;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.commons.constants.ParamConstants;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.EncryptUtils;
import io.metersphere.controller.request.LoginRequest;
import io.metersphere.i18n.Translator;
import io.metersphere.ldap.dao.PersonRepoImpl;
import io.metersphere.ldap.domain.Person;
import io.metersphere.service.SystemParameterService;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.realm.ldap.LdapUtils;
import org.springframework.ldap.AuthenticationException;
import org.springframework.ldap.InvalidNameException;
import org.springframework.ldap.InvalidSearchFilterException;
import org.springframework.ldap.NameNotFoundException;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.AbstractContextMapper;
import org.springframework.ldap.core.support.DefaultDirObjectFactory;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.query.SearchScope;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.naming.directory.DirContext;
import javax.naming.ldap.LdapContext;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
@Service
public class LdapService {
@Resource
private PersonRepoImpl personRepo;
private SystemParameterService service;
public Person authenticate(LoginRequest request) {
public DirContextOperations authenticate(LoginRequest request) {
String username = request.getUsername();
String credentials = request.getPassword();
Person person = null;
DirContextOperations dirContextOperations = null;
try {
person = personRepo.getDnForUser(username);
personRepo.authenticate(person.getDn(), credentials);
// 获取LDAP用户相关信息
dirContextOperations = getContextMapper(username);
// 执行登录认证
authenticate(String.valueOf(dirContextOperations.getDn()), credentials);
} catch (AuthenticationException e) {
MSException.throwException(Translator.get("authentication_failed"));
}
return person;
// 检查属性是否存在
getMappingAttr("name", dirContextOperations);
getMappingAttr("email", dirContextOperations);
return dirContextOperations;
}
public void testConnect() {
personRepo.getConnection();
getConnection();
}
public boolean authenticate(String dn, String credentials) {
LdapTemplate ldapTemplate = getConnection();
return authenticate(dn, credentials, ldapTemplate);
}
private boolean authenticate(String dn, String credentials, LdapTemplate ldapTemplate) throws AuthenticationException {
DirContext ctx = null;
try {
ctx = ldapTemplate.getContextSource().getContext(dn, credentials);
return true;
} finally {
// It is imperative that the created DirContext instance is always closed
LdapUtils.closeContext((LdapContext) ctx);
}
}
public DirContextOperations getContextMapper(String username) {
LdapTemplate ldapTemplate = getConnection();
String filter = getUserFilter();
String[] arr = getUserOu();
List<DirContextOperations> result = null;
// 多OU
for (String ou : arr) {
try {
result = ldapTemplate.search(query().base(ou.trim()).filter(filter, username), new MsContextMapper());
if (result.size() == 1) {
return result.get(0);
}
} catch (NameNotFoundException e) {
MSException.throwException(Translator.get("login_fail_ou_error"));
} catch (InvalidNameException e) {
MSException.throwException(Translator.get("login_fail_ou_error"));
} catch (InvalidSearchFilterException e) {
MSException.throwException(Translator.get("login_fail_filter_error"));
}
}
if (result.size() != 1) {
MSException.throwException(Translator.get("user_not_found_or_not_unique"));
}
return result.get(0);
}
private String getUserFilter() {
String filter = service.getValue(ParamConstants.LDAP.FILTER.getValue());
if (StringUtils.isBlank(filter)) {
MSException.throwException(Translator.get("ldap_user_filter_is_null"));
}
return filter;
}
private String[] getUserOu() {
String ou = service.getValue(ParamConstants.LDAP.OU.getValue());
if (StringUtils.isBlank(ou)) {
MSException.throwException(Translator.get("ldap_ou_is_null"));
}
String[] arr = ou.split("\\|");
return arr;
}
private static class MsContextMapper extends AbstractContextMapper<DirContextOperations> {
@Override
public DirContextOperations doMapFromContext(DirContextOperations context) {
return context;
}
}
public LdapTemplate getConnection() {
String url = service.getValue(ParamConstants.LDAP.URL.getValue());
String dn = service.getValue(ParamConstants.LDAP.DN.getValue());
String password = service.getValue(ParamConstants.LDAP.PASSWORD.getValue());
preConnect(url, dn, password);
String credentials = EncryptUtils.aesDecrypt(password).toString();
LdapContextSource sourceLdapCtx = new LdapContextSource();
sourceLdapCtx.setUrl(url);
sourceLdapCtx.setUserDn(dn);
sourceLdapCtx.setPassword(credentials);
sourceLdapCtx.setDirObjectFactory(DefaultDirObjectFactory.class);
sourceLdapCtx.afterPropertiesSet();
LdapTemplate ldapTemplate = new LdapTemplate(sourceLdapCtx);
ldapTemplate.setIgnorePartialResultException(true);
Map<String, Object> baseEnv = new Hashtable<>();
baseEnv.put("com.sun.jndi.ldap.connect.timeout", "3000");
baseEnv.put("com.sun.jndi.ldap.read.timeout", "3000");
sourceLdapCtx.setBaseEnvironmentProperties(baseEnv);
ldapTemplate.setDefaultSearchScope(SearchScope.SUBTREE.getId());
// ldapTemplate 是否可用
try {
authenticate(dn, credentials, ldapTemplate);
} catch (AuthenticationException e) {
MSException.throwException(Translator.get("ldap_connect_fail_user"));
} catch (Exception e) {
MSException.throwException(Translator.get("ldap_connect_fail"));
}
return ldapTemplate;
}
private void preConnect(String url, String dn, String password) {
if (StringUtils.isBlank(url)) {
MSException.throwException(Translator.get("ldap_url_is_null"));
}
if (StringUtils.isBlank(dn)) {
MSException.throwException(Translator.get("ldap_dn_is_null"));
}
if (StringUtils.isBlank(password)) {
MSException.throwException(Translator.get("ldap_password_is_null"));
}
}
private String getLdapMapping() {
String mapping = service.getValue(ParamConstants.LDAP.MAPPING.getValue());
if (StringUtils.isBlank(mapping)) {
MSException.throwException(Translator.get("ldap_user_mapping_is_null"));
}
return mapping;
}
public String getMappingAttr(String attr, DirContextOperations dirContext) {
// 检查LDAP映射属性
String mapping = getLdapMapping();
JSONObject jsonObject = JSONObject.parseObject(mapping);
String mapAttr = jsonObject.getString(attr);
if (StringUtils.isBlank(mapAttr)) {
MSException.throwException(Translator.get("check_ldap_mapping") + " " + attr);
}
String result = dirContext.getStringAttribute(mapAttr);
if (StringUtils.isBlank(result)) {
MSException.throwException(Translator.get("ldap_mapping_value_null") + " " + mapAttr);
}
return result;
}
}

View File

@ -112,15 +112,19 @@ public class ShiroDBRealm extends AuthorizingRealm {
private AuthenticationInfo loginLdapMode(String userId, String password) {
//
// userId email 有一个相同就返回User
String email = (String) SecurityUtils.getSubject().getSession().getAttribute("email");
UserDTO user = userService.getLoginUser(userId, Arrays.asList(UserSource.LDAP.name(), UserSource.LOCAL.name()));
String msg;
if (user == null) {
msg = "The user does not exist: " + userId;
logger.warn(msg);
throw new UnknownAccountException(Translator.get("user_not_exist") + userId);
user = userService.getLoginUserByEmail(email, Arrays.asList(UserSource.LDAP.name(), UserSource.LOCAL.name()));
if (user == null) {
msg = "The user does not exist: " + userId;
logger.warn(msg);
throw new UnknownAccountException(Translator.get("user_not_exist") + userId);
}
userId = user.getId();
}
userId = user.getId();
SessionUser sessionUser = SessionUser.fromUser(user);
SessionUtils.putUser(sessionUser);
@ -132,7 +136,7 @@ public class ShiroDBRealm extends AuthorizingRealm {
UserDTO user = userService.getLoginUser(userId, Collections.singletonList(UserSource.LOCAL.name()));
String msg;
if (user == null) {
user = userService.getLoginUserByEmail(userId, UserSource.LOCAL.name());
user = userService.getLoginUserByEmail(userId, Collections.singletonList(UserSource.LOCAL.name()));
if (user == null) {
msg = "The user does not exist: " + userId;
logger.warn(msg);

View File

@ -78,8 +78,18 @@ public class UserService {
return getUserDTO(user.getId());
}
public User selectUser(String id) {
return userMapper.selectByPrimaryKey(id);
public User selectUser(String userId, String email) {
User user = userMapper.selectByPrimaryKey(userId);
if (user == null) {
UserExample example = new UserExample();
example.createCriteria().andEmailEqualTo(email);
List<User> users = userMapper.selectByExample(example);
if (!CollectionUtils.isEmpty(users)) {
return users.get(0);
}
}
return user;
}
private void insertUserRole(List<Map<String, Object>> roles, String userId) {
@ -199,9 +209,9 @@ public class UserService {
return getUserDTO(users.get(0).getId());
}
public UserDTO getLoginUserByEmail(String email, String source) {
public UserDTO getLoginUserByEmail(String email, List<String> list) {
UserExample example = new UserExample();
example.createCriteria().andEmailEqualTo(email).andSourceEqualTo(source);
example.createCriteria().andEmailEqualTo(email).andSourceIn(list);
List<User> users = userMapper.selectByExample(example);
if (users == null || users.size() <= 0) {
return null;

View File

@ -126,10 +126,13 @@ ldap_password_is_null=LDAP password is empty
ldap_connect_fail=Connection LDAP failed
ldap_connect_fail_user=Connection LDAP failed, wrong DN or password bound
ldap_user_filter_is_null=LDAP user filter is empty
ldap_user_mapping_is_null=LDAP user mapping is empty
authentication_failed=User authentication failed,wrong user name or password
user_not_found_or_not_unique=User does not exist or is not unique
find_more_user=Multiple users found
ldap_authentication_not_enabled=LDAP authentication is not enabled
login_fail_email_null=Login failed, user mailbox is empty
login_fail_ou_error=Login failed, please check the user OU
login_fail_filter_error=Login failed, please check the user filter
login_fail_filter_error=Login failed, please check the user filter
check_ldap_mapping=Check LDAP attribute mapping
ldap_mapping_value_null=LDAP user attribute mapping field is empty

View File

@ -126,6 +126,7 @@ ldap_password_is_null=LDAP密码为空
ldap_connect_fail=连接LDAP失败
ldap_connect_fail_user=连接LDAP失败绑定的DN或密码错误
ldap_user_filter_is_null=LDAP用户过滤器为空
ldap_user_mapping_is_null=LDAP用户属性映射为空
authentication_failed=用户认证失败,用户名或密码错误
user_not_found_or_not_unique=用户不存在或者不唯一
find_more_user=查找到多个用户
@ -133,5 +134,7 @@ ldap_authentication_not_enabled=LDAP认证未启用
login_fail_email_null=登录失败,用户邮箱为空
login_fail_ou_error=登录失败请检查用户OU
login_fail_filter_error=登录失败,请检查用户过滤器
check_ldap_mapping=检查LDAP属性映射
ldap_mapping_value_null=LDAP用户属性映射字段为空值

View File

@ -126,6 +126,7 @@ ldap_password_is_null=LDAP密碼為空
ldap_connect_fail=連接LDAP失敗
ldap_connect_fail_user=連接LDAP失敗綁定的DN或密碼錯誤
ldap_user_filter_is_null=LDAP用戶過濾器為空
ldap_user_mapping_is_null=LDAP用戶屬性映射為空
authentication_failed=用戶認證失敗,用戶名或密碼錯誤
user_not_found_or_not_unique=用戶不存在或者不唯一
find_more_user=查找到多個用戶
@ -133,3 +134,5 @@ ldap_authentication_not_enabled=LDAP認證未啟用
login_fail_email_null=登錄失敗,用戶郵箱為空
login_fail_ou_error=登錄失敗請檢查用戶OU
login_fail_filter_error=登錄失敗,請檢查用戶過濾器
check_ldap_mapping=檢查LDAP屬性映射
ldap_mapping_value_null=LDAP用戶屬性映射預設為空值

View File

@ -18,9 +18,9 @@
<el-form-item :label="$t('ldap.filter')" prop="filter">
<el-input v-model="form.filter" :placeholder="$t('ldap.input_filter_placeholder')"></el-input>
</el-form-item>
<!-- <el-form-item :label="$t('ldap.mapping')" prop="mapping">-->
<!-- <el-input v-model="form.mapping" :placeholder="$t('ldap.input_mapping')"></el-input>-->
<!-- </el-form-item>-->
<el-form-item :label="$t('ldap.mapping')" prop="mapping">
<el-input v-model="form.mapping" :placeholder="$t('ldap.input_mapping')"></el-input>
</el-form-item>
<el-form-item :label="$t('ldap.open')" prop="open">
<el-checkbox v-model="form.open"></el-checkbox>
</el-form-item>
@ -82,7 +82,8 @@
dn: {required: true, message: this.$t('ldap.input_dn'), trigger: ['change', 'blur']},
password: {required: true, message: this.$t('ldap.input_password'), trigger: ['change', 'blur']},
ou: {required: true, message: this.$t('ldap.input_ou'), trigger: ['change', 'blur']},
filter: {required: true, message: this.$t('ldap.input_filter'), trigger: ['change', 'blur']}
filter: {required: true, message: this.$t('ldap.input_filter'), trigger: ['change', 'blur']},
mapping: {required: true, message: this.$t('ldap.input_mapping'), trigger: ['change', 'blur']}
},
loginFormRules: {
username: {required: true, message: this.$t('ldap.input_username'), trigger: 'blur'},
@ -142,6 +143,11 @@
return false;
}
if (!this.form.mapping) {
this.$warning(this.$t('ldap.mapping_cannot_be_empty'));
return false;
}
this.loginForm = {};
this.loginVisible = true;
},

View File

@ -677,6 +677,7 @@ export default {
ou_cannot_be_empty: 'LDAP OU cannot be empty',
filter_cannot_be_empty: 'LDAP user filter cannot be empty',
password_cannot_be_empty: 'LDAP password cannot be empty',
mapping_cannot_be_empty: 'LDAP mapping cannot be empty',
},
schedule: {
not_set: "Not Set",

View File

@ -675,6 +675,7 @@ export default {
dn_cannot_be_empty: 'LDAP DN不能为空',
ou_cannot_be_empty: 'LDAP OU不能为空',
filter_cannot_be_empty: 'LDAP 用户过滤器不能为空',
mapping_cannot_be_empty: 'LDAP 用户属性映射不能为空',
password_cannot_be_empty: 'LDAP 密码不能为空',
},
schedule: {

View File

@ -675,6 +675,7 @@ export default {
ou_cannot_be_empty: 'LDAP OU不能為空',
filter_cannot_be_empty: 'LDAP 用戶過濾器不能為空',
password_cannot_be_empty: 'LDAP 密碼不能為空',
mapping_cannot_be_empty: 'LDAP 用戶屬性映射不能為空',
},
schedule: {
not_set: "未設置",