From 0320a79e335c36a8cd9ce13a7fc64dc8c2c22483 Mon Sep 17 00:00:00 2001 From: NagoColer Date: Sun, 6 Aug 2023 00:31:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=90=8E=E7=AB=AF=E5=85=A8?= =?UTF-8?q?=E9=83=A8=E5=86=85=E5=AE=B9=EF=BC=8C=E7=AD=89=E5=BE=85=E5=90=8E?= =?UTF-8?q?=E7=BB=AD=E4=BF=AE=E8=A1=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 +- my-project-backend/pom.xml | 6 + .../example/config/SecurityConfiguration.java | 4 +- .../example/config/SwaggerConfiguration.java | 113 ++++++++++++++++++ .../controller/AuthorizeController.java | 15 ++- .../com/example/filter/RequestLogFilter.java | 31 ++++- .../src/main/resources/application-dev.yml | 9 +- .../src/main/resources/application-prod.yml | 8 ++ 8 files changed, 180 insertions(+), 19 deletions(-) create mode 100644 my-project-backend/src/main/java/com/example/config/SwaggerConfiguration.java diff --git a/README.md b/README.md index cddde1c..0fe5f96 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,17 @@ 采用SpringBoot3 + Vue3编写的前后端分离模版项目,集成多种技术栈,使用JWT校验方案。 *** ### 后端功能与技术点 -* 用户注册、用户登录、重置密码 +用户注册、用户登录、重置密码等基础功能以及对应接口 +* 采用Mybatis-Plus作为持久层框架,使用更便捷 +* 采用Redis存储注册/重置操作验证码,带过期时间控制 +* 采用RabbitMQ积压短信发送任务,再由监听器统一处理 +* 采用SpringSecurity作为权限校验框架,手动整合Jwt校验方案 +* 采用Redis进行IP地址限流处理,防刷接口 +* 视图层对象和数据层对象分离,编写工具方法利用反射快速互相转换 +* 错误和异常页面统一采用JSON格式返回,前端处理响应更统一 +* 手动处理跨域,采用过滤器实现 +* 使用Swagger作为接口文档自动生成,已自动配置登录相关接口 +* 采用过滤器实现对所有请求自动生成雪花ID方便线上定位问题 +* 项目整体结构清晰,职责明确,注释全面,开箱即用 ### 前端功能与技术点 diff --git a/my-project-backend/pom.xml b/my-project-backend/pom.xml index 154c56d..085e7e8 100644 --- a/my-project-backend/pom.xml +++ b/my-project-backend/pom.xml @@ -88,6 +88,12 @@ java-jwt 4.3.0 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 + diff --git a/my-project-backend/src/main/java/com/example/config/SecurityConfiguration.java b/my-project-backend/src/main/java/com/example/config/SecurityConfiguration.java index ec9b004..5449614 100644 --- a/my-project-backend/src/main/java/com/example/config/SecurityConfiguration.java +++ b/my-project-backend/src/main/java/com/example/config/SecurityConfiguration.java @@ -53,8 +53,8 @@ public class SecurityConfiguration { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(conf -> conf - .requestMatchers("/api/auth/**").permitAll() - .requestMatchers("/error").permitAll() + .requestMatchers("/api/auth/**", "/error").permitAll() + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() .anyRequest().hasAnyRole(Const.ROLE_DEFAULT) ) .formLogin(conf -> conf diff --git a/my-project-backend/src/main/java/com/example/config/SwaggerConfiguration.java b/my-project-backend/src/main/java/com/example/config/SwaggerConfiguration.java new file mode 100644 index 0000000..d1ee7ad --- /dev/null +++ b/my-project-backend/src/main/java/com/example/config/SwaggerConfiguration.java @@ -0,0 +1,113 @@ +package com.example.config; + +import com.example.entity.RestBean; +import com.example.entity.vo.response.AuthorizeVO; +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.parameters.QueryParameter; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import org.springdoc.core.customizers.OpenApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Swagger开发文档相关配置 + */ +@Configuration +@SecurityScheme(type = SecuritySchemeType.HTTP, scheme = "Bearer", + name = "Authorization", in = SecuritySchemeIn.HEADER) +@OpenAPIDefinition(security = { @SecurityRequirement(name = "Authorization") }) +public class SwaggerConfiguration { + + /** + * 配置文档介绍以及详细信息 + * @return OpenAPI + */ + @Bean + public OpenAPI springShopOpenAPI() { + return new OpenAPI() + .info(new Info().title("示例项目 API 文档") + .description("欢迎来到本示例项目API测试文档,在这里可以快速进行接口调试") + .version("1.0") + .license(new License() + .name("项目开源地址") + .url("https://github.com/Ketuer/SpringBoot-Vue-Template-Jwt") + ) + ) + .externalDocs(new ExternalDocumentation() + .description("我们的官方网站") + .url("https://itbaima.net") + ); + } + + /** + * 配置自定义的OpenApi相关信息 + * @return OpenApiCustomizer + */ + @Bean + public OpenApiCustomizer customerGlobalHeaderOpenApiCustomizer() { + return api -> this.authorizePathItems().forEach(api.getPaths()::addPathItem); + } + + /** + * 登录接口和退出登录接口手动添加一下 + * @return PathItems + */ + private Map authorizePathItems(){ + Map map = new HashMap<>(); + map.put("/api/auth/login", new PathItem() + .post(new Operation() + .tags(List.of("登录校验相关")) + .summary("登录验证接口") + .addParametersItem(new QueryParameter() + .name("username") + .required(true) + ) + .addParametersItem(new QueryParameter() + .name("password") + .required(true) + ) + .responses(new ApiResponses() + .addApiResponse("200", new ApiResponse() + .description("OK") + .content(new Content().addMediaType("*/*", new MediaType() + .example(RestBean.success(new AuthorizeVO()).asJsonString()) + )) + ) + ) + ) + ); + map.put("/api/auth/logout", new PathItem() + .get(new Operation() + .tags(List.of("登录校验相关")) + .summary("退出登录接口") + .responses(new ApiResponses() + .addApiResponse("200", new ApiResponse() + .description("OK") + .content(new Content().addMediaType("*/*", new MediaType() + .example(RestBean.success()) + )) + ) + ) + ) + + ); + return map; + } +} diff --git a/my-project-backend/src/main/java/com/example/controller/AuthorizeController.java b/my-project-backend/src/main/java/com/example/controller/AuthorizeController.java index d43b670..4c47e74 100644 --- a/my-project-backend/src/main/java/com/example/controller/AuthorizeController.java +++ b/my-project-backend/src/main/java/com/example/controller/AuthorizeController.java @@ -4,15 +4,14 @@ import com.example.entity.RestBean; import com.example.entity.vo.request.EmailRegisterVO; import com.example.entity.vo.request.EmailResetVO; import com.example.service.AccountService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import jakarta.validation.constraints.Email; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.function.Supplier; @@ -22,6 +21,7 @@ import java.util.function.Supplier; @Validated @RestController @RequestMapping("/api/auth") +@Tag(name = "登录校验相关", description = "包括用户登录、注册、验证码请求等操作。") public class AuthorizeController { @Resource @@ -34,6 +34,7 @@ public class AuthorizeController { * @return 是否请求成功 */ @PostMapping("/ask-code") + @Operation(summary = "请求邮件验证码") public RestBean askVerifyCode(@RequestParam @Email String email, HttpServletRequest request){ return this.messageHandle(() -> @@ -46,7 +47,8 @@ public class AuthorizeController { * @return 是否注册成功 */ @PostMapping("/register") - public RestBean register(@Valid EmailRegisterVO vo){ + @Operation(summary = "用户注册操作") + public RestBean register(@RequestBody @Valid EmailRegisterVO vo){ return this.messageHandle(() -> accountService.registerEmailAccount(vo)); } @@ -57,7 +59,8 @@ public class AuthorizeController { * @return 是否操作成功 */ @PostMapping("/reset-password") - public RestBean reset(@Valid EmailResetVO vo){ + @Operation(summary = "密码重置操作") + public RestBean reset(@RequestBody @Valid EmailResetVO vo){ return this.messageHandle(() -> accountService.resetEmailAccountPassword(vo)); } diff --git a/my-project-backend/src/main/java/com/example/filter/RequestLogFilter.java b/my-project-backend/src/main/java/com/example/filter/RequestLogFilter.java index db12c91..4ab82b6 100644 --- a/my-project-backend/src/main/java/com/example/filter/RequestLogFilter.java +++ b/my-project-backend/src/main/java/com/example/filter/RequestLogFilter.java @@ -17,6 +17,7 @@ import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.ContentCachingResponseWrapper; import java.io.IOException; +import java.util.Set; /** * 请求日志过滤器,用于记录所有用户请求信息 @@ -28,14 +29,32 @@ public class RequestLogFilter extends OncePerRequestFilter { @Resource SnowflakeIdGenerator generator; + private final Set ignores = Set.of("/swagger-ui", "/v3/api-docs"); + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - long startTime = System.currentTimeMillis(); - this.logRequestStart(request); - ContentCachingResponseWrapper wrapper = new ContentCachingResponseWrapper(response); - filterChain.doFilter(request, wrapper); - this.logRequestEnd(wrapper, startTime); - wrapper.copyBodyToResponse(); + if(this.isIgnoreUrl(request.getServletPath())) { + filterChain.doFilter(request, response); + } else { + long startTime = System.currentTimeMillis(); + this.logRequestStart(request); + ContentCachingResponseWrapper wrapper = new ContentCachingResponseWrapper(response); + filterChain.doFilter(request, wrapper); + this.logRequestEnd(wrapper, startTime); + wrapper.copyBodyToResponse(); + } + } + + /** + * 判定当前请求url是否不需要日志打印 + * @param url 路径 + * @return 是否忽略 + */ + private boolean isIgnoreUrl(String url){ + for (String ignore : ignores) { + if(url.startsWith(ignore)) return true; + } + return false; } /** diff --git a/my-project-backend/src/main/resources/application-dev.yml b/my-project-backend/src/main/resources/application-dev.yml index 34dfa12..134c281 100644 --- a/my-project-backend/src/main/resources/application-dev.yml +++ b/my-project-backend/src/main/resources/application-dev.yml @@ -1,4 +1,8 @@ # 开发环境配置 +springdoc: + paths-to-match: /api/** + swagger-ui: + operations-sorter: alpha spring: mail: host: smtp.163.com @@ -29,12 +33,9 @@ spring: mail-limit: 60 flow: period: 3 - limit: 10 + limit: 50 block: 30 cors: origin: '*' credentials: false methods: '*' -mybatis-plus: - configuration: - log-impl: org.apache.ibatis.logging.stdout.StdOutImpl diff --git a/my-project-backend/src/main/resources/application-prod.yml b/my-project-backend/src/main/resources/application-prod.yml index 632920a..984d8f4 100644 --- a/my-project-backend/src/main/resources/application-prod.yml +++ b/my-project-backend/src/main/resources/application-prod.yml @@ -1,4 +1,12 @@ #生产环境配置 +server: + port: 80 +springdoc: + api-docs: + enabled: false +mybatis-plus: + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl spring: mail: host: smtp.163.com