diff --git a/pom.xml b/pom.xml index e06d014..53fecdd 100644 --- a/pom.xml +++ b/pom.xml @@ -52,26 +52,16 @@ spring-boot-starter-web + - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-websocket + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} - com.mysql mysql-connector-j - 8.4.0 - runtime - - - - org.postgresql - postgresql runtime @@ -101,51 +91,33 @@ spring-boot-starter-test test - - org.springdoc - springdoc-openapi-starter-webmvc-ui - ${springdoc.version} - - - - org.springframework.boot - spring-boot-starter-data-redis - - - - io.jsonwebtoken - jjwt-api - ${jjwt.version} - - - io.jsonwebtoken - jjwt-impl - ${jjwt.version} - runtime - - - io.jsonwebtoken - jjwt-jackson - ${jjwt.version} - runtime - - - org.springframework.boot - spring-boot-starter-mail - - - - org.springframework.boot - spring-boot-starter-quartz - - - - org.springframework.boot - spring-boot-starter-aop + cc.amily49.common + amily-common + 0.0.1-SNAPSHOT + + + amily-nexus + Amily Snapshot Repository + https://nexus.silencelurker.xyz/repository/amily-maven-snapshot/ + + true + always + + + + amily-nexus-release + Amily Release Repository + https://nexus.silencelurker.xyz/repository/amily-maven-release/ + + true + + + + diff --git a/src/main/java/cc/amily49/api/module/ModuleApplication.java b/src/main/java/cc/amily49/api/module/ModuleApplication.java index f577823..12692ae 100644 --- a/src/main/java/cc/amily49/api/module/ModuleApplication.java +++ b/src/main/java/cc/amily49/api/module/ModuleApplication.java @@ -2,22 +2,15 @@ package cc.amily49.api.module; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; -import org.springframework.data.web.config.EnableSpringDataWebSupport; -import org.springframework.scheduling.annotation.EnableAsync; -import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -@SpringBootApplication(scanBasePackages = "cc.amily49.api.module") -@EnableAsync -@EnableScheduling -@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) -@EnableRedisRepositories(basePackages = { - // Add your redis repositories here -}) +@SpringBootApplication +@EnableJpaAuditing +@ComponentScan(basePackages = {"cc.amily49.api.module", "cc.amily49.common"}) public class ModuleApplication { - public static void main(String[] args) { - SpringApplication.run(ModuleApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(ModuleApplication.class, args); + } } \ No newline at end of file diff --git a/src/main/java/cc/amily49/api/module/auth/filter/JwtAuthenticationFilter.java b/src/main/java/cc/amily49/api/module/auth/filter/JwtAuthenticationFilter.java deleted file mode 100644 index 50dc64c..0000000 --- a/src/main/java/cc/amily49/api/module/auth/filter/JwtAuthenticationFilter.java +++ /dev/null @@ -1,82 +0,0 @@ -package cc.amily49.api.module.auth.filter; - -import io.jsonwebtoken.ExpiredJwtException; -import jakarta.annotation.Resource; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; -import org.springframework.security.core.authority.SimpleGrantedAuthority; - -import cc.amily49.api.module.auth.model.JwtUser; -import cc.amily49.api.module.auth.util.JwtUtil; - -import java.io.IOException; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -/** - * Stateless JWT Filter for Module Template - * - * @author Silence_Lurker by Gemini - */ -@Component -public class JwtAuthenticationFilter extends OncePerRequestFilter { - - @Resource - private JwtUtil jwtUtil; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) - throws ServletException, IOException { - - final String requestTokenHeader = request.getHeader("Authorization"); - - String username = null; - String jwtToken = null; - - if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { - jwtToken = requestTokenHeader.substring(7); - try { - username = jwtUtil.getUsernameFromToken(jwtToken); - } catch (IllegalArgumentException e) { - logger.warn("Unable to get JWT Token"); - } catch (ExpiredJwtException e) { - logger.warn("JWT Token has expired"); - } - } - - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { - - // Validate token signature and expiration ONLY (Stateless) - if (jwtUtil.validateToken(jwtToken)) { - - // Extract roles from token - List roles = jwtUtil.getRolesFromToken(jwtToken); - Set authorities = null; - - if (roles != null) { - authorities = roles.stream() - .map(SimpleGrantedAuthority::new) - .collect(Collectors.toSet()); - } - - // Create stateless user details - JwtUser userDetails = new JwtUser(username, authorities); - - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities()); - - auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(auth); - } - } - chain.doFilter(request, response); - } -} diff --git a/src/main/java/cc/amily49/api/module/auth/model/JwtUser.java b/src/main/java/cc/amily49/api/module/auth/model/JwtUser.java deleted file mode 100644 index 67fbf2a..0000000 --- a/src/main/java/cc/amily49/api/module/auth/model/JwtUser.java +++ /dev/null @@ -1,53 +0,0 @@ -package cc.amily49.api.module.auth.model; - -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.Set; - -public class JwtUser implements UserDetails { - - private final String username; - private final Set authorities; - - public JwtUser(String username, Set authorities) { - this.username = username; - this.authorities = authorities; - } - - @Override - public Collection getAuthorities() { - return authorities; - } - - @Override - public String getPassword() { - return null; // No password in token-based auth - } - - @Override - public String getUsername() { - return username; - } - - @Override - public boolean isAccountNonExpired() { - return true; - } - - @Override - public boolean isAccountNonLocked() { - return true; - } - - @Override - public boolean isCredentialsNonExpired() { - return true; - } - - @Override - public boolean isEnabled() { - return true; - } -} diff --git a/src/main/java/cc/amily49/api/module/auth/util/JwtUtil.java b/src/main/java/cc/amily49/api/module/auth/util/JwtUtil.java deleted file mode 100644 index 2d33627..0000000 --- a/src/main/java/cc/amily49/api/module/auth/util/JwtUtil.java +++ /dev/null @@ -1,85 +0,0 @@ -package cc.amily49.api.module.auth.util; - -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import jakarta.annotation.PostConstruct; -import javax.crypto.SecretKey; -import java.io.Serializable; -import java.util.Base64; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.List; -import java.util.function.Function; - -/** - * Utility class for handling JWT tokens. - * Generation, validation, etc. - * - * @author Silence_Lurker by Gemini - */ -@Component -public class JwtUtil implements Serializable { - - private static final long serialVersionUID = -2550185165626007488L; - - // Token validity in milliseconds (e.g., 10 hours) - public static final long JWT_TOKEN_VALIDITY = 10 * 60 * 60 * 1000; - - @Value("${amily.jwt.secret}") - private String secretString; - - private SecretKey secretKey; - - @PostConstruct - public void init() { - byte[] decodedKey = Base64.getDecoder().decode(secretString); - this.secretKey = Keys.hmacShaKeyFor(decodedKey); - } - - public String getUsernameFromToken(String token) { - return getClaimFromToken(token, Claims::getSubject); - } - - public Date getExpirationDateFromToken(String token) { - return getClaimFromToken(token, Claims::getExpiration); - } - - public String getIpFromToken(String token) { - return getClaimFromToken(token, claims -> claims.get("ip", String.class)); - } - - public List getRolesFromToken(String token) { - return getClaimFromToken(token, claims -> claims.get("roles", List.class)); - } - - public T getClaimFromToken(String token, Function claimsResolver) { - final Claims claims = getAllClaimsFromToken(token); - return claimsResolver.apply(claims); - } - - private Claims getAllClaimsFromToken(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload(); - } - - private Boolean isTokenExpired(String token) { - final Date expiration = getExpirationDateFromToken(token); - return expiration.before(new Date()); - } - - /** - * Validate token (Stateless) - * Checks signature (implicit in getAllClaimsFromToken) and expiration. - */ - public Boolean validateToken(String token) { - try { - return !isTokenExpired(token); - } catch (Exception e) { - return false; - } - } -} \ No newline at end of file diff --git a/src/main/java/cc/amily49/api/module/config/QuartzConfig.java b/src/main/java/cc/amily49/api/module/config/QuartzConfig.java deleted file mode 100644 index e77021d..0000000 --- a/src/main/java/cc/amily49/api/module/config/QuartzConfig.java +++ /dev/null @@ -1,17 +0,0 @@ -package cc.amily49.api.module.config; - -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.quartz.QuartzDataSource; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import javax.sql.DataSource; - -@Configuration -public class QuartzConfig { - - @Bean - @QuartzDataSource - public DataSource quartzDataSource(@Qualifier("primaryDataSource") DataSource primaryDataSource) { - return primaryDataSource; - } -} diff --git a/src/main/java/cc/amily49/api/module/config/SecurityConfig.java b/src/main/java/cc/amily49/api/module/config/SecurityConfig.java deleted file mode 100644 index dab75a4..0000000 --- a/src/main/java/cc/amily49/api/module/config/SecurityConfig.java +++ /dev/null @@ -1,71 +0,0 @@ -package cc.amily49.api.module.config; - -import java.util.Arrays; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import cc.amily49.api.module.auth.filter.JwtAuthenticationFilter; -import jakarta.annotation.Resource; - -/** - * Security Configuration for Modules (Stateless, No Login) - * - * @author Silence_Lurker by Gemini - */ -@Configuration -@EnableWebSecurity -@EnableMethodSecurity -public class SecurityConfig { - - @Resource - private JwtAuthenticationFilter jwtAuthenticationFilter; - - @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { - http - .cors(cors -> cors.configurationSource(corsConfigurationSource())) - .csrf(csrf -> csrf.disable()) - .headers(headers -> headers.frameOptions(frame -> frame.sameOrigin())) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(authz -> authz - // Swagger UI - .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html").permitAll() - // Public endpoints (if any) - .requestMatchers("/public/**").permitAll() - // All other endpoints require authentication - .anyRequest().authenticated()); - - return http.build(); - } - - @Bean - CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration strictConfig = new CorsConfiguration(); - strictConfig.setAllowedOriginPatterns(Arrays.asList( - "http://localhost:*", - "http://127.0.0.1:*", - "http://*.amily49.cc", - "https://*.amily49.cc", - "https://amily49.cc" - )); - strictConfig.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); - strictConfig.setAllowCredentials(true); - strictConfig.setAllowedHeaders(Arrays.asList("*")); - strictConfig.setExposedHeaders(Arrays.asList("Authorization")); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", strictConfig); - return source; - } -} \ No newline at end of file diff --git a/src/main/java/cc/amily49/api/module/config/datasource/DataSourceWarmupRunner.java b/src/main/java/cc/amily49/api/module/config/datasource/DataSourceWarmupRunner.java deleted file mode 100644 index c2edcb8..0000000 --- a/src/main/java/cc/amily49/api/module/config/datasource/DataSourceWarmupRunner.java +++ /dev/null @@ -1,39 +0,0 @@ -package cc.amily49.api.module.config.datasource; - -import org.springframework.boot.CommandLineRunner; -import org.springframework.stereotype.Component; -import org.springframework.beans.factory.annotation.Qualifier; -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; - -@Component -public class DataSourceWarmupRunner implements CommandLineRunner { - - private final DataSource primaryDataSource; - - public DataSourceWarmupRunner(@Qualifier("primaryDataSource") DataSource primaryDataSource) { - this.primaryDataSource = primaryDataSource; - } - - @Override - public void run(String... args) { - System.out.println("Wait for database connection warmup..."); - long start = System.currentTimeMillis(); - try (Connection conn = primaryDataSource.getConnection(); - PreparedStatement ps = conn.prepareStatement("SELECT 1")) { - try (ResultSet rs = ps.executeQuery()) { - while(rs.next()) { - // Just consume result - } - } - long end = System.currentTimeMillis(); - System.out.println("Database connection warmup completed in " + (end - start) + "ms."); - } catch (Exception e) { - System.err.println("Database warmup failed: " + e.getMessage()); - // We don't throw exception here to allow app startup even if warmup fails, - // though it might fail later on actual requests. - } - } -} diff --git a/src/main/java/cc/amily49/api/module/config/datasource/PrimaryDataSourceConfig.java b/src/main/java/cc/amily49/api/module/config/datasource/PrimaryDataSourceConfig.java deleted file mode 100644 index 4e1e19c..0000000 --- a/src/main/java/cc/amily49/api/module/config/datasource/PrimaryDataSourceConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -package cc.amily49.api.module.config.datasource; - -import jakarta.persistence.EntityManagerFactory; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Primary; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.annotation.EnableTransactionManagement; - -import javax.sql.DataSource; - -@Configuration -@EnableTransactionManagement -@EnableJpaRepositories( - basePackages = { - "cc.amily49.api.module.auth.repository.jpa", - "cc.amily49.api.module.common.repository.jpa" - }, - entityManagerFactoryRef = "entityManagerFactoryPrimary", - transactionManagerRef = "transactionManagerPrimary" -) -public class PrimaryDataSourceConfig { - - @Bean - @Primary - @ConfigurationProperties("spring.datasource.primary") - public DataSourceProperties primaryDataSourceProperties() { - return new DataSourceProperties(); - } - - @Bean(name = "primaryDataSource") - @Primary - @ConfigurationProperties("spring.datasource.primary.hikari") - public DataSource primaryDataSource() { - return primaryDataSourceProperties().initializeDataSourceBuilder().build(); - } - - @Bean(name = "entityManagerFactoryPrimary") - @Primary - public LocalContainerEntityManagerFactoryBean entityManagerFactoryPrimary( - EntityManagerFactoryBuilder builder, - @Qualifier("primaryDataSource") DataSource dataSource) { - return builder - .dataSource(dataSource) - .packages("cc.amily49.api.module.auth", "cc.amily49.api.module.common") // Explicitly scan business modules - .persistenceUnit("primary") - .build(); - } - - @Bean(name = "transactionManagerPrimary") - @Primary - public PlatformTransactionManager transactionManagerPrimary( - @Qualifier("entityManagerFactoryPrimary") EntityManagerFactory entityManagerFactory) { - return new JpaTransactionManager(entityManagerFactory); - } -} diff --git a/src/main/java/cc/amily49/api/module/config/datasource/SecondaryDataSourceConfig.java b/src/main/java/cc/amily49/api/module/config/datasource/SecondaryDataSourceConfig.java deleted file mode 100644 index 48ad853..0000000 --- a/src/main/java/cc/amily49/api/module/config/datasource/SecondaryDataSourceConfig.java +++ /dev/null @@ -1,63 +0,0 @@ -package cc.amily49.api.module.config.datasource; - -import jakarta.persistence.EntityManagerFactory; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.annotation.EnableTransactionManagement; - -import javax.sql.DataSource; -import java.util.HashMap; -import java.util.Map; - -@Configuration -@EnableTransactionManagement -@EnableJpaRepositories( - basePackages = "cc.amily49.api.module.warehouse", - entityManagerFactoryRef = "entityManagerFactorySecondary", - transactionManagerRef = "transactionManagerSecondary" -) -public class SecondaryDataSourceConfig { - - @Bean - @ConfigurationProperties("spring.datasource.secondary") - public DataSourceProperties secondaryDataSourceProperties() { - return new DataSourceProperties(); - } - - @Bean(name = "secondaryDataSource") - @ConfigurationProperties("spring.datasource.secondary.hikari") - public DataSource secondaryDataSource() { - return secondaryDataSourceProperties().initializeDataSourceBuilder().build(); - } - - @Bean(name = "entityManagerFactorySecondary") - public LocalContainerEntityManagerFactoryBean entityManagerFactorySecondary( - EntityManagerFactoryBuilder builder, - @Qualifier("secondaryDataSource") DataSource dataSource) { - - Map properties = new HashMap<>(); - properties.put("hibernate.hbm2ddl.auto", "update"); // Auto-create tables for warehouse - properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect"); - - return builder - .dataSource(dataSource) - .packages("cc.amily49.api.module.warehouse") - .persistenceUnit("secondary") - .properties(properties) - .build(); - } - - @Bean(name = "transactionManagerSecondary") - public PlatformTransactionManager transactionManagerSecondary( - @Qualifier("entityManagerFactorySecondary") EntityManagerFactory entityManagerFactory) { - return new JpaTransactionManager(entityManagerFactory); - } -} diff --git a/src/main/java/cc/amily49/api/module/filter/FullApiFilter.java b/src/main/java/cc/amily49/api/module/filter/FullApiFilter.java deleted file mode 100644 index ca06f1f..0000000 --- a/src/main/java/cc/amily49/api/module/filter/FullApiFilter.java +++ /dev/null @@ -1,146 +0,0 @@ -package cc.amily49.api.module.filter; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.time.LocalDateTime; -import java.time.ZoneId; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.util.ContentCachingRequestWrapper; - -import cc.amily49.api.module.warehouse.entity.OperationLog; -import cc.amily49.api.module.warehouse.service.LogService; -import jakarta.annotation.Resource; -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.annotation.WebFilter; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -@Component -public class FullApiFilter implements Filter { - - @Resource - private LogService logService; - - @Value("${app.time-zone:Asia/Shanghai}") - private String appTimeZone; - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - HttpServletRequest req = (HttpServletRequest) request; - // Wrap the request to cache the body content for logging - ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(req); - long startTime = System.currentTimeMillis(); - - try { - // Use the wrapped request for the filter chain - chain.doFilter(wrappedRequest, response); - } catch (Exception e) { - req.setAttribute("filter_exception", e.getMessage()); - throw e; - } finally { - try { - HttpServletResponse httpRes = (HttpServletResponse) response; - String servletPath = wrappedRequest.getServletPath(); - String uri = wrappedRequest.getRequestURI(); - - // 跳过高频接口日志 - if ("/message/newest".equals(servletPath)) { - return; - } - - long duration = System.currentTimeMillis() - startTime; - String clientIp = getClientIp(wrappedRequest); - String method = wrappedRequest.getMethod(); - String params; - - // 登录接口登录成功(状态码 2xx)时不记录参数,仅记录失败日志 - if ("/access/login".equals(servletPath) && httpRes.getStatus() >= 200 && httpRes.getStatus() < 300) { - params = "[PROTECTED]"; - } else { - params = getParams(wrappedRequest); - } - - OperationLog opLog = new OperationLog(); - opLog.setIp(clientIp); - opLog.setUrl(uri); - opLog.setMethod(method); - opLog.setParams(params); - opLog.setDuration(duration); - opLog.setCreateTime(LocalDateTime.now(ZoneId.of(appTimeZone))); - opLog.setDescription("API Access Log"); - - // 获取当前登录用户 - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth != null && auth.isAuthenticated() && !"anonymousUser".equals(auth.getPrincipal())) { - opLog.setUsername(auth.getName()); - } - - // 记录可能的异常信息 - Object exception = req.getAttribute("filter_exception"); - if (exception != null) { - opLog.setException(exception.toString()); - } - - logService.saveLog(opLog); - - } catch (Exception ex) { - System.err.println("Failed to save access log: " + ex.getMessage()); - } - } - } - - private String getParams(ContentCachingRequestWrapper request) { - StringBuilder params = new StringBuilder(); - - // Query String - String queryString = request.getQueryString(); - if (queryString != null && !queryString.isEmpty()) { - params.append("Query: ").append(queryString); - } - - // Body - // Note: ContentCachingRequestWrapper only caches content after it has been read. - // If the controller didn't read the body (e.g. GET request or error before reading), this will be empty. - byte[] content = request.getContentAsByteArray(); - if (content.length > 0) { - if (params.length() > 0) { - params.append("; "); - } - try { - String body = new String(content, StandardCharsets.UTF_8); - // Simple truncation or formatting could be added here if needed - params.append("Body: ").append(body); - } catch (Exception e) { - params.append("Body: [Error reading body]"); - } - } - - return params.toString(); - } - - private String getClientIp(HttpServletRequest request) { - String ip = request.getHeader("X-Forwarded-For"); - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("X-Real-IP"); - } - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getRemoteAddr(); - } - - if (ip != null && ip.contains(",")) { - ip = ip.split(",")[0].trim(); - } - - return ip; - } - -} \ No newline at end of file diff --git a/src/main/java/cc/amily49/api/module/util/BaseEncoder.java b/src/main/java/cc/amily49/api/module/util/BaseEncoder.java deleted file mode 100644 index 020ca16..0000000 --- a/src/main/java/cc/amily49/api/module/util/BaseEncoder.java +++ /dev/null @@ -1,98 +0,0 @@ -package cc.amily49.api.module.util; - -import java.util.Base64; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.HexFormat; - -/** - * @author Silence_Lurker - */ -public class BaseEncoder { - - public static enum Algorithm { - BASE64, - BASE64_URL, - HEX, - MD5, - SHA256, - SHA1, - REMOVE_SPECIAL_CHARS, // 你原来的功能 - URL_ENCODE - } - - private BaseEncoder() { - } - - public static String encodeByTarget(String target, Algorithm algorithm) { - if (target == null) { - return null; - } - - try { - return switch (algorithm) { - case BASE64 -> encodeBase64(target); - case BASE64_URL -> encodeBase64Url(target); - case HEX -> encodeHex(target); - case MD5 -> encodeMD5(target); - case SHA256 -> encodeSHA256(target); - case SHA1 -> encodeSHA1(target); - case REMOVE_SPECIAL_CHARS -> removeSpecialChars(target); - case URL_ENCODE -> urlEncode(target); - }; - } catch (Exception e) { - throw new RuntimeException("编码失败: " + e.getMessage(), e); - } - } - - private static String encodeBase64(String target) { - return Base64.getEncoder().encodeToString(target.getBytes(StandardCharsets.UTF_8)); - } - - private static String encodeBase64Url(String target) { - return Base64.getUrlEncoder().encodeToString(target.getBytes(StandardCharsets.UTF_8)); - } - - private static String encodeHex(String target) { - byte[] bytes = target.getBytes(StandardCharsets.UTF_8); - return HexFormat.of().formatHex(bytes); - } - - private static String encodeMD5(String target) throws NoSuchAlgorithmException { - MessageDigest md = MessageDigest.getInstance("MD5"); - byte[] digest = md.digest(target.getBytes(StandardCharsets.UTF_8)); - return HexFormat.of().formatHex(digest); - } - - private static String encodeSHA256(String target) throws NoSuchAlgorithmException { - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] digest = md.digest(target.getBytes(StandardCharsets.UTF_8)); - return HexFormat.of().formatHex(digest); - } - - private static String encodeSHA1(String target) throws NoSuchAlgorithmException { - MessageDigest md = MessageDigest.getInstance("SHA-1"); - byte[] digest = md.digest(target.getBytes(StandardCharsets.UTF_8)); - return HexFormat.of().formatHex(digest); - } - - private static String removeSpecialChars(String target) { - return target.replaceAll("[^a-zA-Z0-9]", ""); - } - - private static String urlEncode(String target) { - return java.net.URLEncoder.encode(target, StandardCharsets.UTF_8); - } - - // 重载方法,保持向后兼容 - public static String encodeByTarget(String target) { - return encodeByTarget(target, Algorithm.REMOVE_SPECIAL_CHARS); - } - - // 工具方法:验证编码结果 - public static boolean verify(String original, String encoded, Algorithm algorithm) { - String newEncoded = encodeByTarget(original, algorithm); - return newEncoded.equals(encoded); - } -} \ No newline at end of file diff --git a/src/main/java/cc/amily49/api/module/util/UUIDGenerater.java b/src/main/java/cc/amily49/api/module/util/UUIDGenerater.java deleted file mode 100644 index acac48c..0000000 --- a/src/main/java/cc/amily49/api/module/util/UUIDGenerater.java +++ /dev/null @@ -1,17 +0,0 @@ -package cc.amily49.api.module.util; - -/** - * @author Silence_Lurker - */ -public class UUIDGenerater { - private UUIDGenerater() { - } - - public static String generate() { - return java.util.UUID.randomUUID().toString(); - } - - public static String generateWithSalt(byte[] salt) { - return java.util.UUID.nameUUIDFromBytes(salt).toString(); - } -} diff --git a/src/main/java/cc/amily49/api/module/warehouse/annotation/Log.java b/src/main/java/cc/amily49/api/module/warehouse/annotation/Log.java deleted file mode 100644 index e2c71fc..0000000 --- a/src/main/java/cc/amily49/api/module/warehouse/annotation/Log.java +++ /dev/null @@ -1,15 +0,0 @@ -package cc.amily49.api.module.warehouse.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface Log { - /** - * Operation description - */ - String value() default ""; -} diff --git a/src/main/java/cc/amily49/api/module/warehouse/aspect/LogAspect.java b/src/main/java/cc/amily49/api/module/warehouse/aspect/LogAspect.java deleted file mode 100644 index bd42fa5..0000000 --- a/src/main/java/cc/amily49/api/module/warehouse/aspect/LogAspect.java +++ /dev/null @@ -1,137 +0,0 @@ -package cc.amily49.api.module.warehouse.aspect; - -import cc.amily49.api.module.warehouse.annotation.Log; -import cc.amily49.api.module.warehouse.entity.OperationLog; -import cc.amily49.api.module.warehouse.service.LogService; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.aspectj.lang.ProceedingJoinPoint; -import org.aspectj.lang.annotation.Around; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.reflect.MethodSignature; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; - -import java.lang.reflect.Method; -import java.util.Arrays; -import java.time.LocalDateTime; -import java.time.ZoneId; -import org.springframework.beans.factory.annotation.Value; - -@Aspect -@Component -@Slf4j -@RequiredArgsConstructor -public class LogAspect { - - private final LogService logService; - private final ObjectMapper objectMapper; - - @Value("${app.time-zone:Asia/Shanghai}") - private String appTimeZone; - - @Around("@annotation(logAnnotation)") - public Object around(ProceedingJoinPoint point, Log logAnnotation) throws Throwable { - long startTime = System.currentTimeMillis(); - Object result = null; - String exceptionMsg = null; - - try { - result = point.proceed(); - } catch (Throwable e) { - exceptionMsg = e.getMessage(); - throw e; - } finally { - long duration = System.currentTimeMillis() - startTime; - recordLog(point, logAnnotation, duration, exceptionMsg); - } - return result; - } - - private void recordLog(ProceedingJoinPoint point, Log logAnnotation, long duration, String exceptionMsg) { - try { - MethodSignature signature = (MethodSignature) point.getSignature(); - - OperationLog operationLog = new OperationLog(); - if (logAnnotation != null) { - operationLog.setDescription(logAnnotation.value()); - } - - // Method info - String className = point.getTarget().getClass().getName(); - String methodName = signature.getName(); - operationLog.setClassMethod(className + "." + methodName + "()"); - - // Request info - ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); - if (attributes != null) { - HttpServletRequest request = attributes.getRequest(); - operationLog.setUrl(request.getRequestURL().toString()); - operationLog.setMethod(request.getMethod()); - operationLog.setIp(getClientIp(request)); - } - - // Args (simplify params to avoid huge logs) - // 如果是登录成功,脱敏参数 - if ("User Login".equals(logAnnotation.value()) && exceptionMsg == null) { - operationLog.setParams("[PROTECTED]"); - } else { - try { - Object[] args = point.getArgs(); - // Filter out non-serializable objects like HttpServletRequest, - // HttpServletResponse - Object[] filteredArgs = Arrays.stream(args) - .filter(arg -> !(arg instanceof jakarta.servlet.http.HttpServletRequest) - && !(arg instanceof jakarta.servlet.http.HttpServletResponse) - && !(arg instanceof org.springframework.web.multipart.MultipartFile)) - .toArray(); - - String params = objectMapper.writeValueAsString(filteredArgs); - // Truncate if too long - if (params.length() > 2000) { - params = params.substring(0, 2000) + "..."; - } - operationLog.setParams(params); - } catch (Exception e) { - operationLog.setParams("Failed to serialize args: " + e.getMessage()); - } - } - - // User info - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication != null && authentication.isAuthenticated() && !"anonymousUser".equals(authentication.getPrincipal())) { - operationLog.setUsername(authentication.getName()); - } else { - operationLog.setUsername("Anonymous"); - } - - operationLog.setException(exceptionMsg); - operationLog.setDuration(duration); - operationLog.setCreateTime(LocalDateTime.now(ZoneId.of(appTimeZone))); - - logService.saveLog(operationLog); - - } catch (Exception e) { - log.error("LogAspect error", e); - } - } - - private String getClientIp(HttpServletRequest request) { - String ip = request.getHeader("X-Forwarded-For"); - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("Proxy-Client-IP"); - } - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getHeader("WL-Proxy-Client-IP"); - } - if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { - ip = request.getRemoteAddr(); - } - return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip; - } -} diff --git a/src/main/java/cc/amily49/api/module/warehouse/controller/LogController.java b/src/main/java/cc/amily49/api/module/warehouse/controller/LogController.java deleted file mode 100644 index d9d4499..0000000 --- a/src/main/java/cc/amily49/api/module/warehouse/controller/LogController.java +++ /dev/null @@ -1,42 +0,0 @@ -package cc.amily49.api.module.warehouse.controller; - -import cc.amily49.api.module.warehouse.entity.OperationLog; -import cc.amily49.api.module.warehouse.repository.OperationLogRepository; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.http.ResponseEntity; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/logs") -@RequiredArgsConstructor -@Tag(name = "System Logs", description = "Operations for querying system logs") -public class LogController { - - private final OperationLogRepository operationLogRepository; - - @Operation(summary = "Get operation logs", description = "Retrieve paginated operation logs") - @PreAuthorize("hasRole('ADMIN')") - @GetMapping - public ResponseEntity> getLogs( - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "20") int size, - @RequestParam(defaultValue = "createTime") String sortBy, - @RequestParam(defaultValue = "desc") String direction) { - - Sort sort = Sort.by(Sort.Direction.fromString(direction), sortBy); - Pageable pageable = PageRequest.of(page, size, sort); - - Page logs = operationLogRepository.findAll(pageable); - return ResponseEntity.ok(logs); - } -} diff --git a/src/main/java/cc/amily49/api/module/warehouse/entity/OperationLog.java b/src/main/java/cc/amily49/api/module/warehouse/entity/OperationLog.java deleted file mode 100644 index c1563e0..0000000 --- a/src/main/java/cc/amily49/api/module/warehouse/entity/OperationLog.java +++ /dev/null @@ -1,76 +0,0 @@ -package cc.amily49.api.module.warehouse.entity; - -import jakarta.persistence.*; -import lombok.Data; -import org.hibernate.annotations.CreationTimestamp; - -import java.time.LocalDateTime; - -@Data -@Entity -@Table(name = "operation_log") -public class OperationLog { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - /** - * Operator username - */ - @Column(length = 50) - private String username; - - /** - * Operation description (from annotation) - */ - @Column(length = 255) - private String description; - - /** - * Request URL - */ - @Column(length = 500) - private String url; - - /** - * HTTP Method (GET, POST, etc.) - */ - @Column(length = 10) - private String method; - - /** - * Class and Method name called - */ - @Column(length = 255) - private String classMethod; - - /** - * Client IP Address - */ - @Column(length = 50) - private String ip; - - /** - * Request Parameters (JSON string, truncated if necessary) - */ - @Column(columnDefinition = "TEXT") - private String params; - - /** - * Exception message if failed - */ - @Column(columnDefinition = "TEXT") - private String exception; - - /** - * Execution duration in milliseconds - */ - private Long duration; - - /** - * Operation time - */ - @Column(updatable = false) - private LocalDateTime createTime; -} diff --git a/src/main/java/cc/amily49/api/module/warehouse/repository/OperationLogRepository.java b/src/main/java/cc/amily49/api/module/warehouse/repository/OperationLogRepository.java deleted file mode 100644 index 32b0223..0000000 --- a/src/main/java/cc/amily49/api/module/warehouse/repository/OperationLogRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package cc.amily49.api.module.warehouse.repository; - -import cc.amily49.api.module.warehouse.entity.OperationLog; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface OperationLogRepository extends JpaRepository { -} diff --git a/src/main/java/cc/amily49/api/module/warehouse/service/LogService.java b/src/main/java/cc/amily49/api/module/warehouse/service/LogService.java deleted file mode 100644 index f5469f4..0000000 --- a/src/main/java/cc/amily49/api/module/warehouse/service/LogService.java +++ /dev/null @@ -1,28 +0,0 @@ -package cc.amily49.api.module.warehouse.service; - -import cc.amily49.api.module.warehouse.entity.OperationLog; -import cc.amily49.api.module.warehouse.repository.OperationLogRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -@Service -@RequiredArgsConstructor -@Slf4j -public class LogService { - - private final OperationLogRepository operationLogRepository; - - /** - * Save log asynchronously - */ - @Async - public void saveLog(OperationLog logEntry) { - try { - operationLogRepository.save(logEntry); - } catch (Exception e) { - log.error("Failed to save operation log", e); - } - } -}