fix: first

This commit is contained in:
Guwan 2025-08-28 07:24:46 +08:00
commit 94c219f139
19 changed files with 3257 additions and 0 deletions

62
.gitignore vendored Normal file
View File

@ -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/

242
README.md Normal file
View File

@ -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 <repository-url>
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 许可证。

110
pom.xml Normal file
View File

@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>ldap-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>LDAP Management System</name>
<description>Spring Boot application for LDAP user and group management</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- LDAP Support -->
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Embedded LDAP for testing -->
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<scope>test</scope>
</dependency>
<!-- Development Tools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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<ApiResponse<List<Group>>> getAllGroups() {
logger.info("API: 获取所有组");
List<Group> groups = groupService.getAllGroups();
return ResponseEntity.ok(ApiResponse.success("获取组列表成功", groups));
}
/**
* 根据组名获取组
*/
@GetMapping("/{groupName}")
public ResponseEntity<ApiResponse<Group>> 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<ApiResponse<Group>> 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<ApiResponse<Group>> 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<ApiResponse<String>> deleteGroup(@PathVariable String groupName) {
logger.info("API: 删除组 {}", groupName);
groupService.deleteGroup(groupName);
return ResponseEntity.ok(ApiResponse.success("组删除成功"));
}
/**
* 添加用户到组
*/
@PostMapping("/{groupName}/members/{username}")
public ResponseEntity<ApiResponse<String>> 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<ApiResponse<String>> 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<ApiResponse<Set<String>>> getGroupMembers(@PathVariable String groupName) {
logger.info("API: 获取组 {} 的成员", groupName);
Set<String> members = groupService.getGroupMembers(groupName);
return ResponseEntity.ok(ApiResponse.success("获取组成员成功", members));
}
/**
* 获取用户所属的所有组
*/
@GetMapping("/user/{username}")
public ResponseEntity<ApiResponse<List<Group>>> getUserGroups(@PathVariable String username) {
logger.info("API: 获取用户 {} 所属的组", username);
List<Group> groups = groupService.getUserGroups(username);
return ResponseEntity.ok(ApiResponse.success("获取用户所属组成功", groups));
}
/**
* 搜索组
*/
@GetMapping("/search")
public ResponseEntity<ApiResponse<List<Group>>> searchGroups(
@RequestParam(required = false) String keyword) {
logger.info("API: 搜索组 {}", keyword);
List<Group> groups = groupService.searchGroups(keyword);
return ResponseEntity.ok(ApiResponse.success("搜索完成", groups));
}
/**
* 批量添加用户到组
*/
@PostMapping("/{groupName}/members/batch")
public ResponseEntity<ApiResponse<String>> addUsersToGroup(
@PathVariable String groupName,
@RequestBody Map<String, List<String>> data) {
logger.info("API: 批量添加用户到组 {}", groupName);
List<String> 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<ApiResponse<String>> removeUsersFromGroup(
@PathVariable String groupName,
@RequestBody Map<String, List<String>> data) {
logger.info("API: 批量从组 {} 移除用户", groupName);
List<String> 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<ApiResponse<Map<String, Object>>> getGroupStatistics() {
logger.info("API: 获取组统计信息");
List<Group> allGroups = groupService.getAllGroups();
int totalMembers = allGroups.stream()
.mapToInt(g -> g.getMembers() != null ? g.getMembers().size() : 0)
.sum();
Map<String, Object> 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));
}
}

View File

