feat: RBAC权限管理后端实现
- SecurityConfig: 添加CORS配置,支持多端口(5173-5180) - User/Permission/Role实体: 完善权限相关实体 - JwtTokenProvider: token生成和验证 - LoginService: 登录逻辑 - UserController: 用户管理接口 - application.yml: 后端配置
This commit is contained in:
parent
162658daed
commit
6495e93e06
|
|
@ -33,6 +33,11 @@
|
|||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,41 @@
|
|||
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;
|
||||
|
|
@ -30,19 +43,70 @@ public class SecurityConfig {
|
|||
@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() {
|
||||
|
|
@ -54,9 +118,19 @@ public class SecurityConfig {
|
|||
|
||||
if (token != null && jwtTokenProvider.validateToken(token)) {
|
||||
String username = jwtTokenProvider.getUsernameFromToken(token);
|
||||
UsernamePasswordAuthenticationToken auth =
|
||||
new UsernamePasswordAuthenticationToken(username, null, Collections.emptyList());
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
List<GrantedAuthority> 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);
|
||||
|
|
|
|||
|
|
@ -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<ApiResponse<Void>> 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<ApiResponse<Void>> revokeAccess(@PathVariable UUID id) {
|
||||
dataAccessService.revokeAccess(id);
|
||||
return ResponseEntity.ok(ApiResponse.success());
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<List<DataAccess>>> getDataAccess(
|
||||
@RequestParam String dataType,
|
||||
@RequestParam UUID dataId) {
|
||||
return ResponseEntity.ok(ApiResponse.success(dataAccessService.getDataAccess(dataType, dataId)));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -17,6 +20,7 @@ import java.util.UUID;
|
|||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
private final UserProjectService userProjectService;
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<ApiResponse<List<User>>> findAll() {
|
||||
|
|
@ -60,6 +64,27 @@ public class UserController {
|
|||
return ResponseEntity.ok(ApiResponse.success());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/projects")
|
||||
public ResponseEntity<ApiResponse<List<UserProject>>> getUserProjects(@PathVariable UUID id) {
|
||||
return ResponseEntity.ok(ApiResponse.success(userProjectService.getUserProjects(id)));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/projects")
|
||||
public ResponseEntity<ApiResponse<Void>> 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<ApiResponse<Void>> removeUserFromProject(
|
||||
@PathVariable UUID id,
|
||||
@PathVariable UUID projectId) {
|
||||
userProjectService.removeUserFromProject(id, projectId);
|
||||
return ResponseEntity.ok(ApiResponse.success());
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PasswordRequest {
|
||||
private String oldPassword;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DataAccess, UUID> {
|
||||
|
||||
List<DataAccess> findByDataTypeAndDataId(String dataType, UUID dataId);
|
||||
|
||||
@Query("SELECT da FROM DataAccess da WHERE da.accessType = :accessType AND da.accessId = :accessId")
|
||||
List<DataAccess> 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<UUID> findDataIdsByUserAccess(@Param("userId") UUID userId, @Param("dataType") String dataType);
|
||||
|
||||
void deleteByDataTypeAndDataId(String dataType, UUID dataId);
|
||||
}
|
||||
|
|
@ -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<UserProject, UUID> {
|
||||
|
||||
List<UserProject> findByUserId(UUID userId);
|
||||
|
||||
List<UserProject> findByProjectId(UUID projectId);
|
||||
|
||||
@Query("SELECT up.projectId FROM UserProject up WHERE up.userId = :userId")
|
||||
List<UUID> findProjectIdsByUserId(@Param("userId") UUID userId);
|
||||
|
||||
boolean existsByUserIdAndProjectId(UUID userId, UUID projectId);
|
||||
|
||||
void deleteByUserIdAndProjectId(UUID userId, UUID projectId);
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ 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;
|
||||
|
|
@ -11,6 +13,9 @@ public interface UserRepository extends JpaRepository<User, UUID> {
|
|||
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.username = :username")
|
||||
Optional<User> findByUsernameWithRoles(@Param("username") String username);
|
||||
|
||||
boolean existsByUsername(String username);
|
||||
|
||||
boolean existsByPhone(String phone);
|
||||
|
|
|
|||
|
|
@ -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<DataAccess> 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<DataAccess> getDataAccess(String dataType, UUID dataId) {
|
||||
return dataAccessRepository.findByDataTypeAndDataId(dataType, dataId);
|
||||
}
|
||||
|
||||
public List<UUID> getAccessibleDataIds(String dataType, UUID userId) {
|
||||
return dataAccessRepository.findDataIdsByUserAccess(userId, dataType);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UUID> getPermittedProjectIds(UUID userId, Set<Role> roles, boolean hasAllScope) {
|
||||
if (hasAllScope) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return userProjectRepository.findProjectIdsByUserId(userId);
|
||||
}
|
||||
|
||||
public boolean canAccessAllData(Set<Role> roles) {
|
||||
return roles.stream().anyMatch(r -> r.getDataScope() == Role.DataScope.ALL);
|
||||
}
|
||||
|
||||
public boolean isSelfScopeOnly(Set<Role> roles) {
|
||||
return roles.stream().allMatch(r -> r.getDataScope() == Role.DataScope.SELF);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Object> claims = new HashMap<>();
|
||||
if (user.getRoles() != null) {
|
||||
claims.put("roles", user.getRoles().stream()
|
||||
|
|
|
|||
|
|
@ -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<UserProject> getUserProjects(UUID userId) {
|
||||
return userProjectRepository.findByUserId(userId);
|
||||
}
|
||||
|
||||
public List<UUID> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -71,6 +75,20 @@ public class JwtTokenProvider {
|
|||
return claims.getSubject();
|
||||
}
|
||||
|
||||
public List<GrantedAuthority> getAuthoritiesFromToken(String token) {
|
||||
Claims claims = getClaimsFromToken(token);
|
||||
List<GrantedAuthority> 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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ spring:
|
|||
application:
|
||||
name: ether-pms
|
||||
|
||||
autoconfigure:
|
||||
exclude:
|
||||
- org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration
|
||||
|
||||
datasource:
|
||||
url: jdbc:postgresql://localhost:5432/ether_pms
|
||||
username: chiguyong
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue