init
This commit is contained in:
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
@ -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
|
||||
164
pom.xml
Normal file
164
pom.xml
Normal file
@ -0,0 +1,164 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.0</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>ZKH Framework</name>
|
||||
<description>A Java framework for ZKH applications</description>
|
||||
<url>https://gitea.jcfd.vip/zkh/zkh-framework</url>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>The Apache Software License, Version 2.0</name>
|
||||
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<name>zkh</name>
|
||||
<email>1650697374@qq.com</email>
|
||||
<organization>横球集团</organization>
|
||||
<organizationUrl>https://www.jcfd.vip</organizationUrl>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<scm>
|
||||
<connection>scm:git:git://gitea.jcfd.vip/zkh/zkh-framework.git</connection>
|
||||
<developerConnection>scm:git:ssh://gitea.jcfd.vip:zkh/zkh-framework.git</developerConnection>
|
||||
<url>https://gitea.jcfd.vip/zkh/zkh-framework</url>
|
||||
</scm>
|
||||
|
||||
<modules>
|
||||
<module>zkh-common</module>
|
||||
<module>zkh-web</module>
|
||||
<module>zkh-data</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<spring-boot.version>3.5.7</spring-boot.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-web</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>release</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- Maven GPG Plugin - 用于签名构件 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<!-- 使用环境变量中的gpg密钥密码 -->
|
||||
<passphrase>${env.GPG_PASSPHRASE}</passphrase>
|
||||
<!-- 指定GPG可执行文件路径 -->
|
||||
<executable>gpg</executable>
|
||||
<!-- 使用默认密钥 -->
|
||||
<useAgent>true</useAgent>
|
||||
<!-- 设置gpg主目录 -->
|
||||
<gpgArguments>
|
||||
<arg>--pinentry-mode</arg>
|
||||
<arg>loopback</arg>
|
||||
</gpgArguments>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<!-- Central Publishing Plugin - 发布到Maven Central -->
|
||||
<plugin>
|
||||
<groupId>org.sonatype.central</groupId>
|
||||
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||
<version>0.9.0</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<publishingServerId>central</publishingServerId>
|
||||
<!-- 确保所有文件都被签名 -->
|
||||
<autoPublish>true</autoPublish>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- 在发布时激活source和javadoc插件 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.3.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<!-- GPG签名 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>sign</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
65
zkh-common/pom.xml
Normal file
65
zkh-common/pom.xml
Normal file
@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zkh-common</artifactId>
|
||||
<name>ZKH Common</name>
|
||||
<description>Common utilities and base classes for ZKH framework</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>jakarta.persistence</groupId>
|
||||
<artifactId>jakarta.persistence-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-annotations</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-commons</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.data</groupId>
|
||||
<artifactId>spring-data-jpa</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
69
zkh-common/src/main/java/vip/jcfd/common/core/R.java
Normal file
69
zkh-common/src/main/java/vip/jcfd/common/core/R.java
Normal file
@ -0,0 +1,69 @@
|
||||
package vip.jcfd.common.core;
|
||||
|
||||
public class R<T> {
|
||||
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 <T> R<T> success(T data) {
|
||||
return new R<>(200, "操作成功", true, data);
|
||||
}
|
||||
|
||||
public static <T> R<T> success(String message) {
|
||||
return new R<>(200, message, true, null);
|
||||
}
|
||||
|
||||
public static <T> R<T> error(String message) {
|
||||
return new R<>(400, message, false, null);
|
||||
}
|
||||
|
||||
public static <T> R<T> 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;
|
||||
}
|
||||
}
|
||||
52
zkh-data/pom.xml
Normal file
52
zkh-data/pom.xml
Normal file
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zkh-data</artifactId>
|
||||
<name>ZKH Data</name>
|
||||
<description>Data layer components for ZKH framework</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
71
zkh-web/pom.xml
Normal file
71
zkh-web/pom.xml
Normal file
@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zkh-web</artifactId>
|
||||
<name>ZKH Web</name>
|
||||
<description>Web components for ZKH framework</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals>
|
||||
<goal>jar-no-fork</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals>
|
||||
<goal>jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -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<String> handleException(Exception e) {
|
||||
log.error("服务异常", e);
|
||||
return R.serverError("服务器繁忙,请稍候重试");
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = BizException.class)
|
||||
public R<String> handleBizException(BizException e) {
|
||||
log.error("业务异常", e);
|
||||
return R.error(e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = NoResourceFoundException.class)
|
||||
public R<String> handleNotFoundException(NoResourceFoundException e) {
|
||||
log.error("404异常", e);
|
||||
return new R<>(404, "您访问的地址不存在", false, null);
|
||||
}
|
||||
}
|
||||
29
zkh-web/src/main/java/vip/jcfd/web/config/RedisConfig.java
Normal file
29
zkh-web/src/main/java/vip/jcfd/web/config/RedisConfig.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
202
zkh-web/src/main/java/vip/jcfd/web/config/WebSecurityConfig.java
Normal file
202
zkh-web/src/main/java/vip/jcfd/web/config/WebSecurityConfig.java
Normal file
@ -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<String> 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<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "未登录", false, null);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
objectMapper.writeValue(response.getWriter(), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
|
||||
log.warn("登录失败", exception);
|
||||
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "用户名或密码错误", false, null);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
objectMapper.writeValue(response.getWriter(), data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
|
||||
log.info("用户「{}」登录成功", authentication.getName());
|
||||
String token = UUID.randomUUID().toString();
|
||||
tokenRedisStorage.put(token, authentication);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
R<Object> data = new R<>(HttpServletResponse.SC_OK, "登录成功", true, token);
|
||||
objectMapper.writeValue(response.getWriter(), data);
|
||||
}
|
||||
}
|
||||
|
||||
private record CustomAccessDeniedHandler(ObjectMapper objectMapper) implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
log.warn("访问被拒绝", accessDeniedException);
|
||||
if (authentication.isAuthenticated()) {
|
||||
log.warn("用户「{}」访问「{}」被拒绝,因为:{}", authentication.getPrincipal(), request.getRequestURI(), accessDeniedException.getMessage());
|
||||
} else {
|
||||
log.warn("匿名用户访问「{}」被拒绝,因为:{}", request.getRequestURI(), accessDeniedException.getMessage());
|
||||
}
|
||||
R<Object> data = new R<>(HttpServletResponse.SC_FORBIDDEN, "无权限", false, null);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
objectMapper.writeValue(response.getWriter(), data);
|
||||
}
|
||||
}
|
||||
|
||||
private record CustomLogoutSuccessHandler(ObjectMapper objectMapper,
|
||||
TokenRedisStorage tokenRedisStorage) implements LogoutHandler {
|
||||
@Override
|
||||
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
|
||||
if (header != null && header.startsWith("Bearer ")) {
|
||||
String token = header.substring(7);
|
||||
authentication = tokenRedisStorage.get(token);
|
||||
tokenRedisStorage.remove(token);
|
||||
}
|
||||
if (authentication != null) {
|
||||
log.info("用户「{}」退出成功", authentication.getName());
|
||||
String all = request.getParameter("all");
|
||||
if ("true".equals(all)) {
|
||||
tokenRedisStorage.removeByUserName(authentication.getName());
|
||||
}
|
||||
}
|
||||
R<Object> data = new R<>(HttpServletResponse.SC_OK, "退出成功", true, null);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
try {
|
||||
objectMapper.writeValue(response.getWriter(), data);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package vip.jcfd.web.config.props;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@ConfigurationProperties(prefix = "zkh.security")
|
||||
public class SecurityProps {
|
||||
|
||||
private String[] ignoreUrls;
|
||||
|
||||
private Duration duration;
|
||||
|
||||
public String[] getIgnoreUrls() {
|
||||
return ignoreUrls;
|
||||
}
|
||||
|
||||
public void setIgnoreUrls(String[] ignoreUrls) {
|
||||
this.ignoreUrls = ignoreUrls;
|
||||
}
|
||||
|
||||
public Duration getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public void setDuration(Duration duration) {
|
||||
this.duration = duration;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package vip.jcfd.web.filter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.ServletInputStream;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class JsonUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public JsonUsernamePasswordAuthenticationFilter(ObjectMapper objectMapper, AuthenticationManager authenticationManager) {
|
||||
super(authenticationManager);
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
private record LoginDTO(String username, String password) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
|
||||
if (!request.getMethod().equals("POST")) {
|
||||
throw new AuthenticationServiceException("登录请求只支持POST");
|
||||
}
|
||||
try {
|
||||
ServletInputStream inputStream = request.getInputStream();
|
||||
LoginDTO loginDTO = objectMapper.readValue(inputStream, LoginDTO.class);
|
||||
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated(loginDTO.username, loginDTO.password);
|
||||
setDetails(request, token);
|
||||
return getAuthenticationManager().authenticate(token);
|
||||
} catch (IOException e) {
|
||||
logger.error("读取请求体失败", e);
|
||||
throw new AuthenticationServiceException("登录请求异常");
|
||||
}
|
||||
}
|
||||
}
|
||||
53
zkh-web/src/main/java/vip/jcfd/web/filter/TokenFilter.java
Normal file
53
zkh-web/src/main/java/vip/jcfd/web/filter/TokenFilter.java
Normal file
@ -0,0 +1,53 @@
|
||||
package vip.jcfd.web.filter;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import vip.jcfd.web.redis.TokenRedisStorage;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class TokenFilter extends OncePerRequestFilter {
|
||||
|
||||
private final TokenRedisStorage tokenRedisStorage;
|
||||
|
||||
public TokenFilter(TokenRedisStorage tokenRedisStorage) {
|
||||
this.tokenRedisStorage = tokenRedisStorage;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
|
||||
// 获取token
|
||||
String token = getToken(request);
|
||||
if (!token.isBlank() && validateToken(token)) {
|
||||
Authentication authentication = extractAuthentication(token);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
boolean validateToken(String token) {
|
||||
return tokenRedisStorage.exists(token);
|
||||
}
|
||||
|
||||
Authentication extractAuthentication(String token) {
|
||||
return tokenRedisStorage.get(token);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
String getToken(HttpServletRequest request) {
|
||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (header != null && header.startsWith("Bearer ")) {
|
||||
return header.substring(7);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
package vip.jcfd.web.redis;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class TokenRedisStorage extends RedisTemplate<String, Authentication> {
|
||||
|
||||
private final static Logger logger = LoggerFactory.getLogger(TokenRedisStorage.class);
|
||||
private final Duration expire;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
private static final String TOKEN_KEY_PREFIX = "TOKEN:";
|
||||
private static final String TOKEN_LIST_KEY_PREFIX = TOKEN_KEY_PREFIX + "LIST:";
|
||||
|
||||
public TokenRedisStorage(Duration expire, StringRedisTemplate stringRedisTemplate) {
|
||||
this.expire = expire;
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
}
|
||||
|
||||
public Authentication get(String token) {
|
||||
return opsForValue().get(TOKEN_KEY_PREFIX + token);
|
||||
}
|
||||
|
||||
public void put(String token, Authentication authentication) {
|
||||
opsForValue().set(TOKEN_KEY_PREFIX + token, authentication, expire);
|
||||
stringRedisTemplate.opsForList().leftPush(TOKEN_LIST_KEY_PREFIX + authentication.getName(), token);
|
||||
}
|
||||
|
||||
public void remove(String token) {
|
||||
Authentication authentication = get(token);
|
||||
if (authentication != null) {
|
||||
stringRedisTemplate.opsForList().remove(TOKEN_LIST_KEY_PREFIX + authentication.getName(), 0, token);
|
||||
expire(TOKEN_KEY_PREFIX + token, Duration.ZERO);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean exists(String token) {
|
||||
return opsForValue().get(TOKEN_KEY_PREFIX + token) != null;
|
||||
}
|
||||
|
||||
public void removeByUserName(String username) {
|
||||
List<String> range = stringRedisTemplate.opsForList().range(TOKEN_LIST_KEY_PREFIX + username, 0, -1);
|
||||
if (CollectionUtils.isEmpty(range)) {
|
||||
return;
|
||||
}
|
||||
for (String s : range) {
|
||||
expire(TOKEN_KEY_PREFIX + s, Duration.ZERO);
|
||||
}
|
||||
expire(TOKEN_LIST_KEY_PREFIX + username, Duration.ZERO);
|
||||
}
|
||||
|
||||
private record TokenStorage(String key, Set<String> tokens) {
|
||||
public void addToken(String token) {
|
||||
tokens.add(token);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearExpiredTokens() {
|
||||
logger.info("开始清理过期token");
|
||||
Set<String> keys = keys(TOKEN_LIST_KEY_PREFIX + "*");
|
||||
if (CollectionUtils.isEmpty(keys)) {
|
||||
logger.info("清理过期token完成");
|
||||
return;
|
||||
}
|
||||
List<TokenStorage> tokenStorages = new ArrayList<>();
|
||||
for (String key : keys) {
|
||||
List<String> range = stringRedisTemplate.opsForList().range(key, 0, -1);
|
||||
if (CollectionUtils.isEmpty(range)) {
|
||||
continue;
|
||||
}
|
||||
TokenStorage tokenStorage = new TokenStorage(key, new HashSet<>());
|
||||
tokenStorages.add(tokenStorage);
|
||||
for (String token : range) {
|
||||
if (!exists(token)) {
|
||||
tokenStorage.addToken(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.info("收集过期token完成,共{}个token", tokenStorages.stream().map(TokenStorage::tokens).mapToInt(Set::size).sum());
|
||||
for (TokenStorage tokenStorage : tokenStorages) {
|
||||
for (String token : tokenStorage.tokens) {
|
||||
stringRedisTemplate.opsForList().remove(tokenStorage.key, 0, token);
|
||||
}
|
||||
}
|
||||
logger.info("清理过期token完成");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user