From 94c219f139fc61f9019ca839355f6151bb80b9a8 Mon Sep 17 00:00:00 2001 From: Guwan Date: Thu, 28 Aug 2025 07:24:46 +0800 Subject: [PATCH] fix: first --- .gitignore | 62 ++ README.md | 242 ++++++++ pom.xml | 110 ++++ .../com/example/ldap/LdapDemoApplication.java | 13 + .../com/example/ldap/config/LdapConfig.java | 43 ++ .../example/ldap/config/SecurityConfig.java | 29 + .../ldap/controller/GroupController.java | 206 +++++++ .../ldap/controller/UserController.java | 164 ++++++ .../ldap/controller/WebController.java | 46 ++ .../com/example/ldap/dto/ApiResponse.java | 97 +++ .../java/com/example/ldap/entity/Group.java | 104 ++++ .../java/com/example/ldap/entity/User.java | 189 ++++++ .../exception/GlobalExceptionHandler.java | 109 ++++ .../example/ldap/service/GroupService.java | 309 ++++++++++ .../com/example/ldap/service/UserService.java | 255 ++++++++ src/main/resources/application.yml | 53 ++ src/main/resources/templates/groups.html | 557 ++++++++++++++++++ src/main/resources/templates/index.html | 169 ++++++ src/main/resources/templates/users.html | 500 ++++++++++++++++ 19 files changed, 3257 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/com/example/ldap/LdapDemoApplication.java create mode 100644 src/main/java/com/example/ldap/config/LdapConfig.java create mode 100644 src/main/java/com/example/ldap/config/SecurityConfig.java create mode 100644 src/main/java/com/example/ldap/controller/GroupController.java create mode 100644 src/main/java/com/example/ldap/controller/UserController.java create mode 100644 src/main/java/com/example/ldap/controller/WebController.java create mode 100644 src/main/java/com/example/ldap/dto/ApiResponse.java create mode 100644 src/main/java/com/example/ldap/entity/Group.java create mode 100644 src/main/java/com/example/ldap/entity/User.java create mode 100644 src/main/java/com/example/ldap/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/ldap/service/GroupService.java create mode 100644 src/main/java/com/example/ldap/service/UserService.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/templates/groups.html create mode 100644 src/main/resources/templates/index.html create mode 100644 src/main/resources/templates/users.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6d3530 --- /dev/null +++ b/.gitignore @@ -0,0 +1,62 @@ +# Compiled class file +*.class + +# Eclipse +.project +.classpath +.settings/ + +# Intellij +*.ipr +*.iml +*.iws +.idea/ + +# Maven +target/ + +# Gradle +build +.gradle + +# Log file +*.log + +# out +**/out/ + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar +*.tar.gz +*.rar +*.pid +*.orig + +*.xlsx + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Mac +.DS_Store + +*.tmp + +*.puml +*.drawio + +**/cert/ + +**/webapp/ + diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9f8b8f --- /dev/null +++ b/README.md @@ -0,0 +1,242 @@ +# LDAP管理系统 + +一个基于Spring Boot的LDAP目录管理系统,提供用户和组的完整管理功能。 + +## 功能特性 + +### 用户管理 +- ✅ 创建、查看、编辑、删除用户 +- ✅ 用户密码管理 +- ✅ 用户信息搜索 +- ✅ 批量用户操作 +- ✅ 用户统计信息 + +### 组管理 +- ✅ 创建、查看、编辑、删除组 +- ✅ 组成员管理(添加/移除用户) +- ✅ 批量成员操作 +- ✅ 组搜索功能 +- ✅ 组统计信息 + +### Web界面 +- ✅ 现代化的响应式Web UI +- ✅ 用户友好的操作界面 +- ✅ 实时数据更新 +- ✅ 错误处理和用户反馈 + +### REST API +- ✅ 完整的RESTful API接口 +- ✅ 统一的响应格式 +- ✅ 全局异常处理 +- ✅ API文档和示例 + +## 技术栈 + +- **后端框架**: Spring Boot 3.2.0 +- **LDAP集成**: Spring LDAP, Spring Security LDAP +- **Web框架**: Spring MVC +- **模板引擎**: Thymeleaf +- **前端**: Bootstrap 5 + Font Awesome +- **构建工具**: Maven +- **Java版本**: 17 + +## 快速开始 + +### 1. 环境要求 + +- Java 17+ +- Maven 3.6+ +- LDAP服务器(如OpenLDAP、Active Directory等) + +### 2. 配置LDAP连接 + +编辑 `src/main/resources/application.yml` 文件: + +```yaml +spring: + ldap: + urls: ldap://localhost:389 # 您的LDAP服务器地址 + base: dc=example,dc=com # LDAP基础DN + username: cn=admin,dc=example,dc=com # 管理员DN + password: admin # 管理员密码 + +ldap: + config: + user-search-base: ou=people # 用户搜索基础 + group-search-base: ou=groups # 组搜索基础 + # 其他LDAP配置... +``` + +### 3. 构建和运行 + +```bash +# 克隆项目 +git clone +cd ldap-demo + +# 构建项目 +mvn clean compile + +# 运行项目 +mvn spring-boot:run +``` + +### 4. 访问应用 + +- **Web界面**: http://localhost:8080/ldap-demo/web/ +- **API接口**: http://localhost:8080/ldap-demo/api/ + +## API接口文档 + +### 用户管理 API + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/users` | 获取所有用户 | +| GET | `/api/users/{username}` | 获取指定用户 | +| POST | `/api/users` | 创建新用户 | +| PUT | `/api/users/{username}` | 更新用户信息 | +| DELETE | `/api/users/{username}` | 删除用户 | +| GET | `/api/users/search?keyword={keyword}` | 搜索用户 | +| PUT | `/api/users/{username}/password` | 更新用户密码 | +| POST | `/api/users/{username}/validate` | 验证用户密码 | +| POST | `/api/users/batch` | 批量创建用户 | +| GET | `/api/users/statistics` | 获取用户统计 | + +### 组管理 API + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/groups` | 获取所有组 | +| GET | `/api/groups/{groupName}` | 获取指定组 | +| POST | `/api/groups` | 创建新组 | +| PUT | `/api/groups/{groupName}` | 更新组信息 | +| DELETE | `/api/groups/{groupName}` | 删除组 | +| GET | `/api/groups/search?keyword={keyword}` | 搜索组 | +| POST | `/api/groups/{groupName}/members/{username}` | 添加用户到组 | +| DELETE | `/api/groups/{groupName}/members/{username}` | 从组中移除用户 | +| GET | `/api/groups/{groupName}/members` | 获取组成员 | +| GET | `/api/groups/user/{username}` | 获取用户所属组 | +| POST | `/api/groups/{groupName}/members/batch` | 批量添加用户到组 | +| DELETE | `/api/groups/{groupName}/members/batch` | 批量从组中移除用户 | +| GET | `/api/groups/statistics` | 获取组统计 | + +### API请求示例 + +#### 创建用户 +```bash +curl -X POST http://localhost:8080/ldap-demo/api/users \ + -H "Content-Type: application/json" \ + -d '{ + "username": "john.doe", + "password": "password123", + "commonName": "John Doe", + "surname": "Doe", + "givenName": "John", + "email": "john.doe@example.com", + "department": "IT", + "title": "Software Engineer" + }' +``` + +#### 创建组 +```bash +curl -X POST http://localhost:8080/ldap-demo/api/groups \ + -H "Content-Type: application/json" \ + -d '{ + "name": "developers", + "description": "Software Development Team", + "category": "department" + }' +``` + +#### 添加用户到组 +```bash +curl -X POST http://localhost:8080/ldap-demo/api/groups/developers/members/john.doe +``` + +## 项目结构 + +``` +src/ +├── main/ +│ ├── java/com/example/ldap/ +│ │ ├── config/ # 配置类 +│ │ ├── controller/ # 控制器 +│ │ ├── dto/ # 数据传输对象 +│ │ ├── entity/ # 实体类 +│ │ ├── exception/ # 异常处理 +│ │ └── service/ # 服务层 +│ └── resources/ +│ ├── templates/ # Thymeleaf模板 +│ └── application.yml # 配置文件 +└── test/ # 测试代码 +``` + +## 自定义配置 + +### LDAP对象类配置 + +如果您的LDAP服务器使用不同的对象类,可以在配置文件中修改: + +```yaml +ldap: + config: + user-object-class: inetOrgPerson # 用户对象类 + group-object-class: groupOfNames # 组对象类 +``` + +### 属性映射配置 + +可以根据您的LDAP架构调整属性映射: + +```yaml +ldap: + config: + group-member-attribute: member # 组成员属性 + group-role-attribute: cn # 组角色属性 +``` + +## 安全考虑 + +1. **密码加密**: 用户密码使用BCrypt加密存储 +2. **LDAP连接**: 支持LDAP连接池配置 +3. **API安全**: 可以集成Spring Security进行API认证 +4. **输入验证**: 所有输入都经过验证和清理 + +## 故障排除 + +### 常见问题 + +1. **连接LDAP失败** + - 检查LDAP服务器地址和端口 + - 验证管理员凭据 + - 确认网络连接 + +2. **用户创建失败** + - 检查用户DN格式 + - 验证必填字段 + - 确认LDAP权限 + +3. **组管理错误** + - 验证组对象类配置 + - 检查成员属性设置 + +### 日志配置 + +在 `application.yml` 中启用详细日志: + +```yaml +logging: + level: + com.example.ldap: DEBUG + org.springframework.ldap: DEBUG +``` + +## 贡献 + +欢迎提交问题和功能请求! + +## 许可证 + +本项目采用 MIT 许可证。 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e8207ca --- /dev/null +++ b/pom.xml @@ -0,0 +1,110 @@ + + + 4.0.0 + + com.example + ldap-demo + 1.0.0 + jar + + LDAP Management System + Spring Boot application for LDAP user and group management + + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + + 17 + 17 + 17 + UTF-8 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-ldap + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + org.springframework.ldap + spring-ldap-core + + + + org.springframework.security + spring-security-ldap + + + + + org.apache.commons + commons-lang3 + + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.unboundid + unboundid-ldapsdk + test + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/com/example/ldap/LdapDemoApplication.java b/src/main/java/com/example/ldap/LdapDemoApplication.java new file mode 100644 index 0000000..45b4dcb --- /dev/null +++ b/src/main/java/com/example/ldap/LdapDemoApplication.java @@ -0,0 +1,13 @@ +package com.example.ldap; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LdapDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(LdapDemoApplication.class, args); + } +} + diff --git a/src/main/java/com/example/ldap/config/LdapConfig.java b/src/main/java/com/example/ldap/config/LdapConfig.java new file mode 100644 index 0000000..6320b9c --- /dev/null +++ b/src/main/java/com/example/ldap/config/LdapConfig.java @@ -0,0 +1,43 @@ +package com.example.ldap.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.core.support.LdapContextSource; + +@Configuration +public class LdapConfig { + + @Value("${spring.ldap.urls}") + private String ldapUrls; + + @Value("${spring.ldap.base}") + private String ldapBase; + + @Value("${spring.ldap.username}") + private String ldapUsername; + + @Value("${spring.ldap.password}") + private String ldapPassword; + + @Bean + public LdapContextSource contextSource() { + LdapContextSource contextSource = new LdapContextSource(); + contextSource.setUrl(ldapUrls); + contextSource.setBase(ldapBase); + contextSource.setUserDn(ldapUsername); + contextSource.setPassword(ldapPassword); + + // 设置连接池参数 + contextSource.setPooled(true); + + return contextSource; + } + + @Bean + public LdapTemplate ldapTemplate() { + return new LdapTemplate(contextSource()); + } +} + diff --git a/src/main/java/com/example/ldap/config/SecurityConfig.java b/src/main/java/com/example/ldap/config/SecurityConfig.java new file mode 100644 index 0000000..9c106b5 --- /dev/null +++ b/src/main/java/com/example/ldap/config/SecurityConfig.java @@ -0,0 +1,29 @@ +package com.example.ldap.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authz -> authz + .requestMatchers("/api/**").permitAll() + .requestMatchers("/web/**").permitAll() + .requestMatchers("/css/**", "/js/**", "/images/**").permitAll() + .requestMatchers("/").permitAll() + .anyRequest().authenticated() + ) + .csrf(csrf -> csrf.disable()) + .headers(headers -> headers.frameOptions().deny()); + + return http.build(); + } +} + diff --git a/src/main/java/com/example/ldap/controller/GroupController.java b/src/main/java/com/example/ldap/controller/GroupController.java new file mode 100644 index 0000000..68393a8 --- /dev/null +++ b/src/main/java/com/example/ldap/controller/GroupController.java @@ -0,0 +1,206 @@ +package com.example.ldap.controller; + +import com.example.ldap.dto.ApiResponse; +import com.example.ldap.entity.Group; +import com.example.ldap.service.GroupService; +import jakarta.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +@RestController +@RequestMapping("/api/groups") +@CrossOrigin(origins = "*") +public class GroupController { + + private static final Logger logger = LoggerFactory.getLogger(GroupController.class); + + @Autowired + private GroupService groupService; + + /** + * 获取所有组 + */ + @GetMapping + public ResponseEntity>> getAllGroups() { + logger.info("API: 获取所有组"); + List groups = groupService.getAllGroups(); + return ResponseEntity.ok(ApiResponse.success("获取组列表成功", groups)); + } + + /** + * 根据组名获取组 + */ + @GetMapping("/{groupName}") + public ResponseEntity> getGroupByName(@PathVariable String groupName) { + logger.info("API: 获取组 {}", groupName); + Group group = groupService.findByName(groupName); + if (group == null) { + return ResponseEntity.ok(ApiResponse.error("组不存在")); + } + return ResponseEntity.ok(ApiResponse.success("获取组成功", group)); + } + + /** + * 创建新组 + */ + @PostMapping + public ResponseEntity> createGroup(@Valid @RequestBody Group group) { + logger.info("API: 创建组 {}", group.getName()); + Group createdGroup = groupService.createGroup(group); + return ResponseEntity.ok(ApiResponse.success("组创建成功", createdGroup)); + } + + /** + * 更新组信息 + */ + @PutMapping("/{groupName}") + public ResponseEntity> updateGroup( + @PathVariable String groupName, + @RequestBody Group group) { + logger.info("API: 更新组 {}", groupName); + Group updatedGroup = groupService.updateGroup(groupName, group); + return ResponseEntity.ok(ApiResponse.success("组更新成功", updatedGroup)); + } + + /** + * 删除组 + */ + @DeleteMapping("/{groupName}") + public ResponseEntity> deleteGroup(@PathVariable String groupName) { + logger.info("API: 删除组 {}", groupName); + groupService.deleteGroup(groupName); + return ResponseEntity.ok(ApiResponse.success("组删除成功")); + } + + /** + * 添加用户到组 + */ + @PostMapping("/{groupName}/members/{username}") + public ResponseEntity> addUserToGroup( + @PathVariable String groupName, + @PathVariable String username) { + logger.info("API: 添加用户 {} 到组 {}", username, groupName); + groupService.addUserToGroup(groupName, username); + return ResponseEntity.ok(ApiResponse.success("用户已添加到组")); + } + + /** + * 从组中移除用户 + */ + @DeleteMapping("/{groupName}/members/{username}") + public ResponseEntity> removeUserFromGroup( + @PathVariable String groupName, + @PathVariable String username) { + logger.info("API: 从组 {} 移除用户 {}", groupName, username); + groupService.removeUserFromGroup(groupName, username); + return ResponseEntity.ok(ApiResponse.success("用户已从组中移除")); + } + + /** + * 获取组的所有成员 + */ + @GetMapping("/{groupName}/members") + public ResponseEntity>> getGroupMembers(@PathVariable String groupName) { + logger.info("API: 获取组 {} 的成员", groupName); + Set members = groupService.getGroupMembers(groupName); + return ResponseEntity.ok(ApiResponse.success("获取组成员成功", members)); + } + + /** + * 获取用户所属的所有组 + */ + @GetMapping("/user/{username}") + public ResponseEntity>> getUserGroups(@PathVariable String username) { + logger.info("API: 获取用户 {} 所属的组", username); + List groups = groupService.getUserGroups(username); + return ResponseEntity.ok(ApiResponse.success("获取用户所属组成功", groups)); + } + + /** + * 搜索组 + */ + @GetMapping("/search") + public ResponseEntity>> searchGroups( + @RequestParam(required = false) String keyword) { + logger.info("API: 搜索组 {}", keyword); + List groups = groupService.searchGroups(keyword); + return ResponseEntity.ok(ApiResponse.success("搜索完成", groups)); + } + + /** + * 批量添加用户到组 + */ + @PostMapping("/{groupName}/members/batch") + public ResponseEntity> addUsersToGroup( + @PathVariable String groupName, + @RequestBody Map> data) { + logger.info("API: 批量添加用户到组 {}", groupName); + + List usernames = data.get("usernames"); + if (usernames == null || usernames.isEmpty()) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("用户名列表不能为空")); + } + + for (String username : usernames) { + groupService.addUserToGroup(groupName, username); + } + + return ResponseEntity.ok(ApiResponse.success("批量添加用户成功")); + } + + /** + * 批量从组中移除用户 + */ + @DeleteMapping("/{groupName}/members/batch") + public ResponseEntity> removeUsersFromGroup( + @PathVariable String groupName, + @RequestBody Map> data) { + logger.info("API: 批量从组 {} 移除用户", groupName); + + List usernames = data.get("usernames"); + if (usernames == null || usernames.isEmpty()) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("用户名列表不能为空")); + } + + for (String username : usernames) { + groupService.removeUserFromGroup(groupName, username); + } + + return ResponseEntity.ok(ApiResponse.success("批量移除用户成功")); + } + + /** + * 获取组统计信息 + */ + @GetMapping("/statistics") + public ResponseEntity>> getGroupStatistics() { + logger.info("API: 获取组统计信息"); + + List allGroups = groupService.getAllGroups(); + int totalMembers = allGroups.stream() + .mapToInt(g -> g.getMembers() != null ? g.getMembers().size() : 0) + .sum(); + + Map stats = Map.of( + "totalGroups", allGroups.size(), + "totalMembers", totalMembers, + "averageMembersPerGroup", allGroups.isEmpty() ? 0 : (double) totalMembers / allGroups.size(), + "groupsWithoutMembers", allGroups.stream() + .mapToLong(g -> (g.getMembers() == null || g.getMembers().isEmpty() || + (g.getMembers().size() == 1 && g.getMembers().contains("cn=dummy"))) ? 1 : 0) + .sum() + ); + + return ResponseEntity.ok(ApiResponse.success("获取统计信息成功", stats)); + } +} + diff --git a/src/main/java/com/example/ldap/controller/UserController.java b/src/main/java/com/example/ldap/controller/UserController.java new file mode 100644 index 0000000..b93e9d9 --- /dev/null +++ b/src/main/java/com/example/ldap/controller/UserController.java @@ -0,0 +1,164 @@ +package com.example.ldap.controller; + +import com.example.ldap.dto.ApiResponse; +import com.example.ldap.entity.User; +import com.example.ldap.service.UserService; +import jakarta.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/users") +@CrossOrigin(origins = "*") +public class UserController { + + private static final Logger logger = LoggerFactory.getLogger(UserController.class); + + @Autowired + private UserService userService; + + /** + * 获取所有用户 + */ + @GetMapping + public ResponseEntity>> getAllUsers() { + logger.info("API: 获取所有用户"); + List users = userService.getAllUsers(); + return ResponseEntity.ok(ApiResponse.success("获取用户列表成功", users)); + } + + /** + * 根据用户名获取用户 + */ + @GetMapping("/{username}") + public ResponseEntity> getUserByUsername(@PathVariable String username) { + logger.info("API: 获取用户 {}", username); + User user = userService.findByUsername(username); + if (user == null) { + return ResponseEntity.ok(ApiResponse.error("用户不存在")); + } + return ResponseEntity.ok(ApiResponse.success("获取用户成功", user)); + } + + /** + * 创建新用户 + */ + @PostMapping + public ResponseEntity> createUser(@Valid @RequestBody User user) { + logger.info("API: 创建用户 {}", user.getUsername()); + User createdUser = userService.createUser(user); + return ResponseEntity.ok(ApiResponse.success("用户创建成功", createdUser)); + } + + /** + * 更新用户信息 + */ + @PutMapping("/{username}") + public ResponseEntity> updateUser( + @PathVariable String username, + @RequestBody User user) { + logger.info("API: 更新用户 {}", username); + User updatedUser = userService.updateUser(username, user); + return ResponseEntity.ok(ApiResponse.success("用户更新成功", updatedUser)); + } + + /** + * 更新用户密码 + */ + @PutMapping("/{username}/password") + public ResponseEntity> updatePassword( + @PathVariable String username, + @RequestBody Map passwordData) { + logger.info("API: 更新用户密码 {}", username); + String newPassword = passwordData.get("newPassword"); + if (newPassword == null || newPassword.trim().isEmpty()) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("新密码不能为空")); + } + + userService.updatePassword(username, newPassword); + return ResponseEntity.ok(ApiResponse.success("密码更新成功")); + } + + /** + * 删除用户 + */ + @DeleteMapping("/{username}") + public ResponseEntity> deleteUser(@PathVariable String username) { + logger.info("API: 删除用户 {}", username); + userService.deleteUser(username); + return ResponseEntity.ok(ApiResponse.success("用户删除成功")); + } + + /** + * 搜索用户 + */ + @GetMapping("/search") + public ResponseEntity>> searchUsers( + @RequestParam(required = false) String keyword) { + logger.info("API: 搜索用户 {}", keyword); + List users = userService.searchUsers(keyword); + return ResponseEntity.ok(ApiResponse.success("搜索完成", users)); + } + + /** + * 验证用户密码 + */ + @PostMapping("/{username}/validate") + public ResponseEntity> validatePassword( + @PathVariable String username, + @RequestBody Map passwordData) { + logger.info("API: 验证用户密码 {}", username); + String password = passwordData.get("password"); + if (password == null) { + return ResponseEntity.badRequest() + .body(ApiResponse.error("密码不能为空")); + } + + boolean isValid = userService.validatePassword(username, password); + return ResponseEntity.ok(ApiResponse.success("验证完成", isValid)); + } + + /** + * 批量创建用户 + */ + @PostMapping("/batch") + public ResponseEntity>> createUsers(@Valid @RequestBody List users) { + logger.info("API: 批量创建用户,数量: {}", users.size()); + + List createdUsers = users.stream() + .map(userService::createUser) + .toList(); + + return ResponseEntity.ok(ApiResponse.success("批量创建用户成功", createdUsers)); + } + + /** + * 获取用户统计信息 + */ + @GetMapping("/statistics") + public ResponseEntity>> getUserStatistics() { + logger.info("API: 获取用户统计信息"); + + List allUsers = userService.getAllUsers(); + Map stats = Map.of( + "totalUsers", allUsers.size(), + "usersWithEmail", allUsers.stream() + .mapToLong(u -> u.getEmail() != null && !u.getEmail().isEmpty() ? 1 : 0) + .sum(), + "usersWithPhone", allUsers.stream() + .mapToLong(u -> u.getPhoneNumber() != null && !u.getPhoneNumber().isEmpty() ? 1 : 0) + .sum() + ); + + return ResponseEntity.ok(ApiResponse.success("获取统计信息成功", stats)); + } +} + diff --git a/src/main/java/com/example/ldap/controller/WebController.java b/src/main/java/com/example/ldap/controller/WebController.java new file mode 100644 index 0000000..bbde54b --- /dev/null +++ b/src/main/java/com/example/ldap/controller/WebController.java @@ -0,0 +1,46 @@ +package com.example.ldap.controller; + +import com.example.ldap.service.GroupService; +import com.example.ldap.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/web") +public class WebController { + + @Autowired + private UserService userService; + + @Autowired + private GroupService groupService; + + /** + * 主页 + */ + @GetMapping("/") + public String index(Model model) { + model.addAttribute("userCount", userService.getAllUsers().size()); + model.addAttribute("groupCount", groupService.getAllGroups().size()); + return "index"; + } + + /** + * 用户管理页面 + */ + @GetMapping("/users") + public String users() { + return "users"; + } + + /** + * 组管理页面 + */ + @GetMapping("/groups") + public String groups() { + return "groups"; + } +} diff --git a/src/main/java/com/example/ldap/dto/ApiResponse.java b/src/main/java/com/example/ldap/dto/ApiResponse.java new file mode 100644 index 0000000..d7df0f4 --- /dev/null +++ b/src/main/java/com/example/ldap/dto/ApiResponse.java @@ -0,0 +1,97 @@ +package com.example.ldap.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ApiResponse { + private boolean success; + private String message; + private T data; + private String error; + private long timestamp; + + public ApiResponse() { + this.timestamp = System.currentTimeMillis(); + } + + public static ApiResponse success(T data) { + ApiResponse response = new ApiResponse<>(); + response.setSuccess(true); + response.setData(data); + response.setMessage("操作成功"); + return response; + } + + public static ApiResponse success(String message, T data) { + ApiResponse response = new ApiResponse<>(); + response.setSuccess(true); + response.setMessage(message); + response.setData(data); + return response; + } + + public static ApiResponse success(String message) { + ApiResponse response = new ApiResponse<>(); + response.setSuccess(true); + response.setMessage(message); + return response; + } + + public static ApiResponse error(String error) { + ApiResponse response = new ApiResponse<>(); + response.setSuccess(false); + response.setError(error); + response.setMessage("操作失败"); + return response; + } + + public static ApiResponse error(String message, String error) { + ApiResponse response = new ApiResponse<>(); + response.setSuccess(false); + response.setMessage(message); + response.setError(error); + return response; + } + + // Getters and Setters + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public T getData() { + return data; + } + + public void setData(T data) { + this.data = data; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } +} + diff --git a/src/main/java/com/example/ldap/entity/Group.java b/src/main/java/com/example/ldap/entity/Group.java new file mode 100644 index 0000000..39d557f --- /dev/null +++ b/src/main/java/com/example/ldap/entity/Group.java @@ -0,0 +1,104 @@ +package com.example.ldap.entity; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +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; + +import java.util.List; +import java.util.Set; + +@Entry(base = "ou=groups", objectClasses = {"groupOfNames", "top"}) +public class Group { + + @Id + private Name dn; + + @Attribute(name = "cn") + @NotBlank(message = "组名不能为空") + @Size(min = 2, max = 50, message = "组名长度必须在2-50个字符之间") + private String name; + + @Attribute(name = "description") + private String description; + + @Attribute(name = "member") + private Set members; + + @Attribute(name = "businessCategory") + private String category; + + @Attribute(name = "ou") + private String organizationalUnit; + + // 构造函数 + public Group() {} + + public Group(String name, String description) { + this.name = name; + this.description = description; + } + + // Getters and Setters + public Name getDn() { + return dn; + } + + public void setDn(Name dn) { + this.dn = dn; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Set getMembers() { + return members; + } + + public void setMembers(Set members) { + this.members = members; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getOrganizationalUnit() { + return organizationalUnit; + } + + public void setOrganizationalUnit(String organizationalUnit) { + this.organizationalUnit = organizationalUnit; + } + + @Override + public String toString() { + return "Group{" + + "name='" + name + '\'' + + ", description='" + description + '\'' + + ", category='" + category + '\'' + + ", memberCount=" + (members != null ? members.size() : 0) + + '}'; + } +} + diff --git a/src/main/java/com/example/ldap/entity/User.java b/src/main/java/com/example/ldap/entity/User.java new file mode 100644 index 0000000..a646c59 --- /dev/null +++ b/src/main/java/com/example/ldap/entity/User.java @@ -0,0 +1,189 @@ +package com.example.ldap.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +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; + +import java.util.List; + +@Entry(base = "ou=people", objectClasses = {"inetOrgPerson"}) +public class User { + + @Id + @JsonIgnore // 排除dn字段的JSON序列化,避免序列化错误 + private Name dn; + + @Attribute(name = "uid") + // @NotBlank(message = "用户名不能为空") + // @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间") + private String username; + + @Attribute(name = "cn") + // @NotBlank(message = "姓名不能为空") + private String commonName; + + @Attribute(name = "sn") + // @NotBlank(message = "姓氏不能为空") + private String surname; + + @Attribute(name = "givenName") + private String givenName; + + @Attribute(name = "mail") + // @Email(message = "邮箱格式不正确") + private String email; + + @Attribute(name = "telephoneNumber") + private String phoneNumber; + + @Attribute(name = "userPassword") + private String password; + + @Attribute(name = "title") + private String title; + + @Attribute(name = "departmentNumber") + private String department; + + @Attribute(name = "description") + private String description; + + @Attribute(name = "employeeNumber") + private String employeeNumber; + + @Attribute(name = "memberOf") + private List memberOf; + + // 构造函数 + public User() {} + + public User(String username, String commonName, String surname) { + this.username = username; + this.commonName = commonName; + this.surname = surname; + } + + // Getters and Setters + public Name getDn() { + return dn; + } + + public void setDn(Name dn) { + this.dn = dn; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getCommonName() { + return commonName; + } + + public void setCommonName(String commonName) { + this.commonName = commonName; + } + + public String getSurname() { + return surname; + } + + public void setSurname(String surname) { + this.surname = surname; + } + + public String getGivenName() { + return givenName; + } + + public void setGivenName(String givenName) { + this.givenName = givenName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDepartment() { + return department; + } + + public void setDepartment(String department) { + this.department = department; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getEmployeeNumber() { + return employeeNumber; + } + + public void setEmployeeNumber(String employeeNumber) { + this.employeeNumber = employeeNumber; + } + + public List getMemberOf() { + return memberOf; + } + + public void setMemberOf(List memberOf) { + this.memberOf = memberOf; + } + + @Override + public String toString() { + return "User{" + + "username='" + username + '\'' + + ", commonName='" + commonName + '\'' + + ", surname='" + surname + '\'' + + ", email='" + email + '\'' + + ", title='" + title + '\'' + + ", department='" + department + '\'' + + '}'; + } +} + diff --git a/src/main/java/com/example/ldap/exception/GlobalExceptionHandler.java b/src/main/java/com/example/ldap/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..1884489 --- /dev/null +++ b/src/main/java/com/example/ldap/exception/GlobalExceptionHandler.java @@ -0,0 +1,109 @@ +package com.example.ldap.exception; + +import com.example.ldap.dto.ApiResponse; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + + +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + + /** + * 处理运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + public ResponseEntity> handleRuntimeException(RuntimeException e) { + logger.error("运行时异常", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("系统错误", e.getMessage())); + } + + /** + * 处理参数验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationException(MethodArgumentNotValidException e) { + logger.error("参数验证失败", e); + + Map errors = new HashMap<>(); + e.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + + return ResponseEntity.badRequest() + .body(ApiResponse.error("参数验证失败", errors.toString())); + } + + /** + * 处理绑定异常 + */ + @ExceptionHandler(BindException.class) + public ResponseEntity> handleBindException(BindException e) { + logger.error("参数绑定失败", e); + + Map errors = new HashMap<>(); + e.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + + return ResponseEntity.badRequest() + .body(ApiResponse.error("参数绑定失败", errors.toString())); + } + + /** + * 处理约束违反异常 + */ + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity> handleConstraintViolationException(ConstraintViolationException e) { + logger.error("约束违反", e); + + Map errors = new HashMap<>(); + for (ConstraintViolation violation : e.getConstraintViolations()) { + String fieldName = violation.getPropertyPath().toString(); + String errorMessage = violation.getMessage(); + errors.put(fieldName, errorMessage); + } + + return ResponseEntity.badRequest() + .body(ApiResponse.error("约束违反", errors.toString())); + } + + /** + * 处理非法参数异常 + */ + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgumentException(IllegalArgumentException e) { + logger.error("非法参数", e); + return ResponseEntity.badRequest() + .body(ApiResponse.error("参数错误", e.getMessage())); + } + + /** + * 处理通用异常 + */ + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGenericException(Exception e) { + logger.error("未知异常", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error("系统异常", "请联系管理员")); + } +} + diff --git a/src/main/java/com/example/ldap/service/GroupService.java b/src/main/java/com/example/ldap/service/GroupService.java new file mode 100644 index 0000000..d6d5f85 --- /dev/null +++ b/src/main/java/com/example/ldap/service/GroupService.java @@ -0,0 +1,309 @@ +package com.example.ldap.service; + +import com.example.ldap.entity.Group; +import com.example.ldap.entity.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.ldap.core.support.LdapContextSource; +import org.springframework.ldap.core.support.BaseLdapPathContextSource; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.filter.AndFilter; +import org.springframework.ldap.filter.EqualsFilter; +import org.springframework.ldap.filter.LikeFilter; +import org.springframework.ldap.query.LdapQuery; +import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.ldap.support.LdapNameBuilder; +import org.springframework.stereotype.Service; + +import javax.naming.Name; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Service +public class GroupService { + + private static final Logger logger = LoggerFactory.getLogger(GroupService.class); + + @Autowired + private LdapTemplate ldapTemplate; + + @Autowired + private UserService userService; + + @Value("${ldap.config.group-search-base}") + private String groupSearchBase; + + @Value("${ldap.config.user-search-base}") + private String userSearchBase; + + @Value("${spring.ldap.base}") + private String ldapBase; + + /** + * 获取所有组 + */ + public List getAllGroups() { + logger.info("获取所有组"); + try { + return ldapTemplate.findAll(Group.class); + } catch (Exception e) { + logger.error("获取所有组失败", e); + throw new RuntimeException("获取组列表失败: " + e.getMessage()); + } + } + + /** + * 根据组名查找组 + */ + public Group findByName(String groupName) { + logger.info("查找组: {}", groupName); + try { + LdapQuery query = LdapQueryBuilder.query() + .base(groupSearchBase) + .where("objectClass").is("groupOfNames") + .and("cn").is(groupName); + + List groups = ldapTemplate.find(query, Group.class); + return groups.isEmpty() ? null : groups.get(0); + } catch (Exception e) { + logger.error("查找组失败: {}", groupName, e); + throw new RuntimeException("查找组失败: " + e.getMessage()); + } + } + + /** + * 创建新组 + */ + public Group createGroup(Group group) { + logger.info("创建组: {}", group.getName()); + try { + if (findByName(group.getName()) != null) { + throw new RuntimeException("组名已存在: " + group.getName()); + } + + // 设置DN + Name dn = LdapNameBuilder.newInstance(groupSearchBase) + .add("cn", group.getName()) + .build(); + group.setDn(dn); + + // 初始化成员集合 + if (group.getMembers() == null) { + group.setMembers(new HashSet<>()); + } + + // groupOfNames 要求至少有一个成员,添加一个虚拟成员 + if (group.getMembers().isEmpty()) { + group.getMembers().add("cn=dummy"); + } + + ldapTemplate.create(group); + logger.info("组创建成功: {}", group.getName()); + return group; + } catch (Exception e) { + logger.error("创建组失败: {}", group.getName(), e); + throw new RuntimeException("创建组失败: " + e.getMessage()); + } + } + + /** + * 更新组信息 + */ + public Group updateGroup(String groupName, Group updatedGroup) { + logger.info("更新组: {}", groupName); + try { + Group existingGroup = findByName(groupName); + if (existingGroup == null) { + throw new RuntimeException("组不存在: " + groupName); + } + + // 更新字段 + if (updatedGroup.getDescription() != null) { + existingGroup.setDescription(updatedGroup.getDescription()); + } + if (updatedGroup.getCategory() != null) { + existingGroup.setCategory(updatedGroup.getCategory()); + } + if (updatedGroup.getOrganizationalUnit() != null) { + existingGroup.setOrganizationalUnit(updatedGroup.getOrganizationalUnit()); + } + + ldapTemplate.update(existingGroup); + logger.info("组更新成功: {}", groupName); + return existingGroup; + } catch (Exception e) { + logger.error("更新组失败: {}", groupName, e); + throw new RuntimeException("更新组失败: " + e.getMessage()); + } + } + + /** + * 删除组 + */ + public void deleteGroup(String groupName) { + logger.info("删除组: {}", groupName); + try { + Group group = findByName(groupName); + if (group == null) { + throw new RuntimeException("组不存在: " + groupName); + } + + ldapTemplate.delete(group); + logger.info("组删除成功: {}", groupName); + } catch (Exception e) { + logger.error("删除组失败: {}", groupName, e); + throw new RuntimeException("删除组失败: " + e.getMessage()); + } + } + + /** + * 添加用户到组 + */ + public void addUserToGroup(String groupName, String username) { + logger.info("添加用户 {} 到组 {}", username, groupName); + try { + Group group = findByName(groupName); + if (group == null) { + throw new RuntimeException("组不存在: " + groupName); + } + + User user = userService.findByUsername(username); + if (user == null) { + throw new RuntimeException("用户不存在: " + username); + } + + // 构建用户DN + String userDn = "uid=" + username + "," + userSearchBase + "," + ((BaseLdapPathContextSource) ldapTemplate.getContextSource()).getBaseLdapPathAsString(); + + if (group.getMembers() == null) { + group.setMembers(new HashSet<>()); + } + + // 移除虚拟成员 + group.getMembers().remove("cn=dummy"); + + // 添加用户 + group.getMembers().add(userDn); + + ldapTemplate.update(group); + logger.info("用户 {} 已添加到组 {}", username, groupName); + } catch (Exception e) { + logger.error("添加用户到组失败: 用户={}, 组={}", username, groupName, e); + throw new RuntimeException("添加用户到组失败: " + e.getMessage()); + } + } + + /** + * 从组中移除用户 + */ + public void removeUserFromGroup(String groupName, String username) { + logger.info("从组 {} 移除用户 {}", groupName, username); + try { + Group group = findByName(groupName); + if (group == null) { + throw new RuntimeException("组不存在: " + groupName); + } + + // 构建用户DN + String userDn = "uid=" + username + "," + userSearchBase + "," + ((BaseLdapPathContextSource) ldapTemplate.getContextSource()).getBaseLdapPathAsString(); + + if (group.getMembers() != null) { + group.getMembers().remove(userDn); + + // 如果没有成员了,添加虚拟成员 + if (group.getMembers().isEmpty()) { + group.getMembers().add("cn=dummy"); + } + + ldapTemplate.update(group); + logger.info("用户 {} 已从组 {} 移除", username, groupName); + } + } catch (Exception e) { + logger.error("从组移除用户失败: 用户={}, 组={}", username, groupName, e); + throw new RuntimeException("从组移除用户失败: " + e.getMessage()); + } + } + + /** + * 获取组的所有成员 + */ + public Set getGroupMembers(String groupName) { + logger.info("获取组 {} 的成员", groupName); + try { + Group group = findByName(groupName); + if (group == null) { + throw new RuntimeException("组不存在: " + groupName); + } + + Set members = new HashSet<>(); + if (group.getMembers() != null) { + for (String memberDn : group.getMembers()) { + // 提取用户名 + if (memberDn.startsWith("uid=") && !memberDn.equals("cn=dummy")) { + String username = memberDn.substring(4, memberDn.indexOf(",")); + members.add(username); + } + } + } + + return members; + } catch (Exception e) { + logger.error("获取组成员失败: {}", groupName, e); + throw new RuntimeException("获取组成员失败: " + e.getMessage()); + } + } + + /** + * 获取用户所属的所有组 + */ + public List getUserGroups(String username) { + logger.info("获取用户 {} 所属的组", username); + try { + // 构建用户DN + String userDn = "uid=" + username + "," + userSearchBase + "," + ((BaseLdapPathContextSource) ldapTemplate.getContextSource()).getBaseLdapPathAsString(); + + LdapQuery query = LdapQueryBuilder.query() + .base(groupSearchBase) + .where("objectClass").is("groupOfNames") + .and("member").is(userDn); + + return ldapTemplate.find(query, Group.class); + } catch (Exception e) { + logger.error("获取用户所属组失败: {}", username, e); + throw new RuntimeException("获取用户所属组失败: " + e.getMessage()); + } + } + + /** + * 搜索组 + */ + public List searchGroups(String keyword) { + logger.info("搜索组: {}", keyword); + try { + LdapQuery query; + + if (keyword != null && !keyword.trim().isEmpty()) { + AndFilter filter = new AndFilter(); + filter.and(new EqualsFilter("objectClass", "groupOfNames")); + filter.and(new LikeFilter("cn", "*" + keyword + "*")); + + query = LdapQueryBuilder.query() + .base(groupSearchBase) + .filter(filter); + } else { + query = LdapQueryBuilder.query() + .base(groupSearchBase) + .where("objectClass").is("groupOfNames"); + } + + return ldapTemplate.find(query, Group.class); + } catch (Exception e) { + logger.error("搜索组失败: {}", keyword, e); + throw new RuntimeException("搜索组失败: " + e.getMessage()); + } + } +} + diff --git a/src/main/java/com/example/ldap/service/UserService.java b/src/main/java/com/example/ldap/service/UserService.java new file mode 100644 index 0000000..8190d27 --- /dev/null +++ b/src/main/java/com/example/ldap/service/UserService.java @@ -0,0 +1,255 @@ +package com.example.ldap.service; + +import com.example.ldap.entity.User; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.ldap.core.LdapTemplate; +import org.springframework.ldap.filter.AndFilter; +import org.springframework.ldap.filter.EqualsFilter; +import org.springframework.ldap.filter.LikeFilter; +import org.springframework.ldap.filter.OrFilter; +import org.springframework.ldap.query.LdapQuery; +import org.springframework.ldap.query.LdapQueryBuilder; +import org.springframework.ldap.support.LdapNameBuilder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import javax.naming.Name; +import java.util.List; + +@Service +public class UserService { + + private static final Logger logger = LoggerFactory.getLogger(UserService.class); + + @Autowired + private LdapTemplate ldapTemplate; + + @Value("${ldap.config.user-search-base}") + private String userSearchBase; + + private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + + /** + * 获取所有用户 + */ + public List getAllUsers() { + logger.info("获取所有用户"); + try { + // 使用更具体的查询,避免重复条目问题 + LdapQuery query = LdapQueryBuilder.query() + .base(userSearchBase) + .where("objectClass").is("inetOrgPerson") + .and("uid").isPresent(); // 只查找有uid属性的用户 + + List users = ldapTemplate.find(query, User.class); + logger.info("找到 {} 个用户", users.size()); + return users; + } catch (Exception e) { + logger.error("获取所有用户失败", e); + throw new RuntimeException("获取用户列表失败: " + e.getMessage()); + } + } + + /** + * 根据用户名查找用户 + */ + public User findByUsername(String username) { + logger.info("查找用户: {}", username); + try { + LdapQuery query = LdapQueryBuilder.query() + .base(userSearchBase) + .where("objectClass").is("inetOrgPerson") + .and("uid").is(username); + + List users = ldapTemplate.find(query, User.class); + return users.isEmpty() ? null : users.get(0); + } catch (Exception e) { + logger.error("查找用户失败: {}", username, e); + throw new RuntimeException("查找用户失败: " + e.getMessage()); + } + } + + /** + * 创建新用户 + */ + public User createUser(User user) { + logger.info("创建用户: {}", user.getUsername()); + try { + if (findByUsername(user.getUsername()) != null) { + throw new RuntimeException("用户名已存在: " + user.getUsername()); + } + + // 设置DN + Name dn = LdapNameBuilder.newInstance(userSearchBase) + .add("uid", user.getUsername()) + .build(); + user.setDn(dn); + + // 加密密码 + if (user.getPassword() != null) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + } + + // 设置默认值 + if (user.getCommonName() == null) { + user.setCommonName(user.getUsername()); + } + if (user.getSurname() == null) { + user.setSurname(user.getUsername()); + } + + ldapTemplate.create(user); + logger.info("用户创建成功: {}", user.getUsername()); + return user; + } catch (Exception e) { + logger.error("创建用户失败: {}", user.getUsername(), e); + throw new RuntimeException("创建用户失败: " + e.getMessage()); + } + } + + /** + * 更新用户信息 + */ + public User updateUser(String username, User updatedUser) { + logger.info("更新用户: {}", username); + try { + User existingUser = findByUsername(username); + if (existingUser == null) { + throw new RuntimeException("用户不存在: " + username); + } + + // 更新字段 + if (updatedUser.getCommonName() != null) { + existingUser.setCommonName(updatedUser.getCommonName()); + } + if (updatedUser.getSurname() != null) { + existingUser.setSurname(updatedUser.getSurname()); + } + if (updatedUser.getGivenName() != null) { + existingUser.setGivenName(updatedUser.getGivenName()); + } + if (updatedUser.getEmail() != null) { + existingUser.setEmail(updatedUser.getEmail()); + } + if (updatedUser.getPhoneNumber() != null) { + existingUser.setPhoneNumber(updatedUser.getPhoneNumber()); + } + if (updatedUser.getTitle() != null) { + existingUser.setTitle(updatedUser.getTitle()); + } + if (updatedUser.getDepartment() != null) { + existingUser.setDepartment(updatedUser.getDepartment()); + } + if (updatedUser.getDescription() != null) { + existingUser.setDescription(updatedUser.getDescription()); + } + if (updatedUser.getEmployeeNumber() != null) { + existingUser.setEmployeeNumber(updatedUser.getEmployeeNumber()); + } + + ldapTemplate.update(existingUser); + logger.info("用户更新成功: {}", username); + return existingUser; + } catch (Exception e) { + logger.error("更新用户失败: {}", username, e); + throw new RuntimeException("更新用户失败: " + e.getMessage()); + } + } + + /** + * 更新用户密码 + */ + public void updatePassword(String username, String newPassword) { + logger.info("更新用户密码: {}", username); + try { + User user = findByUsername(username); + if (user == null) { + throw new RuntimeException("用户不存在: " + username); + } + + user.setPassword(passwordEncoder.encode(newPassword)); + ldapTemplate.update(user); + logger.info("用户密码更新成功: {}", username); + } catch (Exception e) { + logger.error("更新用户密码失败: {}", username, e); + throw new RuntimeException("更新密码失败: " + e.getMessage()); + } + } + + /** + * 删除用户 + */ + public void deleteUser(String username) { + logger.info("删除用户: {}", username); + try { + User user = findByUsername(username); + if (user == null) { + throw new RuntimeException("用户不存在: " + username); + } + + ldapTemplate.delete(user); + logger.info("用户删除成功: {}", username); + } catch (Exception e) { + logger.error("删除用户失败: {}", username, e); + throw new RuntimeException("删除用户失败: " + e.getMessage()); + } + } + + /** + * 搜索用户 + */ + public List searchUsers(String keyword) { + logger.info("搜索用户: {}", keyword); + try { + LdapQuery query; + + if (keyword != null && !keyword.trim().isEmpty()) { + // 使用OrFilter进行多字段搜索 + OrFilter orFilter = new OrFilter(); + orFilter.or(new LikeFilter("uid", "*" + keyword + "*")); + orFilter.or(new LikeFilter("cn", "*" + keyword + "*")); + orFilter.or(new LikeFilter("mail", "*" + keyword + "*")); + orFilter.or(new LikeFilter("sn", "*" + keyword + "*")); + + AndFilter andFilter = new AndFilter(); + andFilter.and(new EqualsFilter("objectClass", "inetOrgPerson")); + andFilter.and(orFilter); + + query = LdapQueryBuilder.query() + .base(userSearchBase) + .filter(andFilter); + } else { + query = LdapQueryBuilder.query() + .base(userSearchBase) + .where("objectClass").is("inetOrgPerson"); + } + + return ldapTemplate.find(query, User.class); + } catch (Exception e) { + logger.error("搜索用户失败: {}", keyword, e); + throw new RuntimeException("搜索用户失败: " + e.getMessage()); + } + } + + /** + * 验证用户密码 + */ + public boolean validatePassword(String username, String password) { + logger.info("验证用户密码: {}", username); + try { + User user = findByUsername(username); + if (user == null) { + return false; + } + + return passwordEncoder.matches(password, user.getPassword()); + } catch (Exception e) { + logger.error("验证用户密码失败: {}", username, e); + return false; + } + } +} + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..2b4f661 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,53 @@ +server: + port: 8080 + # servlet: + # context-path: /ldap-demo + +spring: + application: + name: ldap-management-system + + # LDAP配置 + ldap: + urls: ldap://localhost:389 + base: dc=eryajf,dc=net + username: cn=admin,dc=eryajf,dc=net + password: 123456 + + # Thymeleaf配置 + thymeleaf: + cache: false + encoding: UTF-8 + mode: HTML + prefix: classpath:/templates/ + suffix: .html + +# 自定义LDAP配置 +ldap: + config: + # 用户DN模板 + user-dn-pattern: uid={0},ou=people + # 用户搜索基础 + user-search-base: ou=people + # 支持uid和cn两种格式的用户搜索 + user-search-filter: (|(uid={0})(cn={0})) + # 组搜索基础 - 设置为空字符串,因为组直接在根目录dc=eryajf,dc=net下 + group-search-base: "" + group-search-filter: (cn={0}) + group-role-attribute: cn + # 组成员属性 + group-member-attribute: member + # 用户对象类 + user-object-class: inetOrgPerson + # 组对象类 + group-object-class: groupOfNames + +# 日志配置 +logging: + level: + com.example.ldap: DEBUG + org.springframework.ldap: DEBUG + org.springframework.security: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" + diff --git a/src/main/resources/templates/groups.html b/src/main/resources/templates/groups.html new file mode 100644 index 0000000..be19396 --- /dev/null +++ b/src/main/resources/templates/groups.html @@ -0,0 +1,557 @@ + + + + + + 组管理 - LDAP管理系统 + + + + + + + + + +
+ +
+
+

