diff --git a/module-auth/pom.xml b/module-auth/pom.xml
index 6b884c8..4092e3e 100644
--- a/module-auth/pom.xml
+++ b/module-auth/pom.xml
@@ -33,6 +33,11 @@
spring-boot-starter-security
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
org.springframework.boot
spring-boot-starter-data-jpa
@@ -98,4 +103,4 @@
test
-
+
\ No newline at end of file
diff --git a/module-auth/src/main/java/com/ether/pms/auth/config/SecurityConfig.java b/module-auth/src/main/java/com/ether/pms/auth/config/SecurityConfig.java
index f213211..042819f 100644
--- a/module-auth/src/main/java/com/ether/pms/auth/config/SecurityConfig.java
+++ b/module-auth/src/main/java/com/ether/pms/auth/config/SecurityConfig.java
@@ -1,69 +1,143 @@
package com.ether.pms.auth.config;
import com.ether.pms.auth.util.JwtTokenProvider;
+import com.ether.pms.auth.util.SecurityUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
-import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.access.intercept.AuthorizationFilter;
+import org.springframework.security.web.context.SecurityContextRepository;
+import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
-import java.util.Collections;
+import java.util.List;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
+@Slf4j
public class SecurityConfig {
-
+
private final JwtTokenProvider jwtTokenProvider;
-
+
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
- .csrf(csrf -> csrf.disable())
- .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ .csrf(AbstractHttpConfigurer::disable)
+ .cors(cors -> cors.configurationSource(corsConfigurationSource()))
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
+ .securityContext(context -> context
+ .securityContextRepository(securityContextRepository())
+ )
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login", "/api/auth/refresh").permitAll()
.requestMatchers("/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
)
- .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
-
+ .exceptionHandling(ex -> ex
+ .authenticationEntryPoint(unauthorizedEntryPoint())
+ )
+ .addFilterBefore(jwtAuthenticationFilter(), AuthorizationFilter.class);
+
return http.build();
}
-
+
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+ configuration.setAllowedOrigins(List.of(
+ "http://127.0.0.1:5173",
+ "http://127.0.0.1:5174",
+ "http://127.0.0.1:5175",
+ "http://127.0.0.1:5176",
+ "http://127.0.0.1:5177",
+ "http://127.0.0.1:5178",
+ "http://127.0.0.1:5179",
+ "http://127.0.0.1:5180",
+ "http://localhost:5173",
+ "http://localhost:5174",
+ "http://localhost:5175",
+ "http://localhost:5176",
+ "http://localhost:5177",
+ "http://localhost:5178",
+ "http://localhost:5179",
+ "http://localhost:5180"
+ ));
+ configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
+ configuration.setAllowedHeaders(List.of("*"));
+ configuration.setAllowCredentials(true);
+ configuration.setMaxAge(3600L);
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", configuration);
+ return source;
+ }
+
+ @Bean
+ public SecurityContextRepository securityContextRepository() {
+ return new HttpSessionSecurityContextRepository();
+ }
+
+ @Bean
+ public AuthenticationEntryPoint unauthorizedEntryPoint() {
+ return (request, response, authException) -> {
+ response.setContentType("application/json");
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ response.getWriter().write("{\"code\":403,\"message\":\"禁止访问\",\"data\":null}");
+ };
+ }
+
@Bean
public OncePerRequestFilter jwtAuthenticationFilter() {
return new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
-
+
String token = resolveToken(request);
-
+
if (token != null && jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsernameFromToken(token);
- UsernamePasswordAuthenticationToken auth =
- new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList());
- SecurityContextHolder.getContext().setAuthentication(auth);
+ List authorities = jwtTokenProvider.getAuthoritiesFromToken(token);
+
+ UsernamePasswordAuthenticationToken authentication =
+ new UsernamePasswordAuthenticationToken(username, token, authorities);
+
+ SecurityContext context = SecurityContextHolder.createEmptyContext();
+ context.setAuthentication(authentication);
+ SecurityContextHolder.setContext(context);
+ securityContextRepository().saveContext(context, request, response);
+
+ SecurityUtils.setJwtTokenProvider(jwtTokenProvider);
+
+ log.debug("JWT authenticated: {}, authorities: {}", username, authorities);
}
-
+
filterChain.doFilter(request, response);
}
};
}
-
+
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
@@ -71,4 +145,4 @@ public class SecurityConfig {
}
return null;
}
-}
+}
\ No newline at end of file
diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/DataAccessController.java b/module-auth/src/main/java/com/ether/pms/auth/controller/DataAccessController.java
new file mode 100644
index 0000000..b9c240a
--- /dev/null
+++ b/module-auth/src/main/java/com/ether/pms/auth/controller/DataAccessController.java
@@ -0,0 +1,50 @@
+package com.ether.pms.auth.controller;
+
+import com.ether.pms.auth.controller.dto.DataAccessRequest;
+import com.ether.pms.auth.entity.DataAccess;
+import com.ether.pms.auth.service.DataAccessService;
+import com.ether.pms.auth.util.SecurityUtils;
+import com.ether.pms.common.ApiResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import java.util.List;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/data-access")
+@RequiredArgsConstructor
+public class DataAccessController {
+
+ private final DataAccessService dataAccessService;
+
+ @PostMapping
+ public ResponseEntity> grantAccess(@RequestBody DataAccessRequest request) {
+ UUID currentUserId = SecurityUtils.getCurrentUserId();
+ if (currentUserId == null) {
+ return ResponseEntity.status(401).body(ApiResponse.error(401, "未登录"));
+ }
+ dataAccessService.grantAccess(
+ request.getDataType(),
+ request.getDataId(),
+ request.getAccessType(),
+ request.getAccessId(),
+ request.getAccessLevel(),
+ currentUserId
+ );
+ return ResponseEntity.ok(ApiResponse.success());
+ }
+
+ @DeleteMapping("/{id}")
+ public ResponseEntity> revokeAccess(@PathVariable UUID id) {
+ dataAccessService.revokeAccess(id);
+ return ResponseEntity.ok(ApiResponse.success());
+ }
+
+ @GetMapping
+ public ResponseEntity>> getDataAccess(
+ @RequestParam String dataType,
+ @RequestParam UUID dataId) {
+ return ResponseEntity.ok(ApiResponse.success(dataAccessService.getDataAccess(dataType, dataId)));
+ }
+}
diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/UserController.java b/module-auth/src/main/java/com/ether/pms/auth/controller/UserController.java
index 4a6f869..75807f5 100644
--- a/module-auth/src/main/java/com/ether/pms/auth/controller/UserController.java
+++ b/module-auth/src/main/java/com/ether/pms/auth/controller/UserController.java
@@ -1,6 +1,9 @@
package com.ether.pms.auth.controller;
+import com.ether.pms.auth.controller.dto.UserProjectRequest;
import com.ether.pms.auth.entity.User;
+import com.ether.pms.auth.entity.UserProject;
+import com.ether.pms.auth.service.UserProjectService;
import com.ether.pms.auth.service.UserService;
import com.ether.pms.common.ApiResponse;
import lombok.Data;
@@ -15,8 +18,9 @@ import java.util.UUID;
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
-
+
private final UserService userService;
+ private final UserProjectService userProjectService;
@GetMapping
public ResponseEntity>> findAll() {
@@ -59,6 +63,27 @@ public class UserController {
userService.assignRoles(id, roleIds);
return ResponseEntity.ok(ApiResponse.success());
}
+
+ @GetMapping("/{id}/projects")
+ public ResponseEntity>> getUserProjects(@PathVariable UUID id) {
+ return ResponseEntity.ok(ApiResponse.success(userProjectService.getUserProjects(id)));
+ }
+
+ @PostMapping("/{id}/projects")
+ public ResponseEntity> addUserToProject(
+ @PathVariable UUID id,
+ @RequestBody UserProjectRequest request) {
+ userProjectService.addUserToProject(id, request.getProjectId(), request.getRoleInProject());
+ return ResponseEntity.ok(ApiResponse.success());
+ }
+
+ @DeleteMapping("/{id}/projects/{projectId}")
+ public ResponseEntity> removeUserFromProject(
+ @PathVariable UUID id,
+ @PathVariable UUID projectId) {
+ userProjectService.removeUserFromProject(id, projectId);
+ return ResponseEntity.ok(ApiResponse.success());
+ }
@Data
public static class PasswordRequest {
diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/dto/DataAccessRequest.java b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/DataAccessRequest.java
new file mode 100644
index 0000000..580be26
--- /dev/null
+++ b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/DataAccessRequest.java
@@ -0,0 +1,13 @@
+package com.ether.pms.auth.controller.dto;
+
+import lombok.Data;
+import java.util.UUID;
+
+@Data
+public class DataAccessRequest {
+ private String dataType;
+ private UUID dataId;
+ private String accessType;
+ private UUID accessId;
+ private String accessLevel = "read";
+}
diff --git a/module-auth/src/main/java/com/ether/pms/auth/controller/dto/UserProjectRequest.java b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/UserProjectRequest.java
new file mode 100644
index 0000000..52873a8
--- /dev/null
+++ b/module-auth/src/main/java/com/ether/pms/auth/controller/dto/UserProjectRequest.java
@@ -0,0 +1,10 @@
+package com.ether.pms.auth.controller.dto;
+
+import lombok.Data;
+import java.util.UUID;
+
+@Data
+public class UserProjectRequest {
+ private UUID projectId;
+ private String roleInProject = "member";
+}
diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/DataAccess.java b/module-auth/src/main/java/com/ether/pms/auth/entity/DataAccess.java
new file mode 100644
index 0000000..797154c
--- /dev/null
+++ b/module-auth/src/main/java/com/ether/pms/auth/entity/DataAccess.java
@@ -0,0 +1,44 @@
+package com.ether.pms.auth.entity;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Entity
+@Table(name = "biz_data_access")
+@Data
+public class DataAccess {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ private UUID id;
+
+ @Column(name = "data_type", nullable = false)
+ private String dataType;
+
+ @Column(name = "data_id", nullable = false)
+ private UUID dataId;
+
+ @Column(name = "access_type", nullable = false)
+ private String accessType;
+
+ @Column(name = "access_id", nullable = false)
+ private UUID accessId;
+
+ @Column(name = "access_level", nullable = false)
+ private String accessLevel = "read";
+
+ @Column(name = "granted_by")
+ private UUID grantedBy;
+
+ @Column(name = "granted_at", nullable = false)
+ private LocalDateTime grantedAt = LocalDateTime.now();
+
+ @PrePersist
+ public void prePersist() {
+ if (this.grantedAt == null) {
+ this.grantedAt = LocalDateTime.now();
+ }
+ }
+}
diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/Permission.java b/module-auth/src/main/java/com/ether/pms/auth/entity/Permission.java
index b43656b..cdcdae8 100644
--- a/module-auth/src/main/java/com/ether/pms/auth/entity/Permission.java
+++ b/module-auth/src/main/java/com/ether/pms/auth/entity/Permission.java
@@ -1,7 +1,11 @@
package com.ether.pms.auth.entity;
import jakarta.persistence.*;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
import lombok.Data;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.time.LocalDateTime;
import java.util.List;
import java.util.UUID;
@@ -15,21 +19,30 @@ public class Permission {
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
+ @NotNull(message = "权限代码不能为空")
+ @Size(min = 2, max = 100, message = "权限代码长度必须在2-100位之间")
+ @Pattern(regexp = "^[a-zA-Z0-9_:]+$", message = "权限代码只能包含字母、数字、冒号和下划线")
@Column(unique = true, nullable = false, length = 100)
private String code;
+ @NotNull(message = "权限名称不能为空")
+ @Size(min = 2, max = 100, message = "权限名称长度必须在2-100位之间")
@Column(nullable = false, length = 100)
private String name;
+ @Size(max = 20, message = "权限类型长度不能超过20位")
@Column(length = 20)
private String type;
+ @Size(max = 50, message = "资源路径长度不能超过50位")
@Column(length = 50)
private String resource;
+ @Size(max = 50, message = "请求方法长度不能超过50位")
@Column(length = 50)
private String method;
+ @Size(max = 200, message = "权限描述长度不能超过200位")
@Column(length = 200)
private String description;
@@ -38,6 +51,7 @@ public class Permission {
private Integer sortOrder;
+ @JsonIgnoreProperties({"permissions", "roles"})
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(
name = "auth_role_permission",
diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/Role.java b/module-auth/src/main/java/com/ether/pms/auth/entity/Role.java
index efb892f..7a5dab8 100644
--- a/module-auth/src/main/java/com/ether/pms/auth/entity/Role.java
+++ b/module-auth/src/main/java/com/ether/pms/auth/entity/Role.java
@@ -1,6 +1,9 @@
package com.ether.pms.auth.entity;
import jakarta.persistence.*;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@@ -15,12 +18,18 @@ public class Role {
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
+ @NotNull(message = "角色代码不能为空")
+ @Size(min = 2, max = 50, message = "角色代码长度必须在2-50位之间")
+ @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "角色代码只能包含字母、数字和下划线")
@Column(unique = true, nullable = false, length = 50)
private String code;
+ @NotNull(message = "角色名称不能为空")
+ @Size(min = 2, max = 50, message = "角色名称长度必须在2-50位之间")
@Column(nullable = false, length = 50)
private String name;
+ @Size(max = 200, message = "角色描述长度不能超过200位")
@Column(length = 200)
private String description;
diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/User.java b/module-auth/src/main/java/com/ether/pms/auth/entity/User.java
index 1158092..5b72dad 100644
--- a/module-auth/src/main/java/com/ether/pms/auth/entity/User.java
+++ b/module-auth/src/main/java/com/ether/pms/auth/entity/User.java
@@ -1,6 +1,10 @@
package com.ether.pms.auth.entity;
import jakarta.persistence.*;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@@ -15,9 +19,14 @@ public class User {
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
+ @NotNull(message = "用户名不能为空")
+ @Size(min = 3, max = 50, message = "用户名长度必须在3-50位之间")
+ @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
@Column(unique = true, nullable = false, length = 50)
private String username;
+ @NotNull(message = "密码不能为空")
+ @Size(min = 8, max = 20, message = "密码长度必须在8-20位之间")
@Column(nullable = false)
private String password;
@@ -26,9 +35,11 @@ public class User {
@Column(length = 50)
private String realName;
+ @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
@Column(length = 20)
private String phone;
+ @Email(message = "邮箱格式不正确")
@Column(length = 100)
private String email;
diff --git a/module-auth/src/main/java/com/ether/pms/auth/entity/UserProject.java b/module-auth/src/main/java/com/ether/pms/auth/entity/UserProject.java
new file mode 100644
index 0000000..d5761c2
--- /dev/null
+++ b/module-auth/src/main/java/com/ether/pms/auth/entity/UserProject.java
@@ -0,0 +1,35 @@
+package com.ether.pms.auth.entity;
+
+import jakarta.persistence.*;
+import lombok.Data;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Entity
+@Table(name = "user_project")
+@Data
+public class UserProject {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ private UUID id;
+
+ @Column(name = "user_id", nullable = false)
+ private UUID userId;
+
+ @Column(name = "project_id", nullable = false)
+ private UUID projectId;
+
+ @Column(name = "role_in_project", nullable = false)
+ private String roleInProject = "member";
+
+ @Column(name = "joined_at", nullable = false)
+ private LocalDateTime joinedAt = LocalDateTime.now();
+
+ @PrePersist
+ public void prePersist() {
+ if (this.joinedAt == null) {
+ this.joinedAt = LocalDateTime.now();
+ }
+ }
+}
diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/DataAccessRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/DataAccessRepository.java
new file mode 100644
index 0000000..334775d
--- /dev/null
+++ b/module-auth/src/main/java/com/ether/pms/auth/repository/DataAccessRepository.java
@@ -0,0 +1,23 @@
+package com.ether.pms.auth.repository;
+
+import com.ether.pms.auth.entity.DataAccess;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+import java.util.List;
+import java.util.UUID;
+
+@Repository
+public interface DataAccessRepository extends JpaRepository {
+
+ List findByDataTypeAndDataId(String dataType, UUID dataId);
+
+ @Query("SELECT da FROM DataAccess da WHERE da.accessType = :accessType AND da.accessId = :accessId")
+ List findByAccessTypeAndAccessId(@Param("accessType") String accessType, @Param("accessId") UUID accessId);
+
+ @Query("SELECT da.dataId FROM DataAccess da WHERE da.accessType = 'user' AND da.accessId = :userId AND da.dataType = :dataType")
+ List findDataIdsByUserAccess(@Param("userId") UUID userId, @Param("dataType") String dataType);
+
+ void deleteByDataTypeAndDataId(String dataType, UUID dataId);
+}
diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/UserProjectRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/UserProjectRepository.java
new file mode 100644
index 0000000..9521057
--- /dev/null
+++ b/module-auth/src/main/java/com/ether/pms/auth/repository/UserProjectRepository.java
@@ -0,0 +1,24 @@
+package com.ether.pms.auth.repository;
+
+import com.ether.pms.auth.entity.UserProject;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+import java.util.List;
+import java.util.UUID;
+
+@Repository
+public interface UserProjectRepository extends JpaRepository {
+
+ List findByUserId(UUID userId);
+
+ List findByProjectId(UUID projectId);
+
+ @Query("SELECT up.projectId FROM UserProject up WHERE up.userId = :userId")
+ List findProjectIdsByUserId(@Param("userId") UUID userId);
+
+ boolean existsByUserIdAndProjectId(UUID userId, UUID projectId);
+
+ void deleteByUserIdAndProjectId(UUID userId, UUID projectId);
+}
diff --git a/module-auth/src/main/java/com/ether/pms/auth/repository/UserRepository.java b/module-auth/src/main/java/com/ether/pms/auth/repository/UserRepository.java
index 886377d..1032e45 100644
--- a/module-auth/src/main/java/com/ether/pms/auth/repository/UserRepository.java
+++ b/module-auth/src/main/java/com/ether/pms/auth/repository/UserRepository.java
@@ -2,16 +2,21 @@ package com.ether.pms.auth.repository;
import com.ether.pms.auth.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public interface UserRepository extends JpaRepository {
-
+
Optional findByUsername(String username);
-
+
+ @Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.username = :username")
+ Optional findByUsernameWithRoles(@Param("username") String username);
+
boolean existsByUsername(String username);
-
+
boolean existsByPhone(String phone);
}
diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/DataAccessService.java b/module-auth/src/main/java/com/ether/pms/auth/service/DataAccessService.java
new file mode 100644
index 0000000..f76ff5d
--- /dev/null
+++ b/module-auth/src/main/java/com/ether/pms/auth/service/DataAccessService.java
@@ -0,0 +1,54 @@
+package com.ether.pms.auth.service;
+
+import com.ether.pms.auth.entity.DataAccess;
+import com.ether.pms.auth.repository.DataAccessRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.*;
+
+@Service
+@RequiredArgsConstructor
+public class DataAccessService {
+
+ private final DataAccessRepository dataAccessRepository;
+
+ @Transactional
+ public DataAccess grantAccess(String dataType, UUID dataId, String accessType, UUID accessId, String level, UUID grantedBy) {
+ Optional existing = dataAccessRepository.findAll().stream()
+ .filter(da -> da.getDataType().equals(dataType)
+ && da.getDataId().equals(dataId)
+ && da.getAccessType().equals(accessType)
+ && da.getAccessId().equals(accessId))
+ .findFirst();
+
+ if (existing.isPresent()) {
+ DataAccess existingAccess = existing.get();
+ existingAccess.setAccessLevel(level);
+ existingAccess.setGrantedBy(grantedBy);
+ return dataAccessRepository.save(existingAccess);
+ }
+
+ DataAccess access = new DataAccess();
+ access.setDataType(dataType);
+ access.setDataId(dataId);
+ access.setAccessType(accessType);
+ access.setAccessId(accessId);
+ access.setAccessLevel(level);
+ access.setGrantedBy(grantedBy);
+ return dataAccessRepository.save(access);
+ }
+
+ @Transactional
+ public void revokeAccess(UUID accessId) {
+ dataAccessRepository.deleteById(accessId);
+ }
+
+ public List getDataAccess(String dataType, UUID dataId) {
+ return dataAccessRepository.findByDataTypeAndDataId(dataType, dataId);
+ }
+
+ public List getAccessibleDataIds(String dataType, UUID userId) {
+ return dataAccessRepository.findDataIdsByUserAccess(userId, dataType);
+ }
+}
diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/DataScopeService.java b/module-auth/src/main/java/com/ether/pms/auth/service/DataScopeService.java
new file mode 100644
index 0000000..98d2031
--- /dev/null
+++ b/module-auth/src/main/java/com/ether/pms/auth/service/DataScopeService.java
@@ -0,0 +1,29 @@
+package com.ether.pms.auth.service;
+
+import com.ether.pms.auth.entity.Role;
+import com.ether.pms.auth.repository.UserProjectRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import java.util.*;
+
+@Service
+@RequiredArgsConstructor
+public class DataScopeService {
+
+ private final UserProjectRepository userProjectRepository;
+
+ public List getPermittedProjectIds(UUID userId, Set roles, boolean hasAllScope) {
+ if (hasAllScope) {
+ return Collections.emptyList();
+ }
+ return userProjectRepository.findProjectIdsByUserId(userId);
+ }
+
+ public boolean canAccessAllData(Set roles) {
+ return roles.stream().anyMatch(r -> r.getDataScope() == Role.DataScope.ALL);
+ }
+
+ public boolean isSelfScopeOnly(Set roles) {
+ return roles.stream().allMatch(r -> r.getDataScope() == Role.DataScope.SELF);
+ }
+}
diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/LoginService.java b/module-auth/src/main/java/com/ether/pms/auth/service/LoginService.java
index 80c09b0..c356095 100644
--- a/module-auth/src/main/java/com/ether/pms/auth/service/LoginService.java
+++ b/module-auth/src/main/java/com/ether/pms/auth/service/LoginService.java
@@ -27,7 +27,7 @@ public class LoginService {
throw new BusinessException(ErrorCode.AUTH_002);
}
- User user = userRepository.findByUsername(username).orElse(null);
+ User user = userRepository.findByUsernameWithRoles(username).orElse(null);
if (user == null || !passwordService.matches(password, user.getPassword())) {
if (user != null) {
@@ -45,10 +45,6 @@ public class LoginService {
loginAttemptService.recordSuccess(username);
- user.setLastLoginTime(java.time.LocalDateTime.now());
- user.setLastLoginIp(ip);
- userRepository.save(user);
-
Map claims = new HashMap<>();
if (user.getRoles() != null) {
claims.put("roles", user.getRoles().stream()
diff --git a/module-auth/src/main/java/com/ether/pms/auth/service/UserProjectService.java b/module-auth/src/main/java/com/ether/pms/auth/service/UserProjectService.java
new file mode 100644
index 0000000..18a2d82
--- /dev/null
+++ b/module-auth/src/main/java/com/ether/pms/auth/service/UserProjectService.java
@@ -0,0 +1,45 @@
+package com.ether.pms.auth.service;
+
+import com.ether.pms.auth.entity.UserProject;
+import com.ether.pms.auth.repository.UserProjectRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+public class UserProjectService {
+
+ private final UserProjectRepository userProjectRepository;
+
+ public List getUserProjects(UUID userId) {
+ return userProjectRepository.findByUserId(userId);
+ }
+
+ public List getUserProjectIds(UUID userId) {
+ return userProjectRepository.findProjectIdsByUserId(userId);
+ }
+
+ @Transactional
+ public void addUserToProject(UUID userId, UUID projectId, String role) {
+ if (userProjectRepository.existsByUserIdAndProjectId(userId, projectId)) {
+ return;
+ }
+ UserProject up = new UserProject();
+ up.setUserId(userId);
+ up.setProjectId(projectId);
+ up.setRoleInProject(role != null ? role : "member");
+ userProjectRepository.save(up);
+ }
+
+ @Transactional
+ public void removeUserFromProject(UUID userId, UUID projectId) {
+ userProjectRepository.deleteByUserIdAndProjectId(userId, projectId);
+ }
+
+ public boolean isUserInProject(UUID userId, UUID projectId) {
+ return userProjectRepository.existsByUserIdAndProjectId(userId, projectId);
+ }
+}
diff --git a/module-auth/src/main/java/com/ether/pms/auth/util/JwtTokenProvider.java b/module-auth/src/main/java/com/ether/pms/auth/util/JwtTokenProvider.java
index 070002d..4b7ca21 100644
--- a/module-auth/src/main/java/com/ether/pms/auth/util/JwtTokenProvider.java
+++ b/module-auth/src/main/java/com/ether/pms/auth/util/JwtTokenProvider.java
@@ -3,12 +3,16 @@ package com.ether.pms.auth.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -70,7 +74,21 @@ public class JwtTokenProvider {
Claims claims = getClaimsFromToken(token);
return claims.getSubject();
}
-
+
+ public List getAuthoritiesFromToken(String token) {
+ Claims claims = getClaimsFromToken(token);
+ List authorities = new ArrayList<>();
+
+ Object rolesObj = claims.get("roles");
+ if (rolesObj instanceof List> roles) {
+ for (Object role : roles) {
+ authorities.add(new SimpleGrantedAuthority("ROLE_" + role.toString()));
+ }
+ }
+
+ return authorities;
+ }
+
public boolean validateToken(String token) {
try {
Jwts.parser()
diff --git a/module-auth/src/main/java/com/ether/pms/auth/util/SecurityUtils.java b/module-auth/src/main/java/com/ether/pms/auth/util/SecurityUtils.java
new file mode 100644
index 0000000..aaf1b16
--- /dev/null
+++ b/module-auth/src/main/java/com/ether/pms/auth/util/SecurityUtils.java
@@ -0,0 +1,47 @@
+package com.ether.pms.auth.util;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import java.util.UUID;
+
+public class SecurityUtils {
+
+ private static JwtTokenProvider jwtTokenProvider;
+
+ public static void setJwtTokenProvider(JwtTokenProvider provider) {
+ jwtTokenProvider = provider;
+ }
+
+ public static UUID getCurrentUserId() {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth == null || !auth.isAuthenticated()) {
+ return null;
+ }
+
+ if (jwtTokenProvider != null) {
+ String token = extractTokenFromAuth(auth);
+ if (token != null && jwtTokenProvider.validateToken(token)) {
+ return jwtTokenProvider.getUserIdFromToken(token);
+ }
+ }
+
+ String username = (String) auth.getPrincipal();
+ return UUID.nameUUIDFromBytes(username.getBytes());
+ }
+
+ private static String extractTokenFromAuth(Authentication auth) {
+ Object credentials = auth.getCredentials();
+ if (credentials != null) {
+ return credentials.toString();
+ }
+ return null;
+ }
+
+ public static String getCurrentUsername() {
+ Authentication auth = SecurityContextHolder.getContext().getAuthentication();
+ if (auth == null || !auth.isAuthenticated()) {
+ return null;
+ }
+ return (String) auth.getPrincipal();
+ }
+}
diff --git a/module-auth/src/test/java/com/ether/pms/auth/PasswordEncoderTest.java b/module-auth/src/test/java/com/ether/pms/auth/PasswordEncoderTest.java
new file mode 100644
index 0000000..38838b1
--- /dev/null
+++ b/module-auth/src/test/java/com/ether/pms/auth/PasswordEncoderTest.java
@@ -0,0 +1,27 @@
+package com.ether.pms.auth;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class PasswordEncoderTest {
+
+ @Test
+ void testPasswordEncoding() {
+ BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
+ String rawPassword = "Admin@123";
+ String hashFromDb = "$2a$10$2JRCyrbZANZdGD4sgplVjuIOPvK1P/Be1/4iwXwkUqpbEDo2AHcuC";
+
+ System.out.println("Testing password: " + rawPassword);
+ System.out.println("Hash from DB: " + hashFromDb);
+
+ boolean matches = encoder.matches(rawPassword, hashFromDb);
+ System.out.println("Matches: " + matches);
+
+ String newHash = encoder.encode(rawPassword);
+ System.out.println("New hash: " + newHash);
+
+ assertTrue(matches, "Password should match the hash from database");
+ }
+}
diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/entity/Project.java b/module-mdm/src/main/java/com/ether/pms/mdm/entity/Project.java
index 2375dfc..69f9fcc 100644
--- a/module-mdm/src/main/java/com/ether/pms/mdm/entity/Project.java
+++ b/module-mdm/src/main/java/com/ether/pms/mdm/entity/Project.java
@@ -1,6 +1,9 @@
package com.ether.pms.mdm.entity;
import jakarta.persistence.*;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;
@@ -14,18 +17,26 @@ public class Project {
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
+ @NotNull(message = "项目代码不能为空")
+ @Size(min = 2, max = 50, message = "项目代码长度必须在2-50位之间")
+ @Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "项目代码只能包含字母、数字、连字符和下划线")
@Column(unique = true, nullable = false, length = 50)
private String code;
+ @NotNull(message = "项目名称不能为空")
+ @Size(min = 2, max = 100, message = "项目名称长度必须在2-100位之间")
@Column(nullable = false, length = 100)
private String name;
+ @Size(max = 500, message = "项目描述长度不能超过500位")
@Column(length = 500)
private String description;
+ @Size(max = 100, message = "项目地址长度不能超过100位")
@Column(length = 100)
private String address;
+ @Size(max = 20, message = "项目类型长度不能超过20位")
@Column(length = 20)
private String projectType;
@@ -59,6 +70,7 @@ public class Project {
@Column(length = 200)
private String contact;
+ @Pattern(regexp = "^1[3-9]\\d{9}$", message = "联系电话格式不正确")
@Column(length = 20)
private String contactPhone;
diff --git a/module-mdm/src/main/java/com/ether/pms/mdm/entity/SpaceNode.java b/module-mdm/src/main/java/com/ether/pms/mdm/entity/SpaceNode.java
index cf70fc6..ef49b8c 100644
--- a/module-mdm/src/main/java/com/ether/pms/mdm/entity/SpaceNode.java
+++ b/module-mdm/src/main/java/com/ether/pms/mdm/entity/SpaceNode.java
@@ -1,6 +1,9 @@
package com.ether.pms.mdm.entity;
import jakarta.persistence.*;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.UUID;
@@ -14,18 +17,28 @@ public class SpaceNode {
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
+ @NotNull(message = "空间节点代码不能为空")
+ @Size(min = 2, max = 50, message = "空间节点代码长度必须在2-50位之间")
+ @Pattern(regexp = "^[a-zA-Z0-9_-]+$", message = "空间节点代码只能包含字母、数字、连字符和下划线")
@Column(nullable = false, length = 50)
private String code;
+ @NotNull(message = "空间节点名称不能为空")
+ @Size(min = 2, max = 100, message = "空间节点名称长度必须在2-100位之间")
@Column(nullable = false, length = 100)
private String name;
+ @NotNull(message = "空间节点类型不能为空")
+ @Size(max = 50, message = "空间节点类型长度不能超过50位")
@Column(nullable = false, length = 50)
private String nodeType;
+ @Size(max = 50, message = "父节点代码长度不能超过50位")
@Column(length = 50)
private String parentCode;
+ @NotNull(message = "项目代码不能为空")
+ @Size(max = 50, message = "项目代码长度不能超过50位")
@Column(nullable = false)
private String projectCode;
diff --git a/pms-starter/src/main/java/com/ether/pms/PmsApplication.java b/pms-starter/src/main/java/com/ether/pms/PmsApplication.java
index fba8cfb..14d1b45 100644
--- a/pms-starter/src/main/java/com/ether/pms/PmsApplication.java
+++ b/pms-starter/src/main/java/com/ether/pms/PmsApplication.java
@@ -2,8 +2,10 @@ package com.ether.pms;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
+@ComponentScan(basePackages = {"com.ether.pms"})
public class PmsApplication {
public static void main(String[] args) {
diff --git a/pms-starter/src/main/resources/application.yml b/pms-starter/src/main/resources/application.yml
index a502255..4baecc4 100644
--- a/pms-starter/src/main/resources/application.yml
+++ b/pms-starter/src/main/resources/application.yml
@@ -2,10 +2,14 @@ spring:
application:
name: ether-pms
+ autoconfigure:
+ exclude:
+ - org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
+
datasource:
url: jdbc:postgresql://localhost:5432/ether_pms
username: chiguyong
- password:
+ password:
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 10
@@ -22,8 +26,14 @@ spring:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
+logging:
+ level:
+ com.ether: DEBUG
+ org.springframework.security: DEBUG
+
server:
port: 8080
+ address: 0.0.0.0
management:
endpoints:
@@ -40,7 +50,6 @@ springdoc:
swagger-ui:
path: /swagger-ui.html
-# 密码策略配置
password:
min-length: 8
max-length: 20
@@ -50,12 +59,10 @@ password:
require-special: true
special-chars: "!@#$%^&*()_+-=[]{}|;':\",./<>?"
-# 登录安全配置
login:
max-attempts: 5
lockout-duration-minutes: 10
-# JWT配置
jwt:
secret: ether-pms-secret-key-must-be-at-least-256-bits-long-for-hs256
expiration: 86400000