Compare commits
2
Commits
a7e1c26853
...
042ef9a81e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
042ef9a81e | ||
|
|
fe2240e266 |
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>vip.jcfd</groupId>
|
<groupId>vip.jcfd</groupId>
|
||||||
<artifactId>zkh-framework</artifactId>
|
<artifactId>zkh-framework</artifactId>
|
||||||
<version>1.4</version>
|
<version>1.5</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<name>ZKH Framework</name>
|
<name>ZKH Framework</name>
|
||||||
<description>A Java framework for ZKH applications</description>
|
<description>A Java framework for ZKH applications</description>
|
||||||
@@ -38,6 +38,8 @@
|
|||||||
<module>zkh-common</module>
|
<module>zkh-common</module>
|
||||||
<module>zkh-web</module>
|
<module>zkh-web</module>
|
||||||
<module>zkh-data</module>
|
<module>zkh-data</module>
|
||||||
|
<module>zkh-log</module>
|
||||||
|
<module>zkh-file</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
@@ -71,6 +73,16 @@
|
|||||||
<artifactId>zkh-data</artifactId>
|
<artifactId>zkh-data</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>vip.jcfd</groupId>
|
||||||
|
<artifactId>zkh-log</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>vip.jcfd</groupId>
|
||||||
|
<artifactId>zkh-file</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springdoc</groupId>
|
<groupId>org.springdoc</groupId>
|
||||||
<artifactId>springdoc-openapi-common</artifactId>
|
<artifactId>springdoc-openapi-common</artifactId>
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>vip.jcfd</groupId>
|
<groupId>vip.jcfd</groupId>
|
||||||
<artifactId>zkh-framework</artifactId>
|
<artifactId>zkh-framework</artifactId>
|
||||||
<version>1.4</version>
|
<version>1.5</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>zkh-common</artifactId>
|
<artifactId>zkh-common</artifactId>
|
||||||
|
|||||||
+1
-7
@@ -6,19 +6,13 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>vip.jcfd</groupId>
|
<groupId>vip.jcfd</groupId>
|
||||||
<artifactId>zkh-framework</artifactId>
|
<artifactId>zkh-framework</artifactId>
|
||||||
<version>1.4</version>
|
<version>1.5</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>zkh-data</artifactId>
|
<artifactId>zkh-data</artifactId>
|
||||||
<name>ZKH Data</name>
|
<name>ZKH Data</name>
|
||||||
<description>Data layer components for ZKH framework</description>
|
<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>
|
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>jakarta.persistence</groupId>
|
<groupId>jakarta.persistence</groupId>
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?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.5</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>zkh-file</artifactId>
|
||||||
|
<name>ZKH file</name>
|
||||||
|
<description>
|
||||||
|
文件处理模块,提供文件上传、下载、处理等功能
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>vip.jcfd</groupId>
|
||||||
|
<artifactId>zkh-common</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-io</groupId>
|
||||||
|
<artifactId>commons-io</artifactId>
|
||||||
|
<version>2.20.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.minio</groupId>
|
||||||
|
<artifactId>minio</artifactId>
|
||||||
|
<version>8.5.17</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-all</artifactId>
|
||||||
|
<version>5.8.41</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>2.8.14</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.14.1</version>
|
||||||
|
<configuration>
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<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,23 @@
|
|||||||
|
package vip.jcfd.file.config;
|
||||||
|
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import vip.jcfd.file.config.props.MinioProps;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationPropertiesScan(basePackageClasses = {MinioProps.class})
|
||||||
|
public class MinioConfig {
|
||||||
|
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(name = "minio.endpoint")
|
||||||
|
public MinioClient minioClient(MinioProps minioProps) {
|
||||||
|
return MinioClient.builder()
|
||||||
|
.endpoint(minioProps.endpoint())
|
||||||
|
.credentials(minioProps.accessKey(), minioProps.secretKey())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package vip.jcfd.file.config.props;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
@ConfigurationProperties(prefix = "minio")
|
||||||
|
public record MinioProps(String endpoint, String accessKey, String secretKey, String bucket) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package vip.jcfd.file.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import vip.jcfd.common.core.R;
|
||||||
|
import vip.jcfd.file.dto.FileInfo;
|
||||||
|
import vip.jcfd.file.service.IFileDownloadService;
|
||||||
|
import vip.jcfd.file.service.IFileUploadService;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
@Tag(name = "文件管理", description = "文件上传和下载接口")
|
||||||
|
@RestController("_fileController")
|
||||||
|
@RequestMapping("/file")
|
||||||
|
public class FileController {
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(FileController.class);
|
||||||
|
private final IFileUploadService fileUploadService;
|
||||||
|
private final IFileDownloadService fileDownloadService;
|
||||||
|
|
||||||
|
public FileController(IFileUploadService fileUploadService, IFileDownloadService fileDownloadService) {
|
||||||
|
this.fileUploadService = fileUploadService;
|
||||||
|
this.fileDownloadService = fileDownloadService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "上传文件", description = "上传文件到服务器")
|
||||||
|
@ApiResponse(responseCode = "200", description = "上传成功",
|
||||||
|
content = @Content(mediaType = "application/json",
|
||||||
|
schema = @Schema(implementation = R.class)))
|
||||||
|
@PostMapping
|
||||||
|
public R<FileInfo> upload(
|
||||||
|
@Parameter(description = "要上传的文件", required = true)
|
||||||
|
MultipartFile file) {
|
||||||
|
try {
|
||||||
|
FileInfo upload = fileUploadService.upload(file);
|
||||||
|
return R.success(upload);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("上传失败", e);
|
||||||
|
return R.serverError("上传失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "下载文件", description = "根据文件路径下载文件")
|
||||||
|
@ApiResponse(responseCode = "200", description = "下载成功")
|
||||||
|
@ApiResponse(responseCode = "404", description = "文件未找到")
|
||||||
|
@GetMapping("{path}")
|
||||||
|
public ResponseEntity<Resource> download(
|
||||||
|
@Parameter(description = "文件路径", required = true)
|
||||||
|
@PathVariable String path) {
|
||||||
|
Path normalize = Paths.get(path).normalize();
|
||||||
|
try {
|
||||||
|
Resource download = fileDownloadService.download(normalize.toString());
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header("Content-Disposition", "attachment; filename=\"" + download.getFilename() + "\"")
|
||||||
|
.body(download);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("下载失败", e);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package vip.jcfd.file.dto;
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
public record FileInfo(String filename, String contentType, Long size, String savePath) {
|
||||||
|
|
||||||
|
public static FileInfo fromMultiPartFile(MultipartFile file, String savePath) {
|
||||||
|
return new FileInfo(file.getOriginalFilename(), file.getContentType(), file.getSize(), savePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package vip.jcfd.file.service;
|
||||||
|
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface IFileDownloadService {
|
||||||
|
|
||||||
|
Resource download(String path) throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package vip.jcfd.file.service;
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import vip.jcfd.file.dto.FileInfo;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface IFileUploadService {
|
||||||
|
|
||||||
|
FileInfo upload(MultipartFile file) throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package vip.jcfd.file.service.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.FileUtil;
|
||||||
|
import cn.hutool.core.lang.Assert;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import io.minio.*;
|
||||||
|
import io.minio.errors.*;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.core.io.AbstractResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import vip.jcfd.common.core.BizException;
|
||||||
|
import vip.jcfd.file.dto.FileInfo;
|
||||||
|
import vip.jcfd.file.service.IFileDownloadService;
|
||||||
|
import vip.jcfd.file.service.IFileUploadService;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@ConditionalOnBean(MinioClient.class)
|
||||||
|
public class MinioFileService implements IFileUploadService, IFileDownloadService {
|
||||||
|
private final static Logger log = LoggerFactory.getLogger(MinioFileService.class);
|
||||||
|
private final MinioClient minioClient;
|
||||||
|
private final static String BUCKET_NAME = "upload"; // Changed from private to static
|
||||||
|
private final static String ORIGIN_FILENAME = "origin_filename";
|
||||||
|
|
||||||
|
public MinioFileService(MinioClient minioClient) {
|
||||||
|
this.minioClient = minioClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Resource download(String path) throws IOException {
|
||||||
|
Assert.isTrue(path.startsWith("/" + BUCKET_NAME), () -> new BizException("路径不合法"));
|
||||||
|
String[] split = path.split("/");
|
||||||
|
Assert.isTrue(split.length == 3, () -> new BizException("路径不合法"));
|
||||||
|
String objectName = split[2];
|
||||||
|
GetObjectArgs getObjectArgs = GetObjectArgs.builder().bucket(BUCKET_NAME).object(objectName).build();
|
||||||
|
try {
|
||||||
|
StatObjectResponse objectStat = minioClient.statObject(StatObjectArgs.builder().bucket(BUCKET_NAME).object(objectName).build());
|
||||||
|
Map<String, String> userMetadata = objectStat.userMetadata();
|
||||||
|
GetObjectResponse object = minioClient.getObject(getObjectArgs);
|
||||||
|
|
||||||
|
return new AbstractResource() {
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return userMetadata.getOrDefault(ORIGIN_FILENAME, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public InputStream getInputStream() throws IOException {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFilename() {
|
||||||
|
return userMetadata.getOrDefault(ORIGIN_FILENAME, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long contentLength() throws IOException {
|
||||||
|
return objectStat.size();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (ErrorResponseException e) {
|
||||||
|
log.error("minio 服务异常", e);
|
||||||
|
} catch (InsufficientDataException e) {
|
||||||
|
log.error("数据不完整", e);
|
||||||
|
} catch (InternalException e) {
|
||||||
|
log.error("内部异常", e);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
log.error("无效的密钥", e);
|
||||||
|
} catch (InvalidResponseException e) {
|
||||||
|
log.error("无效的响应", e);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
log.error("没有这样的算法", e);
|
||||||
|
} catch (ServerException e) {
|
||||||
|
log.error("服务器异常", e);
|
||||||
|
} catch (XmlParserException e) {
|
||||||
|
log.error("XML解析异常", e);
|
||||||
|
}
|
||||||
|
throw new BizException("下载失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileInfo upload(MultipartFile file) throws IOException {
|
||||||
|
try {
|
||||||
|
if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(BUCKET_NAME).build())) {
|
||||||
|
minioClient.makeBucket(MakeBucketArgs.builder().bucket(BUCKET_NAME).build());
|
||||||
|
}
|
||||||
|
String string = UUID.randomUUID().toString();
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
String filename = string;
|
||||||
|
if (StrUtil.isNotEmpty(originalFilename)) {
|
||||||
|
String suffix = FileUtil.getSuffix(originalFilename);
|
||||||
|
filename += "." + suffix;
|
||||||
|
}
|
||||||
|
PutObjectArgs putObjectArgs = PutObjectArgs.builder()
|
||||||
|
.stream(file.getInputStream(), file.getSize(), -1)
|
||||||
|
.contentType(file.getContentType())
|
||||||
|
.object(filename)
|
||||||
|
.userMetadata(Map.of(
|
||||||
|
ORIGIN_FILENAME, Optional.ofNullable(originalFilename).orElse(filename)
|
||||||
|
))
|
||||||
|
.bucket(BUCKET_NAME)
|
||||||
|
.build();
|
||||||
|
ObjectWriteResponse response = minioClient.putObject(putObjectArgs);
|
||||||
|
String savePath = "/" + response.bucket() + "/" + response.object();
|
||||||
|
return FileInfo.fromMultiPartFile(file, savePath);
|
||||||
|
} catch (ErrorResponseException e) {
|
||||||
|
log.error("minio 服务异常", e);
|
||||||
|
} catch (InsufficientDataException e) {
|
||||||
|
log.error("数据不完整", e);
|
||||||
|
} catch (InternalException e) {
|
||||||
|
log.error("内部异常", e);
|
||||||
|
} catch (InvalidKeyException e) {
|
||||||
|
log.error("无效的密钥", e);
|
||||||
|
} catch (InvalidResponseException e) {
|
||||||
|
log.error("无效的响应", e);
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
log.error("没有这样的算法", e);
|
||||||
|
} catch (ServerException e) {
|
||||||
|
log.error("服务器异常", e);
|
||||||
|
} catch (XmlParserException e) {
|
||||||
|
log.error("XML解析异常", e);
|
||||||
|
}
|
||||||
|
throw new BizException("上传失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?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.5</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>zkh-log</artifactId>
|
||||||
|
<name>ZKH log</name>
|
||||||
|
<description>Logging utilities for ZKH framework</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</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,14 @@
|
|||||||
|
package vip.jcfd.log;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
|
||||||
|
public class ConsoleLogService implements ILogService {
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(ConsoleLogService.class);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void log(String message, Authentication authentication) {
|
||||||
|
logger.debug("{} {}", authentication.getName(), message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package vip.jcfd.log;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
|
||||||
|
public interface ILogService {
|
||||||
|
|
||||||
|
void log(String message, Authentication authentication);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package vip.jcfd.log.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
public @interface Log {
|
||||||
|
String value();
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package vip.jcfd.log.config;
|
||||||
|
|
||||||
|
import org.aspectj.lang.JoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.AfterReturning;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Pointcut;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import vip.jcfd.log.ConsoleLogService;
|
||||||
|
import vip.jcfd.log.ILogService;
|
||||||
|
import vip.jcfd.log.annotation.Log;
|
||||||
|
|
||||||
|
@Configuration("_logConfiguration")
|
||||||
|
public class LogConfig {
|
||||||
|
|
||||||
|
@Bean("_defaultLogService")
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
@Order
|
||||||
|
public ILogService defaultLogService() {
|
||||||
|
return new ConsoleLogService();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public static class LogAspect {
|
||||||
|
private final ILogService logService;
|
||||||
|
|
||||||
|
public LogAspect(ILogService logService) {
|
||||||
|
this.logService = logService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pointcut("@annotation(vip.jcfd.log.annotation.Log)")
|
||||||
|
public void logAspect() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterReturning(value = "logAspect()")
|
||||||
|
public void afterReturning(JoinPoint joinPoint) {
|
||||||
|
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||||
|
Log log = signature.getMethod().getAnnotation(Log.class);
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
logService.log(log.value(), authentication);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-1
@@ -7,7 +7,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>vip.jcfd</groupId>
|
<groupId>vip.jcfd</groupId>
|
||||||
<artifactId>zkh-framework</artifactId>
|
<artifactId>zkh-framework</artifactId>
|
||||||
<version>1.4</version>
|
<version>1.5</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>zkh-web</artifactId>
|
<artifactId>zkh-web</artifactId>
|
||||||
@@ -19,6 +19,10 @@
|
|||||||
<groupId>vip.jcfd</groupId>
|
<groupId>vip.jcfd</groupId>
|
||||||
<artifactId>zkh-common</artifactId>
|
<artifactId>zkh-common</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>vip.jcfd</groupId>
|
||||||
|
<artifactId>zkh-log</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
|||||||
@@ -13,35 +13,35 @@ import vip.jcfd.common.core.R;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice("_globalExceptionHandler")
|
||||||
public class GlobalExceptionHandler {
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
@ExceptionHandler(value = Exception.class)
|
@ExceptionHandler(value = Exception.class)
|
||||||
public R<String> handleException(Exception e) {
|
public R<String> handleException(Exception e) {
|
||||||
log.error("服务异常", e);
|
log.error("服务异常", e);
|
||||||
return R.serverError("服务器繁忙,请稍候重试");
|
return R.serverError("服务器繁忙,请稍候重试");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = BizException.class)
|
@ExceptionHandler(value = BizException.class)
|
||||||
public R<String> handleBizException(BizException e) {
|
public R<String> handleBizException(BizException e) {
|
||||||
log.error("业务异常", e);
|
log.error("业务异常", e);
|
||||||
return R.error(e.getMessage());
|
return R.error(e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = NoResourceFoundException.class)
|
@ExceptionHandler(value = NoResourceFoundException.class)
|
||||||
public R<String> handleNotFoundException(NoResourceFoundException e) {
|
public R<String> handleNotFoundException(NoResourceFoundException e) {
|
||||||
log.error("404异常", e);
|
log.error("404异常", e);
|
||||||
return new R<>(404, "您访问的地址不存在", false, null);
|
return new R<>(404, "您访问的地址不存在", false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ExceptionHandler(value = BindException.class)
|
@ExceptionHandler(value = BindException.class)
|
||||||
public R<String> handleBindException(BindException e) {
|
public R<String> handleBindException(BindException e) {
|
||||||
log.error("接口入参校验失败", e);
|
log.error("接口入参校验失败", e);
|
||||||
|
|
||||||
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
|
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
|
||||||
return R.error(String.join("。\n", fieldErrors.stream().map(FieldError::getDefaultMessage).toList()));
|
return R.error(String.join("。\n", fieldErrors.stream().map(FieldError::getDefaultMessage).toList()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,26 +10,26 @@ import org.springframework.data.redis.serializer.StringRedisSerializer;
|
|||||||
import vip.jcfd.web.config.props.SecurityProps;
|
import vip.jcfd.web.config.props.SecurityProps;
|
||||||
import vip.jcfd.web.redis.TokenRedisStorage;
|
import vip.jcfd.web.redis.TokenRedisStorage;
|
||||||
|
|
||||||
@Configuration
|
@Configuration("_redisConfiguration")
|
||||||
public class RedisConfig {
|
public class RedisConfig {
|
||||||
|
|
||||||
private final SecurityProps securityProps;
|
private final SecurityProps securityProps;
|
||||||
|
|
||||||
public RedisConfig(SecurityProps securityProps) {
|
public RedisConfig(SecurityProps securityProps) {
|
||||||
this.securityProps = securityProps;
|
this.securityProps = securityProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public TokenRedisStorage tokenRedisTemplate(RedisConnectionFactory factory, StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) {
|
public TokenRedisStorage tokenRedisTemplate(RedisConnectionFactory factory, StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) {
|
||||||
TokenRedisStorage tokenRedisStorage = new TokenRedisStorage(
|
TokenRedisStorage tokenRedisStorage = new TokenRedisStorage(
|
||||||
securityProps.getAccessTokenDuration(),
|
securityProps.getAccessTokenDuration(),
|
||||||
securityProps.getRefreshTokenDuration(),
|
securityProps.getRefreshTokenDuration(),
|
||||||
stringRedisTemplate,
|
stringRedisTemplate,
|
||||||
objectMapper
|
objectMapper
|
||||||
);
|
);
|
||||||
tokenRedisStorage.setConnectionFactory(factory);
|
tokenRedisStorage.setConnectionFactory(factory);
|
||||||
tokenRedisStorage.setValueSerializer(new JdkSerializationRedisSerializer());
|
tokenRedisStorage.setValueSerializer(new JdkSerializationRedisSerializer());
|
||||||
tokenRedisStorage.setKeySerializer(new StringRedisSerializer());
|
tokenRedisStorage.setKeySerializer(new StringRedisSerializer());
|
||||||
return tokenRedisStorage;
|
return tokenRedisStorage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,63 +10,63 @@ import org.springdoc.core.customizers.OpenApiCustomizer;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@Configuration("_springDocConfig")
|
@Configuration("_springDocConfiguration")
|
||||||
public class SpringDocConfig {
|
public class SpringDocConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public OpenApiCustomizer openApiCustomizer() {
|
public OpenApiCustomizer openApiCustomizer() {
|
||||||
return (openAPI) -> {
|
return (openAPI) -> {
|
||||||
openAPI.path("/login", new PathItem()
|
openAPI.path("/login", new PathItem()
|
||||||
.post(new Operation()
|
.post(new Operation()
|
||||||
.summary("登录接口")
|
.summary("登录接口")
|
||||||
.description("用于用户登录,返回token")
|
.description("用于用户登录,返回token")
|
||||||
.addTagsItem("认证管理")
|
.addTagsItem("认证管理")
|
||||||
.requestBody(new RequestBody()
|
.requestBody(new RequestBody()
|
||||||
.description("帐号密码")
|
.description("帐号密码")
|
||||||
.required(true)
|
.required(true)
|
||||||
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
||||||
.addProperty("username", new StringSchema().example("admin"))
|
.addProperty("username", new StringSchema().example("admin"))
|
||||||
.addProperty("password", new StringSchema().example("123456"))))))
|
.addProperty("password", new StringSchema().example("123456"))))))
|
||||||
.responses(new ApiResponses()
|
.responses(new ApiResponses()
|
||||||
.addApiResponse("成功", new ApiResponse()
|
.addApiResponse("成功", new ApiResponse()
|
||||||
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
||||||
.addProperty("data", new JsonSchema()
|
.addProperty("data", new JsonSchema()
|
||||||
.addProperty("accessToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440000"))
|
.addProperty("accessToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440000"))
|
||||||
.addProperty("refreshToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440001"))
|
.addProperty("refreshToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440001"))
|
||||||
.addProperty("tokenType", new StringSchema().example("Bearer"))
|
.addProperty("tokenType", new StringSchema().example("Bearer"))
|
||||||
.addProperty("expiresIn", new NumberSchema().example(1800))
|
.addProperty("expiresIn", new NumberSchema().example(1800))
|
||||||
.addProperty("username", new StringSchema().example("admin"))
|
.addProperty("username", new StringSchema().example("admin"))
|
||||||
)
|
)
|
||||||
.addProperty("success", new BooleanSchema().example(true))
|
.addProperty("success", new BooleanSchema().example(true))
|
||||||
.addProperty("code", new IntegerSchema().example(200))
|
.addProperty("code", new IntegerSchema().example(200))
|
||||||
.addProperty("message", new StringSchema().example("登录成功"))
|
.addProperty("message", new StringSchema().example("登录成功"))
|
||||||
))))
|
))))
|
||||||
.addApiResponse("失败", new ApiResponse()
|
.addApiResponse("失败", new ApiResponse()
|
||||||
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
||||||
.addProperty("data", new StringSchema().example(null))
|
.addProperty("data", new StringSchema().example(null))
|
||||||
.addProperty("success", new BooleanSchema().example(false))
|
.addProperty("success", new BooleanSchema().example(false))
|
||||||
.addProperty("code", new IntegerSchema().example(401))
|
.addProperty("code", new IntegerSchema().example(401))
|
||||||
.addProperty("message", new StringSchema().example("用户名或密码错误"))
|
.addProperty("message", new StringSchema().example("用户名或密码错误"))
|
||||||
))))
|
))))
|
||||||
)));
|
)));
|
||||||
openAPI.path("/logout", new PathItem()
|
openAPI.path("/logout", new PathItem()
|
||||||
.post(new Operation()
|
.post(new Operation()
|
||||||
.summary("登出接口")
|
.summary("登出接口")
|
||||||
.description("用于用户登出")
|
.description("用于用户登出")
|
||||||
.addTagsItem("认证管理")
|
.addTagsItem("认证管理")
|
||||||
.responses(new ApiResponses()
|
.responses(new ApiResponses()
|
||||||
.addApiResponse("成功", new ApiResponse()
|
.addApiResponse("成功", new ApiResponse()
|
||||||
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
|
||||||
.addProperty("data", new StringSchema().example(null))
|
.addProperty("data", new StringSchema().example(null))
|
||||||
.addProperty("success", new BooleanSchema().example(true))
|
.addProperty("success", new BooleanSchema().example(true))
|
||||||
.addProperty("code", new IntegerSchema().example(200))
|
.addProperty("code", new IntegerSchema().example(200))
|
||||||
.addProperty("message", new StringSchema().example("登出成功"))
|
.addProperty("message", new StringSchema().example("登出成功"))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import java.io.IOException;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@Configuration
|
@Configuration("_webSecurityConfiguration")
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@ConfigurationPropertiesScan(basePackageClasses = {SecurityProps.class})
|
@ConfigurationPropertiesScan(basePackageClasses = {SecurityProps.class})
|
||||||
@EnableJpaAuditing
|
@EnableJpaAuditing
|
||||||
@@ -60,191 +60,191 @@ import java.util.UUID;
|
|||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
public class WebSecurityConfig {
|
public class WebSecurityConfig {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(WebSecurityConfig.class);
|
private static final Logger log = LoggerFactory.getLogger(WebSecurityConfig.class);
|
||||||
private final SecurityProps securityProps;
|
private final SecurityProps securityProps;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final TokenRedisStorage tokenRedisStorage;
|
private final TokenRedisStorage tokenRedisStorage;
|
||||||
|
|
||||||
public WebSecurityConfig(SecurityProps securityProps,
|
public WebSecurityConfig(SecurityProps securityProps,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
TokenRedisStorage tokenRedisStorage,
|
TokenRedisStorage tokenRedisStorage,
|
||||||
AuthenticationManagerBuilder builder,
|
AuthenticationManagerBuilder builder,
|
||||||
UserDetailsService userDetailsService) {
|
UserDetailsService userDetailsService) {
|
||||||
this.securityProps = securityProps;
|
this.securityProps = securityProps;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.tokenRedisStorage = tokenRedisStorage;
|
this.tokenRedisStorage = tokenRedisStorage;
|
||||||
builder.authenticationProvider(new RefreshTokenAuthProvider(userDetailsService));
|
builder.authenticationProvider(new RefreshTokenAuthProvider(userDetailsService));
|
||||||
DaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider(userDetailsService);
|
DaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider(userDetailsService);
|
||||||
authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
|
authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
|
||||||
builder.authenticationProvider(authenticationProvider);
|
builder.authenticationProvider(authenticationProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(cron = "0 */30 * * * *")
|
@Scheduled(cron = "0 */30 * * * *")
|
||||||
@Async
|
@Async
|
||||||
public void scheduleClearExpiredTokens() {
|
public void scheduleClearExpiredTokens() {
|
||||||
tokenRedisStorage.clearExpiredTokens();
|
tokenRedisStorage.clearExpiredTokens();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public AuditorAware<String> auditorAware() {
|
public AuditorAware<String> auditorAware() {
|
||||||
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
|
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
|
||||||
.map(SecurityContext::getAuthentication)
|
.map(SecurityContext::getAuthentication)
|
||||||
.map(Authentication::getName)
|
.map(Authentication::getName)
|
||||||
.or(() -> Optional.of("system"));
|
.or(() -> Optional.of("system"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public TokenFilter tokenFilter() {
|
public TokenFilter tokenFilter() {
|
||||||
return new TokenFilter(tokenRedisStorage);
|
return new TokenFilter(tokenRedisStorage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
|
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
|
||||||
return configuration.getAuthenticationManager();
|
return configuration.getAuthenticationManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain security(HttpSecurity http, TokenFilter tokenFilter, AuthenticationManager authenticationManager) throws Exception {
|
public SecurityFilterChain security(HttpSecurity http, TokenFilter tokenFilter, AuthenticationManager authenticationManager) throws Exception {
|
||||||
http.authorizeHttpRequests(config -> {
|
http.authorizeHttpRequests(config -> {
|
||||||
config.requestMatchers(securityProps.getIgnoreUrls()).permitAll();
|
config.requestMatchers(securityProps.getIgnoreUrls()).permitAll();
|
||||||
config.anyRequest().authenticated();
|
config.anyRequest().authenticated();
|
||||||
});
|
});
|
||||||
CustomAuthenticationEntryPoint authenticationEntryPoint = new CustomAuthenticationEntryPoint(objectMapper, tokenRedisStorage);
|
CustomAuthenticationEntryPoint authenticationEntryPoint = new CustomAuthenticationEntryPoint(objectMapper, tokenRedisStorage);
|
||||||
http.formLogin(config -> {
|
http.formLogin(config -> {
|
||||||
config.loginProcessingUrl("/login");
|
config.loginProcessingUrl("/login");
|
||||||
});
|
});
|
||||||
http.csrf(AbstractHttpConfigurer::disable);
|
http.csrf(AbstractHttpConfigurer::disable);
|
||||||
http.logout(config -> {
|
http.logout(config -> {
|
||||||
config.addLogoutHandler(new CustomLogoutSuccessHandler(objectMapper, tokenRedisStorage));
|
config.addLogoutHandler(new CustomLogoutSuccessHandler(objectMapper, tokenRedisStorage));
|
||||||
});
|
});
|
||||||
http.rememberMe(AbstractHttpConfigurer::disable);
|
http.rememberMe(AbstractHttpConfigurer::disable);
|
||||||
http.sessionManagement(AbstractHttpConfigurer::disable);
|
http.sessionManagement(AbstractHttpConfigurer::disable);
|
||||||
http.exceptionHandling(config -> {
|
http.exceptionHandling(config -> {
|
||||||
config.authenticationEntryPoint(authenticationEntryPoint);
|
config.authenticationEntryPoint(authenticationEntryPoint);
|
||||||
config.accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper));
|
config.accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper));
|
||||||
});
|
});
|
||||||
|
|
||||||
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
JsonUsernamePasswordAuthenticationFilter filter = new JsonUsernamePasswordAuthenticationFilter(objectMapper, authenticationManager);
|
JsonUsernamePasswordAuthenticationFilter filter = new JsonUsernamePasswordAuthenticationFilter(objectMapper, authenticationManager);
|
||||||
filter.setAuthenticationSuccessHandler(authenticationEntryPoint);
|
filter.setAuthenticationSuccessHandler(authenticationEntryPoint);
|
||||||
filter.setAuthenticationFailureHandler(authenticationEntryPoint);
|
filter.setAuthenticationFailureHandler(authenticationEntryPoint);
|
||||||
http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
|
http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private record CustomAuthenticationEntryPoint(
|
private record CustomAuthenticationEntryPoint(
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
TokenRedisStorage tokenRedisStorage) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler {
|
TokenRedisStorage tokenRedisStorage) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler {
|
||||||
@Override
|
@Override
|
||||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
|
||||||
log.warn("认证失败", authException);
|
log.warn("认证失败", authException);
|
||||||
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "未登录", false, null);
|
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "未登录", false, null);
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
objectMapper.writeValue(response.getWriter(), data);
|
objectMapper.writeValue(response.getWriter(), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
|
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
|
||||||
log.warn("登录失败", exception);
|
log.warn("登录失败", exception);
|
||||||
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "用户名或密码错误", false, null);
|
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "用户名或密码错误", false, null);
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
objectMapper.writeValue(response.getWriter(), data);
|
objectMapper.writeValue(response.getWriter(), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
|
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
|
||||||
log.info("用户「{}」登录成功", authentication.getName());
|
log.info("用户「{}」登录成功", authentication.getName());
|
||||||
|
|
||||||
// 生成双重Token
|
// 生成双重Token
|
||||||
String accessToken = UUID.randomUUID().toString();
|
String accessToken = UUID.randomUUID().toString();
|
||||||
String refreshToken = UUID.randomUUID().toString();
|
String refreshToken = UUID.randomUUID().toString();
|
||||||
|
|
||||||
// 存储Access Token
|
// 存储Access Token
|
||||||
tokenRedisStorage.putAccessToken(accessToken, authentication);
|
tokenRedisStorage.putAccessToken(accessToken, authentication);
|
||||||
|
|
||||||
// 存储Refresh Token
|
// 存储Refresh Token
|
||||||
String deviceId = extractDeviceId(request);
|
String deviceId = extractDeviceId(request);
|
||||||
tokenRedisStorage.putRefreshToken(refreshToken, authentication.getName(), deviceId);
|
tokenRedisStorage.putRefreshToken(refreshToken, authentication.getName(), deviceId);
|
||||||
|
|
||||||
// 构造登录响应
|
// 构造登录响应
|
||||||
LoginResponse loginResponse = new LoginResponse(
|
LoginResponse loginResponse = new LoginResponse(
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
"Bearer",
|
"Bearer",
|
||||||
1800, // 30分钟,秒数
|
1800, // 30分钟,秒数
|
||||||
authentication.getName()
|
authentication.getName()
|
||||||
);
|
);
|
||||||
|
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
R<LoginResponse> data = new R<>(HttpServletResponse.SC_OK, "登录成功", true, loginResponse);
|
R<LoginResponse> data = new R<>(HttpServletResponse.SC_OK, "登录成功", true, loginResponse);
|
||||||
objectMapper.writeValue(response.getWriter(), data);
|
objectMapper.writeValue(response.getWriter(), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractDeviceId(HttpServletRequest request) {
|
private String extractDeviceId(HttpServletRequest request) {
|
||||||
// 尝试从User-Agent提取设备信息
|
// 尝试从User-Agent提取设备信息
|
||||||
String userAgent = request.getHeader("User-Agent");
|
String userAgent = request.getHeader("User-Agent");
|
||||||
if (userAgent != null) {
|
if (userAgent != null) {
|
||||||
// 简单的设备识别逻辑,生产环境可以使用更复杂的识别算法
|
// 简单的设备识别逻辑,生产环境可以使用更复杂的识别算法
|
||||||
if (userAgent.contains("Mobile") || userAgent.contains("Android") || userAgent.contains("iPhone")) {
|
if (userAgent.contains("Mobile") || userAgent.contains("Android") || userAgent.contains("iPhone")) {
|
||||||
return "mobile-" + request.getRemoteAddr();
|
return "mobile-" + request.getRemoteAddr();
|
||||||
} else if (userAgent.contains("Tablet") || userAgent.contains("iPad")) {
|
} else if (userAgent.contains("Tablet") || userAgent.contains("iPad")) {
|
||||||
return "tablet-" + request.getRemoteAddr();
|
return "tablet-" + request.getRemoteAddr();
|
||||||
} else {
|
} else {
|
||||||
return "desktop-" + request.getRemoteAddr();
|
return "desktop-" + request.getRemoteAddr();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "unknown-" + request.getRemoteAddr();
|
return "unknown-" + request.getRemoteAddr();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record CustomAccessDeniedHandler(ObjectMapper objectMapper) implements AccessDeniedHandler {
|
private record CustomAccessDeniedHandler(ObjectMapper objectMapper) implements AccessDeniedHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
|
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
log.warn("访问被拒绝", accessDeniedException);
|
log.warn("访问被拒绝", accessDeniedException);
|
||||||
if (authentication.isAuthenticated()) {
|
if (authentication.isAuthenticated()) {
|
||||||
log.warn("用户「{}」访问「{}」被拒绝,因为:{}", authentication.getPrincipal(), request.getRequestURI(), accessDeniedException.getMessage());
|
log.warn("用户「{}」访问「{}」被拒绝,因为:{}", authentication.getPrincipal(), request.getRequestURI(), accessDeniedException.getMessage());
|
||||||
} else {
|
} else {
|
||||||
log.warn("匿名用户访问「{}」被拒绝,因为:{}", request.getRequestURI(), accessDeniedException.getMessage());
|
log.warn("匿名用户访问「{}」被拒绝,因为:{}", request.getRequestURI(), accessDeniedException.getMessage());
|
||||||
}
|
}
|
||||||
R<Object> data = new R<>(HttpServletResponse.SC_FORBIDDEN, "无权限", false, null);
|
R<Object> data = new R<>(HttpServletResponse.SC_FORBIDDEN, "无权限", false, null);
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
objectMapper.writeValue(response.getWriter(), data);
|
objectMapper.writeValue(response.getWriter(), data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record CustomLogoutSuccessHandler(ObjectMapper objectMapper,
|
private record CustomLogoutSuccessHandler(ObjectMapper objectMapper,
|
||||||
TokenRedisStorage tokenRedisStorage) implements LogoutHandler {
|
TokenRedisStorage tokenRedisStorage) implements LogoutHandler {
|
||||||
@Override
|
@Override
|
||||||
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
|
||||||
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
|
||||||
|
|
||||||
if (header != null && header.startsWith("Bearer ")) {
|
if (header != null && header.startsWith("Bearer ")) {
|
||||||
String token = header.substring(7);
|
String token = header.substring(7);
|
||||||
authentication = tokenRedisStorage.get(token);
|
authentication = tokenRedisStorage.get(token);
|
||||||
tokenRedisStorage.remove(token);
|
tokenRedisStorage.remove(token);
|
||||||
}
|
}
|
||||||
if (authentication != null) {
|
if (authentication != null) {
|
||||||
log.info("用户「{}」退出成功", authentication.getName());
|
log.info("用户「{}」退出成功", authentication.getName());
|
||||||
String all = request.getParameter("all");
|
String all = request.getParameter("all");
|
||||||
if ("true".equals(all)) {
|
if ("true".equals(all)) {
|
||||||
tokenRedisStorage.removeByUserName(authentication.getName());
|
tokenRedisStorage.removeByUserName(authentication.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R<Object> data = new R<>(HttpServletResponse.SC_OK, "退出成功", true, null);
|
R<Object> data = new R<>(HttpServletResponse.SC_OK, "退出成功", true, null);
|
||||||
response.setContentType("application/json;charset=UTF-8");
|
response.setContentType("application/json;charset=UTF-8");
|
||||||
try {
|
try {
|
||||||
objectMapper.writeValue(response.getWriter(), data);
|
objectMapper.writeValue(response.getWriter(), data);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import vip.jcfd.common.core.R;
|
import vip.jcfd.common.core.R;
|
||||||
import vip.jcfd.common.dto.TokenRefreshRequest;
|
import vip.jcfd.common.dto.TokenRefreshRequest;
|
||||||
import vip.jcfd.common.dto.TokenRefreshResponse;
|
import vip.jcfd.common.dto.TokenRefreshResponse;
|
||||||
|
import vip.jcfd.log.annotation.Log;
|
||||||
import vip.jcfd.web.auth.RefreshTokenAuthenticationToken;
|
import vip.jcfd.web.auth.RefreshTokenAuthenticationToken;
|
||||||
import vip.jcfd.web.redis.TokenRedisStorage;
|
import vip.jcfd.web.redis.TokenRedisStorage;
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/refresh-token")
|
@PostMapping("/refresh-token")
|
||||||
@Operation(summary = "刷新Token", description = "使用Refresh Token获取新的Access Token和Refresh Token")
|
@Operation(summary = "刷新Token", description = "使用Refresh Token获取新的Access Token和Refresh Token")
|
||||||
|
@Log("刷新了token")
|
||||||
public R<TokenRefreshResponse> refreshToken(
|
public R<TokenRefreshResponse> refreshToken(
|
||||||
@Valid @RequestBody TokenRefreshRequest request) {
|
@Valid @RequestBody TokenRefreshRequest request) {
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user