组管理

+

管理LDAP目录中的所有组织结构

+
+
+ + +
+
+
+ + + + + +
+
+
+ + +
+
+ + +
+
+
+
+
组列表
+
+
+
+ + + + + + + + + + + + + +
组名描述类别成员数量操作
+
+
+
+
+
+
+ + + + + + + + + + + + + + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..6993268 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,169 @@ + + + + + + LDAP管理系统 + + + + + + + + + +
+ +
+
+
+

+ 欢迎使用LDAP管理系统 +

+

统一管理您的LDAP用户和组织架构

+
+

通过这个系统,您可以轻松地管理LDAP目录中的用户和组,执行增删改查等操作。

+
+
+
+ + +
+
+
+
+
+
+
用户总数
+

0

+
+
+ +
+
+
+
+
+
+
+
+
+
+
组总数
+

0

+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
+ +
+
用户管理
+

创建、编辑、删除和搜索LDAP用户,管理用户信息和密码。

+ + 进入用户管理 + +
+
+
+
+
+
+
+ +
+
组管理
+

创建和管理组织结构,添加或移除组成员,查看组关系。

+ + 进入组管理 + +
+
+
+
+ + +
+
+
+
+
+ API接口 +
+

系统提供完整的RESTful API接口,支持程序化调用。