@ -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<ApiResponse<List<User>>> getAllUsers() {
logger.info("API: 获取所有用户");
List<User> users = userService.getAllUsers();
return ResponseEntity.ok(ApiResponse.success("获取用户列表成功", users));
}
/**
* 根据用户名获取用户
*/
@GetMapping("/{username}")
public ResponseEntity<ApiResponse<User>> 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<ApiResponse<User>> 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<ApiResponse<User>> 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<ApiResponse<String>> updatePassword(
@PathVariable String username,
@RequestBody Map<String, String> 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<ApiResponse<String>> deleteUser(@PathVariable String username) {
logger.info("API: 删除用户 {}", username);
userService.deleteUser(username);
return ResponseEntity.ok(ApiResponse.success("用户删除成功"));
}
/**
* 搜索用户
*/
@GetMapping("/search")
public ResponseEntity<ApiResponse<List<User>>> searchUsers(
@RequestParam(required = false) String keyword) {
logger.info("API: 搜索用户 {}", keyword);
List<User> users = userService.searchUsers(keyword);
return ResponseEntity.ok(ApiResponse.success("搜索完成", users));
}
/**
* 验证用户密码
*/
@PostMapping("/{username}/validate")
public ResponseEntity<ApiResponse<Boolean>> validatePassword(
@PathVariable String username,
@RequestBody Map<String, String> 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<ApiResponse<List<User>>> createUsers(@Valid @RequestBody List<User> users) {
logger.info("API: 批量创建用户,数量: {}", users.size());
List<User> createdUsers = users.stream()
.map(userService::createUser)
.toList();
return ResponseEntity.ok(ApiResponse.success("批量创建用户成功", createdUsers));
}
/**
* 获取用户统计信息
*/
@GetMapping("/statistics")
public ResponseEntity<ApiResponse<Map<String, Object>>> getUserStatistics() {
logger.info("API: 获取用户统计信息");
List<User> allUsers = userService.getAllUsers();
Map<String, Object> 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));
}
}

View File

@ -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";
}
}

View File

@ -0,0 +1,97 @@
package com.example.ldap.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private String error;
private long timestamp;
public ApiResponse() {
this.timestamp = System.currentTimeMillis();
}
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(true);
response.setData(data);
response.setMessage("操作成功");
return response;
}
public static <T> ApiResponse<T> success(String message, T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(true);
response.setMessage(message);
response.setData(data);
return response;
}
public static <T> ApiResponse<T> success(String message) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(true);
response.setMessage(message);
return response;
}
public static <T> ApiResponse<T> error(String error) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(false);
response.setError(error);
response.setMessage("操作失败");
return response;
}
public static <T> ApiResponse<T> error(String message, String error) {
ApiResponse<T> 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;
}
}

View File

@ -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<String> 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<String> getMembers() {
return members;
}
public void setMembers(Set<String> 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) +
'}';
}
}

View File

@ -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<String> 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<String> getMemberOf() {
return memberOf;
}
public void setMemberOf(List<String> memberOf) {
this.memberOf = memberOf;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", commonName='" + commonName + '\'' +
", surname='" + surname + '\'' +
", email='" + email + '\'' +
", title='" + title + '\'' +
", department='" + department + '\'' +
'}';
}
}

View File

