创建基础项目
This commit is contained in:
commit
f79445ad0b
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.idea
|
||||||
|
log
|
||||||
|
.DS_Store
|
33
itbaima-monitor-client/.gitignore
vendored
Normal file
33
itbaima-monitor-client/.gitignore
vendored
Normal file
@ -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/
|
54
itbaima-monitor-client/pom.xml
Normal file
54
itbaima-monitor-client/pom.xml
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
|
</parent>
|
||||||
|
<groupId>com.example</groupId>
|
||||||
|
<artifactId>itbaima-monitor-client</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>itbaima-monitor-client</name>
|
||||||
|
<description>itbaima-monitor-client</description>
|
||||||
|
<properties>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.example;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class MonitorClientApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(MonitorClientApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
|
8
itbaima-monitor-jwt.iml
Normal file
8
itbaima-monitor-jwt.iml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="GENERAL_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
33
itbaima-monitor-server/.gitignore
vendored
Normal file
33
itbaima-monitor-server/.gitignore
vendored
Normal file
@ -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/
|
159
itbaima-monitor-server/pom.xml
Normal file
159
itbaima-monitor-server/pom.xml
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.1.2</version>
|
||||||
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
|
</parent>
|
||||||
|
<groupId>com.example</groupId>
|
||||||
|
<artifactId>itbaima-monitor-server</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>itbaima-monitor-server</name>
|
||||||
|
<description>my-project-backend</description>
|
||||||
|
<properties>
|
||||||
|
<java.version>17</java.version>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<!-- 邮件发送模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-mail</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- 接口参数校验模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- 授权校验模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- 基础Web模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Redis交互模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Mybatis-Plus框架 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.baomidou</groupId>
|
||||||
|
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||||
|
<version>3.5.3.1</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- MySQL驱动 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- Lombok框架 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
<!-- 测试模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<!-- 消息队列模块 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-amqp</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- FastJSON2框架 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.fastjson2</groupId>
|
||||||
|
<artifactId>fastjson2</artifactId>
|
||||||
|
<version>2.0.25</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- Jwt令牌生成校验框架 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.auth0</groupId>
|
||||||
|
<artifactId>java-jwt</artifactId>
|
||||||
|
<version>4.3.0</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- Swagger文档生成框架 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springdoc</groupId>
|
||||||
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
|
<version>2.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<!-- 开发环境 -->
|
||||||
|
<profile>
|
||||||
|
<id>dev</id>
|
||||||
|
<activation>
|
||||||
|
<activeByDefault>true</activeByDefault>
|
||||||
|
</activation>
|
||||||
|
<properties>
|
||||||
|
<environment>dev</environment>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
<!-- 生产环境 -->
|
||||||
|
<profile>
|
||||||
|
<id>prod</id>
|
||||||
|
<activation>
|
||||||
|
<activeByDefault>false</activeByDefault>
|
||||||
|
</activation>
|
||||||
|
<properties>
|
||||||
|
<environment>prod</environment>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.graalvm.buildtools</groupId>
|
||||||
|
<artifactId>native-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
<excludes>
|
||||||
|
<exclude>application*.yml</exclude>
|
||||||
|
</excludes>
|
||||||
|
</resource>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
<filtering>true</filtering>
|
||||||
|
<includes>
|
||||||
|
<include>application.yml</include>
|
||||||
|
<include>application-${environment}.yml</include>
|
||||||
|
</includes>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.example;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class MonitorServerApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(MonitorServerApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.example.config;
|
||||||
|
|
||||||
|
import org.springframework.amqp.core.Queue;
|
||||||
|
import org.springframework.amqp.core.QueueBuilder;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RabbitMQ消息队列配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RabbitConfiguration {
|
||||||
|
@Bean("mailQueue")
|
||||||
|
public Queue queue(){
|
||||||
|
return QueueBuilder
|
||||||
|
.durable("mail")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
package com.example.config;
|
||||||
|
|
||||||
|
import com.example.entity.RestBean;
|
||||||
|
import com.example.entity.dto.Account;
|
||||||
|
import com.example.entity.vo.response.AuthorizeVO;
|
||||||
|
import com.example.filter.JwtAuthenticationFilter;
|
||||||
|
import com.example.filter.RequestLogFilter;
|
||||||
|
import com.example.service.AccountService;
|
||||||
|
import com.example.utils.Const;
|
||||||
|
import com.example.utils.JwtUtils;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SpringSecurity相关配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
RequestLogFilter requestLogFilter;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
JwtUtils utils;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
AccountService service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 针对于 SpringSecurity 6 的新版配置方法
|
||||||
|
* @param http 配置器
|
||||||
|
* @return 自动构建的内置过滤器链
|
||||||
|
* @throws Exception 可能的异常
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
return http
|
||||||
|
.authorizeHttpRequests(conf -> conf
|
||||||
|
.requestMatchers("/api/auth/**", "/error").permitAll()
|
||||||
|
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||||
|
.anyRequest().hasAnyRole(Const.ROLE_DEFAULT)
|
||||||
|
)
|
||||||
|
.formLogin(conf -> conf
|
||||||
|
.loginProcessingUrl("/api/auth/login")
|
||||||
|
.failureHandler(this::handleProcess)
|
||||||
|
.successHandler(this::handleProcess)
|
||||||
|
.permitAll()
|
||||||
|
)
|
||||||
|
.logout(conf -> conf
|
||||||
|
.logoutUrl("/api/auth/logout")
|
||||||
|
.logoutSuccessHandler(this::onLogoutSuccess)
|
||||||
|
)
|
||||||
|
.exceptionHandling(conf -> conf
|
||||||
|
.accessDeniedHandler(this::handleProcess)
|
||||||
|
.authenticationEntryPoint(this::handleProcess)
|
||||||
|
)
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
.sessionManagement(conf -> conf
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
|
.addFilterBefore(requestLogFilter, UsernamePasswordAuthenticationFilter.class)
|
||||||
|
.addFilterBefore(jwtAuthenticationFilter, RequestLogFilter.class)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将多种类型的Handler整合到同一个方法中,包含:
|
||||||
|
* - 登录成功
|
||||||
|
* - 登录失败
|
||||||
|
* - 未登录拦截/无权限拦截
|
||||||
|
* @param request 请求
|
||||||
|
* @param response 响应
|
||||||
|
* @param exceptionOrAuthentication 异常或是验证实体
|
||||||
|
* @throws IOException 可能的异常
|
||||||
|
*/
|
||||||
|
private void handleProcess(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
Object exceptionOrAuthentication) throws IOException {
|
||||||
|
response.setContentType("application/json;charset=utf-8");
|
||||||
|
PrintWriter writer = response.getWriter();
|
||||||
|
if(exceptionOrAuthentication instanceof AccessDeniedException exception) {
|
||||||
|
writer.write(RestBean
|
||||||
|
.forbidden(exception.getMessage()).asJsonString());
|
||||||
|
} else if(exceptionOrAuthentication instanceof Exception exception) {
|
||||||
|
writer.write(RestBean
|
||||||
|
.unauthorized(exception.getMessage()).asJsonString());
|
||||||
|
} else if(exceptionOrAuthentication instanceof Authentication authentication){
|
||||||
|
User user = (User) authentication.getPrincipal();
|
||||||
|
Account account = service.findAccountByNameOrEmail(user.getUsername());
|
||||||
|
String jwt = utils.createJwt(user, account.getUsername(), account.getId());
|
||||||
|
if(jwt == null) {
|
||||||
|
writer.write(RestBean.forbidden("登录验证频繁,请稍后再试").asJsonString());
|
||||||
|
} else {
|
||||||
|
AuthorizeVO vo = account.asViewObject(AuthorizeVO.class, o -> o.setToken(jwt));
|
||||||
|
vo.setExpire(utils.expireTime());
|
||||||
|
writer.write(RestBean.success(vo).asJsonString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录处理,将对应的Jwt令牌列入黑名单不再使用
|
||||||
|
* @param request 请求
|
||||||
|
* @param response 响应
|
||||||
|
* @param authentication 验证实体
|
||||||
|
* @throws IOException 可能的异常
|
||||||
|
*/
|
||||||
|
private void onLogoutSuccess(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
Authentication authentication) throws IOException {
|
||||||
|
response.setContentType("application/json;charset=utf-8");
|
||||||
|
PrintWriter writer = response.getWriter();
|
||||||
|
String authorization = request.getHeader("Authorization");
|
||||||
|
if(utils.invalidateJwt(authorization)) {
|
||||||
|
writer.write(RestBean.success("退出登录成功").asJsonString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writer.write(RestBean.failure(400, "退出登录失败").asJsonString());
|
||||||
|
}
|
||||||
|
}
|
@ -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<String, PathItem> authorizePathItems(){
|
||||||
|
Map<String, PathItem> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.example.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一般Web服务相关配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class WebConfiguration implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder(){
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
package com.example.controller;
|
||||||
|
|
||||||
|
import com.example.entity.RestBean;
|
||||||
|
import com.example.entity.vo.request.ConfirmResetVO;
|
||||||
|
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 jakarta.validation.constraints.Pattern;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于验证相关Controller包含用户的注册、重置密码等操作
|
||||||
|
*/
|
||||||
|
@Validated
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/auth")
|
||||||
|
@Tag(name = "登录校验相关", description = "包括用户登录、注册、验证码请求等操作。")
|
||||||
|
public class AuthorizeController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
AccountService accountService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求邮件验证码
|
||||||
|
* @param email 请求邮件
|
||||||
|
* @param type 类型
|
||||||
|
* @param request 请求
|
||||||
|
* @return 是否请求成功
|
||||||
|
*/
|
||||||
|
@GetMapping("/ask-code")
|
||||||
|
@Operation(summary = "请求邮件验证码")
|
||||||
|
public RestBean<Void> askVerifyCode(@RequestParam @Email String email,
|
||||||
|
@RequestParam @Pattern(regexp = "(register|reset)") String type,
|
||||||
|
HttpServletRequest request){
|
||||||
|
return this.messageHandle(() ->
|
||||||
|
accountService.registerEmailVerifyCode(type, String.valueOf(email), request.getRemoteAddr()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进行用户注册操作,需要先请求邮件验证码
|
||||||
|
* @param vo 注册信息
|
||||||
|
* @return 是否注册成功
|
||||||
|
*/
|
||||||
|
@PostMapping("/register")
|
||||||
|
@Operation(summary = "用户注册操作")
|
||||||
|
public RestBean<Void> register(@RequestBody @Valid EmailRegisterVO vo){
|
||||||
|
return this.messageHandle(() ->
|
||||||
|
accountService.registerEmailAccount(vo));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行密码重置确认,检查验证码是否正确
|
||||||
|
* @param vo 密码重置信息
|
||||||
|
* @return 是否操作成功
|
||||||
|
*/
|
||||||
|
@PostMapping("/reset-confirm")
|
||||||
|
@Operation(summary = "密码重置确认")
|
||||||
|
public RestBean<Void> resetConfirm(@RequestBody @Valid ConfirmResetVO vo){
|
||||||
|
return this.messageHandle(() -> accountService.resetConfirm(vo));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行密码重置操作
|
||||||
|
* @param vo 密码重置信息
|
||||||
|
* @return 是否操作成功
|
||||||
|
*/
|
||||||
|
@PostMapping("/reset-password")
|
||||||
|
@Operation(summary = "密码重置操作")
|
||||||
|
public RestBean<Void> resetPassword(@RequestBody @Valid EmailResetVO vo){
|
||||||
|
return this.messageHandle(() ->
|
||||||
|
accountService.resetEmailAccountPassword(vo));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 针对于返回值为String作为错误信息的方法进行统一处理
|
||||||
|
* @param action 具体操作
|
||||||
|
* @return 响应结果
|
||||||
|
* @param <T> 响应结果类型
|
||||||
|
*/
|
||||||
|
private <T> RestBean<T> messageHandle(Supplier<String> action){
|
||||||
|
String message = action.get();
|
||||||
|
if(message == null)
|
||||||
|
return RestBean.success();
|
||||||
|
else
|
||||||
|
return RestBean.failure(400, message);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package com.example.controller.exception;
|
||||||
|
|
||||||
|
import com.example.entity.RestBean;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
|
||||||
|
import org.springframework.boot.web.error.ErrorAttributeOptions;
|
||||||
|
import org.springframework.boot.web.servlet.error.ErrorAttributes;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专用用于处理错误页面的Controller
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping({"${server.error.path:${error.path:/error}}"})
|
||||||
|
public class ErrorPageController extends AbstractErrorController {
|
||||||
|
|
||||||
|
public ErrorPageController(ErrorAttributes errorAttributes) {
|
||||||
|
super(errorAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有错误在这里统一处理,自动解析状态码和原因
|
||||||
|
* @param request 请求
|
||||||
|
* @return 失败响应
|
||||||
|
*/
|
||||||
|
@RequestMapping
|
||||||
|
public RestBean<Void> error(HttpServletRequest request) {
|
||||||
|
HttpStatus status = this.getStatus(request);
|
||||||
|
Map<String, Object> errorAttributes = this.getErrorAttributes(request, this.getAttributeOptions());
|
||||||
|
String message = this.convertErrorMessage(status)
|
||||||
|
.orElse(errorAttributes.get("message").toString());
|
||||||
|
return RestBean.failure(status.value(), message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对于一些特殊的状态码,错误信息转换
|
||||||
|
* @param status 状态码
|
||||||
|
* @return 错误信息
|
||||||
|
*/
|
||||||
|
private Optional<String> convertErrorMessage(HttpStatus status){
|
||||||
|
String value = switch (status.value()) {
|
||||||
|
case 400 -> "请求参数有误";
|
||||||
|
case 404 -> "请求的接口不存在";
|
||||||
|
case 405 -> "请求方法错误";
|
||||||
|
case 500 -> "内部错误,请联系管理员";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
return Optional.ofNullable(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误属性获取选项,这里额外添加了错误消息和异常类型
|
||||||
|
* @return 选项
|
||||||
|
*/
|
||||||
|
private ErrorAttributeOptions getAttributeOptions(){
|
||||||
|
return ErrorAttributeOptions
|
||||||
|
.defaults()
|
||||||
|
.including(ErrorAttributeOptions.Include.MESSAGE,
|
||||||
|
ErrorAttributeOptions.Include.EXCEPTION);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.example.controller.exception;
|
||||||
|
|
||||||
|
import com.example.entity.RestBean;
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于接口参数校验处理的控制器
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class ValidationController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与SpringBoot保持一致,校验不通过打印警告信息,而不是直接抛出异常
|
||||||
|
* @param exception 验证异常
|
||||||
|
* @return 校验结果
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(ValidationException.class)
|
||||||
|
public RestBean<Void> validateError(ValidationException exception) {
|
||||||
|
log.warn("Resolved [{}: {}]", exception.getClass().getName(), exception.getMessage());
|
||||||
|
return RestBean.failure(400, "请求参数有误");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package com.example.entity;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于DTO快速转换VO实现,只需将DTO类继承此类即可使用
|
||||||
|
*/
|
||||||
|
public interface BaseData {
|
||||||
|
/**
|
||||||
|
* 创建指定的VO类并将当前DTO对象中的所有成员变量值直接复制到VO对象中
|
||||||
|
* @param clazz 指定VO类型
|
||||||
|
* @param consumer 返回VO对象之前可以使用Lambda进行额外处理
|
||||||
|
* @return 指定VO对象
|
||||||
|
* @param <V> 指定VO类型
|
||||||
|
*/
|
||||||
|
default <V> V asViewObject(Class<V> clazz, Consumer<V> consumer) {
|
||||||
|
V v = this.asViewObject(clazz);
|
||||||
|
consumer.accept(v);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建指定的VO类并将当前DTO对象中的所有成员变量值直接复制到VO对象中
|
||||||
|
* @param clazz 指定VO类型
|
||||||
|
* @return 指定VO对象
|
||||||
|
* @param <V> 指定VO类型
|
||||||
|
*/
|
||||||
|
default <V> V asViewObject(Class<V> clazz) {
|
||||||
|
try {
|
||||||
|
Field[] fields = clazz.getDeclaredFields();
|
||||||
|
Constructor<V> constructor = clazz.getConstructor();
|
||||||
|
V v = constructor.newInstance();
|
||||||
|
Arrays.asList(fields).forEach(field -> convert(field, v));
|
||||||
|
return v;
|
||||||
|
} catch (ReflectiveOperationException exception) {
|
||||||
|
Logger logger = LoggerFactory.getLogger(BaseData.class);
|
||||||
|
logger.error("在VO与DTO转换时出现了一些错误", exception);
|
||||||
|
throw new RuntimeException(exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部使用,快速将当前类中目标对象字段同名字段的值复制到目标对象字段上
|
||||||
|
* @param field 目标对象字段
|
||||||
|
* @param target 目标对象
|
||||||
|
*/
|
||||||
|
private void convert(Field field, Object target){
|
||||||
|
try {
|
||||||
|
Field source = this.getClass().getDeclaredField(field.getName());
|
||||||
|
field.setAccessible(true);
|
||||||
|
source.setAccessible(true);
|
||||||
|
field.set(target, source.get(this));
|
||||||
|
} catch (IllegalAccessException | NoSuchFieldException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package com.example.entity;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.alibaba.fastjson2.JSONWriter;
|
||||||
|
import org.slf4j.MDC;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应实体类封装,Rest风格
|
||||||
|
* @param code 状态码
|
||||||
|
* @param data 响应数据
|
||||||
|
* @param message 其他消息
|
||||||
|
* @param <T> 响应数据类型
|
||||||
|
*/
|
||||||
|
public record RestBean<T> (long id, int code, T data, String message) {
|
||||||
|
public static <T> RestBean<T> success(T data){
|
||||||
|
return new RestBean<>(requestId(), 200, data, "请求成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> RestBean<T> success(){
|
||||||
|
return success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> RestBean<T> forbidden(String message){
|
||||||
|
return failure(403, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> RestBean<T> unauthorized(String message){
|
||||||
|
return failure(401, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> RestBean<T> failure(int code, String message){
|
||||||
|
return new RestBean<>(requestId(), code, null, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速将当前实体转换为JSON字符串格式
|
||||||
|
* @return JSON字符串
|
||||||
|
*/
|
||||||
|
public String asJsonString() {
|
||||||
|
return JSONObject.toJSONString(this, JSONWriter.Feature.WriteNulls);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前请求ID方便快速定位错误
|
||||||
|
* @return ID
|
||||||
|
*/
|
||||||
|
private static long requestId(){
|
||||||
|
String requestId = Optional.ofNullable(MDC.get("reqId")).orElse("0");
|
||||||
|
return Long.parseLong(requestId);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.example.entity.dto;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.example.entity.BaseData;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库中的用户信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("db_account")
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Account implements BaseData {
|
||||||
|
@TableId(type = IdType.AUTO)
|
||||||
|
Integer id;
|
||||||
|
String username;
|
||||||
|
String password;
|
||||||
|
String email;
|
||||||
|
String role;
|
||||||
|
Date registerTime;
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package com.example.entity.vo.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.hibernate.validator.constraints.Length;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class ConfirmResetVO {
|
||||||
|
@Email
|
||||||
|
String email;
|
||||||
|
@Length(max = 6, min = 6)
|
||||||
|
String code;
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package com.example.entity.vo.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.hibernate.validator.constraints.Length;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户注册表单信息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class EmailRegisterVO {
|
||||||
|
@Email
|
||||||
|
String email;
|
||||||
|
@Length(max = 6, min = 6)
|
||||||
|
String code;
|
||||||
|
@Pattern(regexp = "^[a-zA-Z0-9\\u4e00-\\u9fa5]+$")
|
||||||
|
@Length(min = 1, max = 10)
|
||||||
|
String username;
|
||||||
|
@Length(min = 6, max = 20)
|
||||||
|
String password;
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package com.example.entity.vo.request;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.hibernate.validator.constraints.Length;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码重置表单实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class EmailResetVO {
|
||||||
|
@Email
|
||||||
|
String email;
|
||||||
|
@Length(max = 6, min = 6)
|
||||||
|
String code;
|
||||||
|
@Length(min = 6, max = 20)
|
||||||
|
String password;
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.example.entity.vo.response;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录验证成功的用户信息响应
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AuthorizeVO {
|
||||||
|
String username;
|
||||||
|
String role;
|
||||||
|
String token;
|
||||||
|
Date expire;
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
package com.example.filter;
|
||||||
|
|
||||||
|
import com.example.utils.Const;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpFilter;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨域配置过滤器,仅处理跨域,添加跨域响应头
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Order(Const.ORDER_CORS)
|
||||||
|
public class CorsFilter extends HttpFilter {
|
||||||
|
|
||||||
|
@Value("${spring.web.cors.origin}")
|
||||||
|
String origin;
|
||||||
|
|
||||||
|
@Value("${spring.web.cors.credentials}")
|
||||||
|
boolean credentials;
|
||||||
|
|
||||||
|
@Value("${spring.web.cors.methods}")
|
||||||
|
String methods;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||||
|
this.addCorsHeader(request, response);
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加所有跨域相关响应头
|
||||||
|
* @param request 请求
|
||||||
|
* @param response 响应
|
||||||
|
*/
|
||||||
|
private void addCorsHeader(HttpServletRequest request, HttpServletResponse response) {
|
||||||
|
response.addHeader("Access-Control-Allow-Origin", this.resolveOrigin(request));
|
||||||
|
response.addHeader("Access-Control-Allow-Methods", this.resolveMethod());
|
||||||
|
response.addHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
|
||||||
|
if(credentials) {
|
||||||
|
response.addHeader("Access-Control-Allow-Credentials", "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析配置文件中的请求方法
|
||||||
|
* @return 解析得到的请求头值
|
||||||
|
*/
|
||||||
|
private String resolveMethod(){
|
||||||
|
return methods.equals("*") ? "GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE, PATCH" : methods;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析配置文件中的请求原始站点
|
||||||
|
* @param request 请求
|
||||||
|
* @return 解析得到的请求头值
|
||||||
|
*/
|
||||||
|
private String resolveOrigin(HttpServletRequest request){
|
||||||
|
return origin.equals("*") ? request.getHeader("Origin") : origin;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
package com.example.filter;
|
||||||
|
|
||||||
|
import com.example.entity.RestBean;
|
||||||
|
import com.example.utils.Const;
|
||||||
|
import com.example.utils.FlowUtils;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpFilter;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流控制过滤器
|
||||||
|
* 防止用户高频请求接口,借助Redis进行限流
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@Order(Const.ORDER_FLOW_LIMIT)
|
||||||
|
public class FlowLimitingFilter extends HttpFilter {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
StringRedisTemplate template;
|
||||||
|
//指定时间内最大请求次数限制
|
||||||
|
@Value("${spring.web.flow.limit}")
|
||||||
|
int limit;
|
||||||
|
//计数时间周期
|
||||||
|
@Value("${spring.web.flow.period}")
|
||||||
|
int period;
|
||||||
|
//超出请求限制封禁时间
|
||||||
|
@Value("${spring.web.flow.block}")
|
||||||
|
int block;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
FlowUtils utils;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||||
|
String address = request.getRemoteAddr();
|
||||||
|
if (!tryCount(address))
|
||||||
|
this.writeBlockMessage(response);
|
||||||
|
else
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试对指定IP地址请求计数,如果被限制则无法继续访问
|
||||||
|
* @param address 请求IP地址
|
||||||
|
* @return 是否操作成功
|
||||||
|
*/
|
||||||
|
private boolean tryCount(String address) {
|
||||||
|
synchronized (address.intern()) {
|
||||||
|
if(Boolean.TRUE.equals(template.hasKey(Const.FLOW_LIMIT_BLOCK + address)))
|
||||||
|
return false;
|
||||||
|
String counterKey = Const.FLOW_LIMIT_COUNTER + address;
|
||||||
|
String blockKey = Const.FLOW_LIMIT_BLOCK + address;
|
||||||
|
return utils.limitPeriodCheck(counterKey, blockKey, block, limit, period);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为响应编写拦截内容,提示用户操作频繁
|
||||||
|
* @param response 响应
|
||||||
|
* @throws IOException 可能的异常
|
||||||
|
*/
|
||||||
|
private void writeBlockMessage(HttpServletResponse response) throws IOException {
|
||||||
|
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||||
|
response.setContentType("application/json;charset=utf-8");
|
||||||
|
PrintWriter writer = response.getWriter();
|
||||||
|
writer.write(RestBean.forbidden("操作频繁,请稍后再试").asJsonString());
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
package com.example.filter;
|
||||||
|
|
||||||
|
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||||
|
import com.example.utils.Const;
|
||||||
|
import com.example.utils.JwtUtils;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于对请求头中Jwt令牌进行校验的工具,为当前请求添加用户验证信息
|
||||||
|
* 并将用户的ID存放在请求对象属性中,方便后续使用
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
JwtUtils utils;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request,
|
||||||
|
HttpServletResponse response,
|
||||||
|
FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
String authorization = request.getHeader("Authorization");
|
||||||
|
DecodedJWT jwt = utils.resolveJwt(authorization);
|
||||||
|
if(jwt != null) {
|
||||||
|
UserDetails user = utils.toUser(jwt);
|
||||||
|
UsernamePasswordAuthenticationToken authentication =
|
||||||
|
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
|
||||||
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
request.setAttribute(Const.ATTR_USER_ID, utils.toId(jwt));
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,93 @@
|
|||||||
|
package com.example.filter;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.example.utils.Const;
|
||||||
|
import com.example.utils.SnowflakeIdGenerator;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.slf4j.MDC;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求日志过滤器,用于记录所有用户请求信息
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class RequestLogFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
SnowflakeIdGenerator generator;
|
||||||
|
|
||||||
|
private final Set<String> ignores = Set.of("/swagger-ui", "/v3/api-docs");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求结束时的日志打印,包含处理耗时以及响应结果
|
||||||
|
* @param wrapper 用于读取响应结果的包装类
|
||||||
|
* @param startTime 起始时间
|
||||||
|
*/
|
||||||
|
public void logRequestEnd(ContentCachingResponseWrapper wrapper, long startTime){
|
||||||
|
long time = System.currentTimeMillis() - startTime;
|
||||||
|
int status = wrapper.getStatus();
|
||||||
|
String content = status != 200 ?
|
||||||
|
status + " 错误" : new String(wrapper.getContentAsByteArray());
|
||||||
|
log.info("请求处理耗时: {}ms | 响应结果: {}", time, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求开始时的日志打印,包含请求全部信息,以及对应用户角色
|
||||||
|
* @param request 请求
|
||||||
|
*/
|
||||||
|
public void logRequestStart(HttpServletRequest request){
|
||||||
|
long reqId = generator.nextId();
|
||||||
|
MDC.put("reqId", String.valueOf(reqId));
|
||||||
|
JSONObject object = new JSONObject();
|
||||||
|
request.getParameterMap().forEach((k, v) -> object.put(k, v.length > 0 ? v[0] : null));
|
||||||
|
Object id = request.getAttribute(Const.ATTR_USER_ID);
|
||||||
|
if(id != null) {
|
||||||
|
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||||
|
log.info("请求URL: \"{}\" ({}) | 远程IP地址: {} │ 身份: {} (UID: {}) | 角色: {} | 请求参数列表: {}",
|
||||||
|
request.getServletPath(), request.getMethod(), request.getRemoteAddr(),
|
||||||
|
user.getUsername(), id, user.getAuthorities(), object);
|
||||||
|
} else {
|
||||||
|
log.info("请求URL: \"{}\" ({}) | 远程IP地址: {} │ 身份: 未验证 | 请求参数列表: {}",
|
||||||
|
request.getServletPath(), request.getMethod(), request.getRemoteAddr(), object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,64 @@
|
|||||||
|
package com.example.listener;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
|
||||||
|
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.mail.SimpleMailMessage;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于处理邮件发送的消息队列监听器
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@RabbitListener(queues = "mail")
|
||||||
|
public class MailQueueListener {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
JavaMailSender sender;
|
||||||
|
|
||||||
|
@Value("${spring.mail.username}")
|
||||||
|
String username;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理邮件发送
|
||||||
|
* @param data 邮件信息
|
||||||
|
*/
|
||||||
|
@RabbitHandler
|
||||||
|
public void sendMailMessage(Map<String, Object> data) {
|
||||||
|
String email = data.get("email").toString();
|
||||||
|
Integer code = (Integer) data.get("code");
|
||||||
|
SimpleMailMessage message = switch (data.get("type").toString()) {
|
||||||
|
case "register" ->
|
||||||
|
createMessage("欢迎注册我们的网站",
|
||||||
|
"您的邮件注册验证码为: "+code+",有效时间3分钟,为了保障您的账户安全,请勿向他人泄露验证码信息。",
|
||||||
|
email);
|
||||||
|
case "reset" ->
|
||||||
|
createMessage("您的密码重置邮件",
|
||||||
|
"你好,您正在执行重置密码操作,验证码: "+code+",有效时间3分钟,如非本人操作,请无视。",
|
||||||
|
email);
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
if(message == null) return;
|
||||||
|
sender.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速封装简单邮件消息实体
|
||||||
|
* @param title 标题
|
||||||
|
* @param content 内容
|
||||||
|
* @param email 收件人
|
||||||
|
* @return 邮件实体
|
||||||
|
*/
|
||||||
|
private SimpleMailMessage createMessage(String title, String content, String email){
|
||||||
|
SimpleMailMessage message = new SimpleMailMessage();
|
||||||
|
message.setSubject(title);
|
||||||
|
message.setText(content);
|
||||||
|
message.setTo(email);
|
||||||
|
message.setFrom(username);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
package com.example.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.example.entity.dto.Account;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface AccountMapper extends BaseMapper<Account> {
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
package com.example.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
import com.example.entity.dto.Account;
|
||||||
|
import com.example.entity.vo.request.ConfirmResetVO;
|
||||||
|
import com.example.entity.vo.request.EmailRegisterVO;
|
||||||
|
import com.example.entity.vo.request.EmailResetVO;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
|
||||||
|
public interface AccountService extends IService<Account>, UserDetailsService {
|
||||||
|
Account findAccountByNameOrEmail(String text);
|
||||||
|
String registerEmailVerifyCode(String type, String email, String address);
|
||||||
|
String registerEmailAccount(EmailRegisterVO info);
|
||||||
|
String resetEmailAccountPassword(EmailResetVO info);
|
||||||
|
String resetConfirm(ConfirmResetVO info);
|
||||||
|
}
|
@ -0,0 +1,204 @@
|
|||||||
|
package com.example.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.example.entity.dto.Account;
|
||||||
|
import com.example.entity.vo.request.ConfirmResetVO;
|
||||||
|
import com.example.entity.vo.request.EmailRegisterVO;
|
||||||
|
import com.example.entity.vo.request.EmailResetVO;
|
||||||
|
import com.example.mapper.AccountMapper;
|
||||||
|
import com.example.service.AccountService;
|
||||||
|
import com.example.utils.Const;
|
||||||
|
import com.example.utils.FlowUtils;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.amqp.core.AmqpTemplate;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 账户信息处理相关服务
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {
|
||||||
|
|
||||||
|
//验证邮件发送冷却时间限制,秒为单位
|
||||||
|
@Value("${spring.web.verify.mail-limit}")
|
||||||
|
int verifyLimit;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
AmqpTemplate rabbitTemplate;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
FlowUtils flow;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从数据库中通过用户名或邮箱查找用户详细信息
|
||||||
|
* @param username 用户名
|
||||||
|
* @return 用户详细信息
|
||||||
|
* @throws UsernameNotFoundException 如果用户未找到则抛出此异常
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
|
Account account = this.findAccountByNameOrEmail(username);
|
||||||
|
if(account == null)
|
||||||
|
throw new UsernameNotFoundException("用户名或密码错误");
|
||||||
|
return User
|
||||||
|
.withUsername(username)
|
||||||
|
.password(account.getPassword())
|
||||||
|
.roles(account.getRole())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成注册验证码存入Redis中,并将邮件发送请求提交到消息队列等待发送
|
||||||
|
* @param type 类型
|
||||||
|
* @param email 邮件地址
|
||||||
|
* @param address 请求IP地址
|
||||||
|
* @return 操作结果,null表示正常,否则为错误原因
|
||||||
|
*/
|
||||||
|
public String registerEmailVerifyCode(String type, String email, String address){
|
||||||
|
synchronized (address.intern()) {
|
||||||
|
if(!this.verifyLimit(address))
|
||||||
|
return "请求频繁,请稍后再试";
|
||||||
|
Random random = new Random();
|
||||||
|
int code = random.nextInt(899999) + 100000;
|
||||||
|
Map<String, Object> data = Map.of("type",type,"email", email, "code", code);
|
||||||
|
rabbitTemplate.convertAndSend(Const.MQ_MAIL, data);
|
||||||
|
stringRedisTemplate.opsForValue()
|
||||||
|
.set(Const.VERIFY_EMAIL_DATA + email, String.valueOf(code), 3, TimeUnit.MINUTES);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件验证码注册账号操作,需要检查验证码是否正确以及邮箱、用户名是否存在重名
|
||||||
|
* @param info 注册基本信息
|
||||||
|
* @return 操作结果,null表示正常,否则为错误原因
|
||||||
|
*/
|
||||||
|
public String registerEmailAccount(EmailRegisterVO info){
|
||||||
|
String email = info.getEmail();
|
||||||
|
String code = this.getEmailVerifyCode(email);
|
||||||
|
if(code == null) return "请先获取验证码";
|
||||||
|
if(!code.equals(info.getCode())) return "验证码错误,请重新输入";
|
||||||
|
if(this.existsAccountByEmail(email)) return "该邮件地址已被注册";
|
||||||
|
String username = info.getUsername();
|
||||||
|
if(this.existsAccountByUsername(username)) return "该用户名已被他人使用,请重新更换";
|
||||||
|
String password = passwordEncoder.encode(info.getPassword());
|
||||||
|
Account account = new Account(null, info.getUsername(),
|
||||||
|
password, email, Const.ROLE_DEFAULT, new Date());
|
||||||
|
if(!this.save(account)) {
|
||||||
|
return "内部错误,注册失败";
|
||||||
|
} else {
|
||||||
|
this.deleteEmailVerifyCode(email);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件验证码重置密码操作,需要检查验证码是否正确
|
||||||
|
* @param info 重置基本信息
|
||||||
|
* @return 操作结果,null表示正常,否则为错误原因
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String resetEmailAccountPassword(EmailResetVO info) {
|
||||||
|
String verify = resetConfirm(new ConfirmResetVO(info.getEmail(), info.getCode()));
|
||||||
|
if(verify != null) return verify;
|
||||||
|
String email = info.getEmail();
|
||||||
|
String password = passwordEncoder.encode(info.getPassword());
|
||||||
|
boolean update = this.update().eq("email", email).set("password", password).update();
|
||||||
|
if(update) {
|
||||||
|
this.deleteEmailVerifyCode(email);
|
||||||
|
}
|
||||||
|
return update ? null : "更新失败,请联系管理员";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置密码确认操作,验证验证码是否正确
|
||||||
|
* @param info 验证基本信息
|
||||||
|
* @return 操作结果,null表示正常,否则为错误原因
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String resetConfirm(ConfirmResetVO info) {
|
||||||
|
String email = info.getEmail();
|
||||||
|
String code = this.getEmailVerifyCode(email);
|
||||||
|
if(code == null) return "请先获取验证码";
|
||||||
|
if(!code.equals(info.getCode())) return "验证码错误,请重新输入";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除Redis中存储的邮件验证码
|
||||||
|
* @param email 电邮
|
||||||
|
*/
|
||||||
|
private void deleteEmailVerifyCode(String email){
|
||||||
|
String key = Const.VERIFY_EMAIL_DATA + email;
|
||||||
|
stringRedisTemplate.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Redis中存储的邮件验证码
|
||||||
|
* @param email 电邮
|
||||||
|
* @return 验证码
|
||||||
|
*/
|
||||||
|
private String getEmailVerifyCode(String email){
|
||||||
|
String key = Const.VERIFY_EMAIL_DATA + email;
|
||||||
|
return stringRedisTemplate.opsForValue().get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 针对IP地址进行邮件验证码获取限流
|
||||||
|
* @param address 地址
|
||||||
|
* @return 是否通过验证
|
||||||
|
*/
|
||||||
|
private boolean verifyLimit(String address) {
|
||||||
|
String key = Const.VERIFY_EMAIL_LIMIT + address;
|
||||||
|
return flow.limitOnceCheck(key, verifyLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过用户名或邮件地址查找用户
|
||||||
|
* @param text 用户名或邮件
|
||||||
|
* @return 账户实体
|
||||||
|
*/
|
||||||
|
public Account findAccountByNameOrEmail(String text){
|
||||||
|
return this.query()
|
||||||
|
.eq("username", text).or()
|
||||||
|
.eq("email", text)
|
||||||
|
.one();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定邮箱的用户是否已经存在
|
||||||
|
* @param email 邮箱
|
||||||
|
* @return 是否存在
|
||||||
|
*/
|
||||||
|
private boolean existsAccountByEmail(String email){
|
||||||
|
return this.baseMapper.exists(Wrappers.<Account>query().eq("email", email));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定用户名的用户是否已经存在
|
||||||
|
* @param username 用户名
|
||||||
|
* @return 是否存在
|
||||||
|
*/
|
||||||
|
private boolean existsAccountByUsername(String username){
|
||||||
|
return this.baseMapper.exists(Wrappers.<Account>query().eq("username", username));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.example.utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 一些常量字符串整合
|
||||||
|
*/
|
||||||
|
public final class Const {
|
||||||
|
//JWT令牌
|
||||||
|
public final static String JWT_BLACK_LIST = "jwt:blacklist:";
|
||||||
|
public final static String JWT_FREQUENCY = "jwt:frequency:";
|
||||||
|
//请求频率限制
|
||||||
|
public final static String FLOW_LIMIT_COUNTER = "flow:counter:";
|
||||||
|
public final static String FLOW_LIMIT_BLOCK = "flow:block:";
|
||||||
|
//邮件验证码
|
||||||
|
public final static String VERIFY_EMAIL_LIMIT = "verify:email:limit:";
|
||||||
|
public final static String VERIFY_EMAIL_DATA = "verify:email:data:";
|
||||||
|
//过滤器优先级
|
||||||
|
public final static int ORDER_FLOW_LIMIT = -101;
|
||||||
|
public final static int ORDER_CORS = -102;
|
||||||
|
//请求自定义属性
|
||||||
|
public final static String ATTR_USER_ID = "userId";
|
||||||
|
//消息队列
|
||||||
|
public final static String MQ_MAIL = "mail";
|
||||||
|
//用户角色
|
||||||
|
public final static String ROLE_DEFAULT = "user";
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package com.example.utils;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 限流通用工具
|
||||||
|
* 针对于不同的情况进行限流操作,支持限流升级
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class FlowUtils {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
StringRedisTemplate template;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 针对于单次频率限制,请求成功后,在冷却时间内不得再次进行请求,如3秒内不能再次发起请求
|
||||||
|
* @param key 键
|
||||||
|
* @param blockTime 限制时间
|
||||||
|
* @return 是否通过限流检查
|
||||||
|
*/
|
||||||
|
public boolean limitOnceCheck(String key, int blockTime){
|
||||||
|
return this.internalCheck(key, 1, blockTime, (overclock) -> false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 针对于单次频率限制,请求成功后,在冷却时间内不得再次进行请求
|
||||||
|
* 如3秒内不能再次发起请求,如果不听劝阻继续发起请求,将限制更长时间
|
||||||
|
* @param key 键
|
||||||
|
* @param frequency 请求频率
|
||||||
|
* @param baseTime 基础限制时间
|
||||||
|
* @param upgradeTime 升级限制时间
|
||||||
|
* @return 是否通过限流检查
|
||||||
|
*/
|
||||||
|
public boolean limitOnceUpgradeCheck(String key, int frequency, int baseTime, int upgradeTime){
|
||||||
|
return this.internalCheck(key, frequency, baseTime, (overclock) -> {
|
||||||
|
if (overclock)
|
||||||
|
template.opsForValue().set(key, "1", upgradeTime, TimeUnit.SECONDS);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 针对于在时间段内多次请求限制,如3秒内限制请求20次,超出频率则封禁一段时间
|
||||||
|
* @param counterKey 计数键
|
||||||
|
* @param blockKey 封禁键
|
||||||
|
* @param blockTime 封禁时间
|
||||||
|
* @param frequency 请求频率
|
||||||
|
* @param period 计数周期
|
||||||
|
* @return 是否通过限流检查
|
||||||
|
*/
|
||||||
|
public boolean limitPeriodCheck(String counterKey, String blockKey, int blockTime, int frequency, int period){
|
||||||
|
return this.internalCheck(counterKey, frequency, period, (overclock) -> {
|
||||||
|
if (overclock)
|
||||||
|
template.opsForValue().set(blockKey, "", blockTime, TimeUnit.SECONDS);
|
||||||
|
return !overclock;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部使用请求限制主要逻辑
|
||||||
|
* @param key 计数键
|
||||||
|
* @param frequency 请求频率
|
||||||
|
* @param period 计数周期
|
||||||
|
* @param action 限制行为与策略
|
||||||
|
* @return 是否通过限流检查
|
||||||
|
*/
|
||||||
|
private boolean internalCheck(String key, int frequency, int period, LimitAction action){
|
||||||
|
if (Boolean.TRUE.equals(template.hasKey(key))) {
|
||||||
|
Long value = Optional.ofNullable(template.opsForValue().increment(key)).orElse(0L);
|
||||||
|
return action.run(value > frequency);
|
||||||
|
} else {
|
||||||
|
template.opsForValue().set(key, "1", period, TimeUnit.SECONDS);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部使用,限制行为与策略
|
||||||
|
*/
|
||||||
|
private interface LimitAction {
|
||||||
|
boolean run(boolean overclock);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
package com.example.utils;
|
||||||
|
|
||||||
|
import com.auth0.jwt.JWT;
|
||||||
|
import com.auth0.jwt.JWTVerifier;
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm;
|
||||||
|
import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||||
|
import com.auth0.jwt.interfaces.Claim;
|
||||||
|
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于处理Jwt令牌的工具类
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class JwtUtils {
|
||||||
|
|
||||||
|
//用于给Jwt令牌签名校验的秘钥
|
||||||
|
@Value("${spring.security.jwt.key}")
|
||||||
|
private String key;
|
||||||
|
//令牌的过期时间,以小时为单位
|
||||||
|
@Value("${spring.security.jwt.expire}")
|
||||||
|
private int expire;
|
||||||
|
//为用户生成Jwt令牌的冷却时间,防止刷接口频繁登录生成令牌,以秒为单位
|
||||||
|
@Value("${spring.security.jwt.limit.base}")
|
||||||
|
private int limit_base;
|
||||||
|
//用户如果继续恶意刷令牌,更严厉的封禁时间
|
||||||
|
@Value("${spring.security.jwt.limit.upgrade}")
|
||||||
|
private int limit_upgrade;
|
||||||
|
//判定用户在冷却时间内,继续恶意刷令牌的次数
|
||||||
|
@Value("${spring.security.jwt.limit.frequency}")
|
||||||
|
private int limit_frequency;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
StringRedisTemplate template;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
FlowUtils utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 让指定Jwt令牌失效
|
||||||
|
* @param headerToken 请求头中携带的令牌
|
||||||
|
* @return 是否操作成功
|
||||||
|
*/
|
||||||
|
public boolean invalidateJwt(String headerToken){
|
||||||
|
String token = this.convertToken(headerToken);
|
||||||
|
Algorithm algorithm = Algorithm.HMAC256(key);
|
||||||
|
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
|
||||||
|
try {
|
||||||
|
DecodedJWT verify = jwtVerifier.verify(token);
|
||||||
|
return deleteToken(verify.getId(), verify.getExpiresAt());
|
||||||
|
} catch (JWTVerificationException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据配置快速计算过期时间
|
||||||
|
* @return 过期时间
|
||||||
|
*/
|
||||||
|
public Date expireTime() {
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
calendar.add(Calendar.HOUR, expire);
|
||||||
|
return calendar.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据UserDetails生成对应的Jwt令牌
|
||||||
|
* @param user 用户信息
|
||||||
|
* @return 令牌
|
||||||
|
*/
|
||||||
|
public String createJwt(UserDetails user, String username, int userId) {
|
||||||
|
if(this.frequencyCheck(userId)) {
|
||||||
|
Algorithm algorithm = Algorithm.HMAC256(key);
|
||||||
|
Date expire = this.expireTime();
|
||||||
|
return JWT.create()
|
||||||
|
.withJWTId(UUID.randomUUID().toString())
|
||||||
|
.withClaim("id", userId)
|
||||||
|
.withClaim("name", username)
|
||||||
|
.withClaim("authorities", user.getAuthorities()
|
||||||
|
.stream()
|
||||||
|
.map(GrantedAuthority::getAuthority).toList())
|
||||||
|
.withExpiresAt(expire)
|
||||||
|
.withIssuedAt(new Date())
|
||||||
|
.sign(algorithm);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析Jwt令牌
|
||||||
|
* @param headerToken 请求头中携带的令牌
|
||||||
|
* @return DecodedJWT
|
||||||
|
*/
|
||||||
|
public DecodedJWT resolveJwt(String headerToken){
|
||||||
|
String token = this.convertToken(headerToken);
|
||||||
|
if(token == null) return null;
|
||||||
|
Algorithm algorithm = Algorithm.HMAC256(key);
|
||||||
|
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
|
||||||
|
try {
|
||||||
|
DecodedJWT verify = jwtVerifier.verify(token);
|
||||||
|
if(this.isInvalidToken(verify.getId())) return null;
|
||||||
|
Map<String, Claim> claims = verify.getClaims();
|
||||||
|
return new Date().after(claims.get("exp").asDate()) ? null : verify;
|
||||||
|
} catch (JWTVerificationException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将jwt对象中的内容封装为UserDetails
|
||||||
|
* @param jwt 已解析的Jwt对象
|
||||||
|
* @return UserDetails
|
||||||
|
*/
|
||||||
|
public UserDetails toUser(DecodedJWT jwt) {
|
||||||
|
Map<String, Claim> claims = jwt.getClaims();
|
||||||
|
return User
|
||||||
|
.withUsername(claims.get("name").asString())
|
||||||
|
.password("******")
|
||||||
|
.authorities(claims.get("authorities").asArray(String.class))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将jwt对象中的用户ID提取出来
|
||||||
|
* @param jwt 已解析的Jwt对象
|
||||||
|
* @return 用户ID
|
||||||
|
*/
|
||||||
|
public Integer toId(DecodedJWT jwt) {
|
||||||
|
Map<String, Claim> claims = jwt.getClaims();
|
||||||
|
return claims.get("id").asInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 频率检测,防止用户高频申请Jwt令牌,并且采用阶段封禁机制
|
||||||
|
* 如果已经提示无法登录的情况下用户还在刷,那么就封禁更长时间
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @return 是否通过频率检测
|
||||||
|
*/
|
||||||
|
private boolean frequencyCheck(int userId){
|
||||||
|
String key = Const.JWT_FREQUENCY + userId;
|
||||||
|
return utils.limitOnceUpgradeCheck(key, limit_frequency, limit_base, limit_upgrade);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验并转换请求头中的Token令牌
|
||||||
|
* @param headerToken 请求头中的Token
|
||||||
|
* @return 转换后的令牌
|
||||||
|
*/
|
||||||
|
private String convertToken(String headerToken){
|
||||||
|
if(headerToken == null || !headerToken.startsWith("Bearer "))
|
||||||
|
return null;
|
||||||
|
return headerToken.substring(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将Token列入Redis黑名单中
|
||||||
|
* @param uuid 令牌ID
|
||||||
|
* @param time 过期时间
|
||||||
|
* @return 是否操作成功
|
||||||
|
*/
|
||||||
|
private boolean deleteToken(String uuid, Date time){
|
||||||
|
if(this.isInvalidToken(uuid))
|
||||||
|
return false;
|
||||||
|
Date now = new Date();
|
||||||
|
long expire = Math.max(time.getTime() - now.getTime(), 0);
|
||||||
|
template.opsForValue().set(Const.JWT_BLACK_LIST + uuid, "", expire, TimeUnit.MILLISECONDS);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证Token是否被列入Redis黑名单
|
||||||
|
* @param uuid 令牌ID
|
||||||
|
* @return 是否操作成功
|
||||||
|
*/
|
||||||
|
private boolean isInvalidToken(String uuid){
|
||||||
|
return Boolean.TRUE.equals(template.hasKey(Const.JWT_BLACK_LIST + uuid));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
package com.example.utils;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 雪花算法ID生成器
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class SnowflakeIdGenerator {
|
||||||
|
private static final long START_TIMESTAMP = 1691087910202L;
|
||||||
|
|
||||||
|
private static final long DATA_CENTER_ID_BITS = 5L;
|
||||||
|
private static final long WORKER_ID_BITS = 5L;
|
||||||
|
private static final long SEQUENCE_BITS = 12L;
|
||||||
|
|
||||||
|
private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
|
||||||
|
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
|
||||||
|
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
|
||||||
|
|
||||||
|
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
|
||||||
|
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
|
||||||
|
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
|
||||||
|
|
||||||
|
private final long dataCenterId;
|
||||||
|
private final long workerId;
|
||||||
|
private long lastTimestamp = -1L;
|
||||||
|
private long sequence = 0L;
|
||||||
|
|
||||||
|
public SnowflakeIdGenerator(){
|
||||||
|
this(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SnowflakeIdGenerator(long dataCenterId, long workerId) {
|
||||||
|
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
|
||||||
|
throw new IllegalArgumentException("Data center ID can't be greater than " + MAX_DATA_CENTER_ID + " or less than 0");
|
||||||
|
}
|
||||||
|
if (workerId > MAX_WORKER_ID || workerId < 0) {
|
||||||
|
throw new IllegalArgumentException("Worker ID can't be greater than " + MAX_WORKER_ID + " or less than 0");
|
||||||
|
}
|
||||||
|
this.dataCenterId = dataCenterId;
|
||||||
|
this.workerId = workerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成一个新的雪花算法ID加锁
|
||||||
|
* @return 雪花ID
|
||||||
|
*/
|
||||||
|
public synchronized long nextId() {
|
||||||
|
long timestamp = getCurrentTimestamp();
|
||||||
|
if (timestamp < lastTimestamp) {
|
||||||
|
throw new IllegalStateException("Clock moved backwards. Refusing to generate ID.");
|
||||||
|
}
|
||||||
|
if (timestamp == lastTimestamp) {
|
||||||
|
sequence = (sequence + 1) & MAX_SEQUENCE;
|
||||||
|
if (sequence == 0) {
|
||||||
|
timestamp = getNextTimestamp(lastTimestamp);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sequence = 0L;
|
||||||
|
}
|
||||||
|
lastTimestamp = timestamp;
|
||||||
|
return ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT) |
|
||||||
|
(dataCenterId << DATA_CENTER_ID_SHIFT) |
|
||||||
|
(workerId << WORKER_ID_SHIFT) |
|
||||||
|
sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getCurrentTimestamp() {
|
||||||
|
return System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getNextTimestamp(long lastTimestamp) {
|
||||||
|
long timestamp = getCurrentTimestamp();
|
||||||
|
while (timestamp <= lastTimestamp) {
|
||||||
|
timestamp = getCurrentTimestamp();
|
||||||
|
}
|
||||||
|
return timestamp;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
# 开发环境配置
|
||||||
|
springdoc:
|
||||||
|
paths-to-match: /api/**
|
||||||
|
swagger-ui:
|
||||||
|
operations-sorter: alpha
|
||||||
|
spring:
|
||||||
|
mail:
|
||||||
|
host: smtp.163.com
|
||||||
|
username: javastudy111@163.com
|
||||||
|
password: VKQFYZMUSUZGSGEG
|
||||||
|
rabbitmq:
|
||||||
|
addresses: localhost
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
virtual-host: /
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://localhost:3306/test
|
||||||
|
username: root
|
||||||
|
password: 123456
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
security:
|
||||||
|
jwt:
|
||||||
|
key: 'abcdefghijklmn'
|
||||||
|
expire: 72
|
||||||
|
limit:
|
||||||
|
base: 10
|
||||||
|
upgrade: 300
|
||||||
|
frequency: 30
|
||||||
|
filter:
|
||||||
|
order: -100
|
||||||
|
web:
|
||||||
|
verify:
|
||||||
|
mail-limit: 60
|
||||||
|
flow:
|
||||||
|
period: 3
|
||||||
|
limit: 50
|
||||||
|
block: 30
|
||||||
|
cors:
|
||||||
|
origin: '*'
|
||||||
|
credentials: false
|
||||||
|
methods: '*'
|
@ -0,0 +1,45 @@
|
|||||||
|
#生产环境配置
|
||||||
|
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
|
||||||
|
username: javastudy111@163.com
|
||||||
|
password: VKQFYZMUSUZGSGEG
|
||||||
|
rabbitmq:
|
||||||
|
addresses: localhost
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
virtual-host: /
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://localhost:3306/test
|
||||||
|
username: root
|
||||||
|
password: 123456
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
security:
|
||||||
|
jwt:
|
||||||
|
key: 'abcdefghijklmn'
|
||||||
|
expire: 72
|
||||||
|
limit:
|
||||||
|
base: 10
|
||||||
|
upgrade: 300
|
||||||
|
frequency: 30
|
||||||
|
filter:
|
||||||
|
order: -100
|
||||||
|
web:
|
||||||
|
verify:
|
||||||
|
mail-limit: 60
|
||||||
|
flow:
|
||||||
|
period: 3
|
||||||
|
limit: 10
|
||||||
|
block: 30
|
||||||
|
cors:
|
||||||
|
origin: '*'
|
||||||
|
credentials: false
|
||||||
|
methods: '*'
|
@ -0,0 +1,3 @@
|
|||||||
|
spring:
|
||||||
|
profiles:
|
||||||
|
active: '@environment@'
|
34
itbaima-monitor-server/src/main/resources/logback-spring.xml
Normal file
34
itbaima-monitor-server/src/main/resources/logback-spring.xml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
|
||||||
|
|
||||||
|
<property name="CONSOLE_LOG_PATTERN"
|
||||||
|
value="%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr([%15.15t]){faint} %clr(\(%X{reqId:-system}\)){faint} %clr(--){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
|
||||||
|
<property name="FILE_LOG_PATTERN"
|
||||||
|
value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] \(%X{reqId:-system}\) %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
|
||||||
|
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||||
|
<charset>${CONSOLE_LOG_CHARSET}</charset>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${FILE_LOG_PATTERN}</pattern>
|
||||||
|
<charset>${FILE_LOG_CHARSET}</charset>
|
||||||
|
</encoder>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<FileNamePattern>log/%d{yyyy-MM-dd}-spring-%i.log</FileNamePattern>
|
||||||
|
<cleanHistoryOnStart>true</cleanHistoryOnStart>
|
||||||
|
<maxHistory>7</maxHistory>
|
||||||
|
<maxFileSize>10MB</maxFileSize>
|
||||||
|
</rollingPolicy>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
<appender-ref ref="FILE"/>
|
||||||
|
</root>
|
||||||
|
</configuration>
|
Loading…
x
Reference in New Issue
Block a user