diff --git a/backend-java/README.md b/backend-java/README.md new file mode 100644 index 0000000..1dc3cb2 --- /dev/null +++ b/backend-java/README.md @@ -0,0 +1,148 @@ +# 宁夏智慧养殖监管平台 - Java后端 + +这是宁夏智慧养殖监管平台的Java微服务版本后端实现,使用Spring Boot框架构建。 + +## 技术栈 + +- **Java版本**: Java 17 +- **框架**: Spring Boot 3.1.0 +- **安全框架**: Spring Security +- **数据库**: MySQL 8.0+ +- **ORM框架**: Spring Data JPA (Hibernate) +- **API文档**: Springdoc OpenAPI (Swagger 3) +- **认证**: JWT (JSON Web Tokens) +- **密码加密**: BCrypt +- **构建工具**: Maven +- **服务器端口**: 5350 + +## 项目结构 + +``` +backend-java/ +├── src/ +│ ├── main/ +│ │ ├── java/ +│ │ │ └── com/nxxmdata/farmmonitor/ +│ │ │ ├── Application.java # 应用启动类 +│ │ │ ├── config/ # 配置类 +│ │ │ ├── controller/ # 控制器层 +│ │ │ ├── service/ # 服务层 +│ │ │ ├── repository/ # 数据访问层 +│ │ │ ├── model/ # 数据模型 +│ │ │ ├── dto/ # 数据传输对象 +│ │ │ ├── security/ # 安全相关 +│ │ │ └── exception/ # 异常处理 +│ │ └── resources/ +│ │ ├── application.yml # 应用配置 +│ │ └── application-dev.yml # 开发环境配置 +│ └── test/ # 测试代码 +├── pom.xml # Maven配置文件 +└── README.md # 项目说明文档 +``` + +## 功能模块 + +1. **用户认证模块** + - 用户登录/注册 + - JWT Token生成与验证 + - 密码加密存储 + +2. **用户管理模块** + - 用户信息管理 + - 角色权限管理 + +3. **农场管理模块** + - 农场信息管理 + - 地理位置信息处理 + +4. **设备监控模块** + - 设备状态监控 + - 传感器数据处理 + +5. **动物管理模块** + - 动物档案管理 + - 健康状态跟踪 + +6. **预警管理模块** + - 系统告警处理 + - 告警规则配置 + +7. **订单管理模块** + - 产品销售管理 + - 订单处理流程 + +8. **统计分析模块** + - 数据统计分析 + - 报表生成 + +## API文档 + +启动服务后,可通过以下地址访问API文档: + +- Swagger UI: http://localhost:5350/swagger-ui.html +- OpenAPI JSON: http://localhost:5350/v3/api-docs + +## 数据库配置 + +在 `application.yml` 文件中配置数据库连接: + +```yaml +spring: + datasource: + url: jdbc:mysql://192.168.0.240:3307/nxxmdata?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: aiot$Aiot123 + driver-class-name: com.mysql.cj.jdbc.Driver +``` + +## 快速开始 + +### 环境要求 + +- Java 17+ +- Maven 3.6+ +- MySQL 8.0+ + +### 构建项目 + +```bash +mvn clean package +``` + +### 运行项目 + +```bash +mvn spring-boot:run +``` + +或者 + +```bash +java -jar target/farm-monitor-1.0.0.jar +``` + +## 开发规范 + +1. 遵循RESTful API设计规范 +2. 使用JWT进行无状态认证 +3. 密码使用BCrypt加密存储 +4. 代码注释规范清晰 +5. API文档完整 + +## 安全设计 + +1. 使用JWT Token进行身份验证 +2. 密码使用BCrypt加密存储 +3. 基于角色的访问控制(RBAC) +4. SQL注入防护(使用JPA) +5. XSS防护(前端处理) + +## 性能优化 + +1. 数据库连接池配置 +2. Hibernate二级缓存(可选) +3. API响应时间监控 +4. 数据库查询优化 + +--- +*最后更新: 2025年9月* \ No newline at end of file diff --git a/backend-java/pom.xml b/backend-java/pom.xml new file mode 100644 index 0000000..3af9c75 --- /dev/null +++ b/backend-java/pom.xml @@ -0,0 +1,144 @@ + + + 4.0.0 + + com.nxxmdata + farm-monitor + 1.0.0 + jar + + 宁夏智慧养殖监管平台 + 宁夏智慧养殖监管平台Java微服务版本 + + + org.springframework.boot + spring-boot-starter-parent + 3.1.0 + + + + + 17 + 2022.0.3 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.springframework.boot + spring-boot-starter-security + + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + mysql + mysql-connector-java + 8.0.33 + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + \ No newline at end of file diff --git a/backend-java/src/main/java/Application.java b/backend-java/src/main/java/Application.java new file mode 100644 index 0000000..9b84ed4 --- /dev/null +++ b/backend-java/src/main/java/Application.java @@ -0,0 +1,14 @@ +package com.nxxmdata.farmmonitor; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.info.Info; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@OpenAPIDefinition(info = @Info(title = "宁夏智慧养殖监管平台", version = "1.0", description = "宁夏智慧养殖监管平台API文档")) +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/config/SecurityConfig.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/config/SecurityConfig.java new file mode 100644 index 0000000..d33038d --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/config/SecurityConfig.java @@ -0,0 +1,51 @@ +package com.nxxmdata.farmmonitor.config; + +import com.nxxmdata.farmmonitor.security.JwtAuthenticationFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.cors().and().csrf().disable() + .authorizeHttpRequests(authz -> authz + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/api-docs/**").permitAll() + .requestMatchers("/swagger-ui/**").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/config/SwaggerConfig.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/config/SwaggerConfig.java new file mode 100644 index 0000000..34779b4 --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/config/SwaggerConfig.java @@ -0,0 +1,33 @@ +package com.nxxmdata.farmmonitor.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("宁夏智慧养殖监管平台 API") + .version("1.0.0") + .description("宁夏智慧养殖监管平台后端API文档") + .license(new License().name("Apache 2.0") + .url("http://springdoc.org"))) + .addSecurityItem(new SecurityRequirement().addList("bearerAuth")) + .components(new Components() + .addSecuritySchemes("bearerAuth", + new SecurityScheme() + .name("bearerAuth") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/controller/AuthController.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/controller/AuthController.java new file mode 100644 index 0000000..f27a74d --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/controller/AuthController.java @@ -0,0 +1,68 @@ +package com.nxxmdata.farmmonitor.controller; + +import com.nxxmdata.farmmonitor.dto.AuthRequest; +import com.nxxmdata.farmmonitor.dto.AuthResponse; +import com.nxxmdata.farmmonitor.dto.RegisterRequest; +import com.nxxmdata.farmmonitor.model.User; +import com.nxxmdata.farmmonitor.service.AuthService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/auth") +@Tag(name = "Authentication", description = "用户认证相关接口") +public class AuthController { + + @Autowired + private AuthService authService; + + @PostMapping("/login") + @Operation(summary = "用户登录", description = "用户登录接口,成功后返回JWT Token") + public ResponseEntity login(@Valid @RequestBody AuthRequest authRequest) { + String token = authService.authenticateUser(authRequest.getUsername(), authRequest.getPassword()); + + AuthResponse response = new AuthResponse(); + if (token != null) { + response.setSuccess(true); + response.setMessage("登录成功"); + response.setToken(token); + + // 获取用户信息(不包含密码) + // 在实际应用中,应该从数据库查询用户信息 + User user = new User(); + user.setUsername(authRequest.getUsername()); + response.setUser(user); + } else { + response.setSuccess(false); + response.setMessage("用户名或密码错误"); + } + + return ResponseEntity.ok(response); + } + + @PostMapping("/register") + @Operation(summary = "用户注册", description = "用户注册接口") + public ResponseEntity register(@Valid @RequestBody RegisterRequest registerRequest) { + User user = authService.registerUser( + registerRequest.getUsername(), + registerRequest.getEmail(), + registerRequest.getPassword() + ); + + AuthResponse response = new AuthResponse(); + if (user != null) { + response.setSuccess(true); + response.setMessage("注册成功"); + response.setUser(user); + } else { + response.setSuccess(false); + response.setMessage("用户已存在"); + } + + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/dto/AuthRequest.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/dto/AuthRequest.java new file mode 100644 index 0000000..47b19f8 --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/dto/AuthRequest.java @@ -0,0 +1,9 @@ +package com.nxxmdata.farmmonitor.dto; + +import lombok.Data; + +@Data +public class AuthRequest { + private String username; + private String password; +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/dto/AuthResponse.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/dto/AuthResponse.java new file mode 100644 index 0000000..d4f694a --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/dto/AuthResponse.java @@ -0,0 +1,12 @@ +package com.nxxmdata.farmmonitor.dto; + +import com.nxxmdata.farmmonitor.model.User; +import lombok.Data; + +@Data +public class AuthResponse { + private boolean success; + private String message; + private String token; + private User user; +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/dto/RegisterRequest.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/dto/RegisterRequest.java new file mode 100644 index 0000000..ec9a690 --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/dto/RegisterRequest.java @@ -0,0 +1,10 @@ +package com.nxxmdata.farmmonitor.dto; + +import lombok.Data; + +@Data +public class RegisterRequest { + private String username; + private String email; + private String password; +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/exception/GlobalExceptionHandler.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..ff9d299 --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/exception/GlobalExceptionHandler.java @@ -0,0 +1,47 @@ +package com.nxxmdata.farmmonitor.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(UsernameNotFoundException.class) + public ResponseEntity handleUsernameNotFoundException(UsernameNotFoundException ex, WebRequest request) { + Map body = new HashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("message", "用户未找到: " + ex.getMessage()); + body.put("success", false); + + return new ResponseEntity<>(body, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity handleBadCredentialsException(BadCredentialsException ex, WebRequest request) { + Map body = new HashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("message", "用户名或密码错误"); + body.put("success", false); + + return new ResponseEntity<>(body, HttpStatus.UNAUTHORIZED); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleGenericException(Exception ex, WebRequest request) { + Map body = new HashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("message", "服务器内部错误: " + ex.getMessage()); + body.put("success", false); + + return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/model/Role.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/model/Role.java new file mode 100644 index 0000000..43aa6b4 --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/model/Role.java @@ -0,0 +1,33 @@ +package com.nxxmdata.farmmonitor.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "roles") +public class Role { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String name; + + private String description; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/model/User.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/model/User.java new file mode 100644 index 0000000..be03dad --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/model/User.java @@ -0,0 +1,87 @@ +package com.nxxmdata.farmmonitor.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "users") +public class User implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String username; + + @Column(unique = true, nullable = false) + private String email; + + @Column(nullable = false) + private String password; + + private String phone; + + private String avatar; + + @Enumerated(EnumType.STRING) + private UserStatus status = UserStatus.ACTIVE; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } + + // UserDetails接口实现 + @Override + public Collection getAuthorities() { + return List.of(); // 在实际应用中应该返回用户的角色列表 + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return status == UserStatus.ACTIVE; + } + + public enum UserStatus { + ACTIVE, INACTIVE, SUSPENDED + } +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/model/UserRole.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/model/UserRole.java new file mode 100644 index 0000000..a676e79 --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/model/UserRole.java @@ -0,0 +1,34 @@ +package com.nxxmdata.farmmonitor.model; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "user_roles") +public class UserRole { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_id") + private Long userId; + + @Column(name = "role_id") + private Long roleId; + + @Column(name = "assigned_at") + private LocalDateTime assignedAt; + + @PrePersist + protected void onCreate() { + assignedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/repository/RoleRepository.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/repository/RoleRepository.java new file mode 100644 index 0000000..39b8f71 --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/repository/RoleRepository.java @@ -0,0 +1,12 @@ +package com.nxxmdata.farmmonitor.repository; + +import com.nxxmdata.farmmonitor.model.Role; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface RoleRepository extends JpaRepository { + Optional findByName(String name); +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/repository/UserRepository.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/repository/UserRepository.java new file mode 100644 index 0000000..e84bf93 --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/repository/UserRepository.java @@ -0,0 +1,14 @@ +package com.nxxmdata.farmmonitor.repository; + +import com.nxxmdata.farmmonitor.model.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + Optional findByUsername(String username); + Optional findByEmail(String email); + Optional findByUsernameOrEmail(String username, String email); +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/repository/UserRoleRepository.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/repository/UserRoleRepository.java new file mode 100644 index 0000000..3bddddc --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/repository/UserRoleRepository.java @@ -0,0 +1,13 @@ +package com.nxxmdata.farmmonitor.repository; + +import com.nxxmdata.farmmonitor.model.UserRole; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface UserRoleRepository extends JpaRepository { + List findByUserId(Long userId); + List findByRoleId(Long roleId); +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/security/CustomUserDetailsService.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/security/CustomUserDetailsService.java new file mode 100644 index 0000000..db35881 --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/security/CustomUserDetailsService.java @@ -0,0 +1,29 @@ +package com.nxxmdata.farmmonitor.security; + +import com.nxxmdata.farmmonitor.model.User; +import com.nxxmdata.farmmonitor.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + @Autowired + private UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Optional userOptional = userRepository.findByUsernameOrEmail(username, username); + + if (userOptional.isEmpty()) { + throw new UsernameNotFoundException("User not found with username or email: " + username); + } + + return userOptional.get(); + } +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/security/JwtAuthenticationFilter.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..570ee9d --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/security/JwtAuthenticationFilter.java @@ -0,0 +1,66 @@ +package com.nxxmdata.farmmonitor.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + + final String requestTokenHeader = request.getHeader("Authorization"); + + String username = null; + String jwtToken = null; + + // JWT Token is in the form "Bearer token". Remove Bearer word and get only the Token + if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { + jwtToken = requestTokenHeader.substring(7); + try { + username = jwtUtil.getUsernameFromToken(jwtToken); + } catch (Exception e) { + logger.error("Unable to get JWT Token", e); + } + } + + // Once we get the token validate it. + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + + // if token is valid configure Spring Security to manually set authentication + if (jwtUtil.validateToken(jwtToken, userDetails.getUsername())) { + + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + usernamePasswordAuthenticationToken + .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + // After setting the Authentication in the context, we specify + // that the current user is authenticated. So it passes the + // Spring Security Configurations successfully. + SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); + } + } + chain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/security/JwtUtil.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/security/JwtUtil.java new file mode 100644 index 0000000..9df53a3 --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/security/JwtUtil.java @@ -0,0 +1,72 @@ +package com.nxxmdata.farmmonitor.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class JwtUtil { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private Long expiration; + + private SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(secret.getBytes()); + } + + public String getUsernameFromToken(String token) { + return getClaimFromToken(token, Claims::getSubject); + } + + public Date getExpirationDateFromToken(String token) { + return getClaimFromToken(token, Claims::getExpiration); + } + + public T getClaimFromToken(String token, Function claimsResolver) { + final Claims claims = getAllClaimsFromToken(token); + return claimsResolver.apply(claims); + } + + private Claims getAllClaimsFromToken(String token) { + return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody(); + } + + private Boolean isTokenExpired(String token) { + final Date expiration = getExpirationDateFromToken(token); + return expiration.before(new Date()); + } + + public String generateToken(String username, String email, Long id) { + Map claims = new HashMap<>(); + claims.put("email", email); + claims.put("id", id); + return doGenerateToken(claims, username); + } + + private String doGenerateToken(Map claims, String subject) { + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000)) + .signWith(getSigningKey(), SignatureAlgorithm.HS512) + .compact(); + } + + public Boolean validateToken(String token, String username) { + final String tokenUsername = getUsernameFromToken(token); + return (tokenUsername.equals(username) && !isTokenExpired(token)); + } +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/service/AuthService.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/service/AuthService.java new file mode 100644 index 0000000..cecbf05 --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/service/AuthService.java @@ -0,0 +1,63 @@ +package com.nxxmdata.farmmonitor.service; + +import com.nxxmdata.farmmonitor.model.User; +import com.nxxmdata.farmmonitor.security.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class AuthService { + + @Autowired + private UserService userService; + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private AuthenticationManager authenticationManager; + + public String authenticateUser(String username, String password) { + Optional userOptional = userService.findByUsernameOrEmail(username, username); + + if (userOptional.isEmpty()) { + return null; + } + + User user = userOptional.get(); + + // 使用AuthenticationManager进行认证 + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(username, password) + ); + SecurityContextHolder.getContext().setAuthentication(authentication); + } catch (Exception e) { + // 认证失败 + return null; + } + + // 生成JWT token + return jwtUtil.generateToken(user.getUsername(), user.getEmail(), user.getId()); + } + + public User registerUser(String username, String email, String password) { + // 检查用户是否已存在 + if (userService.findByUsernameOrEmail(username, email).isPresent()) { + return null; + } + + User user = new User(); + user.setUsername(username); + user.setEmail(email); + user.setPassword(password); + + return userService.saveUser(user); + } +} \ No newline at end of file diff --git a/backend-java/src/main/java/com/nxxmdata/farmmonitor/service/UserService.java b/backend-java/src/main/java/com/nxxmdata/farmmonitor/service/UserService.java new file mode 100644 index 0000000..a35482a --- /dev/null +++ b/backend-java/src/main/java/com/nxxmdata/farmmonitor/service/UserService.java @@ -0,0 +1,40 @@ +package com.nxxmdata.farmmonitor.service; + +import com.nxxmdata.farmmonitor.model.User; +import com.nxxmdata.farmmonitor.repository.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +public class UserService { + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + public Optional findByUsername(String username) { + return userRepository.findByUsername(username); + } + + public Optional findByEmail(String email) { + return userRepository.findByEmail(email); + } + + public Optional findByUsernameOrEmail(String username, String email) { + return userRepository.findByUsernameOrEmail(username, email); + } + + public User saveUser(User user) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + return userRepository.save(user); + } + + public boolean validatePassword(User user, String rawPassword) { + return passwordEncoder.matches(rawPassword, user.getPassword()); + } +} \ No newline at end of file diff --git a/backend-java/src/main/resources/application.yml b/backend-java/src/main/resources/application.yml new file mode 100644 index 0000000..dc88d26 --- /dev/null +++ b/backend-java/src/main/resources/application.yml @@ -0,0 +1,37 @@ +server: + port: 5350 + +spring: + application: + name: farm-monitor-service + datasource: + url: jdbc:mysql://192.168.0.240:3307/nxxmdata?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: aiot$Aiot123 + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 20 + minimum-idle: 5 + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 + + jpa: + hibernate: + ddl-auto: none + show-sql: true + database-platform: org.hibernate.dialect.MySQL8Dialect + + jackson: + time-zone: Asia/Shanghai + date-format: yyyy-MM-dd HH:mm:ss + +jwt: + secret: your_jwt_secret_key + expiration: 86400 + +logging: + level: + com.nxxmdata.farmmonitor: DEBUG + org.springframework: INFO + org.hibernate: INFO \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..22244cb --- /dev/null +++ b/docs/README.md @@ -0,0 +1,19 @@ +# 项目文档目录说明 + +本文档说明了项目中docs目录的结构和内容。 + +## 目录结构 + +- `config/` - 存放项目的所有配置文档,包括: + - 产品需求文档 (PRD.md) + - 系统架构文档 (arch.md) + - 系统设计文档 (design.md) + - 开发计划文档 (dev-plan.md) + - 任务文档 (task.md) + - 数据同步修复报告 (data-sync-fix-report.md) + - 农场数据导入摘要 (farms-data-import-summary.md) + - 位置数据验证报告 (location-data-validation-report.md) + +- 其他技术文档 + - 百度地图许可说明 (baidu-map-license.md) + - 性能监控说明 (performance-monitoring.md) \ No newline at end of file diff --git a/PRD.md b/docs/config/PRD.md similarity index 100% rename from PRD.md rename to docs/config/PRD.md diff --git a/arch.md b/docs/config/arch.md similarity index 100% rename from arch.md rename to docs/config/arch.md diff --git a/backend/data-sync-fix-report.md b/docs/config/data-sync-fix-report.md similarity index 100% rename from backend/data-sync-fix-report.md rename to docs/config/data-sync-fix-report.md diff --git a/design.md b/docs/config/design.md similarity index 100% rename from design.md rename to docs/config/design.md diff --git a/dev-plan.md b/docs/config/dev-plan.md similarity index 100% rename from dev-plan.md rename to docs/config/dev-plan.md diff --git a/backend/farms-data-import-summary.md b/docs/config/farms-data-import-summary.md similarity index 100% rename from backend/farms-data-import-summary.md rename to docs/config/farms-data-import-summary.md diff --git a/backend/location-data-validation-report.md b/docs/config/location-data-validation-report.md similarity index 100% rename from backend/location-data-validation-report.md rename to docs/config/location-data-validation-report.md diff --git a/task.md b/docs/config/task.md similarity index 100% rename from task.md rename to docs/config/task.md