From 6495e93e0695c0a7e9641cf7c564705e720846e6 Mon Sep 17 00:00:00 2001 From: chiguyong Date: Sun, 22 Mar 2026 01:33:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20RBAC=E6=9D=83=E9=99=90=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=90=8E=E7=AB=AF=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SecurityConfig: 添加CORS配置,支持多端口(5173-5180) - User/Permission/Role实体: 完善权限相关实体 - JwtTokenProvider: token生成和验证 - LoginService: 登录逻辑 - UserController: 用户管理接口 - application.yml: 后端配置 --- module-auth/pom.xml | 7 +- .../ether/pms/auth/config/SecurityConfig.java | 108 +++++++++++++++--- .../auth/controller/DataAccessController.java | 50 ++++++++ .../pms/auth/controller/UserController.java | 27 ++++- .../controller/dto/DataAccessRequest.java | 13 +++ .../controller/dto/UserProjectRequest.java | 10 ++ .../com/ether/pms/auth/entity/DataAccess.java | 44 +++++++ .../com/ether/pms/auth/entity/Permission.java | 14 +++ .../java/com/ether/pms/auth/entity/Role.java | 9 ++ .../java/com/ether/pms/auth/entity/User.java | 11 ++ .../ether/pms/auth/entity/UserProject.java | 35 ++++++ .../auth/repository/DataAccessRepository.java | 23 ++++ .../repository/UserProjectRepository.java | 24 ++++ .../pms/auth/repository/UserRepository.java | 11 +- .../pms/auth/service/DataAccessService.java | 54 +++++++++ .../pms/auth/service/DataScopeService.java | 29 +++++ .../ether/pms/auth/service/LoginService.java | 6 +- .../pms/auth/service/UserProjectService.java | 45 ++++++++ .../ether/pms/auth/util/JwtTokenProvider.java | 20 +++- .../ether/pms/auth/util/SecurityUtils.java | 47 ++++++++ .../ether/pms/auth/PasswordEncoderTest.java | 27 +++++ .../com/ether/pms/mdm/entity/Project.java | 12 ++ .../com/ether/pms/mdm/entity/SpaceNode.java | 13 +++ .../java/com/ether/pms/PmsApplication.java | 2 + .../src/main/resources/application.yml | 15 ++- 25 files changed, 624 insertions(+), 32 deletions(-) create mode 100644 module-auth/src/main/java/com/ether/pms/auth/controller/DataAccessController.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/controller/dto/DataAccessRequest.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/controller/dto/UserProjectRequest.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/entity/DataAccess.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/entity/UserProject.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/repository/DataAccessRepository.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/repository/UserProjectRepository.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/service/DataAccessService.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/service/DataScopeService.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/service/UserProjectService.java create mode 100644 module-auth/src/main/java/com/ether/pms/auth/util/SecurityUtils.java create mode 100644 module-auth/src/test/java/com/ether/pms/auth/PasswordEncoderTest.java 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