From dbf4f87e7b04c1a328c23748727b980f70407337 Mon Sep 17 00:00:00 2001
From: zkh <1650697374@qq.com>
Date: Thu, 20 Nov 2025 18:33:49 +0800
Subject: [PATCH] init
---
.gitignore | 40 ++++
pom.xml | 164 ++++++++++++++
zkh-common/pom.xml | 65 ++++++
.../java/vip/jcfd/common/core/BaseEntity.java | 79 +++++++
.../vip/jcfd/common/core/BizException.java | 23 ++
.../src/main/java/vip/jcfd/common/core/R.java | 69 ++++++
zkh-data/pom.xml | 52 +++++
zkh-web/pom.xml | 71 ++++++
.../web/config/GlobalExceptionHandler.java | 33 +++
.../java/vip/jcfd/web/config/RedisConfig.java | 29 +++
.../jcfd/web/config/WebSecurityConfig.java | 202 ++++++++++++++++++
.../jcfd/web/config/props/SecurityProps.java | 29 +++
...nUsernamePasswordAuthenticationFilter.java | 44 ++++
.../java/vip/jcfd/web/filter/TokenFilter.java | 53 +++++
.../vip/jcfd/web/redis/TokenRedisStorage.java | 97 +++++++++
15 files changed, 1050 insertions(+)
create mode 100644 .gitignore
create mode 100644 pom.xml
create mode 100644 zkh-common/pom.xml
create mode 100644 zkh-common/src/main/java/vip/jcfd/common/core/BaseEntity.java
create mode 100644 zkh-common/src/main/java/vip/jcfd/common/core/BizException.java
create mode 100644 zkh-common/src/main/java/vip/jcfd/common/core/R.java
create mode 100644 zkh-data/pom.xml
create mode 100644 zkh-web/pom.xml
create mode 100644 zkh-web/src/main/java/vip/jcfd/web/config/GlobalExceptionHandler.java
create mode 100644 zkh-web/src/main/java/vip/jcfd/web/config/RedisConfig.java
create mode 100644 zkh-web/src/main/java/vip/jcfd/web/config/WebSecurityConfig.java
create mode 100644 zkh-web/src/main/java/vip/jcfd/web/config/props/SecurityProps.java
create mode 100644 zkh-web/src/main/java/vip/jcfd/web/filter/JsonUsernamePasswordAuthenticationFilter.java
create mode 100644 zkh-web/src/main/java/vip/jcfd/web/filter/TokenFilter.java
create mode 100644 zkh-web/src/main/java/vip/jcfd/web/redis/TokenRedisStorage.java
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a20fba1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,40 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+.kotlin
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
+.idea
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..2495d0d
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,164 @@
+
+
+ 4.0.0
+
+ vip.jcfd
+ zkh-framework
+ 1.0
+ pom
+ ZKH Framework
+ A Java framework for ZKH applications
+ https://gitea.jcfd.vip/zkh/zkh-framework
+
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+
+
+
+
+
+ zkh
+ 1650697374@qq.com
+ 横球集团
+ https://www.jcfd.vip
+
+
+
+
+ scm:git:git://gitea.jcfd.vip/zkh/zkh-framework.git
+ scm:git:ssh://gitea.jcfd.vip:zkh/zkh-framework.git
+ https://gitea.jcfd.vip/zkh/zkh-framework
+
+
+
+ zkh-common
+ zkh-web
+ zkh-data
+
+
+
+ 21
+ 21
+ UTF-8
+ 3.5.7
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ pom
+ import
+
+
+ vip.jcfd
+ zkh-common
+ ${project.version}
+
+
+ vip.jcfd
+ zkh-web
+ ${project.version}
+
+
+
+
+
+
+ release
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.1.0
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+ ${env.GPG_PASSPHRASE}
+
+ gpg
+
+ true
+
+
+ --pinentry-mode
+ loopback
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.9.0
+ true
+
+ central
+
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.0
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.5.0
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 3.1.0
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
+
diff --git a/zkh-common/pom.xml b/zkh-common/pom.xml
new file mode 100644
index 0000000..48d9de6
--- /dev/null
+++ b/zkh-common/pom.xml
@@ -0,0 +1,65 @@
+
+
+ 4.0.0
+
+ vip.jcfd
+ zkh-framework
+ 1.0
+
+
+ zkh-common
+ ZKH Common
+ Common utilities and base classes for ZKH framework
+
+
+
+ jakarta.persistence
+ jakarta.persistence-api
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+ org.springframework.data
+ spring-data-commons
+
+
+ org.springframework.data
+ spring-data-jpa
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.4.1
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+
diff --git a/zkh-common/src/main/java/vip/jcfd/common/core/BaseEntity.java b/zkh-common/src/main/java/vip/jcfd/common/core/BaseEntity.java
new file mode 100644
index 0000000..76b5f72
--- /dev/null
+++ b/zkh-common/src/main/java/vip/jcfd/common/core/BaseEntity.java
@@ -0,0 +1,79 @@
+package vip.jcfd.common.core;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import jakarta.persistence.*;
+import org.springframework.data.annotation.CreatedBy;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedBy;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import java.time.LocalDateTime;
+
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public class BaseEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.AUTO)
+ private Long id;
+
+ @Column
+ @CreatedDate
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime createTime;
+
+ @Column
+ @LastModifiedDate
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+ private LocalDateTime updateTime;
+
+ @Column
+ @CreatedBy
+ private String createBy;
+
+ @Column
+ @LastModifiedBy
+ private String updateBy;
+
+ public Long getId() {
+ return id;
+ }
+
+ public void setId(Long id) {
+ this.id = id;
+ }
+
+ public LocalDateTime getCreateTime() {
+ return createTime;
+ }
+
+ public void setCreateTime(LocalDateTime createTime) {
+ this.createTime = createTime;
+ }
+
+ public LocalDateTime getUpdateTime() {
+ return updateTime;
+ }
+
+ public void setUpdateTime(LocalDateTime updateTime) {
+ this.updateTime = updateTime;
+ }
+
+ public String getCreateBy() {
+ return createBy;
+ }
+
+ public void setCreateBy(String createBy) {
+ this.createBy = createBy;
+ }
+
+ public String getUpdateBy() {
+ return updateBy;
+ }
+
+ public void setUpdateBy(String updateBy) {
+ this.updateBy = updateBy;
+ }
+
+
+}
diff --git a/zkh-common/src/main/java/vip/jcfd/common/core/BizException.java b/zkh-common/src/main/java/vip/jcfd/common/core/BizException.java
new file mode 100644
index 0000000..0fb7f55
--- /dev/null
+++ b/zkh-common/src/main/java/vip/jcfd/common/core/BizException.java
@@ -0,0 +1,23 @@
+package vip.jcfd.common.core;
+
+
+public class BizException extends RuntimeException {
+ public BizException() {
+ }
+
+ public BizException(String message) {
+ super(message);
+ }
+
+ public BizException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public BizException(Throwable cause) {
+ super(cause);
+ }
+
+ public BizException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+}
diff --git a/zkh-common/src/main/java/vip/jcfd/common/core/R.java b/zkh-common/src/main/java/vip/jcfd/common/core/R.java
new file mode 100644
index 0000000..74b3b43
--- /dev/null
+++ b/zkh-common/src/main/java/vip/jcfd/common/core/R.java
@@ -0,0 +1,69 @@
+package vip.jcfd.common.core;
+
+public class R {
+ private int code;
+ private String message;
+ private boolean success;
+ private T data;
+
+ public R() {
+ this.code = 200;
+ this.message = "操作成功";
+ this.success = true;
+ }
+
+ public R(int code, String message, boolean success, T data) {
+ this.code = code;
+ this.message = message;
+ this.success = success;
+ this.data = data;
+ }
+
+ public static R success(T data) {
+ return new R<>(200, "操作成功", true, data);
+ }
+
+ public static R success(String message) {
+ return new R<>(200, message, true, null);
+ }
+
+ public static R error(String message) {
+ return new R<>(400, message, false, null);
+ }
+
+ public static R serverError(String message) {
+ return new R<>(500, message, false, null);
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public void setCode(int code) {
+ this.code = code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public void setSuccess(boolean success) {
+ this.success = success;
+ }
+
+ public T getData() {
+ return data;
+ }
+
+ public void setData(T data) {
+ this.data = data;
+ }
+}
diff --git a/zkh-data/pom.xml b/zkh-data/pom.xml
new file mode 100644
index 0000000..c194a44
--- /dev/null
+++ b/zkh-data/pom.xml
@@ -0,0 +1,52 @@
+
+
+ 4.0.0
+
+ vip.jcfd
+ zkh-framework
+ 1.0
+
+
+ zkh-data
+ ZKH Data
+ Data layer components for ZKH framework
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.4.1
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+
diff --git a/zkh-web/pom.xml b/zkh-web/pom.xml
new file mode 100644
index 0000000..ecc2c75
--- /dev/null
+++ b/zkh-web/pom.xml
@@ -0,0 +1,71 @@
+
+
+ 4.0.0
+
+
+ vip.jcfd
+ zkh-framework
+ 1.0
+
+
+ zkh-web
+ ZKH Web
+ Web components for ZKH framework
+
+
+
+ vip.jcfd
+ zkh-common
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.2.1
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.4.1
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+
diff --git a/zkh-web/src/main/java/vip/jcfd/web/config/GlobalExceptionHandler.java b/zkh-web/src/main/java/vip/jcfd/web/config/GlobalExceptionHandler.java
new file mode 100644
index 0000000..a61251c
--- /dev/null
+++ b/zkh-web/src/main/java/vip/jcfd/web/config/GlobalExceptionHandler.java
@@ -0,0 +1,33 @@
+package vip.jcfd.web.config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.servlet.resource.NoResourceFoundException;
+import vip.jcfd.common.core.BizException;
+import vip.jcfd.common.core.R;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+ @ExceptionHandler(value = Exception.class)
+ public R handleException(Exception e) {
+ log.error("服务异常", e);
+ return R.serverError("服务器繁忙,请稍候重试");
+ }
+
+ @ExceptionHandler(value = BizException.class)
+ public R handleBizException(BizException e) {
+ log.error("业务异常", e);
+ return R.error(e.getMessage());
+ }
+
+ @ExceptionHandler(value = NoResourceFoundException.class)
+ public R handleNotFoundException(NoResourceFoundException e) {
+ log.error("404异常", e);
+ return new R<>(404, "您访问的地址不存在", false, null);
+ }
+}
diff --git a/zkh-web/src/main/java/vip/jcfd/web/config/RedisConfig.java b/zkh-web/src/main/java/vip/jcfd/web/config/RedisConfig.java
new file mode 100644
index 0000000..ed9a1f3
--- /dev/null
+++ b/zkh-web/src/main/java/vip/jcfd/web/config/RedisConfig.java
@@ -0,0 +1,29 @@
+package vip.jcfd.web.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import vip.jcfd.web.config.props.SecurityProps;
+import vip.jcfd.web.redis.TokenRedisStorage;
+
+@Configuration
+public class RedisConfig {
+
+ private final SecurityProps securityProps;
+
+ public RedisConfig(SecurityProps securityProps) {
+ this.securityProps = securityProps;
+ }
+
+ @Bean
+ public TokenRedisStorage tokenRedisTemplate(RedisConnectionFactory factory, StringRedisTemplate stringRedisTemplate) {
+ TokenRedisStorage tokenRedisStorage = new TokenRedisStorage(securityProps.getDuration(), stringRedisTemplate);
+ tokenRedisStorage.setConnectionFactory(factory);
+ tokenRedisStorage.setValueSerializer(new JdkSerializationRedisSerializer());
+ tokenRedisStorage.setKeySerializer(new StringRedisSerializer());
+ return tokenRedisStorage;
+ }
+}
diff --git a/zkh-web/src/main/java/vip/jcfd/web/config/WebSecurityConfig.java b/zkh-web/src/main/java/vip/jcfd/web/config/WebSecurityConfig.java
new file mode 100644
index 0000000..50cd570
--- /dev/null
+++ b/zkh-web/src/main/java/vip/jcfd/web/config/WebSecurityConfig.java
@@ -0,0 +1,202 @@
+package vip.jcfd.web.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.domain.AuditorAware;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+import org.springframework.http.HttpHeaders;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
+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.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.logout.LogoutHandler;
+import vip.jcfd.common.core.R;
+import vip.jcfd.web.config.props.SecurityProps;
+import vip.jcfd.web.filter.JsonUsernamePasswordAuthenticationFilter;
+import vip.jcfd.web.filter.TokenFilter;
+import vip.jcfd.web.redis.TokenRedisStorage;
+
+import java.io.IOException;
+import java.util.Optional;
+import java.util.UUID;
+
+@Configuration
+@EnableWebSecurity
+@ConfigurationPropertiesScan(basePackageClasses = {SecurityProps.class})
+@EnableJpaAuditing
+@EnableAsync
+@EnableScheduling
+public class WebSecurityConfig {
+
+ private static final Logger log = LoggerFactory.getLogger(WebSecurityConfig.class);
+ private final SecurityProps securityProps;
+ private final ObjectMapper objectMapper;
+ private final TokenRedisStorage tokenRedisStorage;
+
+ public WebSecurityConfig(SecurityProps securityProps, ObjectMapper objectMapper, TokenRedisStorage tokenRedisStorage) {
+ this.securityProps = securityProps;
+ this.objectMapper = objectMapper;
+ this.tokenRedisStorage = tokenRedisStorage;
+ }
+
+ @Scheduled(cron = "0 */30 * * * *")
+ @Async
+ public void scheduleClearExpiredTokens() {
+ tokenRedisStorage.clearExpiredTokens();
+ }
+
+ @Bean
+ public AuditorAware auditorAware() {
+ return () -> Optional.ofNullable(SecurityContextHolder.getContext())
+ .map(SecurityContext::getAuthentication)
+ .map(Authentication::getName)
+ .or(() -> Optional.of("system"));
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public TokenFilter tokenFilter() {
+ return new TokenFilter(tokenRedisStorage);
+ }
+
+ @Bean
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
+ return configuration.getAuthenticationManager();
+ }
+
+ @Bean
+ public SecurityFilterChain security(HttpSecurity http, TokenFilter tokenFilter, AuthenticationManager authenticationManager) throws Exception {
+ http.authorizeHttpRequests(config -> {
+ config.requestMatchers(securityProps.getIgnoreUrls()).permitAll();
+ config.anyRequest().authenticated();
+ });
+ CustomAuthenticationEntryPoint authenticationEntryPoint = new CustomAuthenticationEntryPoint(objectMapper, tokenRedisStorage);
+ http.formLogin(config -> {
+ config.loginProcessingUrl("/login");
+ config.failureHandler(authenticationEntryPoint);
+ config.successHandler(authenticationEntryPoint);
+ });
+ http.csrf(AbstractHttpConfigurer::disable);
+ http.logout(config -> {
+ config.addLogoutHandler(new CustomLogoutSuccessHandler(objectMapper, tokenRedisStorage));
+ });
+ http.rememberMe(AbstractHttpConfigurer::disable);
+ http.sessionManagement(AbstractHttpConfigurer::disable);
+ http.exceptionHandling(config -> {
+ config.authenticationEntryPoint(authenticationEntryPoint);
+ config.accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper));
+ });
+
+ http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
+ JsonUsernamePasswordAuthenticationFilter filter = new JsonUsernamePasswordAuthenticationFilter(objectMapper, authenticationManager);
+ filter.setAuthenticationSuccessHandler(authenticationEntryPoint);
+ http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
+ return http.build();
+ }
+
+ private record CustomAuthenticationEntryPoint(
+ ObjectMapper objectMapper,
+ TokenRedisStorage tokenRedisStorage) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler {
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
+ log.warn("认证失败", authException);
+ R