+
+
+
用户API:
+
    +
  • GET /api/users - 获取所有用户
  • +
  • POST /api/users - 创建用户
  • +
  • PUT /api/users/{username} - 更新用户
  • +
  • DELETE /api/users/{username} - 删除用户
  • +
+
+
+
组API:
+
    +
  • GET /api/groups - 获取所有组
  • +
  • POST /api/groups - 创建组
  • +
  • PUT /api/groups/{groupName} - 更新组
  • +
  • DELETE /api/groups/{groupName} - 删除组
  • +
+
+
+
+
+
+
+
+ + +
+
+

© 2024 LDAP管理系统. 版权所有.

+
+
+ + + + diff --git a/src/main/resources/templates/users.html b/src/main/resources/templates/users.html new file mode 100644 index 0000000..aa2a49a --- /dev/null +++ b/src/main/resources/templates/users.html @@ -0,0 +1,500 @@ + + + + + + 用户管理 - LDAP管理系统 + + + + + + + + + +
+ +
+
+

用户管理

+

管理LDAP目录中的所有用户

+
+
+ + +
+
+
+ + + + + +
+
+
+ + +
+
+ + +
+
+
+
+
用户列表
+
+
+
+ + + + + + + + + + + + + + +
用户名姓名邮箱部门职位操作
+
+
+
+
+
+
+ + + + + + + + + + +