From b9ca3339588920cdb088e5e21e0b1616ecdbff85 Mon Sep 17 00:00:00 2001 From: suyiiyii Date: Tue, 3 Sep 2024 16:13:00 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD=E5=8F=8AS3=E9=9B=86?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新版本中添加了文件上传功能,并集成了S3进行文件存储。这包括控制器、服务和工具类的更新,以及配置S3客户端的bean配置。 --- pom.xml | 14 +- .../top/suyiiyii/sims/common/S3Config.java | 17 +++ .../sims/controller/FileController.java | 40 ++++++ .../suyiiyii/sims/service/FileService.java | 18 +++ .../top/suyiiyii/sims/utils/S3Client.java | 123 ++++++++++++++++++ src/main/resources/application.yaml | 15 ++- 6 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 src/main/java/top/suyiiyii/sims/common/S3Config.java create mode 100644 src/main/java/top/suyiiyii/sims/controller/FileController.java create mode 100644 src/main/java/top/suyiiyii/sims/service/FileService.java create mode 100644 src/main/java/top/suyiiyii/sims/utils/S3Client.java diff --git a/pom.xml b/pom.xml index 4374986..7611098 100644 --- a/pom.xml +++ b/pom.xml @@ -95,16 +95,28 @@ spring-restdocs-mockmvc test + org.springdoc springdoc-openapi-starter-webmvc-ui - 2.3.0 + 2.6.0 + org.xerial sqlite-jdbc test + + com.amazonaws + aws-java-sdk-s3 + 1.12.706 + + + commons-io + commons-io + 2.11.0 + diff --git a/src/main/java/top/suyiiyii/sims/common/S3Config.java b/src/main/java/top/suyiiyii/sims/common/S3Config.java new file mode 100644 index 0000000..900618a --- /dev/null +++ b/src/main/java/top/suyiiyii/sims/common/S3Config.java @@ -0,0 +1,17 @@ +package top.suyiiyii.sims.common; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import top.suyiiyii.sims.utils.S3Client; + +@Configuration +public class S3Config { + @Bean + public static S3Client Config(@Value("${S3.ENDPOINT}") String endpoint, + @Value("${S3.ACCESS_KEY}") String accessKey, + @Value("${S3.SECRET_KEY}") String secretKey, + @Value("${S3.BUCKET}") String bucket) { + return new S3Client(endpoint, accessKey, secretKey, bucket); + } +} diff --git a/src/main/java/top/suyiiyii/sims/controller/FileController.java b/src/main/java/top/suyiiyii/sims/controller/FileController.java new file mode 100644 index 0000000..d66f458 --- /dev/null +++ b/src/main/java/top/suyiiyii/sims/controller/FileController.java @@ -0,0 +1,40 @@ +package top.suyiiyii.sims.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.parameters.RequestBody; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; +import top.suyiiyii.sims.common.AuthAccess; +import top.suyiiyii.sims.service.FileService; + +import java.io.IOException; +import java.io.InputStream; + +@Slf4j +@RestController +public class FileController { + @Autowired + FileService fileService; + + @AuthAccess(allowRoles = {"user"}) + @Operation(summary = "上传文件", description = "使用form-data格式\nfile:文件\nfilename: 文件名\n返回可访问的路径") + @PostMapping("/upload") + public String uploadFile( + @Parameter String filename, + @RequestBody(content = @Content(mediaType = "multipart/form-data", + schema = @Schema(type = "string", format = "binary"))) MultipartFile file) { + try (InputStream in = file.getInputStream()) { + log.info("文件上传,文件名:{},描述:{}", file.getOriginalFilename(), filename); + return fileService.uploadFile(in, filename); + } catch (IOException e) { + log.warn("文件上传失败", e); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/top/suyiiyii/sims/service/FileService.java b/src/main/java/top/suyiiyii/sims/service/FileService.java new file mode 100644 index 0000000..f632a56 --- /dev/null +++ b/src/main/java/top/suyiiyii/sims/service/FileService.java @@ -0,0 +1,18 @@ +package top.suyiiyii.sims.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import top.suyiiyii.sims.utils.S3Client; + +import java.io.InputStream; + +@Service +public class FileService { + @Autowired + private S3Client s3Client; + + public String uploadFile(InputStream input,String fileName) { + String extension = fileName.substring(fileName.lastIndexOf(".")); + return s3Client.uploadFile(input, extension); + } +} diff --git a/src/main/java/top/suyiiyii/sims/utils/S3Client.java b/src/main/java/top/suyiiyii/sims/utils/S3Client.java new file mode 100644 index 0000000..837f121 --- /dev/null +++ b/src/main/java/top/suyiiyii/sims/utils/S3Client.java @@ -0,0 +1,123 @@ +package top.suyiiyii.sims.utils; + + +import com.amazonaws.ClientConfiguration; +import com.amazonaws.Protocol; +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.S3ClientOptions; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.S3Object; +import org.apache.commons.io.IOUtils; + +import java.io.InputStream; +import java.io.OutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.UUID; + + +public class S3Client { + private final String endpoint; + private final String bucket; + private final AmazonS3 s3client; + + public S3Client(String endpoint, String accessKey, String secretKey, String bucket) { + this.endpoint = endpoint; + this.bucket = bucket; + URL endpointUrl; + try { + endpointUrl = new URL(endpoint); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + String protocol = endpointUrl.getProtocol(); + int port = endpointUrl.getPort() == -1 ? endpointUrl.getDefaultPort() : endpointUrl.getPort(); + ClientConfiguration clientConfig = new ClientConfiguration(); + clientConfig.setSignerOverride("S3SignerType"); + clientConfig.setProtocol(Protocol.valueOf(protocol.toUpperCase())); + // 禁用证书检查,避免https自签证书校验失败 + System.setProperty("com.amazonaws.sdk.disableCertChecking", "true"); + // 屏蔽 AWS 的 MD5 校验,避免校验导致的下载抛出异常问题 + System.setProperty("com.amazonaws.services.s3.disableGetObjectMD5Validation", "true"); + AWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey); + // 创建 S3Client 实例 + AmazonS3 s3client = new AmazonS3Client(awsCredentials, clientConfig); + s3client.setEndpoint(endpointUrl.getHost() + ":" + port); + s3client.setS3ClientOptions(S3ClientOptions.builder().setPathStyleAccess(true).build()); + this.s3client = s3client; + } + + + public boolean bucketExists(String bucket) { + try { + return s3client.doesBucketExist(bucket); + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + public boolean existObject(String bucket, String objectId) { + try { + return s3client.doesObjectExist(bucket, objectId); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + + public InputStream download(String bucket, String objectId) { + try { + S3Object o = s3client.getObject(bucket, objectId); + return o.getObjectContent(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public void download(String bucket, String objectId, OutputStream out) { + S3Object o = s3client.getObject(bucket, objectId); + try (InputStream in = o.getObjectContent()) { + IOUtils.copyLarge(in, out); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void upload(String bucket, String objectId, InputStream input) { + try { + // 创建文件上传的元数据 + ObjectMetadata meta = new ObjectMetadata(); + // 设置文件上传长度 + meta.setContentLength(input.available()); + // 上传 + s3client.putObject(bucket, objectId, input, meta); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public String uploadFile(InputStream input) { + String objectID = UUID.randomUUID().toString(); + upload(bucket, objectID, input); + return endpoint + "/" + bucket + "/" + objectID; + } + + /** + * 接收文件流,自动使用随机uuid命名并保留扩展名 + * 返回公网上可以直接访问的URL + * + * @param input 文件流 + * @param extensionName 扩展名 + * @return 文件的URL + */ + public String uploadFile(InputStream input, String extensionName) { + String objectID = UUID.randomUUID() + extensionName; + upload(bucket, objectID, input); + return endpoint + "/" + bucket + "/" + objectID; + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 4b7599a..17c0cca 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -2,6 +2,10 @@ spring: profiles: active: prod + servlet: + multipart: + max-file-size: 100MB + max-request-size: 100MB datasource: url: ${DATASOURCE_URL} username: ${DATASOURCE_USERNAME} @@ -14,4 +18,13 @@ auto-table: model-package: top.suyiiyii.sims.entity jwt: - secret: ${JWT_SECRET} \ No newline at end of file + secret: ${JWT_SECRET} + +S3: + ENDPOINT: ${S3_ENDPOINT} + BUCKET: ${S3_BUCKET} + ACCESS_KEY: ${S3_ACCESS_KEY} + SECRET_KEY: ${S3_SECRET_KEY} + +springdoc: + default-support-form-data: true \ No newline at end of file From d353304f3a8e7681d6a07fdb991d5d3bb7e1cce1 Mon Sep 17 00:00:00 2001 From: suyiiyii Date: Tue, 3 Sep 2024 16:34:35 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E5=9C=A8=E6=B5=8B=E8=AF=95=E4=B8=ADmock=20?= =?UTF-8?q?S3Client=EF=BC=8C=E9=81=BF=E5=85=8D=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 6 ++++++ src/main/resources/application.yaml | 3 --- .../java/top/suyiiyii/sims/service/RbacServiceTest.java | 8 ++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 7611098..df911c3 100644 --- a/pom.xml +++ b/pom.xml @@ -117,6 +117,12 @@ commons-io 2.11.0 + + org.mockito + mockito-core + 4.0.0 + test + diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 17c0cca..d54a8ca 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -25,6 +25,3 @@ S3: BUCKET: ${S3_BUCKET} ACCESS_KEY: ${S3_ACCESS_KEY} SECRET_KEY: ${S3_SECRET_KEY} - -springdoc: - default-support-form-data: true \ No newline at end of file diff --git a/src/test/java/top/suyiiyii/sims/service/RbacServiceTest.java b/src/test/java/top/suyiiyii/sims/service/RbacServiceTest.java index b06cec6..f46b2ae 100644 --- a/src/test/java/top/suyiiyii/sims/service/RbacServiceTest.java +++ b/src/test/java/top/suyiiyii/sims/service/RbacServiceTest.java @@ -1,10 +1,14 @@ package top.suyiiyii.sims.service; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ActiveProfiles; import top.suyiiyii.sims.entity.Role; +import top.suyiiyii.sims.utils.S3Client; import java.util.List; @@ -12,11 +16,15 @@ import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @ActiveProfiles("test") +@ExtendWith(MockitoExtension.class) class RbacServiceTest { @Autowired private RbacService rbacService; + @MockBean + private S3Client s3Client; + @Test void addRoleWithUserId() { int userId = 1; // mock userId From 28f5b146d82930d5dd0b2fc7d4c0056b37fa48c8 Mon Sep 17 00:00:00 2001 From: suyiiyii Date: Tue, 3 Sep 2024 17:31:12 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E5=A2=9E=E5=BC=BAJWT=E6=8B=A6=E6=88=AA?= =?UTF-8?q?=E5=99=A8=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增异常处理,专门针对无法识别的token异常,提升错误提示的准确性。当token验证失败时,现在将抛出一个具体的"token验证失败,请重新登录"错误,以区别于登录过期错误。 --- src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java b/src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java index 63b0254..10759c6 100644 --- a/src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java +++ b/src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java @@ -48,6 +48,8 @@ public class JwtInterceptor implements HandlerInterceptor { } } catch (TokenExpiredException e) { throw new ServiceException("401", "登录已过期,请重新登录"); + } catch (Exception e) { + throw new ServiceException("401", "token验证失败,请重新登录"); } // 获取 token 中的 user id Integer userId = Integer.parseInt(Objects.requireNonNull(JwtUtils.extractUserId(token))); From 8ee13b5f8f06a34f8c7f1fc41ae9d38169b25183 Mon Sep 17 00:00:00 2001 From: suyiiyii Date: Thu, 5 Sep 2024 18:24:59 +0800 Subject: [PATCH 4/5] =?UTF-8?q?refactor(sims):=20=E9=87=8D=E6=9E=84JwtInte?= =?UTF-8?q?rceptor=E5=B9=B6=E4=BF=AE=E5=A4=8D=E7=94=A8=E6=88=B7=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在JwtInterceptor中新增getUserIdFromReq方法,用于从请求中获取用户ID。 - 在UserController的getSelf方法中使用新方法获取当前用户ID并修复。 - 重构UserService中关于用户角色加载的代码,消除不必要的循环,提高执行效率。 - 修复了在UserService中findUser方法传入空值的问题,并增加用户不存在时的异常处理。 --- .../suyiiyii/sims/common/JwtInterceptor.java | 3 +++ .../sims/controller/UserController.java | 6 +++-- .../suyiiyii/sims/service/UserService.java | 22 ++++--------------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java b/src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java index 10759c6..9c634f1 100644 --- a/src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java +++ b/src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java @@ -57,4 +57,7 @@ public class JwtInterceptor implements HandlerInterceptor { request.setAttribute("userId", userId); return true; } + public static int getUserIdFromReq(HttpServletRequest request){ + return (int) request.getAttribute("userId"); + } } diff --git a/src/main/java/top/suyiiyii/sims/controller/UserController.java b/src/main/java/top/suyiiyii/sims/controller/UserController.java index a9a606d..7ece1a7 100644 --- a/src/main/java/top/suyiiyii/sims/controller/UserController.java +++ b/src/main/java/top/suyiiyii/sims/controller/UserController.java @@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import top.suyiiyii.sims.common.AuthAccess; +import top.suyiiyii.sims.common.JwtInterceptor; import top.suyiiyii.sims.common.Result; import top.suyiiyii.sims.dto.CommonResponse; import top.suyiiyii.sims.dto.UserDto; @@ -100,8 +101,9 @@ public class UserController { @Operation(description = "获取当前用户信息") @AuthAccess(allowRoles = {"user"}) @GetMapping("/user/me") - public Result getSelf() { - UserDto user = userService.findUser(0); + public Result getSelf(HttpServletRequest request) { + int userId = JwtInterceptor.getUserIdFromReq(request); + UserDto user = userService.findUser(userId); return Result.success(user); } diff --git a/src/main/java/top/suyiiyii/sims/service/UserService.java b/src/main/java/top/suyiiyii/sims/service/UserService.java index e7f8f40..e63a278 100644 --- a/src/main/java/top/suyiiyii/sims/service/UserService.java +++ b/src/main/java/top/suyiiyii/sims/service/UserService.java @@ -124,14 +124,6 @@ public class UserService { UserDto.setUserGroup(user.getUserGroup()); UserDto.setRoles(new ArrayList<>()); Integer id = user.getId(); - List roles = roleMapper.selectRolesById(id); - for (Role role : roles) { - Integer roleId = role.getId(); - // 获取一个角色的名称列表 - List roleNameList = roleMapper.selectRoleNamesByRoleId(roleId); - // 累加角色名称到用户的角色列表中 - UserDto.getRoles().addAll(roleNameList); - } UserDtos.add(UserDto); } return UserDtos; @@ -141,21 +133,15 @@ public class UserService { UserDto UserDto = new UserDto(); User user = userMapper.selectById(id); + if (user == null) { + throw new ServiceException("用户不存在"); + } UserDto.setUserId(user.getId()); UserDto.setUsername(user.getUsername()); UserDto.setGrade(user.getGrade()); UserDto.setUserGroup(user.getUserGroup()); UserDto.setRoles(new ArrayList<>()); - List roles = roleMapper.selectRolesById(id); - for (Role role : roles) { - Integer roleId = role.getId(); - // 获取一个角色的名称列表 - List roleNameList = roleMapper.selectRoleNamesByRoleId(roleId); - // 累加角色名称到用户的角色列表中 - UserDto.getRoles().addAll(roleNameList); - } - - + //TODO: 获取用户角色 return UserDto; } From 4579dbda81299d207de88db0f073bc48301e931a Mon Sep 17 00:00:00 2001 From: suyiiyii Date: Thu, 5 Sep 2024 18:27:30 +0800 Subject: [PATCH 5/5] =?UTF-8?q?Revert=20"refactor(sims):=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84JwtInterceptor=E5=B9=B6=E4=BF=AE=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E8=A7=92=E8=89=B2=E5=8A=A0=E8=BD=BD"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 8ee13b5f8f06a34f8c7f1fc41ae9d38169b25183. --- .../suyiiyii/sims/common/JwtInterceptor.java | 3 --- .../sims/controller/UserController.java | 6 ++--- .../suyiiyii/sims/service/UserService.java | 22 +++++++++++++++---- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java b/src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java index 9c634f1..10759c6 100644 --- a/src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java +++ b/src/main/java/top/suyiiyii/sims/common/JwtInterceptor.java @@ -57,7 +57,4 @@ public class JwtInterceptor implements HandlerInterceptor { request.setAttribute("userId", userId); return true; } - public static int getUserIdFromReq(HttpServletRequest request){ - return (int) request.getAttribute("userId"); - } } diff --git a/src/main/java/top/suyiiyii/sims/controller/UserController.java b/src/main/java/top/suyiiyii/sims/controller/UserController.java index 7ece1a7..a9a606d 100644 --- a/src/main/java/top/suyiiyii/sims/controller/UserController.java +++ b/src/main/java/top/suyiiyii/sims/controller/UserController.java @@ -8,7 +8,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import top.suyiiyii.sims.common.AuthAccess; -import top.suyiiyii.sims.common.JwtInterceptor; import top.suyiiyii.sims.common.Result; import top.suyiiyii.sims.dto.CommonResponse; import top.suyiiyii.sims.dto.UserDto; @@ -101,9 +100,8 @@ public class UserController { @Operation(description = "获取当前用户信息") @AuthAccess(allowRoles = {"user"}) @GetMapping("/user/me") - public Result getSelf(HttpServletRequest request) { - int userId = JwtInterceptor.getUserIdFromReq(request); - UserDto user = userService.findUser(userId); + public Result getSelf() { + UserDto user = userService.findUser(0); return Result.success(user); } diff --git a/src/main/java/top/suyiiyii/sims/service/UserService.java b/src/main/java/top/suyiiyii/sims/service/UserService.java index e63a278..e7f8f40 100644 --- a/src/main/java/top/suyiiyii/sims/service/UserService.java +++ b/src/main/java/top/suyiiyii/sims/service/UserService.java @@ -124,6 +124,14 @@ public class UserService { UserDto.setUserGroup(user.getUserGroup()); UserDto.setRoles(new ArrayList<>()); Integer id = user.getId(); + List roles = roleMapper.selectRolesById(id); + for (Role role : roles) { + Integer roleId = role.getId(); + // 获取一个角色的名称列表 + List roleNameList = roleMapper.selectRoleNamesByRoleId(roleId); + // 累加角色名称到用户的角色列表中 + UserDto.getRoles().addAll(roleNameList); + } UserDtos.add(UserDto); } return UserDtos; @@ -133,15 +141,21 @@ public class UserService { UserDto UserDto = new UserDto(); User user = userMapper.selectById(id); - if (user == null) { - throw new ServiceException("用户不存在"); - } UserDto.setUserId(user.getId()); UserDto.setUsername(user.getUsername()); UserDto.setGrade(user.getGrade()); UserDto.setUserGroup(user.getUserGroup()); UserDto.setRoles(new ArrayList<>()); - //TODO: 获取用户角色 + List roles = roleMapper.selectRolesById(id); + for (Role role : roles) { + Integer roleId = role.getId(); + // 获取一个角色的名称列表 + List roleNameList = roleMapper.selectRoleNamesByRoleId(roleId); + // 累加角色名称到用户的角色列表中 + UserDto.getRoles().addAll(roleNameList); + } + + return UserDto; }