@ -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<ApiResponse<Object>> handleRuntimeException(RuntimeException e) {
logger.error("运行时异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("系统错误", e.getMessage()));
}
/**
* 处理参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Object>> handleValidationException(MethodArgumentNotValidException e) {
logger.error("参数验证失败", e);
Map<String, String> 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<ApiResponse<Object>> handleBindException(BindException e) {
logger.error("参数绑定失败", e);
Map<String, String> 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<ApiResponse<Object>> handleConstraintViolationException(ConstraintViolationException e) {
logger.error("约束违反", e);
Map<String, String> 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<ApiResponse<Object>> handleIllegalArgumentException(IllegalArgumentException e) {
logger.error("非法参数", e);
return ResponseEntity.badRequest()
.body(ApiResponse.error("参数错误", e.getMessage()));
}
/**
* 处理通用异常
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Object>> handleGenericException(Exception e) {
logger.error("未知异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("系统异常", "请联系管理员"));
}
}

View File

@ -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<Group> 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<Group> 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<String> getGroupMembers(String groupName) {
logger.info("获取组 {} 的成员", groupName);
try {
Group group = findByName(groupName);
if (group == null) {
throw new RuntimeException("组不存在: " + groupName);
}
Set<String> 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<Group> 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<Group> 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());
}
}
}

View File

@ -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<User> getAllUsers() {
logger.info("获取所有用户");
try {
// 使用更具体的查询避免重复条目问题
LdapQuery query = LdapQueryBuilder.query()
.base(userSearchBase)
.where("objectClass").is("inetOrgPerson")
.and("uid").isPresent(); // 只查找有uid属性的用户
List<User> 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<User> 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<User> 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;
}
}
}

View File

@ -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"

View File

@ -0,0 +1,557 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>组管理 - LDAP管理系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.table-hover tbody tr:hover {
background-color: #f8f9fa;
}
.btn-group-sm .btn {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.member-badge {
font-size: 0.75rem;
}
</style>
</head>
<body class="bg-light">
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/web/">
<i class="fas fa-users-cog me-2"></i>LDAP管理系统
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/web/users">
<i class="fas fa-users me-1"></i>用户管理
</a>
<a class="nav-link active" href="/web/groups">
<i class="fas fa-layer-group me-1"></i>组管理
</a>
</div>
</div>
</nav>
<!-- 主要内容 -->
<div class="container mt-4">
<!-- 页面标题 -->
<div class="row mb-4">
<div class="col-12">
<h2><i class="fas fa-layer-group me-2"></i>组管理</h2>
<p class="text-muted">管理LDAP目录中的所有组织结构</p>
</div>
</div>
<!-- 操作工具栏 -->
<div class="row mb-4">
<div class="col-md-8">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" id="searchInput" placeholder="搜索组名或描述...">
<button class="btn btn-outline-secondary" type="button" onclick="searchGroups()">搜索</button>
</div>
</div>
<div class="col-md-4 text-end">
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#createGroupModal">
<i class="fas fa-plus me-1"></i>创建组
</button>
<button type="button" class="btn btn-secondary" onclick="refreshGroups()">
<i class="fas fa-refresh me-1"></i>刷新
</button>
</div>
</div>
<!-- 组列表 -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">组列表</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>组名</th>
<th>描述</th>
<th>类别</th>
<th>成员数量</th>
<th>操作</th>
</tr>
</thead>
<tbody id="groupTableBody">
<!-- 组数据将通过JavaScript动态加载 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 创建组模态框 -->
<div class="modal fade" id="createGroupModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">创建新组</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createGroupForm">
<div class="mb-3">
<label for="groupName" class="form-label">组名 *</label>
<input type="text" class="form-control" id="groupName" required>
</div>
<div class="mb-3">
<label for="groupDescription" class="form-label">描述</label>
<textarea class="form-control" id="groupDescription" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="groupCategory" class="form-label">类别</label>
<input type="text" class="form-control" id="groupCategory" placeholder="如:部门、项目组等">
</div>
<div class="mb-3">
<label for="groupOu" class="form-label">组织单位</label>
<input type="text" class="form-control" id="groupOu">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-success" onclick="createGroup()">创建组</button>
</div>
</div>
</div>
</div>
<!-- 编辑组模态框 -->
<div class="modal fade" id="editGroupModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑组</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editGroupForm">
<input type="hidden" id="editGroupName">
<div class="mb-3">
<label for="editGroupDescription" class="form-label">描述</label>
<textarea class="form-control" id="editGroupDescription" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="editGroupCategory" class="form-label">类别</label>
<input type="text" class="form-control" id="editGroupCategory">
</div>
<div class="mb-3">
<label for="editGroupOu" class="form-label">组织单位</label>
<input type="text" class="form-control" id="editGroupOu">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-success" onclick="updateGroup()">保存更改</button>
</div>
</div>
</div>
</div>
<!-- 管理组成员模态框 -->
<div class="modal fade" id="manageMembersModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">管理组成员</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="currentGroupName">
<div class="row">
<div class="col-md-6">
<h6>当前成员</h6>
<div class="border rounded p-3" style="height: 300px; overflow-y: auto;">
<div id="currentMembers">
<!-- 当前成员列表 -->
</div>
</div>
</div>
<div class="col-md-6">
<h6>添加新成员</h6>
<div class="mb-2">
<div class="input-group input-group-sm">
<input type="text" class="form-control" id="newMemberInput" placeholder="用户名">
<button class="btn btn-outline-primary" type="button" onclick="addMember()">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<div class="border rounded p-3" style="height: 250px; overflow-y: auto;">
<div id="availableUsers">
<!-- 可用用户列表 -->
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 页面加载时获取组列表
document.addEventListener('DOMContentLoaded', function() {
loadGroups();
});
// 加载组列表
function loadGroups() {
fetch('/api/groups')
.then(response => response.json())
.then(data => {
if (data.success) {
displayGroups(data.data);
} else {
showAlert('获取组列表失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
// 显示组列表
function displayGroups(groups) {
const tbody = document.getElementById('groupTableBody');
tbody.innerHTML = '';
groups.forEach(group => {
const memberCount = group.members ? group.members.filter(m => m !== 'cn=dummy').length : 0;
const row = tbody.insertRow();
row.innerHTML = `
<td>${group.name || ''}</td>
<td>${group.description || ''}</td>
<td>${group.category || ''}</td>
<td>
<span class="badge bg-info member-badge">${memberCount} 人</span>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-info" onclick="manageMembers('${group.name}')" title="管理成员">
<i class="fas fa-users"></i>
</button>
<button type="button" class="btn btn-outline-primary" onclick="editGroup('${group.name}')" title="编辑">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger" onclick="deleteGroup('${group.name}')" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
`;
});
}
// 创建组
function createGroup() {
const groupData = {
name: document.getElementById('groupName').value,
description: document.getElementById('groupDescription').value,
category: document.getElementById('groupCategory').value,
organizationalUnit: document.getElementById('groupOu').value
};
fetch('/api/groups', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(groupData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('组创建成功', 'success');
document.getElementById('createGroupForm').reset();
bootstrap.Modal.getInstance(document.getElementById('createGroupModal')).hide();
loadGroups();
} else {
showAlert('创建组失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
// 编辑组
function editGroup(groupName) {
fetch(`/api/groups/${groupName}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const group = data.data;
document.getElementById('editGroupName').value = group.name;
document.getElementById('editGroupDescription').value = group.description || '';
document.getElementById('editGroupCategory').value = group.category || '';
document.getElementById('editGroupOu').value = group.organizationalUnit || '';
new bootstrap.Modal(document.getElementById('editGroupModal')).show();
} else {
showAlert('获取组信息失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
// 更新组
function updateGroup() {
const groupName = document.getElementById('editGroupName').value;
const groupData = {
description: document.getElementById('editGroupDescription').value,
category: document.getElementById('editGroupCategory').value,
organizationalUnit: document.getElementById('editGroupOu').value
};
fetch(`/api/groups/${groupName}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(groupData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('组更新成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('editGroupModal')).hide();
loadGroups();
} else {
showAlert('更新组失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
// 删除组
function deleteGroup(groupName) {
if (confirm(`确定要删除组 "${groupName}" 吗?此操作不可撤销。`)) {
fetch(`/api/groups/${groupName}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('组删除成功', 'success');
loadGroups();
} else {
showAlert('删除组失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
}
// 管理组成员
function manageMembers(groupName) {
document.getElementById('currentGroupName').value = groupName;
// 加载当前成员
loadCurrentMembers(groupName);
// 加载所有用户
loadAvailableUsers();
new bootstrap.Modal(document.getElementById('manageMembersModal')).show();
}
// 加载当前成员
function loadCurrentMembers(groupName) {
fetch(`/api/groups/${groupName}/members`)
.then(response => response.json())
.then(data => {
if (data.success) {
const membersDiv = document.getElementById('currentMembers');
membersDiv.innerHTML = '';
if (data.data && data.data.length > 0) {
data.data.forEach(member => {
const memberDiv = document.createElement('div');
memberDiv.className = 'mb-2 d-flex justify-content-between align-items-center';
memberDiv.innerHTML = `
<span>${member}</span>
<button class="btn btn-sm btn-outline-danger" onclick="removeMember('${member}')">
<i class="fas fa-times"></i>
</button>
`;
membersDiv.appendChild(memberDiv);
});
} else {
membersDiv.innerHTML = '<p class="text-muted">暂无成员</p>';
}
}
})
.catch(error => {
showAlert('加载成员失败: ' + error.message, 'danger');
});
}
// 加载可用用户
function loadAvailableUsers() {
fetch('/api/users')
.then(response => response.json())
.then(data => {
if (data.success) {
const usersDiv = document.getElementById('availableUsers');
usersDiv.innerHTML = '';
data.data.forEach(user => {
const userDiv = document.createElement('div');
userDiv.className = 'mb-2 d-flex justify-content-between align-items-center';
userDiv.innerHTML = `
<span>${user.username} (${user.commonName || ''})</span>
<button class="btn btn-sm btn-outline-primary" onclick="addUserToGroup('${user.username}')">
<i class="fas fa-plus"></i>
</button>
`;
usersDiv.appendChild(userDiv);
});
}
})
.catch(error => {
showAlert('加载用户失败: ' + error.message, 'danger');
});
}
// 添加成员
function addMember() {
const username = document.getElementById('newMemberInput').value.trim();
if (username) {
addUserToGroup(username);
document.getElementById('newMemberInput').value = '';
}
}
// 添加用户到组
function addUserToGroup(username) {
const groupName = document.getElementById('currentGroupName').value;
fetch(`/api/groups/${groupName}/members/${username}`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('用户添加成功', 'success');
loadCurrentMembers(groupName);
} else {
showAlert('添加用户失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
// 移除成员
function removeMember(username) {
const groupName = document.getElementById('currentGroupName').value;
if (confirm(`确定要从组中移除用户 "${username}" 吗?`)) {
fetch(`/api/groups/${groupName}/members/${username}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('用户移除成功', 'success');
loadCurrentMembers(groupName);
} else {
showAlert('移除用户失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
}
// 搜索组
function searchGroups() {
const keyword = document.getElementById('searchInput').value;
const url = keyword ? `/api/groups/search?keyword=${encodeURIComponent(keyword)}` : '/api/groups';
fetch(url)
.then(response => response.json())
.then(data => {
if (data.success) {
displayGroups(data.data);
} else {
showAlert('搜索失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
// 刷新组列表
function refreshGroups() {
document.getElementById('searchInput').value = '';
loadGroups();
}
// 显示提示信息
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
const container = document.querySelector('.container');
container.insertBefore(alertDiv, container.firstChild);
// 3秒后自动消失
setTimeout(() => {
alertDiv.remove();
}, 3000);
}
// 回车搜索
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
searchGroups();
}
});
// 回车添加成员
document.getElementById('newMemberInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
addMember();
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LDAP管理系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.card-hover:hover {
transform: translateY(-5px);
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
transition: all 0.3s ease;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.feature-card {
border: none;
border-radius: 15px;
}
</style>
</head>
<body class="bg-light">
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/web/">
<i class="fas fa-users-cog me-2"></i>LDAP管理系统
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link" href="/web/users">
<i class="fas fa-users me-1"></i>用户管理
</a>
<a class="nav-link" href="/web/groups">
<i class="fas fa-layer-group me-1"></i>组管理
</a>
</div>
</div>
</nav>
<!-- 主要内容 -->
<div class="container mt-4">
<!-- 欢迎信息 -->
<div class="row mb-4">
<div class="col-12">
<div class="jumbotron bg-white p-5 rounded shadow-sm">
<h1 class="display-4">
<i class="fas fa-home me-3 text-primary"></i>欢迎使用LDAP管理系统
</h1>
<p class="lead">统一管理您的LDAP用户和组织架构</p>
<hr class="my-4">
<p>通过这个系统您可以轻松地管理LDAP目录中的用户和组执行增删改查等操作。</p>
</div>
</div>
</div>
<!-- 统计卡片 -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card stat-card card-hover">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">用户总数</h5>
<h2 class="mb-0" th:text="${userCount}">0</h2>
</div>
<div class="text-white-50">
<i class="fas fa-users fa-3x"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card stat-card card-hover">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="card-title">组总数</h5>
<h2 class="mb-0" th:text="${groupCount}">0</h2>
</div>
<div class="text-white-50">
<i class="fas fa-layer-group fa-3x"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 功能卡片 -->
<div class="row">
<div class="col-md-6 mb-4">
<div class="card feature-card card-hover h-100">
<div class="card-body text-center">
<div class="mb-3">
<i class="fas fa-user-plus fa-4x text-primary"></i>
</div>
<h5 class="card-title">用户管理</h5>
<p class="card-text">创建、编辑、删除和搜索LDAP用户管理用户信息和密码。</p>
<a href="/web/users" class="btn btn-primary">
<i class="fas fa-arrow-right me-1"></i>进入用户管理
</a>
</div>
</div>
</div>
<div class="col-md-6 mb-4">
<div class="card feature-card card-hover h-100">
<div class="card-body text-center">
<div class="mb-3">
<i class="fas fa-sitemap fa-4x text-success"></i>
</div>
<h5 class="card-title">组管理</h5>
<p class="card-text">创建和管理组织结构,添加或移除组成员,查看组关系。</p>
<a href="/web/groups" class="btn btn-success">
<i class="fas fa-arrow-right me-1"></i>进入组管理
</a>
</div>
</div>
</div>
</div>
<!-- API文档链接 -->
<div class="row mt-4">
<div class="col-12">
<div class="card">
<div class="card-body">
<h5 class="card-title">
<i class="fas fa-code me-2"></i>API接口
</h5>
<p class="card-text">系统提供完整的RESTful API接口支持程序化调用。</p>
<div class="row">
<div class="col-md-6">
<h6>用户API</h6>
<ul class="list-unstyled">
<li><code>GET /api/users</code> - 获取所有用户</li>
<li><code>POST /api/users</code> - 创建用户</li>
<li><code>PUT /api/users/{username}</code> - 更新用户</li>
<li><code>DELETE /api/users/{username}</code> - 删除用户</li>
</ul>
</div>
<div class="col-md-6">
<h6>组API</h6>
<ul class="list-unstyled">
<li><code>GET /api/groups</code> - 获取所有组</li>
<li><code>POST /api/groups</code> - 创建组</li>
<li><code>PUT /api/groups/{groupName}</code> - 更新组</li>
<li><code>DELETE /api/groups/{groupName}</code> - 删除组</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 页脚 -->
<footer class="bg-dark text-white text-center py-3 mt-5">
<div class="container">
<p class="mb-0">&copy; 2024 LDAP管理系统. 版权所有.</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,500 @@
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理 - LDAP管理系统</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
.table-hover tbody tr:hover {
background-color: #f8f9fa;
}
.btn-group-sm .btn {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
</style>
</head>
<body class="bg-light">
<!-- 导航栏 -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/web/">
<i class="fas fa-users-cog me-2"></i>LDAP管理系统
</a>
<div class="navbar-nav ms-auto">
<a class="nav-link active" href="/web/users">
<i class="fas fa-users me-1"></i>用户管理
</a>
<a class="nav-link" href="/web/groups">
<i class="fas fa-layer-group me-1"></i>组管理
</a>
</div>
</div>
</nav>
<!-- 主要内容 -->
<div class="container mt-4">
<!-- 页面标题 -->
<div class="row mb-4">
<div class="col-12">
<h2><i class="fas fa-users me-2"></i>用户管理</h2>
<p class="text-muted">管理LDAP目录中的所有用户</p>
</div>
</div>
<!-- 操作工具栏 -->
<div class="row mb-4">
<div class="col-md-8">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input type="text" class="form-control" id="searchInput" placeholder="搜索用户名、姓名或邮箱...">
<button class="btn btn-outline-secondary" type="button" onclick="searchUsers()">搜索</button>
</div>
</div>
<div class="col-md-4 text-end">
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createUserModal">
<i class="fas fa-plus me-1"></i>创建用户
</button>
<button type="button" class="btn btn-secondary" onclick="refreshUsers()">
<i class="fas fa-refresh me-1"></i>刷新
</button>
</div>
</div>
<!-- 用户列表 -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">用户列表</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>用户名</th>
<th>姓名</th>
<th>邮箱</th>
<th>部门</th>
<th>职位</th>
<th>操作</th>
</tr>
</thead>
<tbody id="userTableBody">
<!-- 用户数据将通过JavaScript动态加载 -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 创建用户模态框 -->
<div class="modal fade" id="createUserModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">创建新用户</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="createUserForm">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="username" class="form-label">用户名 *</label>
<input type="text" class="form-control" id="username" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="password" class="form-label">密码 *</label>
<input type="password" class="form-control" id="password" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="commonName" class="form-label">姓名 *</label>
<input type="text" class="form-control" id="commonName" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="surname" class="form-label">姓氏 *</label>
<input type="text" class="form-control" id="surname" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="givenName" class="form-label">名字</label>
<input type="text" class="form-control" id="givenName">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="email" class="form-label">邮箱</label>
<input type="email" class="form-control" id="email">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="phoneNumber" class="form-label">电话</label>
<input type="text" class="form-control" id="phoneNumber">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="title" class="form-label">职位</label>
<input type="text" class="form-control" id="title">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="department" class="form-label">部门</label>
<input type="text" class="form-control" id="department">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="employeeNumber" class="form-label">工号</label>
<input type="text" class="form-control" id="employeeNumber">
</div>
</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">描述</label>
<textarea class="form-control" id="description" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="createUser()">创建用户</button>
</div>
</div>
</div>
</div>
<!-- 编辑用户模态框 -->
<div class="modal fade" id="editUserModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">编辑用户</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="editUserForm">
<input type="hidden" id="editUsername">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editCommonName" class="form-label">姓名</label>
<input type="text" class="form-control" id="editCommonName">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="editSurname" class="form-label">姓氏</label>
<input type="text" class="form-control" id="editSurname">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editGivenName" class="form-label">名字</label>
<input type="text" class="form-control" id="editGivenName">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="editEmail" class="form-label">邮箱</label>
<input type="email" class="form-control" id="editEmail">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editPhoneNumber" class="form-label">电话</label>
<input type="text" class="form-control" id="editPhoneNumber">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="editTitle" class="form-label">职位</label>
<input type="text" class="form-control" id="editTitle">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="editDepartment" class="form-label">部门</label>
<input type="text" class="form-control" id="editDepartment">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="editEmployeeNumber" class="form-label">工号</label>
<input type="text" class="form-control" id="editEmployeeNumber">
</div>
</div>
</div>
<div class="mb-3">
<label for="editDescription" class="form-label">描述</label>
<textarea class="form-control" id="editDescription" rows="3"></textarea>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="updateUser()">保存更改</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
// 页面加载时获取用户列表
document.addEventListener('DOMContentLoaded', function() {
loadUsers();
});
// 加载用户列表
function loadUsers() {
fetch('/api/users')
.then(response => response.json())
.then(data => {
if (data.success) {
displayUsers(data.data);
} else {
showAlert('获取用户列表失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
// 显示用户列表
function displayUsers(users) {
const tbody = document.getElementById('userTableBody');
tbody.innerHTML = '';
users.forEach(user => {
const row = tbody.insertRow();
row.innerHTML = `
<td>${user.username || ''}</td>
<td>${user.commonName || ''}</td>
<td>${user.email || ''}</td>
<td>${user.department || ''}</td>
<td>${user.title || ''}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" onclick="editUser('${user.username}')">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger" onclick="deleteUser('${user.username}')">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
`;
});
}
// 创建用户
function createUser() {
const userData = {
username: document.getElementById('username').value,
password: document.getElementById('password').value,
commonName: document.getElementById('commonName').value,
surname: document.getElementById('surname').value,
givenName: document.getElementById('givenName').value,
email: document.getElementById('email').value,
phoneNumber: document.getElementById('phoneNumber').value,
title: document.getElementById('title').value,
department: document.getElementById('department').value,
employeeNumber: document.getElementById('employeeNumber').value,
description: document.getElementById('description').value
};
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('用户创建成功', 'success');
document.getElementById('createUserForm').reset();
bootstrap.Modal.getInstance(document.getElementById('createUserModal')).hide();
loadUsers();
} else {
showAlert('创建用户失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
// 编辑用户
function editUser(username) {
fetch(`/api/users/${username}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const user = data.data;
document.getElementById('editUsername').value = user.username;
document.getElementById('editCommonName').value = user.commonName || '';
document.getElementById('editSurname').value = user.surname || '';
document.getElementById('editGivenName').value = user.givenName || '';
document.getElementById('editEmail').value = user.email || '';
document.getElementById('editPhoneNumber').value = user.phoneNumber || '';
document.getElementById('editTitle').value = user.title || '';
document.getElementById('editDepartment').value = user.department || '';
document.getElementById('editEmployeeNumber').value = user.employeeNumber || '';
document.getElementById('editDescription').value = user.description || '';
new bootstrap.Modal(document.getElementById('editUserModal')).show();
} else {
showAlert('获取用户信息失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
// 更新用户
function updateUser() {
const username = document.getElementById('editUsername').value;
const userData = {
commonName: document.getElementById('editCommonName').value,
surname: document.getElementById('editSurname').value,
givenName: document.getElementById('editGivenName').value,
email: document.getElementById('editEmail').value,
phoneNumber: document.getElementById('editPhoneNumber').value,
title: document.getElementById('editTitle').value,
department: document.getElementById('editDepartment').value,
employeeNumber: document.getElementById('editEmployeeNumber').value,
description: document.getElementById('editDescription').value
};
fetch(`/api/users/${username}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('用户更新成功', 'success');
bootstrap.Modal.getInstance(document.getElementById('editUserModal')).hide();
loadUsers();
} else {
showAlert('更新用户失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
// 删除用户
function deleteUser(username) {
if (confirm(`确定要删除用户 "${username}" 吗?此操作不可撤销。`)) {
fetch(`/api/users/${username}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
showAlert('用户删除成功', 'success');
loadUsers();
} else {
showAlert('删除用户失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
}
// 搜索用户
function searchUsers() {
const keyword = document.getElementById('searchInput').value;
const url = keyword ? `/api/users/search?keyword=${encodeURIComponent(keyword)}` : '/api/users';
fetch(url)
.then(response => response.json())
.then(data => {
if (data.success) {
displayUsers(data.data);
} else {
showAlert('搜索失败: ' + data.error, 'danger');
}
})
.catch(error => {
showAlert('网络错误: ' + error.message, 'danger');
});
}
// 刷新用户列表
function refreshUsers() {
document.getElementById('searchInput').value = '';
loadUsers();
}
// 显示提示信息
function showAlert(message, type) {
const alertDiv = document.createElement('div');
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
alertDiv.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
const container = document.querySelector('.container');
container.insertBefore(alertDiv, container.firstChild);
// 3秒后自动消失
setTimeout(() => {
alertDiv.remove();
}, 3000);
}
// 回车搜索
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
searchUsers();
}
});
</script>
</body>
</html>