From bcbb401b57132226010ea57fe31450d70a8a2497 Mon Sep 17 00:00:00 2001 From: wol <1293433164@qq.com> Date: Sun, 22 Dec 2024 21:20:30 +0800 Subject: [PATCH] 015 --- weblog-springboot-015/pom.xml | 167 ++++++++++++++++++ .../weblog-module-admin/.gitignore | 33 ++++ .../weblog-module-admin/pom.xml | 56 ++++++ .../admin/config/Knife4jAdminConfig.java | 53 ++++++ .../admin/config/WebSecurityConfig.java | 63 +++++++ .../quanxiaoha/weblog/admin/package-info.java | 7 + .../WeblogModuleAdminApplicationTests.java | 13 ++ .../weblog-module-common/.gitignore | 33 ++++ .../weblog-module-common/pom.xml | 70 ++++++++ .../weblog/common/aspect/ApiOperationLog.java | 17 ++ .../common/aspect/ApiOperationLogAspect.java | 102 +++++++++++ .../weblog/common/config/JacksonConfig.java | 55 ++++++ .../common/config/MybatisPlusConfig.java | 15 ++ .../weblog/common/domain/dos/UserDO.java | 38 ++++ .../common/domain/mapper/UserMapper.java | 19 ++ .../weblog/common/domain/package-info.java | 7 + .../weblog/common/enums/ResponseCodeEnum.java | 33 ++++ .../exception/BaseExceptionInterface.java | 13 ++ .../weblog/common/exception/BizException.java | 24 +++ .../exception/GlobalExceptionHandler.java | 85 +++++++++ .../weblog/common/package-info.java | 7 + .../weblog/common/utils/JsonUtil.java | 27 +++ .../weblog/common/utils/Response.java | 77 ++++++++ .../WeblogModuleCommonApplicationTests.java | 19 ++ .../weblog-module-jwt/.gitignore | 33 ++++ .../weblog-module-jwt/pom.xml | 70 ++++++++ .../JwtAuthenticationSecurityConfig.java | 58 ++++++ .../jwt/config/PasswordEncoderConfig.java | 27 +++ .../UsernameOrPasswordNullException.java | 19 ++ .../jwt/filter/JwtAuthenticationFilter.java | 58 ++++++ .../jwt/filter/TokenAuthenticationFilter.java | 101 +++++++++++ .../jwt/handler/RestAccessDeniedHandler.java | 28 +++ .../handler/RestAuthenticationEntryPoint.java | 38 ++++ .../RestAuthenticationFailureHandler.java | 43 +++++ .../RestAuthenticationSuccessHandler.java | 47 +++++ .../weblog/jwt/model/LoginRspVO.java | 25 +++ .../jwt/service/UserDetailServiceImpl.java | 46 +++++ .../weblog/jwt/utils/JwtTokenHelper.java | 146 +++++++++++++++ .../weblog/jwt/utils/ResultUtil.java | 73 ++++++++ .../jwt/WeblogModuleJwtApplicationTests.java | 13 ++ weblog-springboot-015/weblog-web/.gitignore | 33 ++++ weblog-springboot-015/weblog-web/pom.xml | 75 ++++++++ .../weblog/web/WeblogWebApplication.java | 15 ++ .../weblog/web/config/Knife4jConfig.java | 53 ++++++ .../weblog/web/controller/TestController.java | 47 +++++ .../com/quanxiaoha/weblog/web/model/User.java | 50 ++++++ .../src/main/resources/application-dev.yml | 21 +++ .../src/main/resources/application-prod.yml | 24 +++ .../src/main/resources/application.yml | 18 ++ .../src/main/resources/logback-weblog.xml | 46 +++++ .../src/main/resources/spy.properties | 24 +++ .../weblog/web/WeblogWebApplicationTests.java | 49 +++++ 52 files changed, 2313 insertions(+) create mode 100644 weblog-springboot-015/pom.xml create mode 100644 weblog-springboot-015/weblog-module-admin/.gitignore create mode 100644 weblog-springboot-015/weblog-module-admin/pom.xml create mode 100644 weblog-springboot-015/weblog-module-admin/src/main/java/com/quanxiaoha/weblog/admin/config/Knife4jAdminConfig.java create mode 100644 weblog-springboot-015/weblog-module-admin/src/main/java/com/quanxiaoha/weblog/admin/config/WebSecurityConfig.java create mode 100644 weblog-springboot-015/weblog-module-admin/src/main/java/com/quanxiaoha/weblog/admin/package-info.java create mode 100644 weblog-springboot-015/weblog-module-admin/src/test/java/com/quanxiaoha/weblog/admin/WeblogModuleAdminApplicationTests.java create mode 100644 weblog-springboot-015/weblog-module-common/.gitignore create mode 100644 weblog-springboot-015/weblog-module-common/pom.xml create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/aspect/ApiOperationLog.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/aspect/ApiOperationLogAspect.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/config/JacksonConfig.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/config/MybatisPlusConfig.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/domain/dos/UserDO.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/domain/mapper/UserMapper.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/domain/package-info.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/enums/ResponseCodeEnum.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/exception/BaseExceptionInterface.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/exception/BizException.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/exception/GlobalExceptionHandler.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/package-info.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/utils/JsonUtil.java create mode 100644 weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/utils/Response.java create mode 100644 weblog-springboot-015/weblog-module-common/src/test/java/com/quanxiaoha/weblog/common/WeblogModuleCommonApplicationTests.java create mode 100644 weblog-springboot-015/weblog-module-jwt/.gitignore create mode 100644 weblog-springboot-015/weblog-module-jwt/pom.xml create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/config/JwtAuthenticationSecurityConfig.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/config/PasswordEncoderConfig.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/exception/UsernameOrPasswordNullException.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/filter/JwtAuthenticationFilter.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/filter/TokenAuthenticationFilter.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAccessDeniedHandler.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAuthenticationEntryPoint.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAuthenticationFailureHandler.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAuthenticationSuccessHandler.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/model/LoginRspVO.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/service/UserDetailServiceImpl.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/utils/JwtTokenHelper.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/utils/ResultUtil.java create mode 100644 weblog-springboot-015/weblog-module-jwt/src/test/java/com/quanxiaoha/weblog/jwt/WeblogModuleJwtApplicationTests.java create mode 100644 weblog-springboot-015/weblog-web/.gitignore create mode 100644 weblog-springboot-015/weblog-web/pom.xml create mode 100644 weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/WeblogWebApplication.java create mode 100644 weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/config/Knife4jConfig.java create mode 100644 weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/controller/TestController.java create mode 100644 weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/model/User.java create mode 100644 weblog-springboot-015/weblog-web/src/main/resources/application-dev.yml create mode 100644 weblog-springboot-015/weblog-web/src/main/resources/application-prod.yml create mode 100644 weblog-springboot-015/weblog-web/src/main/resources/application.yml create mode 100644 weblog-springboot-015/weblog-web/src/main/resources/logback-weblog.xml create mode 100644 weblog-springboot-015/weblog-web/src/main/resources/spy.properties create mode 100644 weblog-springboot-015/weblog-web/src/test/java/com/quanxiaoha/weblog/web/WeblogWebApplicationTests.java diff --git a/weblog-springboot-015/pom.xml b/weblog-springboot-015/pom.xml new file mode 100644 index 0000000..f8aa5a3 --- /dev/null +++ b/weblog-springboot-015/pom.xml @@ -0,0 +1,167 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + + 2.6.3 + + + + com.quanxiaoha + weblog-springboot + ${revision} + weblog-springboot + + 前后端分离博客 Weblog By 犬小哈 + + + pom + + + + + weblog-web + + weblog-module-admin + + weblog-module-common + + weblog-module-jwt + + + + + + + 0.0.1-SNAPSHOT + 1.8 + UTF-8 + + ${java.version} + ${java.version} + + + 1.18.28 + 31.1-jre + 3.12.0 + 2.15.2 + 4.3.0 + 3.5.2 + 3.9.1 + 0.11.2 + + + + + + + com.quanxiaoha + weblog-module-admin + ${revision} + + + + com.quanxiaoha + weblog-module-common + ${revision} + + + + com.quanxiaoha + weblog-module-jwt + ${revision} + + + + + com.google.guava + guava + ${guava.version} + + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + + com.github.xiaoymin + knife4j-openapi2-spring-boot-starter + ${knife4j.version} + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + + p6spy + p6spy + ${p6spy.version} + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + + + + + aliyunmaven + aliyun + https://maven.aliyun.com/repository/public + + + diff --git a/weblog-springboot-015/weblog-module-admin/.gitignore b/weblog-springboot-015/weblog-module-admin/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/weblog-springboot-015/weblog-module-admin/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/weblog-springboot-015/weblog-module-admin/pom.xml b/weblog-springboot-015/weblog-module-admin/pom.xml new file mode 100644 index 0000000..b6a3c4e --- /dev/null +++ b/weblog-springboot-015/weblog-module-admin/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + com.quanxiaoha + weblog-springboot + ${revision} + + + com.quanxiaoha + weblog-module-admin + weblog-module-admin + weblog-admin (负责管理后台相关功能) + + + + com.quanxiaoha + weblog-module-common + + + + com.quanxiaoha + weblog-module-jwt + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + com.github.xiaoymin + knife4j-openapi2-spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-security + + + + + \ No newline at end of file diff --git a/weblog-springboot-015/weblog-module-admin/src/main/java/com/quanxiaoha/weblog/admin/config/Knife4jAdminConfig.java b/weblog-springboot-015/weblog-module-admin/src/main/java/com/quanxiaoha/weblog/admin/config/Knife4jAdminConfig.java new file mode 100644 index 0000000..3c2b2da --- /dev/null +++ b/weblog-springboot-015/weblog-module-admin/src/main/java/com/quanxiaoha/weblog/admin/config/Knife4jAdminConfig.java @@ -0,0 +1,53 @@ +package com.quanxiaoha.weblog.admin.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-16 7:53 + * @description: Knife4j 配置 + **/ +@Configuration +@EnableSwagger2WebMvc +@Profile("dev") // 只在 dev 环境中开启 +public class Knife4jAdminConfig { + + @Bean("adminApi") + public Docket createApiDoc() { + Docket docket = new Docket(DocumentationType.SWAGGER_2) + .apiInfo(buildApiInfo()) + // 分组名称 + .groupName("Admin 后台接口") + .select() + // 这里指定 Controller 扫描包路径 + .apis(RequestHandlerSelectors.basePackage("com.quanxiaoha.weblog.admin.controller")) + .paths(PathSelectors.any()) + .build(); + return docket; + } + + /** + * 构建 API 信息 + * @return + */ + private ApiInfo buildApiInfo() { + return new ApiInfoBuilder() + .title("Weblog 博客 Admin 后台接口文档") // 标题 + .description("Weblog 是一款由 Spring Boot + Vue 3.2 + Vite 4.3 开发的前后端分离博客。") // 描述 + .termsOfServiceUrl("https://www.quanxiaoha.com/") // API 服务条款 + .contact(new Contact("犬小哈", "https://www.quanxiaoha.com", "871361652@qq.com")) // 联系人 + .version("1.0") // 版本号 + .build(); + } +} diff --git a/weblog-springboot-015/weblog-module-admin/src/main/java/com/quanxiaoha/weblog/admin/config/WebSecurityConfig.java b/weblog-springboot-015/weblog-module-admin/src/main/java/com/quanxiaoha/weblog/admin/config/WebSecurityConfig.java new file mode 100644 index 0000000..f3215ab --- /dev/null +++ b/weblog-springboot-015/weblog-module-admin/src/main/java/com/quanxiaoha/weblog/admin/config/WebSecurityConfig.java @@ -0,0 +1,63 @@ +package com.quanxiaoha.weblog.admin.config; + +import com.quanxiaoha.weblog.jwt.config.JwtAuthenticationSecurityConfig; +import com.quanxiaoha.weblog.jwt.filter.TokenAuthenticationFilter; +import com.quanxiaoha.weblog.jwt.handler.RestAccessDeniedHandler; +import com.quanxiaoha.weblog.jwt.handler.RestAuthenticationEntryPoint; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +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.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-23 15:48 + * @description: Spring Security 配置类 + **/ +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + private JwtAuthenticationSecurityConfig jwtAuthenticationSecurityConfig; + @Autowired + private RestAuthenticationEntryPoint authEntryPoint; + @Autowired + private RestAccessDeniedHandler deniedHandler; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable(). // 禁用 csrf + formLogin().disable() // 禁用表单登录 + .apply(jwtAuthenticationSecurityConfig) // 设置用户登录认证相关配置 + .and() + .authorizeHttpRequests() + .mvcMatchers("/admin/**").authenticated() // 认证所有以 /admin 为前缀的 URL 资源 + .anyRequest().permitAll() // 其他都需要放行,无需认证 + .and() + .httpBasic().authenticationEntryPoint(authEntryPoint) // 处理用户未登录访问受保护的资源的情况 + .and() + .exceptionHandling().accessDeniedHandler(deniedHandler) // 处理登录成功后访问受保护的资源,但是权限不够的情况 + .and() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 前后端分离,无需创建会话 + .and() + .addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) // 将 Token 校验过滤器添加到用户认证过滤器之前 + ; + } + + /** + * Token 校验过滤器 + * @return + */ + @Bean + public TokenAuthenticationFilter tokenAuthenticationFilter() { + return new TokenAuthenticationFilter(); + } + +} diff --git a/weblog-springboot-015/weblog-module-admin/src/main/java/com/quanxiaoha/weblog/admin/package-info.java b/weblog-springboot-015/weblog-module-admin/src/main/java/com/quanxiaoha/weblog/admin/package-info.java new file mode 100644 index 0000000..8a825e4 --- /dev/null +++ b/weblog-springboot-015/weblog-module-admin/src/main/java/com/quanxiaoha/weblog/admin/package-info.java @@ -0,0 +1,7 @@ +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-16 9:28 + * @description: TODO + **/ +package com.quanxiaoha.weblog.admin; \ No newline at end of file diff --git a/weblog-springboot-015/weblog-module-admin/src/test/java/com/quanxiaoha/weblog/admin/WeblogModuleAdminApplicationTests.java b/weblog-springboot-015/weblog-module-admin/src/test/java/com/quanxiaoha/weblog/admin/WeblogModuleAdminApplicationTests.java new file mode 100644 index 0000000..dab9a4b --- /dev/null +++ b/weblog-springboot-015/weblog-module-admin/src/test/java/com/quanxiaoha/weblog/admin/WeblogModuleAdminApplicationTests.java @@ -0,0 +1,13 @@ +package com.quanxiaoha.weblog.admin; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class WeblogModuleAdminApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/weblog-springboot-015/weblog-module-common/.gitignore b/weblog-springboot-015/weblog-module-common/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/weblog-springboot-015/weblog-module-common/pom.xml b/weblog-springboot-015/weblog-module-common/pom.xml new file mode 100644 index 0000000..0e587e2 --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + com.quanxiaoha + weblog-springboot + ${revision} + + + com.quanxiaoha + weblog-module-common + weblog-module-common + weblog-module-common (此模块用于存放一些通用的功能) + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.baomidou + mybatis-plus-boot-starter + + + + + mysql + mysql-connector-java + + + + p6spy + p6spy + + + + + + + + + diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/aspect/ApiOperationLog.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/aspect/ApiOperationLog.java new file mode 100644 index 0000000..38eeb0f --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/aspect/ApiOperationLog.java @@ -0,0 +1,17 @@ +package com.quanxiaoha.weblog.common.aspect; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@Documented +public @interface ApiOperationLog { + /** + * API 功能描述 + * + * @return + */ + String description() default ""; + +} + diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/aspect/ApiOperationLogAspect.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/aspect/ApiOperationLogAspect.java new file mode 100644 index 0000000..ef199b4 --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/aspect/ApiOperationLogAspect.java @@ -0,0 +1,102 @@ + +package com.quanxiaoha.weblog.common.aspect; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.quanxiaoha.weblog.common.utils.JsonUtil; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.*; +import org.aspectj.lang.reflect.MethodSignature; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.UUID; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Aspect +@Component +@Slf4j +public class ApiOperationLogAspect { + + /** 以自定义 @ApiOperationLog 注解为切点,凡是添加 @ApiOperationLog 的方法,都会执行环绕中的代码 */ + @Pointcut("@annotation(com.quanxiaoha.weblog.common.aspect.ApiOperationLog)") + public void apiOperationLog() {} + + /** + * 环绕 + * @param joinPoint + * @return + * @throws Throwable + */ + @Around("apiOperationLog()") + public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { + try { + // 请求开始时间 + long startTime = System.currentTimeMillis(); + + // MDC + MDC.put("traceId", UUID.randomUUID().toString()); + + // 获取被请求的类和方法 + String className = joinPoint.getTarget().getClass().getSimpleName(); + String methodName = joinPoint.getSignature().getName(); + + // 请求入参 + Object[] args = joinPoint.getArgs(); + // 入参转 JSON 字符串 + String argsJsonStr = Arrays.stream(args).map(toJsonStr()).collect(Collectors.joining(", ")); + + // 功能描述信息 + String description = getApiOperationLogDescription(joinPoint); + + // 打印请求相关参数 + log.info("====== 请求开始: [{}], 入参: {}, 请求类: {}, 请求方法: {} =================================== ", + description, argsJsonStr, className, methodName); + + // 执行切点方法 + Object result = joinPoint.proceed(); + + // 执行耗时 + long executionTime = System.currentTimeMillis() - startTime; + + // 打印出参等相关信息 + log.info("====== 请求结束: [{}], 耗时: {}ms, 出参: {} =================================== ", + description, executionTime, JsonUtil.toJsonString(result)); + + return result; + } finally { + MDC.clear(); + } + } + + /** + * 获取注解的描述信息 + * @param joinPoint + * @return + */ + private String getApiOperationLogDescription(ProceedingJoinPoint joinPoint) { + // 1. 从 ProceedingJoinPoint 获取 MethodSignature + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + + // 2. 使用 MethodSignature 获取当前被注解的 Method + Method method = signature.getMethod(); + + // 3. 从 Method 中提取 LogExecution 注解 + ApiOperationLog apiOperationLog = method.getAnnotation(ApiOperationLog.class); + + // 4. 从 LogExecution 注解中获取 description 属性 + return apiOperationLog.description(); + } + + /** + * 转 JSON 字符串 + * @return + */ + private Function toJsonStr() { + return arg -> JsonUtil.toJsonString(arg); + } + +} diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/config/JacksonConfig.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/config/JacksonConfig.java new file mode 100644 index 0000000..4ba3a35 --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/config/JacksonConfig.java @@ -0,0 +1,55 @@ +package com.quanxiaoha.weblog.common.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.TimeZone; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-17 16:08 + * @description: 自定义 Jackson + **/ +@Configuration +public class JacksonConfig { + + @Bean + public ObjectMapper objectMapper() { + // 初始化一个 ObjectMapper 对象,用于自定义 Jackson 的行为 + ObjectMapper objectMapper = new ObjectMapper(); + // JavaTimeModule 用于指定序列化和反序列化规则 + JavaTimeModule javaTimeModule = new JavaTimeModule(); + + // 支持 LocalDateTime、LocalDate、LocalTime + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss"))); + javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern("HH:mm:ss"))); + + objectMapper.registerModule(javaTimeModule); + + // 设置时区 + objectMapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")); + + // 设置凡是为 null 的字段,返参中均不返回,请根据项目组约定是否开启 + // objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + return objectMapper; + } +} diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/config/MybatisPlusConfig.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/config/MybatisPlusConfig.java new file mode 100644 index 0000000..019dd36 --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/config/MybatisPlusConfig.java @@ -0,0 +1,15 @@ +package com.quanxiaoha.weblog.common.config; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.context.annotation.Configuration; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-22 16:52 + * @description: Mybatis Plus 配置文件 + **/ +@Configuration +@MapperScan("com.quanxiaoha.weblog.common.domain.mapper") +public class MybatisPlusConfig { +} diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/domain/dos/UserDO.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/domain/dos/UserDO.java new file mode 100644 index 0000000..6d7f991 --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/domain/dos/UserDO.java @@ -0,0 +1,38 @@ +package com.quanxiaoha.weblog.common.domain.dos; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-22 17:01 + * @description: TODO + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +@TableName("t_user") +public class UserDO { + + @TableId(type = IdType.AUTO) + private Long id; + + private String username; + + private String password; + + private LocalDateTime createTime; + + private LocalDateTime updateTime; + + private Boolean isDeleted; +} diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/domain/mapper/UserMapper.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/domain/mapper/UserMapper.java new file mode 100644 index 0000000..2cfeada --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/domain/mapper/UserMapper.java @@ -0,0 +1,19 @@ +package com.quanxiaoha.weblog.common.domain.mapper; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.quanxiaoha.weblog.common.domain.dos.UserDO; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-22 17:06 + * @description: TODO + **/ +public interface UserMapper extends BaseMapper { + default UserDO findByUsername(String username) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(UserDO::getUsername, username); + return selectOne(wrapper); + } +} diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/domain/package-info.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/domain/package-info.java new file mode 100644 index 0000000..948fa16 --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/domain/package-info.java @@ -0,0 +1,7 @@ +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-22 16:57 + * @description: TODO + **/ +package com.quanxiaoha.weblog.common.domain; \ No newline at end of file diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/enums/ResponseCodeEnum.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/enums/ResponseCodeEnum.java new file mode 100644 index 0000000..887820a --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/enums/ResponseCodeEnum.java @@ -0,0 +1,33 @@ +package com.quanxiaoha.weblog.common.enums; + +import com.quanxiaoha.weblog.common.exception.BaseExceptionInterface; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-15 10:33 + * @description: 响应异常码 + **/ +@Getter +@AllArgsConstructor +public enum ResponseCodeEnum implements BaseExceptionInterface { + + // ----------- 通用异常状态码 ----------- + SYSTEM_ERROR("10000", "出错啦,后台小哥正在努力修复中..."), + PARAM_NOT_VALID("10001", "参数错误"), + + + // ----------- 业务异常状态码 ----------- + LOGIN_FAIL("20000", "登录失败"), + USERNAME_OR_PWD_ERROR("20001", "用户名或密码错误"), + UNAUTHORIZED("20002", "无访问权限,请先登录!"), + ; + + // 异常码 + private String errorCode; + // 错误信息 + private String errorMessage; + +} diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/exception/BaseExceptionInterface.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/exception/BaseExceptionInterface.java new file mode 100644 index 0000000..ad51d50 --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/exception/BaseExceptionInterface.java @@ -0,0 +1,13 @@ +package com.quanxiaoha.weblog.common.exception; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-15 9:54 + * @description: 通用异常接口 + **/ +public interface BaseExceptionInterface { + String getErrorCode(); + + String getErrorMessage(); +} diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/exception/BizException.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/exception/BizException.java new file mode 100644 index 0000000..cf618c2 --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/exception/BizException.java @@ -0,0 +1,24 @@ +package com.quanxiaoha.weblog.common.exception; + +import lombok.Getter; +import lombok.Setter; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-15 9:52 + * @description: 业务异常 + **/ +@Getter +@Setter +public class BizException extends RuntimeException { + // 异常码 + private String errorCode; + // 错误信息 + private String errorMessage; + + public BizException(BaseExceptionInterface baseExceptionInterface) { + this.errorCode = baseExceptionInterface.getErrorCode(); + this.errorMessage = baseExceptionInterface.getErrorMessage(); + } +} diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/exception/GlobalExceptionHandler.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..27f41bb --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,85 @@ +package com.quanxiaoha.weblog.common.exception; + +import com.quanxiaoha.weblog.common.enums.ResponseCodeEnum; +import com.quanxiaoha.weblog.common.utils.Response; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; + +import javax.servlet.http.HttpServletRequest; +import java.util.Optional; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-15 10:14 + * @description: 全局异常处理 + **/ +@ControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + /** + * 捕获自定义业务异常 + * @return + */ + @ExceptionHandler({ BizException.class }) + @ResponseBody + public Response handleBizException(HttpServletRequest request, BizException e) { + log.warn("{} request fail, errorCode: {}, errorMessage: {}", request.getRequestURI(), e.getErrorCode(), e.getErrorMessage()); + return Response.fail(e); + } + + /** + * 捕获参数校验异常 + * @return + */ + @ExceptionHandler({ MethodArgumentNotValidException.class }) + @ResponseBody + public Response handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) { + // 参数错误异常码 + String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode(); + + // 获取 BindingResult + BindingResult bindingResult = e.getBindingResult(); + + StringBuilder sb = new StringBuilder(); + + // 获取校验不通过的字段,并组合错误信息,格式为: email 邮箱格式不正确, 当前值: '123124qq.com'; + Optional.ofNullable(bindingResult.getFieldErrors()).ifPresent(errors -> { + errors.forEach(error -> + sb.append(error.getField()) + .append(" ") + .append(error.getDefaultMessage()) + .append(", 当前值: '") + .append(error.getRejectedValue()) + .append("'; ") + + ); + }); + + // 错误信息 + String errorMessage = sb.toString(); + + log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage); + + return Response.fail(errorCode, errorMessage); + } + + /** + * 其他类型异常 + * @param request + * @param e + * @return + */ + @ExceptionHandler({ Exception.class }) + @ResponseBody + public Response handleOtherException(HttpServletRequest request, Exception e) { + log.error("{} request error, ", request.getRequestURI(), e); + return Response.fail(ResponseCodeEnum.SYSTEM_ERROR); + } +} diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/package-info.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/package-info.java new file mode 100644 index 0000000..f36a31e --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/package-info.java @@ -0,0 +1,7 @@ +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-10 9:20 + * @description: TODO + **/ +package com.quanxiaoha.weblog.common; \ No newline at end of file diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/utils/JsonUtil.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/utils/JsonUtil.java new file mode 100644 index 0000000..05d0a22 --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/utils/JsonUtil.java @@ -0,0 +1,27 @@ +package com.quanxiaoha.weblog.common.utils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-14 16:27 + * @description: JSON 工具类 + **/ +@Slf4j +public class JsonUtil { + + private static final ObjectMapper INSTANCE = new ObjectMapper(); + + public static String toJsonString(Object obj) { + try { + return INSTANCE.writeValueAsString(obj); + } catch (JsonProcessingException e) { + return obj.toString(); + } + } +} diff --git a/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/utils/Response.java b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/utils/Response.java new file mode 100644 index 0000000..dfa1743 --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/main/java/com/quanxiaoha/weblog/common/utils/Response.java @@ -0,0 +1,77 @@ +package com.quanxiaoha.weblog.common.utils; + +import com.quanxiaoha.weblog.common.exception.BaseExceptionInterface; +import com.quanxiaoha.weblog.common.exception.BizException; +import lombok.Data; + +import java.io.Serializable; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-11 19:50 + * @description: 响应参数工具类 + **/ +@Data +public class Response implements Serializable { + + // 是否成功,默认为 true + private boolean success = true; + // 响应消息 + private String message; + // 异常码 + private String errorCode; + // 响应数据 + private T data; + + // =================================== 成功响应 =================================== + public static Response success() { + Response response = new Response<>(); + return response; + } + + public static Response success(T data) { + Response response = new Response<>(); + response.setData(data); + return response; + } + + // =================================== 失败响应 =================================== + public static Response fail() { + Response response = new Response<>(); + response.setSuccess(false); + return response; + } + + public static Response fail(String errorMessage) { + Response response = new Response<>(); + response.setSuccess(false); + response.setMessage(errorMessage); + return response; + } + + public static Response fail(String errorCode, String errorMessage) { + Response response = new Response<>(); + response.setSuccess(false); + response.setErrorCode(errorCode); + response.setMessage(errorMessage); + return response; + } + + public static Response fail(BizException bizException) { + Response response = new Response<>(); + response.setSuccess(false); + response.setErrorCode(bizException.getErrorCode()); + response.setMessage(bizException.getErrorMessage()); + return response; + } + + public static Response fail(BaseExceptionInterface baseExceptionInterface) { + Response response = new Response<>(); + response.setSuccess(false); + response.setErrorCode(baseExceptionInterface.getErrorCode()); + response.setMessage(baseExceptionInterface.getErrorMessage()); + return response; + } + +} diff --git a/weblog-springboot-015/weblog-module-common/src/test/java/com/quanxiaoha/weblog/common/WeblogModuleCommonApplicationTests.java b/weblog-springboot-015/weblog-module-common/src/test/java/com/quanxiaoha/weblog/common/WeblogModuleCommonApplicationTests.java new file mode 100644 index 0000000..fdcf4c1 --- /dev/null +++ b/weblog-springboot-015/weblog-module-common/src/test/java/com/quanxiaoha/weblog/common/WeblogModuleCommonApplicationTests.java @@ -0,0 +1,19 @@ +package com.quanxiaoha.weblog.common; + +import com.quanxiaoha.weblog.common.domain.dos.UserDO; +import com.quanxiaoha.weblog.common.domain.mapper.UserMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.util.Date; + +@SpringBootTest +class WeblogModuleCommonApplicationTests { + + @Test + void contextLoads() { + } + + +} diff --git a/weblog-springboot-015/weblog-module-jwt/.gitignore b/weblog-springboot-015/weblog-module-jwt/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/weblog-springboot-015/weblog-module-jwt/pom.xml b/weblog-springboot-015/weblog-module-jwt/pom.xml new file mode 100644 index 0000000..3645a4c --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + com.quanxiaoha + weblog-springboot + ${revision} + + + com.quanxiaoha + weblog-module-jwt + weblog-module-jwt + weblog-module-jwt (JWT 模块,管理用户认证、鉴权) + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-security + + + + + io.jsonwebtoken + jjwt-api + + + io.jsonwebtoken + jjwt-impl + + + io.jsonwebtoken + jjwt-jackson + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.apache.commons + commons-lang3 + + + + + com.quanxiaoha + weblog-module-common + + + + + diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/config/JwtAuthenticationSecurityConfig.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/config/JwtAuthenticationSecurityConfig.java new file mode 100644 index 0000000..52d7c16 --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/config/JwtAuthenticationSecurityConfig.java @@ -0,0 +1,58 @@ +package com.quanxiaoha.weblog.jwt.config; + +import com.quanxiaoha.weblog.jwt.filter.JwtAuthenticationFilter; +import com.quanxiaoha.weblog.jwt.handler.RestAuthenticationFailureHandler; +import com.quanxiaoha.weblog.jwt.handler.RestAuthenticationSuccessHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-24 16:45 + * @description: 认证功能相关配置 + **/ +@Configuration +public class JwtAuthenticationSecurityConfig extends SecurityConfigurerAdapter { + + @Autowired + private RestAuthenticationSuccessHandler restAuthenticationSuccessHandler; + + @Autowired + private RestAuthenticationFailureHandler restAuthenticationFailureHandler; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private UserDetailsService userDetailsService; + + @Override + public void configure(HttpSecurity httpSecurity) throws Exception { + // 自定义的用于 JWT 身份验证的过滤器 + JwtAuthenticationFilter filter = new JwtAuthenticationFilter(); + filter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class)); + + // 设置登录认证对应的处理类(成功处理、失败处理) + filter.setAuthenticationSuccessHandler(restAuthenticationSuccessHandler); + filter.setAuthenticationFailureHandler(restAuthenticationFailureHandler); + + // 直接使用 DaoAuthenticationProvider, 它是 Spring Security 提供的默认的身份验证提供者之一 + DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + // 设置 userDetailService,用于获取用户的详细信息 + provider.setUserDetailsService(userDetailsService); + // 设置加密算法 + provider.setPasswordEncoder(passwordEncoder); + httpSecurity.authenticationProvider(provider); + // 将这个过滤器添加到 UsernamePasswordAuthenticationFilter 之前执行 + httpSecurity.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/config/PasswordEncoderConfig.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/config/PasswordEncoderConfig.java new file mode 100644 index 0000000..1556051 --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/config/PasswordEncoderConfig.java @@ -0,0 +1,27 @@ +package com.quanxiaoha.weblog.jwt.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-24 9:17 + * @description: 密码加密 + **/ +@Component +public class PasswordEncoderConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + // BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入“盐”,增加密码的安全性。 + return new BCryptPasswordEncoder(); + } + + public static void main(String[] args) { + BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); + System.out.println(encoder.encode("quanxiaoha")); + } +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/exception/UsernameOrPasswordNullException.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/exception/UsernameOrPasswordNullException.java new file mode 100644 index 0000000..e50ba96 --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/exception/UsernameOrPasswordNullException.java @@ -0,0 +1,19 @@ +package com.quanxiaoha.weblog.jwt.exception; + +import org.springframework.security.core.AuthenticationException; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-24 17:11 + * @description: 用户名或者密码为空异常 + **/ +public class UsernameOrPasswordNullException extends AuthenticationException { + public UsernameOrPasswordNullException(String msg, Throwable cause) { + super(msg, cause); + } + + public UsernameOrPasswordNullException(String msg) { + super(msg); + } +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/filter/JwtAuthenticationFilter.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..67605ad --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/filter/JwtAuthenticationFilter.java @@ -0,0 +1,58 @@ +package com.quanxiaoha.weblog.jwt.filter; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.quanxiaoha.weblog.jwt.exception.UsernameOrPasswordNullException; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-24 9:36 + * @description: 用户认证过滤器 + **/ +public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter { + + + /** + * 指定用户登录的访问地址 + */ + public JwtAuthenticationFilter() { + super(new AntPathRequestMatcher("/login", "POST")); + } + + @Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { + ObjectMapper mapper = new ObjectMapper(); + // 解析提交的 JSON 数据 + JsonNode jsonNode = mapper.readTree(request.getInputStream()); + JsonNode usernameNode = jsonNode.get("username"); + JsonNode passwordNode = jsonNode.get("password"); + + // 判断用户名、密码是否为空 + if (Objects.isNull(usernameNode) || Objects.isNull(passwordNode) + || StringUtils.isBlank(usernameNode.textValue()) || StringUtils.isBlank(passwordNode.textValue())) { + throw new UsernameOrPasswordNullException("用户名或密码不能为空"); + } + + String username = usernameNode.textValue(); + String password = passwordNode.textValue(); + + // 将用户名、密码封装到 Token 中 + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken + = new UsernamePasswordAuthenticationToken(username, password); + return getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken); + } +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/filter/TokenAuthenticationFilter.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/filter/TokenAuthenticationFilter.java new file mode 100644 index 0000000..b13d8eb --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/filter/TokenAuthenticationFilter.java @@ -0,0 +1,101 @@ +package com.quanxiaoha.weblog.jwt.filter; + +import com.quanxiaoha.weblog.jwt.utils.JwtTokenHelper; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.SignatureException; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.CredentialsExpiredException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-27 16:58 + * @description: Token 校验过滤器 + **/ +@Slf4j +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + @Value("${jwt.tokenPrefix}") + private String tokenPrefix; + + @Value("${jwt.tokenHeaderKey}") + private String tokenHeaderKey; + + @Autowired + private JwtTokenHelper jwtTokenHelper; + + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private AuthenticationEntryPoint authenticationEntryPoint; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + // 从请求头中获取 key 为 Authorization 的值 + String header = request.getHeader(tokenHeaderKey); + + // 判断 value 值是否以 Bearer 开头 + if (StringUtils.startsWith(header, tokenPrefix)) { + // 截取 Token 令牌 + String token = StringUtils.substring(header, 7); + log.info("Token: {}", token); + + // 判空 Token + if (StringUtils.isNotBlank(token)) { + try { + // 校验 Token 是否可用, 若解析异常,针对不同异常做出不同的响应参数 + jwtTokenHelper.validateToken(token); + } catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) { + // 抛出异常,统一让 AuthenticationEntryPoint 处理响应参数 + authenticationEntryPoint.commence(request, response, new AuthenticationServiceException("Token 不可用")); + return; + } catch (ExpiredJwtException e) { + authenticationEntryPoint.commence(request, response, new AuthenticationServiceException("Token 已失效")); + return; + } + + // 从 Token 中解析出用户名 + String username = jwtTokenHelper.getUsernameByToken(token); + + if (StringUtils.isNotBlank(username) + && Objects.isNull(SecurityContextHolder.getContext().getAuthentication())) { + // 根据用户名获取用户详情信息 + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + + // 将用户信息存入 authentication,方便后续校验 + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, + userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + // 将 authentication 存入 ThreadLocal,方便后续获取用户信息 + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + } + + // 继续执行写一个过滤器 + filterChain.doFilter(request, response); + } +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAccessDeniedHandler.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAccessDeniedHandler.java new file mode 100644 index 0000000..7505bd9 --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAccessDeniedHandler.java @@ -0,0 +1,28 @@ +package com.quanxiaoha.weblog.jwt.handler; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-27 17:32 + * @description: 登录成功访问收保护的资源,但是权限不够 + **/ +@Slf4j +@Component +public class RestAccessDeniedHandler implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + log.warn("登录成功访问收保护的资源,但是权限不够: ", accessDeniedException); + // 预留,后面引入多角色时会用到 + } +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAuthenticationEntryPoint.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAuthenticationEntryPoint.java new file mode 100644 index 0000000..850ca63 --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAuthenticationEntryPoint.java @@ -0,0 +1,38 @@ +package com.quanxiaoha.weblog.jwt.handler; + +import com.quanxiaoha.weblog.common.enums.ResponseCodeEnum; +import com.quanxiaoha.weblog.common.utils.Response; +import com.quanxiaoha.weblog.jwt.utils.ResultUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-27 17:27 + * @description: 用户未登录访问受保护的资源 + **/ +@Slf4j +@Component +public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + log.warn("用户未登录访问受保护的资源: ", authException); + if (authException instanceof InsufficientAuthenticationException) { + ResultUtil.fail(response, HttpStatus.UNAUTHORIZED.value(), Response.fail(ResponseCodeEnum.UNAUTHORIZED)); + } + + ResultUtil.fail(response, HttpStatus.UNAUTHORIZED.value(), Response.fail(authException.getMessage())); + } +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAuthenticationFailureHandler.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAuthenticationFailureHandler.java new file mode 100644 index 0000000..82ff738 --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAuthenticationFailureHandler.java @@ -0,0 +1,43 @@ +package com.quanxiaoha.weblog.jwt.handler; + +import com.quanxiaoha.weblog.common.enums.ResponseCodeEnum; +import com.quanxiaoha.weblog.common.utils.Response; +import com.quanxiaoha.weblog.jwt.exception.UsernameOrPasswordNullException; +import com.quanxiaoha.weblog.jwt.utils.ResultUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-24 15:19 + * @description: 认证失败处理器 + **/ +@Component +@Slf4j +public class RestAuthenticationFailureHandler implements AuthenticationFailureHandler { + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { + log.warn("AuthenticationException: ", exception); + if (exception instanceof UsernameOrPasswordNullException) { + // 用户名或密码为空 + ResultUtil.fail(response, Response.fail(exception.getMessage())); + } else if (exception instanceof BadCredentialsException) { + // 用户名或密码错误 + ResultUtil.fail(response, Response.fail(ResponseCodeEnum.USERNAME_OR_PWD_ERROR)); + } + + // 登录失败 + ResultUtil.fail(response, Response.fail(ResponseCodeEnum.LOGIN_FAIL)); + } +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAuthenticationSuccessHandler.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAuthenticationSuccessHandler.java new file mode 100644 index 0000000..d57015a --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/handler/RestAuthenticationSuccessHandler.java @@ -0,0 +1,47 @@ +package com.quanxiaoha.weblog.jwt.handler; + +import com.quanxiaoha.weblog.common.utils.Response; +import com.quanxiaoha.weblog.jwt.model.LoginRspVO; +import com.quanxiaoha.weblog.jwt.utils.JwtTokenHelper; +import com.quanxiaoha.weblog.jwt.utils.ResultUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-24 15:19 + * @description: 认证成功处理器 + **/ +@Component +@Slf4j +public class RestAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + @Autowired + private JwtTokenHelper jwtTokenHelper; + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + // 从 authentication 对象中获取用户的 UserDetails 实例,这里是获取用户的用户名 + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + + // 通过用户名生成 Token + String username = userDetails.getUsername(); + String token = jwtTokenHelper.generateToken(username); + + // 返回 Token + LoginRspVO loginRspVO = LoginRspVO.builder().token(token).build(); + + ResultUtil.ok(response, Response.success(loginRspVO)); + } +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/model/LoginRspVO.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/model/LoginRspVO.java new file mode 100644 index 0000000..b8c8e3e --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/model/LoginRspVO.java @@ -0,0 +1,25 @@ +package com.quanxiaoha.weblog.jwt.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-24 9:43 + * @description: 用户登录 + **/ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class LoginRspVO { + + /** + * Token 值 + */ + private String token; + +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/service/UserDetailServiceImpl.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/service/UserDetailServiceImpl.java new file mode 100644 index 0000000..a6b73d4 --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/service/UserDetailServiceImpl.java @@ -0,0 +1,46 @@ +package com.quanxiaoha.weblog.jwt.service; + +import com.quanxiaoha.weblog.common.domain.dos.UserDO; +import com.quanxiaoha.weblog.common.domain.mapper.UserMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Arrays; +import java.util.Objects; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-24 9:14 + * @description: TODO + **/ +@Service +@Slf4j +public class UserDetailServiceImpl implements UserDetailsService { + + @Autowired + private UserMapper userMapper; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 从数据库中查询 + UserDO userDO = userMapper.findByUsername(username); + + // 判断用户是否存在 + if (Objects.isNull(userDO)) { + throw new UsernameNotFoundException("该用户不存在"); + } + + // authorities 用于指定角色,这里写死为 ADMIN 管理员 + return User.withUsername(userDO.getUsername()) + .password(userDO.getPassword()) + .authorities("ADMIN") + .build(); + } +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/utils/JwtTokenHelper.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/utils/JwtTokenHelper.java new file mode 100644 index 0000000..3e67020 --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/utils/JwtTokenHelper.java @@ -0,0 +1,146 @@ +package com.quanxiaoha.weblog.jwt.utils; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.CredentialsExpiredException; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.sql.Date; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Base64; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-24 8:16 + * @description: JWT Token 工具类 + **/ +@Component +public class JwtTokenHelper implements InitializingBean { + + /** + * 签发人 + */ + @Value("${jwt.issuer}") + private String issuer; + + /** + * Token 失效时间(分钟) + */ + @Value("${jwt.tokenExpireTime}") + private Long tokenExpireTime; + /** + * 秘钥 + */ + private Key key; + + /** + * JWT 解析 + */ + private JwtParser jwtParser; + + /** + * 解码配置文件中配置的 Base 64 编码 key 为秘钥 + * @param base64Key + */ + @Value("${jwt.secret}") + public void setBase64Key(String base64Key) { + key = Keys.hmacShaKeyFor(Base64.getDecoder().decode(base64Key)); + } + + + /** + * 初始化 JwtParser + * @throws Exception + */ + @Override + public void afterPropertiesSet() throws Exception { + // 考虑到不同服务器之间可能存在时钟偏移,setAllowedClockSkewSeconds 用于设置能够容忍的最大的时钟误差 + jwtParser = Jwts.parserBuilder().requireIssuer(issuer) + .setSigningKey(key).setAllowedClockSkewSeconds(10) + .build(); + } + + /** + * 生成 Token + * @param username + * @return + */ + public String generateToken(String username) { + LocalDateTime now = LocalDateTime.now(); + // 设置 Token 失效时间 + LocalDateTime expireTime = now.plusMinutes(tokenExpireTime); + + return Jwts.builder().setSubject(username) + .setIssuer(issuer) + .setIssuedAt(Date.from(now.atZone(ZoneId.systemDefault()).toInstant())) + .setExpiration(Date.from(expireTime.atZone(ZoneId.systemDefault()).toInstant())) + .signWith(key) + .compact(); + } + + /** + * 解析 Token + * @param token + * @return + */ + public Jws parseToken(String token) { + try { + return jwtParser.parseClaimsJws(token); + } catch (SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) { + throw new BadCredentialsException("Token 不可用", e); + } catch (ExpiredJwtException e) { + throw new CredentialsExpiredException("Token 失效", e); + } + } + + /** + * 校验 Token 是否可用 + * @param token + * @return + */ + public void validateToken(String token) { + jwtParser.parseClaimsJws(token); + } + + /** + * 解析 Token 获取用户名 + * @param token + * @return + */ + public String getUsernameByToken(String token) { + try { + Claims claims = jwtParser.parseClaimsJws(token).getBody(); + String username = claims.getSubject(); + return username; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + /** + * 生成一个 Base64 的安全秘钥 + * @return + */ + private static String generateBase64Key() { + // 生成安全秘钥 + Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512); + + // 将密钥进行 Base64 编码 + String base64Key = Base64.getEncoder().encodeToString(secretKey.getEncoded()); + + return base64Key; + } + + public static void main(String[] args) { + String key = generateBase64Key(); + System.out.println("key: " + key); + } +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/utils/ResultUtil.java b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/utils/ResultUtil.java new file mode 100644 index 0000000..2102e41 --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/main/java/com/quanxiaoha/weblog/jwt/utils/ResultUtil.java @@ -0,0 +1,73 @@ +package com.quanxiaoha.weblog.jwt.utils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.quanxiaoha.weblog.common.utils.Response; +import org.springframework.http.HttpStatus; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-04-18 15:05 + * @description: 响参工具 + **/ +public class ResultUtil { + + /** + * 成功响参 + * @param response + * @param result + * @throws IOException + */ + public static void ok(HttpServletResponse response, Response result) throws IOException { + response.setCharacterEncoding("UTF-8"); + response.setStatus(HttpStatus.OK.value()); + response.setContentType("application/json"); + PrintWriter writer = response.getWriter(); + + ObjectMapper mapper = new ObjectMapper(); + writer.write(mapper.writeValueAsString(result)); + writer.flush(); + writer.close(); + } + + /** + * 失败响参 + * @param response + * @param result + * @throws IOException + */ + public static void fail(HttpServletResponse response, Response result) throws IOException { + response.setCharacterEncoding("UTF-8"); + response.setStatus(HttpStatus.OK.value()); + response.setContentType("application/json"); + PrintWriter writer = response.getWriter(); + + ObjectMapper mapper = new ObjectMapper(); + writer.write(mapper.writeValueAsString(result)); + writer.flush(); + writer.close(); + } + + /** + * 失败响参 + * @param response + * @param status 可指定响应码,如 401 等 + * @param result + * @throws IOException + */ + public static void fail(HttpServletResponse response, int status, Response result) throws IOException { + response.setCharacterEncoding("UTF-8"); + response.setStatus(status); + response.setContentType("application/json"); + PrintWriter writer = response.getWriter(); + + ObjectMapper mapper = new ObjectMapper(); + writer.write(mapper.writeValueAsString(result)); + writer.flush(); + writer.close(); + } +} diff --git a/weblog-springboot-015/weblog-module-jwt/src/test/java/com/quanxiaoha/weblog/jwt/WeblogModuleJwtApplicationTests.java b/weblog-springboot-015/weblog-module-jwt/src/test/java/com/quanxiaoha/weblog/jwt/WeblogModuleJwtApplicationTests.java new file mode 100644 index 0000000..177d76f --- /dev/null +++ b/weblog-springboot-015/weblog-module-jwt/src/test/java/com/quanxiaoha/weblog/jwt/WeblogModuleJwtApplicationTests.java @@ -0,0 +1,13 @@ +package com.quanxiaoha.weblog.jwt; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class WeblogModuleJwtApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/weblog-springboot-015/weblog-web/.gitignore b/weblog-springboot-015/weblog-web/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/weblog-springboot-015/weblog-web/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/weblog-springboot-015/weblog-web/pom.xml b/weblog-springboot-015/weblog-web/pom.xml new file mode 100644 index 0000000..4e07007 --- /dev/null +++ b/weblog-springboot-015/weblog-web/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + + com.quanxiaoha + weblog-springboot + ${revision} + + + com.quanxiaoha + weblog-web + weblog-web + weblog-web (入口项目,负责博客前台展示相关功能,打包也放在这个模块负责) + + + + com.quanxiaoha + weblog-module-common + + + + com.quanxiaoha + weblog-module-admin + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.github.xiaoymin + knife4j-openapi2-spring-boot-starter + + + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/WeblogWebApplication.java b/weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/WeblogWebApplication.java new file mode 100644 index 0000000..ff25423 --- /dev/null +++ b/weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/WeblogWebApplication.java @@ -0,0 +1,15 @@ +package com.quanxiaoha.weblog.web; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; + +@SpringBootApplication +@ComponentScan({"com.quanxiaoha.weblog.*"}) // 多模块项目中,必需手动指定扫描 com.quanxiaoha.weblog 包下面的所有类 +public class WeblogWebApplication { + + public static void main(String[] args) { + SpringApplication.run(WeblogWebApplication.class, args); + } + +} diff --git a/weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/config/Knife4jConfig.java b/weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/config/Knife4jConfig.java new file mode 100644 index 0000000..e0edb26 --- /dev/null +++ b/weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/config/Knife4jConfig.java @@ -0,0 +1,53 @@ +package com.quanxiaoha.weblog.web.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-16 7:53 + * @description: Knife4j 配置 + **/ +@Configuration +@EnableSwagger2WebMvc +@Profile("dev") // 只在 dev 环境中开启 +public class Knife4jConfig { + + @Bean("webApi") + public Docket createApiDoc() { + Docket docket = new Docket(DocumentationType.SWAGGER_2) + .apiInfo(buildApiInfo()) + // 分组名称 + .groupName("Web 前台接口") + .select() + // 这里指定 Controller 扫描包路径 + .apis(RequestHandlerSelectors.basePackage("com.quanxiaoha.weblog.web.controller")) + .paths(PathSelectors.any()) + .build(); + return docket; + } + + /** + * 构建 API 信息 + * @return + */ + private ApiInfo buildApiInfo() { + return new ApiInfoBuilder() + .title("Weblog 博客前台接口文档") // 标题 + .description("Weblog 是一款由 Spring Boot + Vue 3.2 + Vite 4.3 开发的前后端分离博客。") // 描述 + .termsOfServiceUrl("https://www.quanxiaoha.com/") // API 服务条款 + .contact(new Contact("犬小哈", "https://www.quanxiaoha.com", "871361652@qq.com")) // 联系人 + .version("1.0") // 版本号 + .build(); + } +} diff --git a/weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/controller/TestController.java b/weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/controller/TestController.java new file mode 100644 index 0000000..bfde7fa --- /dev/null +++ b/weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/controller/TestController.java @@ -0,0 +1,47 @@ +package com.quanxiaoha.weblog.web.controller; + +import com.quanxiaoha.weblog.common.utils.JsonUtil; +import com.quanxiaoha.weblog.common.utils.Response; +import com.quanxiaoha.weblog.web.model.User; +import com.quanxiaoha.weblog.common.aspect.ApiOperationLog; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Date; + + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-10 10:34 + * @description: TODO + **/ +@RestController +@Slf4j +@Api(tags = "首页模块") +public class TestController { + + @PostMapping("/admin/test") + @ApiOperationLog(description = "测试接口") + @ApiOperation(value = "测试接口") + public Response test(@RequestBody @Validated User user) { + // 打印入参 + log.info(JsonUtil.toJsonString(user)); + + // 设置三种日期字段值 + user.setCreateTime(LocalDateTime.now()); + user.setUpdateDate(LocalDate.now()); + user.setTime(LocalTime.now()); + + return Response.success(user); + } + +} diff --git a/weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/model/User.java b/weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/model/User.java new file mode 100644 index 0000000..07216dd --- /dev/null +++ b/weblog-springboot-015/weblog-web/src/main/java/com/quanxiaoha/weblog/web/model/User.java @@ -0,0 +1,50 @@ +package com.quanxiaoha.weblog.web.model; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Date; + +/** + * @author: 犬小哈 + * @url: www.quanxiaoha.com + * @date: 2023-08-10 10:35 + * @description: TODO + **/ +@Data +@ApiModel(value = "用户实体类") +public class User { + // 用户名 + @NotBlank(message = "用户名不能为空") // 注解确保用户名不为空 + @ApiModelProperty(value = "用户名") + private String username; + // 性别 + @NotNull(message = "性别不能为空") // 注解确保性别不为空 + @ApiModelProperty(value = "用户性别") + private Integer sex; + + // 年龄 + @NotNull(message = "年龄不能为空") + @Min(value = 18, message = "年龄必须大于或等于 18") // 注解确保年龄大于等于 18 + @Max(value = 100, message = "年龄必须小于或等于 100") // 注解确保年龄小于等于 100 + @ApiModelProperty(value = "年龄") + private Integer age; + + // 邮箱 + @NotBlank(message = "邮箱不能为空") + @Email(message = "邮箱格式不正确") // 注解确保邮箱格式正确 + @ApiModelProperty(value = "邮箱") + private String email; + + // 创建时间 + private LocalDateTime createTime; + // 更新日期 + private LocalDate updateDate; + // 时间 + private LocalTime time; +} \ No newline at end of file diff --git a/weblog-springboot-015/weblog-web/src/main/resources/application-dev.yml b/weblog-springboot-015/weblog-web/src/main/resources/application-dev.yml new file mode 100644 index 0000000..567e274 --- /dev/null +++ b/weblog-springboot-015/weblog-web/src/main/resources/application-dev.yml @@ -0,0 +1,21 @@ +spring: + datasource: + # 指定数据库驱动类 + driver-class-name: com.p6spy.engine.spy.P6SpyDriver + # 数据库连接信息 + url: jdbc:p6spy:mysql://127.0.0.1:3306/weblog?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull + username: root + password: 123456 + hikari: # 数据库连接池使用 Hikari + minimum-idle: 5 # 连接池中最小空闲连接数 + maximum-pool-size: 20 # 连接池中允许的最大连接数 + auto-commit: true # 是否自动提交事务 + idle-timeout: 30000 # 连接在连接池中闲置的最长时间,超过这个时间会被释放。 + pool-name: Weblog-HikariCP # 自定义连接池的名字 + max-lifetime: 1800000 # 连接在连接池中的最大存活时间,超过这个时间会被强制关闭。 + connection-timeout: 30000 # 连接的超时时间 + connection-test-query: SELECT 1 # 用于测试连接是否可用的SQL查询 + security: + user: + name: admin + password: 123456 diff --git a/weblog-springboot-015/weblog-web/src/main/resources/application-prod.yml b/weblog-springboot-015/weblog-web/src/main/resources/application-prod.yml new file mode 100644 index 0000000..e407795 --- /dev/null +++ b/weblog-springboot-015/weblog-web/src/main/resources/application-prod.yml @@ -0,0 +1,24 @@ +spring: + datasource: + # 指定数据库驱动类 + driver-class-name: com.mysql.cj.jdbc.Driver + # 数据库连接信息 + url: jdbc:mysql://127.0.0.1:3306/weblog?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&useSSL=false&zeroDateTimeBehavior=convertToNull + username: root + password: 123456 + hikari: # 数据库连接池使用 Hikari + minimum-idle: 5 # 连接池中最小空闲连接数 + maximum-pool-size: 20 # 连接池中允许的最大连接数 + auto-commit: true # 是否自动提交事务 + idle-timeout: 30000 # 连接在连接池中闲置的最长时间,超过这个时间会被释放。 + pool-name: Weblog-HikariCP # 自定义连接池的名字 + max-lifetime: 1800000 # 连接在连接池中的最大存活时间,超过这个时间会被强制关闭。 + connection-timeout: 30000 # 连接的超时时间 + connection-test-query: SELECT 1 # 用于测试连接是否可用的SQL查询 + + +#================================================================= +# log 日志 +#================================================================= +logging: + config: classpath:logback-weblog.xml diff --git a/weblog-springboot-015/weblog-web/src/main/resources/application.yml b/weblog-springboot-015/weblog-web/src/main/resources/application.yml new file mode 100644 index 0000000..dd9f9c9 --- /dev/null +++ b/weblog-springboot-015/weblog-web/src/main/resources/application.yml @@ -0,0 +1,18 @@ +spring: + profiles: + # 默认激活 dev 环境 + active: dev + +jwt: + # 签发人 + issuer: quanxiaoha + # 秘钥 + secret: jElxcSUj38+Bnh73T68lNs0DfBSit6U3whQlcGO2XwnI+Bo3g4xsiCIPg8PV/L0fQMis08iupNwhe2PzYLB9Xg== + # token 过期时间(单位:分钟) 24*60 + tokenExpireTime: 1440 + # token 请求头中的 key 值 + tokenHeaderKey: Authorization + # token 请求头中的 value 值前缀 + tokenPrefix: Bearer + + diff --git a/weblog-springboot-015/weblog-web/src/main/resources/logback-weblog.xml b/weblog-springboot-015/weblog-web/src/main/resources/logback-weblog.xml new file mode 100644 index 0000000..922dc0d --- /dev/null +++ b/weblog-springboot-015/weblog-web/src/main/resources/logback-weblog.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + ${LOG_FILE}-%i.log + + 30 + + + 10MB + + + + + ${FILE_LOG_PATTERN} + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/weblog-springboot-015/weblog-web/src/main/resources/spy.properties b/weblog-springboot-015/weblog-web/src/main/resources/spy.properties new file mode 100644 index 0000000..f035706 --- /dev/null +++ b/weblog-springboot-015/weblog-web/src/main/resources/spy.properties @@ -0,0 +1,24 @@ +#3.2.1???? +modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory +#3.2.1????????? +#modulelist=com.p6spy.engine.logging.P6LogFactory,com.p6spy.engine.outage.P6OutageFactory +# ??????? +logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger +#???????? +appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger +# ???????? sql +#appender=com.p6spy.engine.spy.appender.Slf4JLogger +# ?? p6spy driver ?? +deregisterdrivers=true +# ??JDBC URL?? +useprefix=true +# ???? Log ??,????????error,info,batch,debug,statement,commit,rollback,result,resultset. +excludecategories=info,debug,result,commit,resultset +# ???? +dateformat=yyyy-MM-dd HH:mm:ss +# ??????? +#driverlist=org.h2.Driver +# ?????SQL?? +outagedetection=true +# ?SQL???? 2 ? +outagedetectioninterval=2 \ No newline at end of file diff --git a/weblog-springboot-015/weblog-web/src/test/java/com/quanxiaoha/weblog/web/WeblogWebApplicationTests.java b/weblog-springboot-015/weblog-web/src/test/java/com/quanxiaoha/weblog/web/WeblogWebApplicationTests.java new file mode 100644 index 0000000..0442ce4 --- /dev/null +++ b/weblog-springboot-015/weblog-web/src/test/java/com/quanxiaoha/weblog/web/WeblogWebApplicationTests.java @@ -0,0 +1,49 @@ +package com.quanxiaoha.weblog.web; + +import com.quanxiaoha.weblog.common.domain.dos.UserDO; +import com.quanxiaoha.weblog.common.domain.mapper.UserMapper; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import java.time.LocalDateTime; +import java.util.Date; + +@SpringBootTest +@Slf4j +class WeblogWebApplicationTests { + + @Autowired + private UserMapper userMapper; + + @Test + void contextLoads() { + } + + @Test + void testLog() { + log.info("这是一行 Info 级别日志"); + log.warn("这是一行 Warn 级别日志"); + log.error("这是一行 Error 级别日志"); + + // 占位符 + String author = "犬小哈"; + log.info("这是一行带有占位符日志,作者:{}", author); + } + + @Test + void insertTest() { + // 构建数据库实体类 + UserDO userDO = UserDO.builder() + .username("犬小哈") + .password("123456") + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .isDeleted(false) + .build(); + + userMapper.insert(userDO); + } + +}