refactor: 支持LDAP登录

This commit is contained in:
caiyuchao
2025-08-22 19:07:04 +08:00
parent e422c7080b
commit 2bc448e913
6 changed files with 258 additions and 19 deletions

View File

@@ -163,6 +163,11 @@
<artifactId>hutool-extra</artifactId> <!-- 邮件 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -0,0 +1,31 @@
package org.agt.module.system.controller.admin.user.vo.user;
import lombok.Data;
import org.springframework.ldap.odm.annotations.Attribute;
import org.springframework.ldap.odm.annotations.Entry;
import org.springframework.ldap.odm.annotations.Id;
import javax.naming.Name;
@Data
@Entry(base = "ou=users", objectClasses = {"inetOrgPerson", "organizationalPerson", "person", "top"})
public class LdapUser {
@Id
private Name dn;
@Attribute(name = "cn")
private String username;
@Attribute(name = "sn")
private String surname;
@Attribute(name = "userPassword")
private String password;
@Attribute(name = "mail")
private String email;
@Attribute(name = "gecos")
private String gecos;
}

View File

@@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j;
import org.agt.framework.common.enums.CommonStatusEnum;
import org.agt.framework.common.enums.UserTypeEnum;
import org.agt.framework.common.util.monitor.TracerUtils;
import org.agt.framework.common.util.object.BeanUtils;
import org.agt.framework.common.util.servlet.ServletUtils;
import org.agt.framework.common.util.validation.ValidationUtils;
import org.agt.module.system.api.logger.dto.LoginLogCreateReqDTO;
@@ -19,7 +20,17 @@ import org.agt.module.system.api.sms.SmsCodeApi;
import org.agt.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
import org.agt.module.system.api.social.dto.SocialUserBindReqDTO;
import org.agt.module.system.api.social.dto.SocialUserRespDTO;
import org.agt.module.system.controller.admin.auth.vo.*;
import org.agt.module.system.controller.admin.auth.vo.AuthLoginReqVO;
import org.agt.module.system.controller.admin.auth.vo.AuthLoginRespVO;
import org.agt.module.system.controller.admin.auth.vo.AuthRegisterReqVO;
import org.agt.module.system.controller.admin.auth.vo.AuthResetPasswordByNameReqVO;
import org.agt.module.system.controller.admin.auth.vo.AuthResetPasswordReqVO;
import org.agt.module.system.controller.admin.auth.vo.AuthSmsLoginReqVO;
import org.agt.module.system.controller.admin.auth.vo.AuthSmsSendReqVO;
import org.agt.module.system.controller.admin.auth.vo.AuthSocialLoginReqVO;
import org.agt.module.system.controller.admin.auth.vo.CaptchaVerificationReqVO;
import org.agt.module.system.controller.admin.user.vo.user.LdapUser;
import org.agt.module.system.controller.admin.user.vo.user.UserSaveReqVO;
import org.agt.module.system.convert.auth.AuthConvert;
import org.agt.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO;
import org.agt.module.system.dal.dataobject.user.AdminUserDO;
@@ -30,17 +41,29 @@ import org.agt.module.system.enums.sms.SmsSceneEnum;
import org.agt.module.system.service.logger.LoginLogService;
import org.agt.module.system.service.member.MemberService;
import org.agt.module.system.service.oauth2.OAuth2TokenService;
import org.agt.module.system.service.permission.PermissionService;
import org.agt.module.system.service.social.SocialUserService;
import org.agt.module.system.service.user.AdminUserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.query.LdapQuery;
import org.springframework.ldap.query.LdapQueryBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Objects;
import java.util.Set;
import static org.agt.framework.common.exception.util.ServiceExceptionUtil.exception;
import static org.agt.framework.common.util.servlet.ServletUtils.getClientIP;
import static org.agt.module.system.enums.ErrorCodeConstants.*;
import static org.agt.module.system.enums.ErrorCodeConstants.AUTH_LOGIN_BAD_CREDENTIALS;
import static org.agt.module.system.enums.ErrorCodeConstants.AUTH_LOGIN_CAPTCHA_CODE_ERROR;
import static org.agt.module.system.enums.ErrorCodeConstants.AUTH_LOGIN_USER_DISABLED;
import static org.agt.module.system.enums.ErrorCodeConstants.AUTH_MOBILE_NOT_EXISTS;
import static org.agt.module.system.enums.ErrorCodeConstants.AUTH_REGISTER_CAPTCHA_CODE_ERROR;
import static org.agt.module.system.enums.ErrorCodeConstants.AUTH_THIRD_LOGIN_NOT_BIND;
import static org.agt.module.system.enums.ErrorCodeConstants.USER_MOBILE_NOT_EXISTS;
import static org.agt.module.system.enums.ErrorCodeConstants.USER_NOT_EXISTS;
/**
* Auth Service 实现类
@@ -67,6 +90,10 @@ public class AdminAuthServiceImpl implements AdminAuthService {
private CaptchaService captchaService;
@Resource
private SmsCodeApi smsCodeApi;
@Resource
private LdapTemplate ldapTemplate;
@Resource
private PermissionService permissionService;
/**
* 验证码的开关,默认为 true
@@ -96,13 +123,50 @@ public class AdminAuthServiceImpl implements AdminAuthService {
return user;
}
public AdminUserDO authenticateByLdap(String username, String password) {
final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME;
LdapUser ldapUsers;
try {
LdapQuery query = LdapQueryBuilder.query()
.where("uid").is(username).or("mail").is(username);
ldapUsers = ldapTemplate.findOne(query, LdapUser.class);
ldapTemplate.authenticate(query, password);
} catch (Exception e) {
log.error("登录失败:{}", e.getMessage());
createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
}
if (ldapUsers == null) {
createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS);
throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
}
// 校验账号是否存在
AdminUserDO user = userService.getUserByUsername(username);
if (user == null) {
UserSaveReqVO userSaveReqVO = new UserSaveReqVO();
userSaveReqVO.setUsername(username);
userSaveReqVO.setPassword(password);
userSaveReqVO.setNickname(ldapUsers.getGecos());
userSaveReqVO.setEmail(ldapUsers.getEmail());
Long userId = userService.createUser(userSaveReqVO);
permissionService.assignUserRole(userId, Set.of(3L));
user = BeanUtils.toBean(userSaveReqVO, AdminUserDO.class);
user.setId(userId);
}
return user;
}
@Override
public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
// 校验验证码
validateCaptcha(reqVO);
// 使用账号密码,进行登录
AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
// AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
AdminUserDO user = authenticateByLdap(reqVO.getUsername(), reqVO.getPassword());
// 首次登录不返回token
if (user.getLoginDate() == null) {

View File

@@ -4,6 +4,13 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.annotations.VisibleForTesting;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.agt.framework.common.enums.CommonStatusEnum;
import org.agt.framework.common.exception.ServiceException;
import org.agt.framework.common.pojo.PageResult;
@@ -15,6 +22,7 @@ import org.agt.module.infra.api.config.ConfigApi;
import org.agt.module.system.controller.admin.auth.vo.AuthRegisterReqVO;
import org.agt.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO;
import org.agt.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO;
import org.agt.module.system.controller.admin.user.vo.user.LdapUser;
import org.agt.module.system.controller.admin.user.vo.user.UserImportExcelVO;
import org.agt.module.system.controller.admin.user.vo.user.UserImportRespVO;
import org.agt.module.system.controller.admin.user.vo.user.UserPageReqVO;
@@ -31,26 +39,52 @@ import org.agt.module.system.service.dept.DeptService;
import org.agt.module.system.service.dept.PostService;
import org.agt.module.system.service.permission.PermissionService;
import org.agt.module.system.service.tenant.TenantService;
import com.google.common.annotations.VisibleForTesting;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.service.impl.DiffParseFunction;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.agt.module.system.util.ldap.SHA512CryptVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.query.LdapQuery;
import org.springframework.ldap.query.LdapQueryBuilder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import java.time.LocalDateTime;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.agt.framework.common.exception.util.ServiceExceptionUtil.exception;
import static org.agt.framework.common.util.collection.CollectionUtils.*;
import static org.agt.module.system.enums.ErrorCodeConstants.*;
import static org.agt.module.system.enums.LogRecordConstants.*;
import static org.agt.framework.common.util.collection.CollectionUtils.convertList;
import static org.agt.framework.common.util.collection.CollectionUtils.convertSet;
import static org.agt.framework.common.util.collection.CollectionUtils.singleton;
import static org.agt.module.system.enums.ErrorCodeConstants.USER_COUNT_MAX;
import static org.agt.module.system.enums.ErrorCodeConstants.USER_EMAIL_EXISTS;
import static org.agt.module.system.enums.ErrorCodeConstants.USER_IMPORT_INIT_PASSWORD;
import static org.agt.module.system.enums.ErrorCodeConstants.USER_IMPORT_LIST_IS_EMPTY;
import static org.agt.module.system.enums.ErrorCodeConstants.USER_IS_DISABLE;
import static org.agt.module.system.enums.ErrorCodeConstants.USER_MOBILE_EXISTS;
import static org.agt.module.system.enums.ErrorCodeConstants.USER_NOT_EXISTS;
import static org.agt.module.system.enums.ErrorCodeConstants.USER_PASSWORD_FAILED;
import static org.agt.module.system.enums.ErrorCodeConstants.USER_REGISTER_DISABLED;
import static org.agt.module.system.enums.ErrorCodeConstants.USER_USERNAME_EXISTS;
import static org.agt.module.system.enums.LogRecordConstants.SYSTEM_USER_CREATE_SUB_TYPE;
import static org.agt.module.system.enums.LogRecordConstants.SYSTEM_USER_CREATE_SUCCESS;
import static org.agt.module.system.enums.LogRecordConstants.SYSTEM_USER_DELETE_SUB_TYPE;
import static org.agt.module.system.enums.LogRecordConstants.SYSTEM_USER_DELETE_SUCCESS;
import static org.agt.module.system.enums.LogRecordConstants.SYSTEM_USER_TYPE;
import static org.agt.module.system.enums.LogRecordConstants.SYSTEM_USER_UPDATE_PASSWORD_SUB_TYPE;
import static org.agt.module.system.enums.LogRecordConstants.SYSTEM_USER_UPDATE_PASSWORD_SUCCESS;
import static org.agt.module.system.enums.LogRecordConstants.SYSTEM_USER_UPDATE_SUB_TYPE;
import static org.agt.module.system.enums.LogRecordConstants.SYSTEM_USER_UPDATE_SUCCESS;
/**
* 后台用户 Service 实现类
@@ -87,6 +121,8 @@ public class AdminUserServiceImpl implements AdminUserService {
private ConfigApi configApi;
@Autowired
private RoleMapper roleMapper;
@Resource
private LdapTemplate ldapTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
@@ -199,11 +235,13 @@ public class AdminUserServiceImpl implements AdminUserService {
@Override
public void updateUserPassword(Long id, UserProfileUpdatePasswordReqVO reqVO) {
// 校验旧密码密码
validateOldPassword(id, reqVO.getOldPassword());
AdminUserDO user = validateOldPassword(id, reqVO.getOldPassword());
// 执行更新
AdminUserDO updateObj = new AdminUserDO().setId(id);
updateObj.setPassword(encodePassword(reqVO.getNewPassword())); // 加密密码
userMapper.updateById(updateObj);
updateLdapPassword(user.getUsername(), reqVO.getNewPassword());
}
@Override
@@ -219,6 +257,8 @@ public class AdminUserServiceImpl implements AdminUserService {
updateObj.setPassword(encodePassword(password)); // 加密密码
userMapper.updateById(updateObj);
updateLdapPassword(user.getUsername(), password);
// 3. 记录操作日志上下文
LogRecordContext.putVariable("user", user);
LogRecordContext.putVariable("newPassword", updateObj.getPassword());
@@ -235,6 +275,28 @@ public class AdminUserServiceImpl implements AdminUserService {
updateObj.setPassword(encodePassword(password)); // 加密密码
updateObj.setLoginDate(LocalDateTime.now());
userMapper.updateById(updateObj);
updateLdapPassword(user.getUsername(), password);
}
private void updateLdapPassword(String username, String password) {
try {
LdapQuery query = LdapQueryBuilder.query()
.where("uid").is(username).or("mail").is(username);
LdapUser ldapUsers = ldapTemplate.findOne(query, LdapUser.class);
password = SHA512CryptVerifier.generateHash(password, "OY7N5bGk");
// 创建修改操作
ModificationItem[] modificationItems = new ModificationItem[1];
modificationItems[0] = new ModificationItem(
DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("userPassword", password)
);
ldapTemplate.modifyAttributes(ldapUsers.getDn(), modificationItems);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw exception(USER_PASSWORD_FAILED, e.getMessage());
}
}
@Override
@@ -375,7 +437,7 @@ public class AdminUserServiceImpl implements AdminUserService {
}
private AdminUserDO validateUserForCreateOrUpdate(Long id, String username, String mobile, String email,
Long deptId, Set<Long> postIds) {
Long deptId, Set<Long> postIds) {
// 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确
return DataPermissionUtils.executeIgnore(() -> {
// 校验用户存在
@@ -462,11 +524,12 @@ public class AdminUserServiceImpl implements AdminUserService {
/**
* 校验旧密码
*
* @param id 用户 id
* @param oldPassword 旧密码
*/
@VisibleForTesting
void validateOldPassword(Long id, String oldPassword) {
AdminUserDO validateOldPassword(Long id, String oldPassword) {
AdminUserDO user = userMapper.selectById(id);
if (user == null) {
throw exception(USER_NOT_EXISTS);
@@ -474,6 +537,17 @@ public class AdminUserServiceImpl implements AdminUserService {
if (!isPasswordMatch(oldPassword, user.getPassword())) {
throw exception(USER_PASSWORD_FAILED);
}
try {
String username = user.getUsername();
LdapQuery query = LdapQueryBuilder.query()
.where("uid").is(username).or("mail").is(username);
ldapTemplate.authenticate(query, oldPassword);
} catch (Exception e) {
log.error("密码校验失败:{}", e.getMessage());
throw exception(USER_PASSWORD_FAILED);
}
return user;
}
@Override
@@ -496,7 +570,7 @@ public class AdminUserServiceImpl implements AdminUserService {
// 2.1.1 校验字段是否符合要求
try {
ValidationUtils.validate(BeanUtils.toBean(importUser, UserSaveReqVO.class).setPassword(initPassword));
} catch (ConstraintViolationException ex){
} catch (ConstraintViolationException ex) {
respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage());
return;
}

View File

@@ -0,0 +1,58 @@
package org.agt.module.system.util.ldap;
import org.apache.commons.codec.digest.Sha2Crypt;
public class SHA512CryptVerifier {
/**
* 验证密码是否匹配SHA-512 Crypt哈希
* @param password 明文密码
* @param hash 完整的哈希字符串(包括{CRYPT}前缀)
* @return 匹配返回true否则返回false
*/
public static boolean verifyPassword(String password, String hash) {
// 去除{CRYPT}前缀(如果存在)
if (hash.startsWith("{CRYPT}")) {
hash = hash.substring(7);
}
try {
// 使用Apache Commons Codec的Sha2Crypt进行验证
String computedHash = Sha2Crypt.sha512Crypt(password.getBytes(), hash);
return computedHash.equals(hash);
} catch (Exception e) {
System.err.println("验证失败: " + e.getMessage());
return false;
}
}
/**
* 生成SHA-512 Crypt哈希
* @param password 明文密码
* @param salt 盐值可选如果为null则自动生成
* @return 哈希值(包含{CRYPT}前缀)
*/
public static String generateHash(String password, String salt) {
String hash;
if (salt != null) {
hash = Sha2Crypt.sha512Crypt(password.getBytes(), "$6$" + salt);
} else {
hash = Sha2Crypt.sha512Crypt(password.getBytes());
}
return "{CRYPT}" + hash;
}
public static void main(String[] args) {
String storedHash = "{CRYPT}$6$OY7N5bGk$WF6pEJOYU1SEySOuvLxVBNo2MRMsNAX4PX9JnzgUTYfDzevl1/fruztCTM0mIuiAUb3eJ//DEYxzFABmOlIzm/";
String password = "test"; // 要验证的密码
// 验证密码
boolean isValid = verifyPassword(password, storedHash);
System.out.println("密码验证结果: " + isValid);
// 生成新哈希(使用相同盐值)
String newHash = generateHash(password, "OY7N5bGk");
System.out.println("生成的哈希: " + newHash);
System.out.println("哈希匹配: " + newHash.equals(storedHash));
}
}

View File

@@ -250,4 +250,11 @@ justauth:
--- #################### iot相关配置 TODO 芋艿【IOT】再瞅瞅 ####################
pf4j:
# pluginsDir: /tmp/
pluginsDir: ../plugins
pluginsDir: ../plugins
spring:
ldap:
urls: ldap://192.168.88.205
base: dc=agrandtech,dc=com
username: uid=root,cn=users,dc=agrandtech,dc=com
password: Tian7989!