feat: RBAC权限管理后端实现

- SecurityConfig: 添加CORS配置,支持多端口(5173-5180)
- User/Permission/Role实体: 完善权限相关实体
- JwtTokenProvider: token生成和验证
- LoginService: 登录逻辑
- UserController: 用户管理接口
- application.yml: 后端配置
This commit is contained in:
chiguyong 2026-03-22 01:33:21 +08:00
parent 162658daed
commit 6495e93e06
25 changed files with 624 additions and 32 deletions

View File

@ -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>
@ -98,4 +103,4 @@
<scope>test</scope>
</dependency>
</dependencies>
</project>
</project>

View File

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

View File

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

View File

@ -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<ApiResponse<List<User>>> findAll() {
@ -59,6 +63,27 @@ public class UserController {
userService.assignRoles(id, roleIds);
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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {

View File

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