Compare commits
4
Commits
5d45ed8aa3
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc9220c345 | ||
|
|
a3eb2510ae | ||
|
|
f897270145 | ||
|
|
dfb943919d |
+83
@@ -0,0 +1,83 @@
|
||||
# ==================== Maven ====================
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
# ==================== IntelliJ IDEA ====================
|
||||
.idea/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
# ==================== Eclipse ====================
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
# ==================== NetBeans ====================
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
# ==================== VS Code ====================
|
||||
.vscode/
|
||||
|
||||
# ==================== Kotlin ====================
|
||||
.kotlin/
|
||||
|
||||
# ==================== 日志 ====================
|
||||
*.log
|
||||
logs/
|
||||
log/
|
||||
|
||||
# ==================== 编译产物 / 临时文件 ====================
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
*.tmp
|
||||
*.bak
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# ==================== 数据库相关 ====================
|
||||
*.db
|
||||
*.sqlite
|
||||
|
||||
# ==================== 系统文件 ====================
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# ==================== 敏感信息 ====================
|
||||
# 如果使用本地配置或密钥文件,务必忽略,避免密码泄露
|
||||
application-local.yml
|
||||
application-local.properties
|
||||
application-dev.yml
|
||||
application-prod.yml
|
||||
*.pem
|
||||
*.key
|
||||
*.p12
|
||||
*.jks
|
||||
.env
|
||||
|
||||
# ==================== 上传目录 ====================
|
||||
# 如果有本地上传文件夹,根据实际情况调整
|
||||
/upload/
|
||||
/uploads/
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
Generated
+30
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="true" />
|
||||
<profile name="Maven default annotation processors profile" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
</profile>
|
||||
<profile name="Annotation profile for metaloop" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<processorPath useClasspath="false">
|
||||
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct-processor/1.6.3/mapstruct-processor-1.6.3.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.42/lombok-1.18.42.jar" />
|
||||
<entry name="$MAVEN_REPOSITORY$/org/mapstruct/mapstruct-processor/1.6.3/mapstruct-processor-1.6.3.jar" />
|
||||
</processorPath>
|
||||
<module name="metaloop" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||
<module name="metaloop" options="-parameters" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+25
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Central Repository" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Central Repository" />
|
||||
<option name="url" value="https://maven.aliyun.com/repository/public" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+15
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="KubernetesApiProvider"><![CDATA[{}]]></component>
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="ms-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/metaloop.iml" filepath="$PROJECT_DIR$/metaloop.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="AdditionalModuleElements">
|
||||
<content url="file://$MODULE_DIR$" dumb="true">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
</content>
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,158 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.metalloop</groupId>
|
||||
<artifactId>metaloop</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.13</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<sa-token.version>1.46.0</sa-token.version>
|
||||
<mybatis-plus.version>3.5.9</mybatis-plus.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<redisson.version>3.40.0</redisson.version>
|
||||
<hutool.version>5.8.31</hutool.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- ==================== Spring Boot Web ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</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-aop</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== Redis ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== 数据库 ==================== -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<!-- MyBatis-Plus 3.5.9+ 需要额外引入 jsqlparser 模块 -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== API 文档 ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.8.13</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== Sa-Token ==================== -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot3-starter</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-redis-jackson</artifactId>
|
||||
<version>${sa-token.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== Redisson(分布式锁等) ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.redisson</groupId>
|
||||
<artifactId>redisson-spring-boot-starter</artifactId>
|
||||
<version>${redisson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== 工具库 ==================== -->
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-json</artifactId>
|
||||
<version>${hutool.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</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>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,15 @@
|
||||
//TIP 要<b>运行</b>代码,请按 <shortcut actionId="Run"/> 或
|
||||
// 点击装订区域中的 <icon src="AllIcons.Actions.Execute"/> 图标。
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
//TIP 当文本光标位于高亮显示的文本处时按 <shortcut actionId="ShowIntentionActions"/>
|
||||
// 查看 IntelliJ IDEA 建议如何修正。
|
||||
System.out.printf("Hello and welcome!");
|
||||
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
//TIP 按 <shortcut actionId="Debug"/> 开始调试代码。我们已经设置了一个 <icon src="AllIcons.Debugger.Db_set_breakpoint"/> 断点
|
||||
// 但您始终可以通过按 <shortcut actionId="ToggleLineBreakpoint"/> 添加更多断点。
|
||||
System.out.println("i = " + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.metalloop;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
@SpringBootApplication
|
||||
|
||||
@EnableTransactionManagement
|
||||
@EnableScheduling
|
||||
public class Application {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.metalloop.common.auth;
|
||||
|
||||
|
||||
import com.metalloop.common.auth.permission.Permission;
|
||||
import com.metalloop.common.auth.userdetails.UserDetails;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 认证后的用户信息
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-21
|
||||
*/
|
||||
public class Authentication {
|
||||
|
||||
/**
|
||||
* 权限集合
|
||||
*/
|
||||
Collection<? extends Permission> permissions;
|
||||
|
||||
/**
|
||||
* 用户Id
|
||||
*/
|
||||
String userId;
|
||||
|
||||
/**
|
||||
* 用户登录名
|
||||
*/
|
||||
String username;
|
||||
|
||||
/**
|
||||
* 访问令牌
|
||||
*/
|
||||
String accessToken;
|
||||
|
||||
|
||||
/**
|
||||
* 返回授予用户的权限。无法返回null 。
|
||||
*
|
||||
* @return 按自然键排序的权限集合。
|
||||
*/
|
||||
public Collection<? extends Permission> getPermissions() {
|
||||
return this.permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回用于对用户进行身份验证的用户Id。无法返回null 。
|
||||
*
|
||||
* @return 用户Id
|
||||
*/
|
||||
public String getUserId() {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回用于对用户进行身份验证的用户名。无法返回null 。
|
||||
*
|
||||
* @return 用户名
|
||||
*/
|
||||
public String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取访问令牌
|
||||
*
|
||||
* @return 访问令牌
|
||||
*/
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public void setPermissions(Collection<? extends Permission> permissions) {
|
||||
this.permissions = permissions;
|
||||
}
|
||||
|
||||
public void setUserId(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public void setAccessToken(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从UserDetails中构建认证通过的用户
|
||||
*
|
||||
* @param userDetails UserDetails
|
||||
* @return 认证通过的用户
|
||||
*/
|
||||
public static Builder withUserDetails(UserDetails userDetails) {
|
||||
return new Builder(userDetails);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final Collection<? extends Permission> permissions;
|
||||
private final String userId;
|
||||
private final String username;
|
||||
private String accessToken;
|
||||
|
||||
public Builder(UserDetails userDetails) {
|
||||
this.permissions = userDetails.getPermissions();
|
||||
this.userId = userDetails.getUserId();
|
||||
this.username = userDetails.getUsername();
|
||||
}
|
||||
|
||||
public Builder accessToken(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Authentication build() {
|
||||
Authentication authentication = new Authentication();
|
||||
authentication.setPermissions(this.permissions);
|
||||
authentication.setUserId(this.userId);
|
||||
authentication.setUsername(this.username);
|
||||
authentication.setAccessToken(this.accessToken);
|
||||
return authentication;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.metalloop.common.auth.config;
|
||||
|
||||
import com.metalloop.common.enums.LoginType;
|
||||
import com.metalloop.common.auth.password.PasswordDecryptor;
|
||||
import com.metalloop.common.auth.password.RsaPasswordDecryptor;
|
||||
import com.metalloop.modules.auth.controller.AuthController;
|
||||
import com.metalloop.common.auth.encrypt.KeyProperties;
|
||||
import com.metalloop.modules.auth.controller.RsaPublicKeyController;
|
||||
import com.metalloop.modules.auth.model.req.AuthenticationRequest;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
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.util.Assert;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 认证服务器配置类
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
|
||||
@SuppressWarnings({"SpringJavaAutowiredFieldsWarningInspection", "SpringJavaInjectionPointsAutowiringInspection"})
|
||||
@Configuration
|
||||
@EnableConfigurationProperties({KeyProperties.class})
|
||||
public class AuthServerConfiguration {
|
||||
|
||||
@Autowired
|
||||
private List<AuthServerConfigurer> configurers = Collections.emptyList();
|
||||
|
||||
@Autowired
|
||||
private RequestMappingHandlerMapping handlerMapping;
|
||||
|
||||
@Autowired
|
||||
private KeyProperties keyProperties;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 检查是否有相关的配置
|
||||
Assert.notEmpty(configurers, "找不到认证服务器的相关配置");
|
||||
// 遍历每个配置器
|
||||
for (AuthServerConfigurer configurer : configurers) {
|
||||
AuthServerEndpointsConfigurer endpointsConfigurer = new AuthServerEndpointsConfigurer();
|
||||
try {
|
||||
configurer.configure(endpointsConfigurer);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("无法配置认证端点", e);
|
||||
}
|
||||
// 为每个服务创建并注册一个 AuthController
|
||||
LoginType loginType = endpointsConfigurer.getLoginType();
|
||||
AuthController authController = new AuthController(loginType, endpointsConfigurer.getUserDetailsService(),
|
||||
endpointsConfigurer.getLoginSuccessHandler(), endpointsConfigurer.getLogoutSuccessHandler(),
|
||||
passwordDecryptor(keyProperties), passwordEncoder(), endpointsConfigurer.getVerifyCodeHandler(), endpointsConfigurer.getPasswordCheckFailureHandler());
|
||||
// 注册登录和登出端点
|
||||
if ("wechat".equals(loginType.getType())) {
|
||||
// registerEndpoint(endpointsConfigurer.getLoginProcessingUrl(), "miniAppLogin", authController, RequestMethod.POST, AuthenticationMiniAppRequest.class, HttpServletRequest.class);
|
||||
// registerEndpoint(endpointsConfigurer.getLogoutProcessingUrl(), "miniAppLogout", authController, RequestMethod.POST);
|
||||
} else {
|
||||
registerEndpoint(endpointsConfigurer.getLoginProcessingUrl(), "login", authController, RequestMethod.POST, AuthenticationRequest.class, HttpServletRequest.class);
|
||||
registerEndpoint(endpointsConfigurer.getLogoutProcessingUrl(), "logout", authController, RequestMethod.POST);
|
||||
}
|
||||
// 为每个服务创建并注册一个 RsaPublicKeyController
|
||||
RsaPublicKeyController rsaPublicKeyController = new RsaPublicKeyController(keyProperties);
|
||||
// 注册公钥获取端点
|
||||
registerEndpoint(endpointsConfigurer.getPublicKeyFetchUrl(), "getPublicKey", rsaPublicKeyController, RequestMethod.GET);
|
||||
// 如果开启了单点登陆
|
||||
if (Boolean.TRUE.equals(endpointsConfigurer.getEnableSso())) {
|
||||
authController.initSso(endpointsConfigurer.getSsoCodeService(), endpointsConfigurer.getSsoCodeTimeOut());
|
||||
// 注册单点登陆相关端点
|
||||
registerEndpoint(endpointsConfigurer.getSsoCodeFetchUrl(), "getSsoCode", authController, RequestMethod.GET);
|
||||
registerEndpoint(endpointsConfigurer.getSsoUserinfoFetchUrl(), "getSsoUserInfo", authController, RequestMethod.GET, String.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册端点的辅助方法
|
||||
*/
|
||||
private void registerEndpoint(String url, String methodName, Object controller, RequestMethod requestMethod, Class<?>... parameterTypes) {
|
||||
RequestMappingInfo mappingInfo = RequestMappingInfo.paths(url).methods(requestMethod).build();
|
||||
try {
|
||||
handlerMapping.registerMapping(mappingInfo, controller, controller.getClass().getMethod(methodName, parameterTypes));
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IllegalStateException("注册端点出错", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PasswordDecryptor.class)
|
||||
public PasswordDecryptor passwordDecryptor(KeyProperties keyProperties) {
|
||||
return new RsaPasswordDecryptor(keyProperties);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.metalloop.common.auth.config;
|
||||
|
||||
/**
|
||||
* 认证服务器配置器
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public interface AuthServerConfigurer {
|
||||
|
||||
/**
|
||||
* 配置操作
|
||||
*
|
||||
* @param configurer 配置器
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
void configure(AuthServerEndpointsConfigurer configurer) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.metalloop.common.auth.config;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* 认证服务器配置器适配器
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public class AuthServerConfigurerAdapter implements AuthServerConfigurer {
|
||||
@Override
|
||||
public void configure(AuthServerEndpointsConfigurer configurer) throws Exception {
|
||||
Assert.notNull(configurer, "认证服务器端点配置器不能为空");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
package com.metalloop.common.auth.config;
|
||||
|
||||
|
||||
import com.metalloop.common.auth.handler.*;
|
||||
import com.metalloop.common.auth.sso.SsoCodeService;
|
||||
import com.metalloop.common.auth.userdetails.UserDetailsService;
|
||||
import com.metalloop.common.enums.LoginType;
|
||||
|
||||
/**
|
||||
* 认证服务器端点配置器
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public class AuthServerEndpointsConfigurer {
|
||||
|
||||
/**
|
||||
* 账号类型
|
||||
*/
|
||||
private LoginType loginType = LoginType.DEFAULT;
|
||||
|
||||
/**
|
||||
* 用户详细信息服务
|
||||
*/
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
/**
|
||||
* 登录处理Url
|
||||
*/
|
||||
private String loginProcessingUrl = "/login";
|
||||
|
||||
/**
|
||||
* 登录处理Url
|
||||
*/
|
||||
private String verifyCodeProcessingUrl = "/getVerifyCode";
|
||||
|
||||
/**
|
||||
* 登出处理Url
|
||||
*/
|
||||
private String logoutProcessingUrl = "/logout";
|
||||
|
||||
/**
|
||||
* 公钥提取Url
|
||||
*/
|
||||
private String publicKeyFetchUrl = "/rsa/public-key";
|
||||
|
||||
/**
|
||||
* 登录成功处理器
|
||||
*/
|
||||
private LoginSuccessHandler loginSuccessHandler = new DefaultLoginSuccessHandler();
|
||||
|
||||
/**
|
||||
* 验证码生成处理器
|
||||
*/
|
||||
private VerifyCodeHandler verifyCodeHandler = new DefaultVerifyCodeHandler();
|
||||
|
||||
/**
|
||||
* 密码校验失败处理器
|
||||
*/
|
||||
private PasswordCheckFailureHandler passwordCheckFailureHandler = new DefaultPasswordCheckFailureHandler();
|
||||
|
||||
/**
|
||||
* 登出成功处理器
|
||||
*/
|
||||
private LogoutSuccessHandler logoutSuccessHandler = new DefaultLogoutSuccessHandler();
|
||||
|
||||
/**
|
||||
* 是否启用单点登录
|
||||
*/
|
||||
private Boolean enableSso = false;
|
||||
|
||||
/**
|
||||
* 单点登录授权码服务
|
||||
*/
|
||||
private SsoCodeService ssoCodeService;
|
||||
|
||||
/**
|
||||
* 单点登录授权码过期时间
|
||||
*/
|
||||
private Integer ssoCodeTimeOut = 300;
|
||||
|
||||
/**
|
||||
* 单点登录授权码获取Url
|
||||
*/
|
||||
private String ssoCodeFetchUrl = "/sso/code";
|
||||
|
||||
/**
|
||||
* 单点登录授权码用户信息获取Url
|
||||
*/
|
||||
private String ssoUserinfoFetchUrl = "/sso/userinfo";
|
||||
|
||||
|
||||
public AuthServerEndpointsConfigurer loginType(LoginType loginType) {
|
||||
this.loginType = loginType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer userDetailsService(UserDetailsService userDetailsService) {
|
||||
this.userDetailsService = userDetailsService;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer loginProcessingUrl(String loginProcessingUrl) {
|
||||
this.loginProcessingUrl = loginProcessingUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer verifyCodeProcessingUrl(String verifyCodeProcessingUrl) {
|
||||
this.verifyCodeProcessingUrl = verifyCodeProcessingUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer logoutProcessingUrl(String logoutProcessingUrl) {
|
||||
this.logoutProcessingUrl = logoutProcessingUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer publicKeyFetchUrl(String publicKeyFetchUrl) {
|
||||
this.publicKeyFetchUrl = publicKeyFetchUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer loginSuccessHandler(LoginSuccessHandler loginSuccessHandler) {
|
||||
this.loginSuccessHandler = loginSuccessHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer verifyCodeHandler(VerifyCodeHandler verifyCodeHandler) {
|
||||
this.verifyCodeHandler = verifyCodeHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer passwordCheckFailureHandler(PasswordCheckFailureHandler passwordCheckFailureHandler) {
|
||||
this.passwordCheckFailureHandler = passwordCheckFailureHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer logoutSuccessHandler(LogoutSuccessHandler logoutSuccessHandler) {
|
||||
this.logoutSuccessHandler = logoutSuccessHandler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer enableSso(boolean enableSso) {
|
||||
this.enableSso = enableSso;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer ssoCodeService(SsoCodeService ssoCodeService) {
|
||||
this.ssoCodeService = ssoCodeService;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer ssoCodeTimeOut(int ssoCodeTimeOut) {
|
||||
this.ssoCodeTimeOut = ssoCodeTimeOut;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer ssoCodeFetchUrl(String ssoCodeFetchUrl) {
|
||||
this.ssoCodeFetchUrl = ssoCodeFetchUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthServerEndpointsConfigurer ssoUserinfoFetchUrl(String ssoUserinfoFetchUrl) {
|
||||
this.ssoUserinfoFetchUrl = ssoUserinfoFetchUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoginType getLoginType() {
|
||||
return loginType;
|
||||
}
|
||||
|
||||
public UserDetailsService getUserDetailsService() {
|
||||
return userDetailsService;
|
||||
}
|
||||
|
||||
public String getLoginProcessingUrl() {
|
||||
return loginProcessingUrl;
|
||||
}
|
||||
|
||||
public String getVerifyCodeProcessingUrl() {
|
||||
return verifyCodeProcessingUrl;
|
||||
}
|
||||
|
||||
public String getLogoutProcessingUrl() {
|
||||
return logoutProcessingUrl;
|
||||
}
|
||||
|
||||
public LoginSuccessHandler getLoginSuccessHandler() {
|
||||
return loginSuccessHandler;
|
||||
}
|
||||
|
||||
public VerifyCodeHandler getVerifyCodeHandler() {
|
||||
return verifyCodeHandler;
|
||||
}
|
||||
|
||||
public PasswordCheckFailureHandler getPasswordCheckFailureHandler(){
|
||||
return passwordCheckFailureHandler;
|
||||
}
|
||||
|
||||
public LogoutSuccessHandler getLogoutSuccessHandler() {
|
||||
return logoutSuccessHandler;
|
||||
}
|
||||
|
||||
public String getPublicKeyFetchUrl() {
|
||||
return publicKeyFetchUrl;
|
||||
}
|
||||
|
||||
public Boolean getEnableSso() {
|
||||
return enableSso;
|
||||
}
|
||||
|
||||
public SsoCodeService getSsoCodeService() {
|
||||
return ssoCodeService;
|
||||
}
|
||||
|
||||
public Integer getSsoCodeTimeOut() {
|
||||
return ssoCodeTimeOut;
|
||||
}
|
||||
|
||||
public String getSsoCodeFetchUrl() {
|
||||
return ssoCodeFetchUrl;
|
||||
}
|
||||
|
||||
public String getSsoUserinfoFetchUrl() {
|
||||
return ssoUserinfoFetchUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.metalloop.common.auth.config;
|
||||
|
||||
import com.metalloop.framework.satoken.admin.config.SaTokenAdminConfiguration;
|
||||
import com.metalloop.framework.satoken.self.config.SaTokenSelfConfiguration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 开启认证服务器
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
@Import({AuthServerConfiguration.class, SaTokenAdminConfiguration.class, SaTokenSelfConfiguration.class})
|
||||
public @interface EnableAuthServer {
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.metalloop.common.auth.encrypt;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* 加密解密配置
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
@ConfigurationProperties(KeyProperties.PREFIX)
|
||||
public class KeyProperties {
|
||||
|
||||
/**
|
||||
* ConfigurationProperties prefix for KeyProperties.
|
||||
*/
|
||||
public static final String PREFIX = "auth-server.encrypt";
|
||||
|
||||
/**
|
||||
* A symmetric key. As a stronger alternative, consider using a keystore.
|
||||
*/
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* A salt for the symmetric key, in the form of a hex-encoded byte array. As a
|
||||
* stronger alternative, consider using a keystore.
|
||||
*/
|
||||
private String salt = "deadbeef";
|
||||
|
||||
/**
|
||||
* Flag to say that a process should fail if there is an encryption or decryption
|
||||
* error.
|
||||
*/
|
||||
private boolean failOnError = true;
|
||||
|
||||
/**
|
||||
* The key store properties for locating a key in a Java Key Store (a file in a format
|
||||
* defined and understood by the JVM).
|
||||
*/
|
||||
private KeyStore keyStore = new KeyStore();
|
||||
|
||||
public boolean isFailOnError() {
|
||||
return this.failOnError;
|
||||
}
|
||||
|
||||
public void setFailOnError(boolean failOnError) {
|
||||
this.failOnError = failOnError;
|
||||
}
|
||||
|
||||
public String getKey() {
|
||||
return this.key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getSalt() {
|
||||
return this.salt;
|
||||
}
|
||||
|
||||
public void setSalt(String salt) {
|
||||
this.salt = salt;
|
||||
}
|
||||
|
||||
public KeyStore getKeyStore() {
|
||||
return this.keyStore;
|
||||
}
|
||||
|
||||
public void setKeyStore(KeyStore keyStore) {
|
||||
this.keyStore = keyStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key store properties.
|
||||
*/
|
||||
public static class KeyStore {
|
||||
|
||||
/**
|
||||
* Location of the key store file, e.g. classpath:/keystore.jks.
|
||||
*/
|
||||
private Resource location;
|
||||
|
||||
/**
|
||||
* Password that locks the keystore.
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* Alias for a key in the store.
|
||||
*/
|
||||
private String alias;
|
||||
|
||||
/**
|
||||
* Secret protecting the key (defaults to the same as the password).
|
||||
*/
|
||||
private String secret;
|
||||
|
||||
/**
|
||||
* The KeyStore type. Defaults to jks.
|
||||
*/
|
||||
private String type = "jks";
|
||||
|
||||
public String getAlias() {
|
||||
return this.alias;
|
||||
}
|
||||
|
||||
public void setAlias(String alias) {
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
public Resource getLocation() {
|
||||
return this.location;
|
||||
}
|
||||
|
||||
public void setLocation(Resource location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getSecret() {
|
||||
return this.secret == null ? this.password : this.secret;
|
||||
}
|
||||
|
||||
public void setSecret(String secret) {
|
||||
this.secret = secret;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.metalloop.common.auth.encrypt;
|
||||
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.RSAPrivateCrtKey;
|
||||
import java.security.spec.RSAPublicKeySpec;
|
||||
|
||||
/**
|
||||
* Factory for RSA key pairs from a JKS keystore file. User provides a {@link Resource} location of a keystore file and
|
||||
* the password to unlock it, and the factory grabs the keypairs from the store by name (and optionally password).
|
||||
*
|
||||
* @author Dave Syer
|
||||
*/
|
||||
public class KeyStoreKeyFactory {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(KeyStoreKeyFactory.class);
|
||||
|
||||
private final Resource resource;
|
||||
|
||||
private final char[] password;
|
||||
|
||||
private KeyStore store;
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
public KeyStoreKeyFactory(Resource resource, char[] password) {
|
||||
this.resource = resource;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public KeyPair getKeyPair(String alias) {
|
||||
return getKeyPair(alias, password);
|
||||
}
|
||||
|
||||
public KeyPair getKeyPair(String alias, char[] password) {
|
||||
InputStream inputStream = null;
|
||||
try {
|
||||
synchronized (lock) {
|
||||
if (store == null) {
|
||||
synchronized (lock) {
|
||||
store = KeyStore.getInstance("jks");
|
||||
inputStream = resource.getInputStream();
|
||||
store.load(inputStream, this.password);
|
||||
}
|
||||
}
|
||||
}
|
||||
RSAPrivateCrtKey key = (RSAPrivateCrtKey) store.getKey(alias, password);
|
||||
RSAPublicKeySpec spec = new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent());
|
||||
PublicKey publicKey = KeyFactory.getInstance("RSA").generatePublic(spec);
|
||||
return new KeyPair(publicKey, key);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot load keys from store: " + resource, e);
|
||||
} finally {
|
||||
try {
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.warn("Cannot close open stream: ", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.metalloop.common.auth.handler;
|
||||
|
||||
|
||||
import com.metalloop.common.auth.Authentication;
|
||||
import com.metalloop.common.model.Result;
|
||||
import com.metalloop.modules.auth.model.resp.AuthenticationResponse;
|
||||
|
||||
/**
|
||||
* 登录成功处理器默认实现
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public class DefaultLoginSuccessHandler implements LoginSuccessHandler {
|
||||
@Override
|
||||
public Object onSuccess(Authentication authentication) {
|
||||
AuthenticationResponse authenticationResponse = new AuthenticationResponse();
|
||||
authenticationResponse.setAccessToken(authentication.getAccessToken());
|
||||
authenticationResponse.setUsername(authentication.getUsername());
|
||||
authenticationResponse.setUserId(authentication.getUserId());
|
||||
return Result.ok(authenticationResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.metalloop.common.auth.handler;
|
||||
|
||||
|
||||
import com.metalloop.common.auth.Authentication;
|
||||
import com.metalloop.common.model.Result;
|
||||
|
||||
/**
|
||||
* 登出成功处理器默认实现
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public class DefaultLogoutSuccessHandler implements LogoutSuccessHandler {
|
||||
@Override
|
||||
public Object onSuccess(Authentication authentication) {
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.metalloop.common.auth.handler;
|
||||
|
||||
|
||||
import com.metalloop.common.auth.userdetails.UserDetails;
|
||||
|
||||
/**
|
||||
* 密码校验失败处理器默认实现
|
||||
*
|
||||
* @author lifan
|
||||
* @date 2024/3/26 18:04
|
||||
*/
|
||||
public class DefaultPasswordCheckFailureHandler implements PasswordCheckFailureHandler {
|
||||
|
||||
@Override
|
||||
public void onFailure(String ip, UserDetails userDetails) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(String ip, UserDetails userDetails) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkAccountLockStatus(String ip, UserDetails userDetails) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.metalloop.common.auth.handler;
|
||||
|
||||
public class DefaultVerifyCodeHandler implements VerifyCodeHandler{
|
||||
|
||||
|
||||
@Override
|
||||
public boolean createVerifyCode(String phoneNum) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkVerifyCode(String phoneNum, String verifyCode) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.metalloop.common.auth.handler;
|
||||
|
||||
|
||||
import com.metalloop.common.auth.Authentication;
|
||||
|
||||
/**
|
||||
* 登录成功处理器
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public interface LoginSuccessHandler {
|
||||
|
||||
/**
|
||||
* 处理登录成功的情况,例如生成令牌、返回用户信息等。
|
||||
*
|
||||
* @param authentication 认证信息
|
||||
* @return 登录成功后的响应对象
|
||||
*/
|
||||
Object onSuccess(Authentication authentication);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.metalloop.common.auth.handler;
|
||||
|
||||
|
||||
import com.metalloop.common.auth.Authentication;
|
||||
|
||||
/**
|
||||
* 登出成功处理器
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public interface LogoutSuccessHandler {
|
||||
|
||||
/**
|
||||
* 处理登出成功的情况,认证信息可能为null
|
||||
*
|
||||
* @param authentication 认证信息,可能为null
|
||||
* @return 登录成功后的响应对象
|
||||
*/
|
||||
Object onSuccess(Authentication authentication);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.metalloop.common.auth.handler;
|
||||
|
||||
|
||||
import com.metalloop.common.auth.userdetails.UserDetails;
|
||||
|
||||
/**
|
||||
* @author lifan
|
||||
* @date 2024/3/26 17:58
|
||||
*/
|
||||
public interface PasswordCheckFailureHandler {
|
||||
|
||||
/**
|
||||
* 处理密码校验失败的情况
|
||||
*
|
||||
* @param ip 用户ip
|
||||
* @param userDetails 账户信息
|
||||
*/
|
||||
void onFailure(String ip, UserDetails userDetails);
|
||||
|
||||
/**
|
||||
* 处理密码校验成功的情况
|
||||
*
|
||||
* @param ip 用户ip
|
||||
* @param userDetails 账户信息
|
||||
*/
|
||||
void onSuccess(String ip, UserDetails userDetails);
|
||||
|
||||
/**
|
||||
* 检查账户锁定状态
|
||||
*
|
||||
* @param ip 用户ip
|
||||
* @param userDetails 账户信息
|
||||
*/
|
||||
void checkAccountLockStatus(String ip, UserDetails userDetails);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.metalloop.common.auth.handler;
|
||||
|
||||
public interface VerifyCodeHandler {
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*
|
||||
* @param phoneNum 手机号
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean createVerifyCode(String phoneNum);
|
||||
|
||||
/**
|
||||
* 校验验证码
|
||||
*
|
||||
* @param phoneNum 手机号
|
||||
* @param verifyCode 待校验的验证码
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean checkVerifyCode(String phoneNum, String verifyCode);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.metalloop.common.auth.model;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
/**
|
||||
* 小程序公众号认证请求
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
public class AuthenticationMiniAppRequest {
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* openId
|
||||
*/
|
||||
@NotBlank(message = "openId")
|
||||
private String openId;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
private String verifyCode;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.metalloop.common.auth.model;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
/**
|
||||
* 认证请求
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
public class AuthenticationRequest {
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
private String verifyCode;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.metalloop.common.auth.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 认证响应
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
public class AuthenticationResponse {
|
||||
|
||||
/**
|
||||
* 访问令牌
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
/**
|
||||
* 用户登录名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.metalloop.common.auth.password;
|
||||
|
||||
/**
|
||||
* 密码解密器
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-02-02
|
||||
*/
|
||||
public interface PasswordDecryptor {
|
||||
|
||||
/**
|
||||
* 解密密码密文
|
||||
*
|
||||
* @param rawPassword 密码密文
|
||||
* @return 密码明文
|
||||
*/
|
||||
String decrypt(String rawPassword);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.metalloop.common.auth.password;
|
||||
|
||||
import com.metalloop.common.exception.BadCredentialsException;
|
||||
import com.metalloop.common.utils.RsaUtils;
|
||||
import com.metalloop.common.auth.encrypt.KeyProperties;
|
||||
import com.metalloop.common.auth.encrypt.KeyStoreKeyFactory;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.PrivateKey;
|
||||
|
||||
/**
|
||||
* RSA 密码解密器
|
||||
*
|
||||
* @author zhaowenhao
|
||||
*/
|
||||
public class RsaPasswordDecryptor implements PasswordDecryptor {
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
/**
|
||||
* Key encryption properties.
|
||||
*/
|
||||
private final KeyProperties keyProperties;
|
||||
|
||||
public RsaPasswordDecryptor(KeyProperties keyProperties) {
|
||||
this.keyProperties = keyProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decrypt(String rawPassword) {
|
||||
if (keyProperties != null) {
|
||||
KeyPair keyPair = new KeyStoreKeyFactory(keyProperties.getKeyStore().getLocation(), keyProperties.getKeyStore().getSecret().toCharArray()).getKeyPair(keyProperties.getKeyStore().getAlias());
|
||||
PrivateKey privateKey = keyPair.getPrivate();
|
||||
try {
|
||||
return RsaUtils.decrypt(rawPassword, privateKey);
|
||||
} catch (Exception e) {
|
||||
logger.debug("Authentication failed: 解密密码出错: " + e.getMessage());
|
||||
throw new BadCredentialsException("用户名或密码错误");
|
||||
}
|
||||
}
|
||||
return rawPassword;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.metalloop.common.auth.permission;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 表示权限,包括权限码和权限对应的URI。
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public interface Permission extends Serializable {
|
||||
|
||||
/**
|
||||
* 获取权限码
|
||||
*
|
||||
* @return 权限码
|
||||
*/
|
||||
String getPermissionCode();
|
||||
|
||||
/**
|
||||
* 获取权限对应的URI
|
||||
*
|
||||
* @return 权限对应的URI
|
||||
*/
|
||||
String getPermissionUri();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.metalloop.common.auth.permission;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* 权限服务
|
||||
*
|
||||
* @author zhaowenhao
|
||||
*/
|
||||
public interface PermissionService {
|
||||
|
||||
/**
|
||||
* 加载所有权限,实现者应缓存权限列表,避免每次都查询数据库
|
||||
*
|
||||
* @return 权限列表
|
||||
*/
|
||||
Future<List<? extends Permission>> loadAllPermissions();
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.metalloop.common.auth.permission;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 简单的权限实现
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class SimplePermission implements Permission {
|
||||
|
||||
/**
|
||||
* 权限码
|
||||
*/
|
||||
private String permissionCode;
|
||||
|
||||
/**
|
||||
* 权限对应的URI
|
||||
*/
|
||||
private String permissionUri;
|
||||
|
||||
public SimplePermission(String permissionCode, String permissionUri) {
|
||||
this.permissionCode = permissionCode;
|
||||
this.permissionUri = permissionUri;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.metalloop.common.auth.sso;
|
||||
|
||||
|
||||
import com.metalloop.common.auth.sso.model.SsoUserInfo;
|
||||
import com.metalloop.common.exception.BadCredentialsException;
|
||||
|
||||
/**
|
||||
* 单点登录授权码服务,需要一个服务来生成授权码,并验证授权码的有效性
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2024-02-28
|
||||
*/
|
||||
public interface SsoCodeService {
|
||||
|
||||
/**
|
||||
* 生成指定用户的单点登陆授权码
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param timeout 过期时间,单位:秒
|
||||
* @return 单点登陆授权码
|
||||
*/
|
||||
String generateCode(String userId, long timeout);
|
||||
|
||||
/**
|
||||
* 验证单点登陆授权码的有效性,如果授权码有效,返回授权码对应的用户信息
|
||||
*
|
||||
* @param code 单点登陆授权码
|
||||
* @return 单点登陆用户信息
|
||||
*/
|
||||
SsoUserInfo validateCode(String code) throws BadCredentialsException;
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package com.metalloop.common.auth.sso.model;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 简单的用户信息实现
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class SimpleSsoUserInfo implements SsoUserInfo {
|
||||
|
||||
// region 用户基本信息
|
||||
/**
|
||||
* 用户在数据库中的唯一标识符
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 用户登录名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 用户全名
|
||||
*/
|
||||
private String fullName;
|
||||
|
||||
/**
|
||||
* 用户昵称
|
||||
*/
|
||||
private String nickName;
|
||||
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 执法类型
|
||||
*/
|
||||
private String lawType;
|
||||
|
||||
/**
|
||||
* 职位/职称
|
||||
*/
|
||||
private String jobTitle;
|
||||
|
||||
/**
|
||||
* 用户个人资料页面的 URL
|
||||
*/
|
||||
private String profile;
|
||||
|
||||
/**
|
||||
* 用户个人资料图片的 URL
|
||||
*/
|
||||
private String picture;
|
||||
|
||||
/**
|
||||
* 用户的首选电子邮件地址
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 用户的首选电话号码
|
||||
*/
|
||||
private String phoneNumber;
|
||||
|
||||
/**
|
||||
* 用户的性别
|
||||
*/
|
||||
private String gender;
|
||||
|
||||
/**
|
||||
* 用户的出生日期
|
||||
*/
|
||||
private String birthdate;
|
||||
|
||||
/**
|
||||
* 用户的地址
|
||||
*/
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 用户的身份证号
|
||||
*/
|
||||
private String idNumber;
|
||||
|
||||
/**
|
||||
* 用户的工作单位
|
||||
*/
|
||||
private String workUnit;
|
||||
// endregion 用户基本信息
|
||||
|
||||
// region 用户机构信息
|
||||
/**
|
||||
* 用户机构在数据库中的唯一标识符,为了兼容不同的情况,这里使用字符串类型
|
||||
*/
|
||||
private String orgId;
|
||||
/**
|
||||
* 用户的机构编码
|
||||
*/
|
||||
private String orgCode;
|
||||
|
||||
/**
|
||||
* 用户的机构名称
|
||||
*/
|
||||
private String orgName;
|
||||
|
||||
/**
|
||||
* 用户的机构级别
|
||||
*/
|
||||
private String orgLevel;
|
||||
|
||||
/**
|
||||
* 用户的机构类型
|
||||
*/
|
||||
private String orgType;
|
||||
// endregion 用户机构信息
|
||||
|
||||
public SimpleSsoUserInfo(Builder builder) {
|
||||
this.userId = builder.userId;
|
||||
this.username = builder.username;
|
||||
this.fullName = builder.fullName;
|
||||
this.nickName = builder.nickName;
|
||||
this.type = builder.type;
|
||||
this.lawType = builder.lawType;
|
||||
this.jobTitle = builder.jobTitle;
|
||||
this.profile = builder.profile;
|
||||
this.picture = builder.picture;
|
||||
this.email = builder.email;
|
||||
this.phoneNumber = builder.phoneNumber;
|
||||
this.gender = builder.gender;
|
||||
this.birthdate = builder.birthdate;
|
||||
this.address = builder.address;
|
||||
this.idNumber = builder.idNumber;
|
||||
this.workUnit = builder.workUnit;
|
||||
this.orgId = builder.orgId;
|
||||
this.orgCode = builder.orgCode;
|
||||
this.orgName = builder.orgName;
|
||||
this.orgLevel = builder.orgLevel;
|
||||
this.orgType = builder.orgType;
|
||||
}
|
||||
|
||||
public static Builder withUserId(String userId) {
|
||||
return new Builder(userId);
|
||||
}
|
||||
|
||||
|
||||
public static class Builder {
|
||||
private final String userId;
|
||||
private String username;
|
||||
private String fullName;
|
||||
private String nickName;
|
||||
private String type;
|
||||
private String lawType;
|
||||
private String jobTitle;
|
||||
private String profile;
|
||||
private String picture;
|
||||
private String email;
|
||||
private String phoneNumber;
|
||||
private String gender;
|
||||
private String birthdate;
|
||||
private String address;
|
||||
private String idNumber;
|
||||
private String workUnit;
|
||||
private String orgId;
|
||||
private String orgCode;
|
||||
private String orgName;
|
||||
private String orgLevel;
|
||||
private String orgType;
|
||||
private List<String> roleNames;
|
||||
|
||||
public Builder(String userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public Builder username(String username) {
|
||||
this.username = username;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder fullName(String fullName) {
|
||||
this.fullName = fullName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder nickName(String nickName) {
|
||||
this.nickName = nickName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder type(String type) {
|
||||
this.type = type;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder lawType(String lawType) {
|
||||
this.lawType = lawType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder jobTitle(String jobTitle) {
|
||||
this.jobTitle = jobTitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder profile(String profile) {
|
||||
this.profile = profile;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder picture(String picture) {
|
||||
this.picture = picture;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder email(String email) {
|
||||
this.email = email;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder phoneNumber(String phoneNumber) {
|
||||
this.phoneNumber = phoneNumber;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder gender(String gender) {
|
||||
this.gender = gender;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder birthdate(String birthdate) {
|
||||
this.birthdate = birthdate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder address(String address) {
|
||||
this.address = address;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder idNumber(String idNumber) {
|
||||
this.idNumber = idNumber;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder workUnit(String workUnit) {
|
||||
this.workUnit = workUnit;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder orgId(String orgId) {
|
||||
this.orgId = orgId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder orgCode(String orgCode) {
|
||||
this.orgCode = orgCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder orgName(String orgName) {
|
||||
this.orgName = orgName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder orgLevel(String orgLevel) {
|
||||
this.orgLevel = orgLevel;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder orgType(String orgType) {
|
||||
this.orgType = orgType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder roleNames(List<String> roleNames) {
|
||||
this.roleNames = roleNames;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public SimpleSsoUserInfo build() {
|
||||
return new SimpleSsoUserInfo(this);
|
||||
}
|
||||
|
||||
public List<String> getRoleNames() {
|
||||
return roleNames;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.metalloop.common.auth.sso.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 单点登录用户信息接口,相较于UserInfo,该接口只包含用户的非敏感基本信息,不包含用户的角色和权限信息。
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2024-02-28
|
||||
*/
|
||||
public interface SsoUserInfo extends Serializable {
|
||||
|
||||
// region 用户基本信息
|
||||
|
||||
/**
|
||||
* 用户在数据库中的唯一标识符,为了兼容不同的情况,这里使用字符串类型。
|
||||
*
|
||||
* @return 用户在数据库中的唯一标识符。
|
||||
*/
|
||||
String getUserId();
|
||||
|
||||
/**
|
||||
* 获取用户的Long类型唯一标识符,适配用户唯一标识符为 Long 类型的情况,减少转换代码。
|
||||
*
|
||||
* @return 用户在数据库中的Long类型唯一标识符
|
||||
*/
|
||||
@JsonIgnore
|
||||
default Long getUserIdAsLong() {
|
||||
if (getUserId() == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(getUserId());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回用户登录名。
|
||||
*
|
||||
* @return 返回用户登录名。
|
||||
*/
|
||||
String getUsername();
|
||||
|
||||
/**
|
||||
* 返回用户的全名。
|
||||
*
|
||||
* @return 返回用户的全名
|
||||
*/
|
||||
String getFullName();
|
||||
|
||||
/**
|
||||
* 返回用户的昵称。
|
||||
*
|
||||
* @return 返回用户的昵称
|
||||
*/
|
||||
String getNickName();
|
||||
|
||||
/**
|
||||
* 返回用户的类型。
|
||||
*
|
||||
* @return 返回用户的类型
|
||||
*/
|
||||
String getType();
|
||||
|
||||
/**
|
||||
* 返回执法类型。
|
||||
*
|
||||
* @return 返回执法类型
|
||||
*/
|
||||
String getLawType();
|
||||
|
||||
/**
|
||||
* 返回职位/职称
|
||||
*
|
||||
* @return 返回职位/职称
|
||||
*/
|
||||
String getJobTitle();
|
||||
|
||||
|
||||
/**
|
||||
* 返回用户个人资料页面的URL。
|
||||
*
|
||||
* @return 返回用户个人资料页面的URL。
|
||||
*/
|
||||
String getProfile();
|
||||
|
||||
/**
|
||||
* 返回用户个人资料图片的URL。
|
||||
*
|
||||
* @return 用户个人资料图片的URL。
|
||||
*/
|
||||
String getPicture();
|
||||
|
||||
/**
|
||||
* 返回用户的首选电子邮件地址。
|
||||
*
|
||||
* @return 户的首选电子邮件地址
|
||||
*/
|
||||
String getEmail();
|
||||
|
||||
/**
|
||||
* 返回用户的首选电话号码。
|
||||
*
|
||||
* @return 首选电话号码
|
||||
*/
|
||||
String getPhoneNumber();
|
||||
|
||||
/**
|
||||
* 返回用户的性别。
|
||||
*
|
||||
* @return 返回用户的性别
|
||||
*/
|
||||
String getGender();
|
||||
|
||||
/**
|
||||
* 返回用户的出生日期。
|
||||
*
|
||||
* @return 用户的出生日期
|
||||
*/
|
||||
String getBirthdate();
|
||||
|
||||
/**
|
||||
* 返回用户的地址。
|
||||
*
|
||||
* @return 返回用户的地址
|
||||
*/
|
||||
String getAddress();
|
||||
|
||||
/**
|
||||
* 返回用户的身份证号。
|
||||
*
|
||||
* @return 返回用户的身份证号
|
||||
*/
|
||||
String getIdNumber();
|
||||
|
||||
/**
|
||||
* 返回用户的工作单位。
|
||||
*
|
||||
* @return 返回用户的工作单位
|
||||
*/
|
||||
String getWorkUnit();
|
||||
|
||||
// endregion 用户基本信息
|
||||
|
||||
// region 用户机构信息
|
||||
|
||||
/**
|
||||
* 用户机构在数据库中的唯一标识符,为了兼容不同的情况,这里使用字符串类型。
|
||||
*
|
||||
* @return 用户机构在数据库中的唯一标识符
|
||||
*/
|
||||
String getOrgId();
|
||||
|
||||
/**
|
||||
* 获取用户机构的Long类型唯一标识符,适配用户机构唯一标识符为 Long 类型的情况,减少转换代码。
|
||||
*
|
||||
* @return 用户机构在数据库中的Long类型唯一标识符
|
||||
*/
|
||||
@JsonIgnore
|
||||
default Long getOrgIdAsLong() {
|
||||
if (getOrgId() == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Long.valueOf(getOrgId());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回用户的机构编码。
|
||||
*
|
||||
* @return 返回用户的机构编码
|
||||
*/
|
||||
String getOrgCode();
|
||||
|
||||
/**
|
||||
* 返回用户的机构名称。
|
||||
*
|
||||
* @return 返回用户的机构名称。
|
||||
*/
|
||||
String getOrgName();
|
||||
|
||||
/**
|
||||
* 返回用户的机构级别。
|
||||
*
|
||||
* @return 机构级别
|
||||
*/
|
||||
String getOrgLevel();
|
||||
|
||||
/**
|
||||
* 返回用户的机构类型。
|
||||
*
|
||||
* @return 机构类型
|
||||
*/
|
||||
String getOrgType();
|
||||
// endregion 用户机构信息
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.metalloop.common.auth.userdetails;
|
||||
|
||||
import com.metalloop.common.auth.permission.SimplePermission;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 简单的用户核心信息实现
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class SimpleUserDetails implements UserDetails {
|
||||
|
||||
private String userId;
|
||||
private String phoneNum;
|
||||
private String username;
|
||||
private String password;
|
||||
private boolean accountNonExpired;
|
||||
private boolean accountNonLocked;
|
||||
private boolean credentialsNonExpired;
|
||||
private boolean enabled;
|
||||
private String lawType;
|
||||
private Collection<SimplePermission> permissions;
|
||||
|
||||
public SimpleUserDetails(String userId, String phoneNum,String username, String password,
|
||||
Collection<SimplePermission> permissions,
|
||||
boolean accountNonExpired, boolean accountNonLocked,
|
||||
boolean credentialsNonExpired, boolean enabled, String lawType) {
|
||||
this.userId = userId;
|
||||
this.phoneNum = phoneNum;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.permissions = permissions;
|
||||
this.accountNonExpired = accountNonExpired;
|
||||
this.accountNonLocked = accountNonLocked;
|
||||
this.credentialsNonExpired = credentialsNonExpired;
|
||||
this.enabled = enabled;
|
||||
this.lawType = lawType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.metalloop.common.auth.userdetails;
|
||||
|
||||
|
||||
import com.metalloop.common.auth.permission.Permission;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* 提供核心用户信息,这些信息稍后会封装到Authentication对象中。这允许将与安
|
||||
* 全无关的用户信息(例如电子邮件地址、电话号码等)存储在方便的位置。
|
||||
* 具体的实现必须特别小心,以确保强制执行为每个方法详细说明的非空契约。
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @see UserDetailsService
|
||||
* @see Permission
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public interface UserDetails extends Serializable {
|
||||
|
||||
/**
|
||||
* 返回授予用户的权限。无法返回null 。
|
||||
*
|
||||
* @return 按自然键排序的权限集合。
|
||||
*/
|
||||
Collection<? extends Permission> getPermissions();
|
||||
|
||||
/**
|
||||
* 返回用于对用户进行身份验证的用户Id。无法返回null 。
|
||||
*
|
||||
* @return 用户Id
|
||||
*/
|
||||
String getUserId();
|
||||
|
||||
/**
|
||||
* 用户手机号
|
||||
*
|
||||
* @return 用户手机号
|
||||
*/
|
||||
String getPhoneNum();
|
||||
|
||||
/**
|
||||
* 返回用于对用户进行身份验证的用户名。无法返回null 。
|
||||
*
|
||||
* @return 用户名
|
||||
*/
|
||||
String getUsername();
|
||||
|
||||
/**
|
||||
* 返回用于验证用户身份的密码。
|
||||
*
|
||||
* @return 密码
|
||||
*/
|
||||
String getPassword();
|
||||
|
||||
/**
|
||||
* 指示用户的帐户是否已过期。无法验证过期的帐户。
|
||||
*
|
||||
* @return 如果用户的帐户有效(即未过期)则为true ,如果不再有效(即已过期) false
|
||||
*/
|
||||
boolean isAccountNonExpired();
|
||||
|
||||
/**
|
||||
* 指示用户是锁定还是解锁。无法对锁定的用户进行身份验证。
|
||||
*
|
||||
* @return 如果用户未被锁定, true ,否则为false
|
||||
*/
|
||||
boolean isAccountNonLocked();
|
||||
|
||||
/**
|
||||
* 指示用户的凭据(密码)是否已过期。过期的凭据会阻止身份验证。
|
||||
*
|
||||
* @return 如果用户的凭据有效(即未过期), true ;如果不再有效(即,已过期), false
|
||||
*/
|
||||
boolean isCredentialsNonExpired();
|
||||
|
||||
/**
|
||||
* 指示用户是启用还是禁用。无法对禁用的用户进行身份验证。
|
||||
*
|
||||
* @return 如果用户已启用, true ,否则为false
|
||||
*/
|
||||
boolean isEnabled();
|
||||
|
||||
/**
|
||||
* 执法类型:0-调度员;1-指挥员;2-办件员
|
||||
* @return 用户执法类型
|
||||
*/
|
||||
String getLawType();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.metalloop.common.auth.userdetails;
|
||||
//import com.wanji.software.tocc.common.exception.UsernameNotFoundException;
|
||||
|
||||
//import com.wanji.software.tocc.common.exception.UsernameNotFoundException;
|
||||
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
|
||||
/**
|
||||
* 加载用户特定数据的核心接口,它在整个框架中用作用户 DAO,
|
||||
* 该接口只需要一种只读方法,这简化了对新数据访问策略的支持
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public interface UserDetailsService {
|
||||
|
||||
/**
|
||||
* 根据用户名获取用户详情
|
||||
*
|
||||
* @param username 用户登录名
|
||||
* @return 用户详情
|
||||
* @throws UsernameNotFoundException 用户名不存在
|
||||
*/
|
||||
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.metalloop.common.constant;
|
||||
|
||||
/**
|
||||
* 全局公共常量
|
||||
*
|
||||
* @author zlt
|
||||
* @since 2018/10/29
|
||||
*/
|
||||
public interface CommonConstant {
|
||||
/**
|
||||
* 项目版本号(banner使用)
|
||||
*/
|
||||
String PROJECT_VERSION = "1.0.0";
|
||||
|
||||
/**
|
||||
* token请求头名称
|
||||
*/
|
||||
String TOKEN_HEADER = "Authorization";
|
||||
|
||||
/**
|
||||
* The access token issued by the authorization server. This value is REQUIRED.
|
||||
*/
|
||||
String ACCESS_TOKEN = "access_token";
|
||||
|
||||
String BEARER_TYPE = "Bearer";
|
||||
|
||||
/**
|
||||
* 标签 header key
|
||||
*/
|
||||
String HEADER_LABEL = "x-label";
|
||||
|
||||
/**
|
||||
* 标签 header 分隔符
|
||||
*/
|
||||
String HEADER_LABEL_SPLIT = ",";
|
||||
|
||||
/**
|
||||
* 标签或 名称
|
||||
*/
|
||||
String LABEL_OR = "labelOr";
|
||||
|
||||
/**
|
||||
* 标签且 名称
|
||||
*/
|
||||
String LABEL_AND = "labelAnd";
|
||||
|
||||
/**
|
||||
* 权重key
|
||||
*/
|
||||
String WEIGHT_KEY = "weight";
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
String STATUS_DEL = "1";
|
||||
|
||||
/**
|
||||
* 正常
|
||||
*/
|
||||
String STATUS_NORMAL = "0";
|
||||
|
||||
/**
|
||||
* 锁定
|
||||
*/
|
||||
String STATUS_LOCK = "9";
|
||||
|
||||
/**
|
||||
* 目录
|
||||
*/
|
||||
Integer CATALOG = -1;
|
||||
|
||||
/**
|
||||
* 菜单
|
||||
*/
|
||||
Integer MENU = 1;
|
||||
|
||||
/**
|
||||
* 权限
|
||||
*/
|
||||
Integer PERMISSION = 2;
|
||||
|
||||
/**
|
||||
* 删除标记
|
||||
*/
|
||||
String DEL_FLAG = "is_del";
|
||||
|
||||
/**
|
||||
* 超级管理员用户名
|
||||
*/
|
||||
String ADMIN_USER_NAME = "admin";
|
||||
|
||||
/**
|
||||
* 公共日期格式
|
||||
*/
|
||||
String MONTH_FORMAT = "yyyy-MM";
|
||||
String DATE_FORMAT = "yyyy-MM-dd";
|
||||
String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
String SIMPLE_MONTH_FORMAT = "yyyyMM";
|
||||
String SIMPLE_DATE_FORMAT = "yyyyMMdd";
|
||||
String SIMPLE_DATETIME_FORMAT = "yyyyMMddHHmmss";
|
||||
String TIME_ZONE_GMT8 = "GMT+8";
|
||||
|
||||
String DEF_USER_PASSWORD = "123456";
|
||||
|
||||
String LOCK_KEY_PREFIX = "LOCK_KEY";
|
||||
|
||||
/**
|
||||
* 租户id参数
|
||||
*/
|
||||
String TENANT_ID_PARAM = "tenantId";
|
||||
|
||||
|
||||
/**
|
||||
* 日志链路追踪id信息头
|
||||
*/
|
||||
String TRACE_ID_HEADER = "x-traceId-header";
|
||||
/**
|
||||
* 日志链路追踪id日志标志
|
||||
*/
|
||||
String LOG_TRACE_ID = "traceId";
|
||||
/**
|
||||
* 负载均衡策略-版本号 信息头
|
||||
*/
|
||||
String Z_L_T_VERSION = "z-l-t-version";
|
||||
/**
|
||||
* 注册中心元数据 版本号
|
||||
*/
|
||||
String METADATA_VERSION = "version";
|
||||
|
||||
/**
|
||||
* 文件分隔符
|
||||
*/
|
||||
String PATH_SPLIT = "/";
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.metalloop.common.constant;
|
||||
/**
|
||||
* Security 相关常量
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public interface SecurityConstants {
|
||||
|
||||
|
||||
/**
|
||||
* 用户ID请求头
|
||||
*/
|
||||
String HEADER_USER_USERID = "x-userid";
|
||||
|
||||
/**
|
||||
* 用户信息在session中的key
|
||||
*/
|
||||
String SESSION_KEY_AUTHENTICATION = "AUTHENTICATION_SESSION_KEY";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.metalloop.common.core.lock;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 分布式锁顶级接口
|
||||
*
|
||||
* @author zlt
|
||||
* @date 2018/5/29 14:12
|
||||
* <p>
|
||||
* Blog: https://zlt2000.gitee.io
|
||||
* Github: https://github.com/zlt2000
|
||||
*/
|
||||
public interface DistributedLock {
|
||||
/**
|
||||
* 获取锁,如果获取不成功则一直等待直到lock被获取
|
||||
*
|
||||
* @param key 锁的key
|
||||
* @param leaseTime 加锁的时间,超过这个时间后锁便自动解锁;
|
||||
* 如果leaseTime为-1,则保持锁定直到显式解锁
|
||||
* @param unit {@code leaseTime} 参数的时间单位
|
||||
* @param isFair 是否公平锁
|
||||
* @return 锁对象
|
||||
*/
|
||||
ZLock lock(String key, long leaseTime, TimeUnit unit, boolean isFair) throws Exception;
|
||||
|
||||
default ZLock lock(String key, long leaseTime, TimeUnit unit) throws Exception {
|
||||
return this.lock(key, leaseTime, unit, false);
|
||||
}
|
||||
|
||||
default ZLock lock(String key, boolean isFair) throws Exception {
|
||||
return this.lock(key, -1, null, isFair);
|
||||
}
|
||||
|
||||
default ZLock lock(String key) throws Exception {
|
||||
return this.lock(key, -1, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取锁,如果锁不可用则等待最多waitTime时间后放弃
|
||||
*
|
||||
* @param key 锁的key
|
||||
* @param waitTime 获取锁的最大尝试时间(单位 {@code unit})
|
||||
* @param leaseTime 加锁的时间,超过这个时间后锁便自动解锁;
|
||||
* 如果leaseTime为-1,则保持锁定直到显式解锁
|
||||
* @param unit {@code waitTime} 和 {@code leaseTime} 参数的时间单位
|
||||
* @return 锁对象,如果获取锁失败则为null
|
||||
*/
|
||||
ZLock tryLock(String key, long waitTime, long leaseTime, TimeUnit unit, boolean isFair) throws Exception;
|
||||
|
||||
default ZLock tryLock(String key, long waitTime, long leaseTime, TimeUnit unit) throws Exception {
|
||||
return this.tryLock(key, waitTime, leaseTime, unit, false);
|
||||
}
|
||||
|
||||
default ZLock tryLock(String key, long waitTime, TimeUnit unit, boolean isFair) throws Exception {
|
||||
return this.tryLock(key, waitTime, -1, unit, isFair);
|
||||
}
|
||||
|
||||
default ZLock tryLock(String key, long waitTime, TimeUnit unit) throws Exception {
|
||||
return this.tryLock(key, waitTime, -1, unit, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放锁
|
||||
*
|
||||
* @param lock 锁对象
|
||||
*/
|
||||
void unlock(Object lock) throws Exception;
|
||||
|
||||
/**
|
||||
* 释放锁
|
||||
*
|
||||
* @param zLock 锁抽象对象
|
||||
*/
|
||||
default void unlock(ZLock zLock) throws Exception {
|
||||
if (zLock != null) {
|
||||
this.unlock(zLock.getLock());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.metalloop.common.core.lock;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* @author zlt
|
||||
* @date 2020/6/6
|
||||
* <p>
|
||||
* Blog: https://zlt2000.gitee.io
|
||||
* Github: https://github.com/zlt2000
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface Lock {
|
||||
/**
|
||||
* 锁的key
|
||||
*/
|
||||
String key();
|
||||
|
||||
/**
|
||||
* 获取锁的最大尝试时间(单位 {@code unit})
|
||||
* 该值大于0则使用 locker.tryLock 方法加锁,否则使用 locker.lock 方法
|
||||
*/
|
||||
long waitTime() default 0;
|
||||
|
||||
/**
|
||||
* 加锁的时间(单位 {@code unit}),超过这个时间后锁便自动解锁;
|
||||
* 如果leaseTime为-1,则保持锁定直到显式解锁
|
||||
*/
|
||||
long leaseTime() default -1;
|
||||
|
||||
/**
|
||||
* 参数的时间单位
|
||||
*/
|
||||
TimeUnit unit() default TimeUnit.SECONDS;
|
||||
|
||||
/**
|
||||
* 是否公平锁
|
||||
*/
|
||||
boolean isFair() default false;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.metalloop.common.core.lock;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.metalloop.common.exception.LockException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
/**
|
||||
* 分布式锁切面
|
||||
*
|
||||
* @author zlt
|
||||
* @date 2020/6/6
|
||||
* <p>
|
||||
* Blog: https://zlt2000.gitee.io
|
||||
* Github: https://github.com/zlt2000
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
public class LockAspect {
|
||||
@Autowired(required = false)
|
||||
private DistributedLock locker;
|
||||
|
||||
/**
|
||||
* 用于SpEL表达式解析.
|
||||
*/
|
||||
private SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
|
||||
/**
|
||||
* 用于获取方法参数定义名字.
|
||||
*/
|
||||
private DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
@Around("@within(lock) || @annotation(lock)")
|
||||
public Object aroundLock(ProceedingJoinPoint point, Lock lock) throws Throwable {
|
||||
if (lock == null) {
|
||||
// 获取类上的注解
|
||||
lock = point.getTarget().getClass().getDeclaredAnnotation(Lock.class);
|
||||
}
|
||||
String lockKey = lock.key();
|
||||
if (locker == null) {
|
||||
throw new LockException("DistributedLock is null");
|
||||
}
|
||||
if (StrUtil.isEmpty(lockKey)) {
|
||||
throw new LockException("lockKey is null");
|
||||
}
|
||||
|
||||
if (lockKey.contains("#")) {
|
||||
MethodSignature methodSignature = (MethodSignature) point.getSignature();
|
||||
// 获取方法参数值
|
||||
Object[] args = point.getArgs();
|
||||
lockKey = getValBySpEL(lockKey, methodSignature, args);
|
||||
}
|
||||
ZLock lockObj = null;
|
||||
try {
|
||||
// 加锁
|
||||
if (lock.waitTime() > 0) {
|
||||
lockObj = locker.tryLock(lockKey, lock.waitTime(), lock.leaseTime(), lock.unit(), lock.isFair());
|
||||
} else {
|
||||
lockObj = locker.lock(lockKey, lock.leaseTime(), lock.unit(), lock.isFair());
|
||||
}
|
||||
|
||||
if (lockObj != null) {
|
||||
return point.proceed();
|
||||
} else {
|
||||
throw new LockException("锁等待超时");
|
||||
}
|
||||
} finally {
|
||||
locker.unlock(lockObj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析spEL表达式
|
||||
*/
|
||||
private String getValBySpEL(String spEL, MethodSignature methodSignature, Object[] args) {
|
||||
// 获取方法形参名数组
|
||||
String[] paramNames = nameDiscoverer.getParameterNames(methodSignature.getMethod());
|
||||
if (paramNames != null && paramNames.length > 0) {
|
||||
Expression expression = spelExpressionParser.parseExpression(spEL);
|
||||
// spring的表达式上下文对象
|
||||
EvaluationContext context = new StandardEvaluationContext();
|
||||
// 给上下文赋值
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
context.setVariable(paramNames[i], args[i]);
|
||||
}
|
||||
return expression.getValue(context).toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.metalloop.common.core.lock;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* LockCondition 是一个封装类,它包含了在实现幂等性操作时所需要的关键信息。
|
||||
* <p>
|
||||
* 这个类的主要组成部分包括:
|
||||
* <p>
|
||||
* - 是否开启获取锁及检查(enabled):如果为 true,则执行获取锁及检查;如果为 false,则跳过获取锁及检查。
|
||||
* <p>
|
||||
* - 锁定键(lockKey):用于分布式锁的键,唯一标识了应该被锁定的资源。
|
||||
* <p>
|
||||
* - 查询包装器(queryWrapper):定义了用于检查数据是否已存在的查询条件。
|
||||
* <p>
|
||||
* - 消息(msg):如果已存在符合查询条件的数据,那么这个消息将被用于抛出 IdempotencyException 异常。
|
||||
* <p>
|
||||
* 使用 LockCondition 类可以让代码更加清晰和易于理解。它提供了一种方式,使得在实现多条件幂等性检查时,可以将关键信息组织在一起。
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-05-14
|
||||
*/
|
||||
@Getter
|
||||
public class LockCondition<T> {
|
||||
|
||||
private final boolean enabled;
|
||||
|
||||
private final String lockKey;
|
||||
|
||||
private final QueryWrapper<T> queryWrapper;
|
||||
|
||||
private final String msg;
|
||||
|
||||
|
||||
public LockCondition(boolean enabled, String lockKey, QueryWrapper<T> queryWrapper, String msg) {
|
||||
this.lockKey = lockKey;
|
||||
this.queryWrapper = queryWrapper;
|
||||
this.msg = msg;
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public LockCondition(String lockKey, QueryWrapper<T> queryWrapper, String msg) {
|
||||
this.enabled = true;
|
||||
this.lockKey = lockKey;
|
||||
this.queryWrapper = queryWrapper;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.metalloop.common.core.lock;
|
||||
|
||||
import com.metalloop.common.utils.QueryWrapperBuilder;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* 幂等性请求 DTO
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-09-11
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LockConditionReqDto<Q extends Serializable> implements Serializable {
|
||||
/**
|
||||
* 是否开启获取锁及检查(enabled):如果为 true,则执行获取锁及检查;如果为 false,则跳过获取锁及检查。
|
||||
*/
|
||||
@Schema(description = "是否开启获取锁及检查")
|
||||
private Boolean enabled;
|
||||
|
||||
/**
|
||||
* 锁定键(lockKey):用于分布式锁的键,唯一标识了应该被锁定的资源。
|
||||
*/
|
||||
@Schema(description = "锁定键")
|
||||
private String lockKey;
|
||||
|
||||
/**
|
||||
* 查询对象:定义了用于检查数据是否已存在的查询条件。
|
||||
*/
|
||||
@Schema(description = "查询对象")
|
||||
private Q query;
|
||||
|
||||
/**
|
||||
* 消息(msg):如果已存在符合查询条件的数据,那么这个消息将被用于抛出 IdempotencyException 异常
|
||||
*/
|
||||
@Schema(description = "消息")
|
||||
private String msg;
|
||||
|
||||
/**
|
||||
* 转为通用的 LockCondition 对象
|
||||
*
|
||||
* @return LockCondition 对象
|
||||
*/
|
||||
public <T> LockCondition<T> toLockCondition(Function<Q, Object> queryMapper) {
|
||||
return new LockCondition<>(enabled, lockKey, QueryWrapperBuilder.build(queryMapper.apply(query)), msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.metalloop.common.core.lock;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 锁对象抽象
|
||||
*
|
||||
* @author zlt
|
||||
* @date 2020/7/28
|
||||
* <p>
|
||||
* Blog: https://zlt2000.gitee.io
|
||||
* Github: https://github.com/zlt2000
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class ZLock implements AutoCloseable {
|
||||
@Getter
|
||||
private final Object lock;
|
||||
|
||||
private final DistributedLock locker;
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
locker.unlock(lock);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.metalloop.common.enums;
|
||||
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 排序方式枚举 ASC-升序 DESC-降序
|
||||
*
|
||||
* @author yy
|
||||
*/
|
||||
public enum Direction {
|
||||
|
||||
/**
|
||||
* 升序
|
||||
*/
|
||||
ASC,
|
||||
|
||||
/**
|
||||
* 降序
|
||||
*/
|
||||
DESC;
|
||||
|
||||
/**
|
||||
* 是否升序
|
||||
*/
|
||||
public boolean isAscending() {
|
||||
return this.equals(ASC);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否降序
|
||||
*/
|
||||
public boolean isDescending() {
|
||||
return this.equals(DESC);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过字符串获取排序枚举
|
||||
*
|
||||
* @param value 字符串
|
||||
* @throws IllegalArgumentException 无法解析错误.
|
||||
*/
|
||||
public static Direction fromString(String value) {
|
||||
|
||||
try {
|
||||
return Direction.valueOf(value.toUpperCase(Locale.US));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"无效值 '%s'! 必须为 'desc' 或者 'asc' (大小写不敏感).", value), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.metalloop.common.enums;
|
||||
|
||||
import cn.dev33.satoken.stp.StpLogic;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 自定义账号类型,多账号体系时以此值区分,sa-token框架默认的账号类型值为login,可以根据需求自定义,但是必须
|
||||
* 在本类同包的下面增加对应的新的权限认证类,比如: StpXxxUtil.java。
|
||||
* 将cn.dev33.satoken.stp.StpUtil.java类的全部代码复制粘贴到 StpXxxUtil.java里。
|
||||
* 更改一下其 stpLogic,从此枚举类中获取。
|
||||
* <p>
|
||||
* 自定义的账号类型要记录到下面:
|
||||
* <p>
|
||||
* admin: 管理员系统使用的账号类型
|
||||
* <p>
|
||||
* app: App 使用的账号类型,和login的区别是会话超时时间,app的会话超时时间长一些
|
||||
* <p>
|
||||
* self: 自助服务终端使用的账号类型,和login的区别是会话超时时间
|
||||
*/
|
||||
@Getter
|
||||
public enum LoginType {
|
||||
|
||||
/**
|
||||
* login
|
||||
*/
|
||||
DEFAULT(StpUtil.getLoginType(), StpUtil.getStpLogic()),
|
||||
|
||||
/**
|
||||
* admin
|
||||
*/
|
||||
ADMIN("admin", new StpLogic("admin")),
|
||||
|
||||
/**
|
||||
* self
|
||||
*/
|
||||
SELF("self", new StpLogic("self")),
|
||||
|
||||
/**
|
||||
* wechat
|
||||
*/
|
||||
WECHAT("wechat", new StpLogic("wechat"));
|
||||
|
||||
private final String type;
|
||||
private final StpLogic stpLogic;
|
||||
|
||||
private static final Map<String, LoginType> TYPE_TO_ENUM = Arrays.stream(LoginType.values())
|
||||
.collect(Collectors.toMap(LoginType::getType, Function.identity()));
|
||||
|
||||
private static final Map<StpLogic, LoginType> STPLOGIC_TO_ENUM = Arrays.stream(LoginType.values())
|
||||
.collect(Collectors.toMap(LoginType::getStpLogic, Function.identity()));
|
||||
|
||||
LoginType(String type, StpLogic stpLogic) {
|
||||
this.type = type;
|
||||
this.stpLogic = stpLogic;
|
||||
}
|
||||
|
||||
public static LoginType of(String type) {
|
||||
return Optional.ofNullable(TYPE_TO_ENUM.get(type))
|
||||
.orElseThrow(() -> new IllegalArgumentException("账号类型错误"));
|
||||
}
|
||||
|
||||
public static LoginType of(StpLogic stpLogic) {
|
||||
return Optional.ofNullable(STPLOGIC_TO_ENUM.get(stpLogic))
|
||||
.orElseThrow(() -> new IllegalArgumentException("账号类型错误"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
/**
|
||||
* 如果身份验证请求因为账号已过期而被拒绝,则抛出该异常。
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-20
|
||||
*/
|
||||
public class AccountExpiredException extends AccountStatusException {
|
||||
|
||||
/**
|
||||
* 构造带有指定消息的AccountExpiredException。
|
||||
*
|
||||
* @param msg 详细信息
|
||||
*/
|
||||
public AccountExpiredException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造具有指定消息和根本原因的AccountExpiredException。
|
||||
*
|
||||
* @param msg 详细信息
|
||||
* @param cause 根本原因
|
||||
*/
|
||||
public AccountExpiredException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
/**
|
||||
* 由特定账户状态(锁定、禁用等)引起的身份验证异常的基类。
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-20
|
||||
*/
|
||||
public abstract class AccountStatusException extends AuthenticationException {
|
||||
|
||||
public AccountStatusException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public AccountStatusException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
/**
|
||||
* 任何原因无效的身份验证对象相关异常的抽象超类
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public abstract class AuthenticationException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 构造具有指定消息和根本原因的AuthenticationException 。
|
||||
*
|
||||
* @param msg 详细信息
|
||||
* @param cause 根本原因
|
||||
*/
|
||||
public AuthenticationException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造具有指定消息且无根本原因的AuthenticationException 。
|
||||
*
|
||||
* @param msg 详细信息
|
||||
*/
|
||||
public AuthenticationException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
/**
|
||||
* 如果身份验证请求因凭据无效而被拒绝,则抛出该异常。抛出这个异常,说明账号既没有被锁定也没有被禁用.
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public class BadCredentialsException extends AuthenticationException {
|
||||
|
||||
/**
|
||||
* 构造带有指定消息的BadCredentialsException 。
|
||||
*
|
||||
* @param msg 详细信息
|
||||
*/
|
||||
public BadCredentialsException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造具有指定消息和根本原因的BadCredentialsException 。
|
||||
*
|
||||
* @param msg 详细信息
|
||||
* @param cause 根本原因
|
||||
*/
|
||||
public BadCredentialsException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
|
||||
import com.metalloop.common.exception.code.CodeMsg;
|
||||
import com.metalloop.common.exception.code.ResponseCode;
|
||||
|
||||
/**
|
||||
* 业务异常
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2022-12-27
|
||||
*/
|
||||
public class BusinessException extends RuntimeException {
|
||||
private static final long serialVersionUID = 6610083281801529147L;
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private final Integer code;
|
||||
|
||||
|
||||
public BusinessException(int code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public BusinessException(String message) {
|
||||
this(ResponseCode.FAIL.getCode(), message);
|
||||
}
|
||||
|
||||
public BusinessException(CodeMsg codeMsg) {
|
||||
this(codeMsg.getCode(), codeMsg.getMsg());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误码
|
||||
*
|
||||
* @return 错误码
|
||||
*/
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据错误码和错误消息构建业务异常
|
||||
*
|
||||
* @param code 错误码
|
||||
* @param message 错误消息
|
||||
* @return 业务异常
|
||||
*/
|
||||
public static BusinessException with(int code, String message) {
|
||||
return new BusinessException(code, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据错误消息构建业务异常
|
||||
*
|
||||
* @param message 错误消息
|
||||
* @return 业务异常
|
||||
*/
|
||||
public static BusinessException message(String message) {
|
||||
return new BusinessException(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据错误码与错误消息顶级接口的实现类构建业务异常
|
||||
*
|
||||
* @param codeMsg 错误码与错误消息顶级接口的实现类
|
||||
* @return 业务异常
|
||||
*/
|
||||
public static BusinessException with(CodeMsg codeMsg) {
|
||||
return new BusinessException(codeMsg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
/**
|
||||
* 如果身份验证请求因账户凭据已过期而被拒绝,则抛出该异常。
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-20
|
||||
*/
|
||||
public class CredentialsExpiredException extends AccountStatusException {
|
||||
|
||||
/**
|
||||
* 构造带有指定消息的CredentialsExpiredException 。
|
||||
*
|
||||
* @param msg 详细消息
|
||||
*/
|
||||
public CredentialsExpiredException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造具有指定消息和根本原因的CredentialsExpiredException
|
||||
*
|
||||
* @param msg 详细消息
|
||||
* @param cause 根本原因
|
||||
*/
|
||||
public CredentialsExpiredException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
import cn.dev33.satoken.exception.*;
|
||||
import cn.hutool.core.exceptions.ExceptionUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import com.metalloop.common.exception.code.ResponseCode;
|
||||
import com.metalloop.common.exception.model.ApiErrorLog;
|
||||
import com.metalloop.common.model.Result;
|
||||
import com.metalloop.common.utils.json.JsonUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.sql.SQLException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 异常通用处理
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-25
|
||||
*/
|
||||
@ResponseBody
|
||||
@Slf4j
|
||||
public abstract class DefaultExceptionAdvice {
|
||||
|
||||
/**
|
||||
* 自定义消息包装正则表达式,用于提取自定义消息,特别是错误消息由框架接管,拼接了不友好的信息情况下的消息还原,比如参数校验异常
|
||||
*/
|
||||
public static final Pattern CUSTOM_MESSAGE_PATTERN = Pattern.compile("\\{CustomMessage\\{([^}]+)}}");
|
||||
|
||||
/**
|
||||
* IllegalArgumentException异常处理返回json
|
||||
*/
|
||||
@ExceptionHandler({IllegalArgumentException.class})
|
||||
public Result<?> badRequestException(IllegalArgumentException e) {
|
||||
return doHandle("参数解析失败", e);
|
||||
}
|
||||
|
||||
/**
|
||||
* AccessDeniedException异常处理返回json
|
||||
*/
|
||||
@ExceptionHandler({AccessDeniedException.class})
|
||||
public Result<?> badMethodExpressException(AccessDeniedException e) {
|
||||
return doHandle("没有权限请求当前方法", e);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@ExceptionHandler({HttpRequestMethodNotSupportedException.class})
|
||||
public Result<?> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
|
||||
return doHandle("不支持当前请求方法", e);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
@ExceptionHandler({HttpMediaTypeNotSupportedException.class})
|
||||
public Result<?> handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
|
||||
return doHandle("不支持当前媒体类型", e);
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLException sql异常处理
|
||||
*/
|
||||
@ExceptionHandler({SQLException.class})
|
||||
public Result<?> handleSqlException(SQLException e) {
|
||||
return doHandle("服务异常", e, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* BusinessException 业务异常处理
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public Result<?> handleException(BusinessException e) {
|
||||
log.error("业务异常", e);
|
||||
return Result.fail(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* ValidationFailedException 参数校验异常
|
||||
*/
|
||||
@ExceptionHandler(ValidationFailedException.class)
|
||||
public Result<?> handleValidationFailedException(ValidationFailedException e) {
|
||||
log.error("参数校验异常", e);
|
||||
return Result.fail(e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* IdempotencyException 幂等性异常
|
||||
*/
|
||||
@ExceptionHandler(IdempotencyException.class)
|
||||
public Result<?> handleException(IdempotencyException e) {
|
||||
log.error("幂等性异常", e);
|
||||
return doHandle(e.getMessage(), e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数校验异常
|
||||
*
|
||||
* @param e MethodArgumentNotValidException
|
||||
* @return Result
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Result<?> handlerException(MethodArgumentNotValidException e) {
|
||||
log.error("参数校验异常", e);
|
||||
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
|
||||
String fieldErrorMsg = getFieldErrorMsg(fieldErrors);
|
||||
return doHandle(fieldErrorMsg, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数校验异常
|
||||
*
|
||||
* @param e BindException
|
||||
* @return Result
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public Result<?> handlerException(BindException e) {
|
||||
log.error("参数校验异常", e);
|
||||
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
|
||||
String fieldErrorMsg = getFieldErrorMsg(fieldErrors);
|
||||
return doHandle(fieldErrorMsg, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证异常
|
||||
*
|
||||
* @param e AuthenticationException
|
||||
* @return Result
|
||||
*/
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
public Result<?> handlerException(AuthenticationException e) {
|
||||
log.error("认证异常", e);
|
||||
return doHandle(e.getMessage(), e);
|
||||
}
|
||||
|
||||
// region sa-token 异常处理
|
||||
|
||||
@ExceptionHandler(value = {NotLoginException.class})
|
||||
public Result<?> handle(NotLoginException e) {
|
||||
log.error("NotLoginException:{}", e.getMessage());
|
||||
return Result.fail(ResponseCode.NO_SESSION);
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = {NotPermissionException.class})
|
||||
public Result<?> handle(NotPermissionException e) {
|
||||
log.error("NotPermissionException:{}", e.getMessage());
|
||||
return Result.fail(ResponseCode.ACCESS_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = {NotRoleException.class})
|
||||
public Result<?> handle(NotRoleException e) {
|
||||
log.error("NotRoleException:{}", e.getMessage());
|
||||
return Result.fail(ResponseCode.ACCESS_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = {NotSafeException.class})
|
||||
public Result<?> handle(NotSafeException e) {
|
||||
log.error("NotSafeException:{}", e.getMessage());
|
||||
return Result.fail(ResponseCode.ACCESS_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@ExceptionHandler(value = {SameTokenInvalidException.class})
|
||||
public Result<?> handle(SameTokenInvalidException e) {
|
||||
log.error("SameTokenInvalidException:{}", e.getMessage());
|
||||
return Result.fail(ResponseCode.TOKEN_INVALID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Sa-Token的异常
|
||||
*
|
||||
* @param e Sa-Token异常
|
||||
* @return Result
|
||||
*/
|
||||
@ExceptionHandler(value = {SaTokenException.class})
|
||||
public Result<?> handle(SaTokenException e) {
|
||||
log.error("认证异常", e);
|
||||
return doHandle(e.getMessage(), e);
|
||||
}
|
||||
// endregion sa-token 异常处理
|
||||
|
||||
/**
|
||||
* 所有未知异常统一处理
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Result<?> handleException(HttpServletRequest req, Exception ex) {
|
||||
log.error("[defaultExceptionHandler]", ex);
|
||||
// 插入异常日志
|
||||
this.createExceptionLog(req, ex);
|
||||
// 返回 ERROR Result
|
||||
return doHandle("系统繁忙", ex, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理异常,默认不打印异常堆栈,并返回前端异常信息
|
||||
*
|
||||
* @param msg 返给前端的异常信息
|
||||
* @param e 异常
|
||||
* @return 返回前端的响应体
|
||||
*/
|
||||
private Result<?> doHandle(String msg, Exception e) {
|
||||
return doHandle(msg, e, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理异常,并返回前端异常信息
|
||||
*
|
||||
* @param msg 返给前端的异常信息
|
||||
* @param e 异常
|
||||
* @param ifPrintStackTrace 是否打印异常堆栈,系统级或未知异常建议打印
|
||||
* @return 返回前端的响应体
|
||||
*/
|
||||
private Result<?> doHandle(String msg, Exception e, boolean ifPrintStackTrace) {
|
||||
if (ifPrintStackTrace) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
log.error(msg, e);
|
||||
return Result.fail(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数校验错误信息
|
||||
*
|
||||
* @param fieldErrors fieldErrors
|
||||
* @return 参数校验错误信息
|
||||
*/
|
||||
private String getFieldErrorMsg(List<FieldError> fieldErrors) {
|
||||
StringBuilder errorMsg = new StringBuilder("参数校验失败:");
|
||||
fieldErrors.forEach(fieldError -> {
|
||||
String defaultMessage = fieldError.getDefaultMessage();
|
||||
String message = extractCustomMessageIfNecessary(defaultMessage);
|
||||
errorMsg.append(fieldError.getField()).append("-").append(message).append(";");
|
||||
});
|
||||
return errorMsg.toString();
|
||||
}
|
||||
|
||||
private void createExceptionLog(HttpServletRequest req, Throwable e) {
|
||||
// 插入错误日志
|
||||
ApiErrorLog errorLog = new ApiErrorLog();
|
||||
try {
|
||||
// 初始化 errorLog
|
||||
initExceptionLog(errorLog, req, e);
|
||||
// 执行插入 errorLog
|
||||
appendErrorLog(errorLog);
|
||||
} catch (Throwable th) {
|
||||
log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取自定义消息如果有的话
|
||||
*
|
||||
* @param message 原始消息,可能包含 {CustomMessage{...}}
|
||||
* @return 提取后的自定义消息
|
||||
*/
|
||||
private String extractCustomMessageIfNecessary(String message) {
|
||||
Matcher matcher = CUSTOM_MESSAGE_PATTERN.matcher(message);
|
||||
StringBuilder customMessages = new StringBuilder();
|
||||
boolean found = false;
|
||||
|
||||
while (matcher.find()) {
|
||||
customMessages.append(matcher.group(1)).append(";");
|
||||
found = true;
|
||||
}
|
||||
|
||||
if (found) {
|
||||
// 移除最后的分号
|
||||
if (customMessages.length() > 0 && customMessages.charAt(customMessages.length() - 1) == ';') {
|
||||
customMessages.setLength(customMessages.length() - 1);
|
||||
}
|
||||
return customMessages.toString();
|
||||
}
|
||||
|
||||
// 如果没有匹配到自定义消息,返回原始消息
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入错误日志, 子类实现
|
||||
*
|
||||
* @param errorLog 错误日志
|
||||
*/
|
||||
protected void appendErrorLog(ApiErrorLog errorLog) {
|
||||
// 插入错误日志,默认不做任何事情
|
||||
}
|
||||
|
||||
private void initExceptionLog(ApiErrorLog errorLog, HttpServletRequest request, Throwable e) {
|
||||
// 设置异常字段
|
||||
errorLog.setExceptionName(e.getClass().getName());
|
||||
errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));
|
||||
errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
|
||||
errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e));
|
||||
StackTraceElement[] stackTraceElements = e.getStackTrace();
|
||||
Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
|
||||
StackTraceElement stackTraceElement = stackTraceElements[0];
|
||||
errorLog.setExceptionClassName(stackTraceElement.getClassName());
|
||||
errorLog.setExceptionFileName(stackTraceElement.getFileName());
|
||||
errorLog.setExceptionMethodName(stackTraceElement.getMethodName());
|
||||
errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
|
||||
// 设置其它字段
|
||||
errorLog.setTraceId(MDC.get("traceId"));
|
||||
errorLog.setRequestUrl(request.getRequestURI());
|
||||
// Map<String, Object> requestParams = MapUtil.<String, Object>builder()
|
||||
// .put("query", ServletUtil.getParamMap(request))
|
||||
// .put("body", ServletUtil.getBody(request)).build();
|
||||
// errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
|
||||
// errorLog.setRequestMethod(request.getMethod());
|
||||
// errorLog.setUserAgent(ServletUtils.getUserAgent(request));
|
||||
// errorLog.setUserIp(ServletUtil.getClientIP(request));
|
||||
// errorLog.setExceptionTime(LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
/**
|
||||
* 如果身份验证请求因账户被禁用而被拒绝,则抛出该异常。
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-20
|
||||
*/
|
||||
public class DisabledException extends AccountStatusException {
|
||||
|
||||
/**
|
||||
* 构造带有指定消息的DisabledException 。
|
||||
*
|
||||
* @param msg 详细消息
|
||||
*/
|
||||
public DisabledException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造具有指定消息和根本原因的DisabledException 。
|
||||
*
|
||||
* @param msg 详细消息
|
||||
* @param cause 根本原因
|
||||
*/
|
||||
public DisabledException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
/**
|
||||
* 幂等性异常
|
||||
*
|
||||
* @author zlt
|
||||
*/
|
||||
public class IdempotencyException extends RuntimeException {
|
||||
private static final long serialVersionUID = 6610083281801529147L;
|
||||
|
||||
public IdempotencyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
/**
|
||||
* 分布式锁异常
|
||||
*
|
||||
* @author zlt
|
||||
*/
|
||||
public class LockException extends RuntimeException {
|
||||
private static final long serialVersionUID = 6610083281801529147L;
|
||||
|
||||
public LockException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
/**
|
||||
* 如果身份验证请求因账户被锁定而被拒绝,则抛出该异常。
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-20
|
||||
*/
|
||||
public class LockedException extends AccountStatusException {
|
||||
|
||||
/**
|
||||
* 构造一个带有指定消息的LockedException 。
|
||||
*
|
||||
* @param msg 详细信息
|
||||
*/
|
||||
public LockedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造具有指定消息和根本原因的LockedException 。
|
||||
*
|
||||
* @param msg 详细信息
|
||||
* @param cause 根本原因
|
||||
*/
|
||||
public LockedException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
/**
|
||||
* 如果UserDetailsService实现无法通过用户名找到用户,则抛出该异常。
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public class UsernameNotFoundException extends AuthenticationException {
|
||||
|
||||
/**
|
||||
* 构造带有指定消息的UsernameNotFoundException 。
|
||||
*
|
||||
* @param msg 详细信息。
|
||||
*/
|
||||
public UsernameNotFoundException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造具有指定消息和根本原因的UsernameNotFoundException 。
|
||||
* 形参:
|
||||
*
|
||||
* @param msg 详细信息
|
||||
* @param cause 根本原因
|
||||
*/
|
||||
public UsernameNotFoundException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.metalloop.common.exception;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 参数校验失败异常
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @version 2023-05-04
|
||||
*/
|
||||
public class ValidationFailedException extends RuntimeException {
|
||||
private final Map<String, List<String>> linkedEntities;
|
||||
|
||||
public ValidationFailedException(String message) {
|
||||
super(message);
|
||||
this.linkedEntities = Collections.emptyMap();
|
||||
}
|
||||
|
||||
public ValidationFailedException(String message, Map<String, List<String>> linkedEntities) {
|
||||
super(message);
|
||||
this.linkedEntities = linkedEntities;
|
||||
}
|
||||
|
||||
public ValidationFailedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.linkedEntities = Collections.emptyMap();
|
||||
}
|
||||
|
||||
public Map<String, List<String>> getLinkedEntities() {
|
||||
return linkedEntities;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.metalloop.common.exception.code;
|
||||
|
||||
/**
|
||||
* 错误码与错误消息的顶级接口
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2022-12-27
|
||||
*/
|
||||
public interface CodeMsg {
|
||||
/**
|
||||
* 获取错误码
|
||||
*
|
||||
* @return 获取错误码
|
||||
*/
|
||||
Integer getCode();
|
||||
|
||||
/**
|
||||
* 获取错误消息
|
||||
*
|
||||
* @return 获取错误消息
|
||||
*/
|
||||
String getMsg();
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.metalloop.common.exception.code;
|
||||
|
||||
/**
|
||||
* 通用错误码
|
||||
*
|
||||
* @author Lei
|
||||
* @since 2022/10/26 20:12
|
||||
*/
|
||||
@SuppressWarnings("AlibabaEnumConstantsMustHaveComment")
|
||||
public enum ResponseCode implements CodeMsg {
|
||||
|
||||
SUCCESS(0, "操作成功"),
|
||||
FAIL(1, "操作失败"),
|
||||
|
||||
SYSTEM_BUSY(500001, "系统繁忙,请稍候再试"),
|
||||
OPERATION_ERROR(500002, "操作失败"),
|
||||
FIELD_BINDING_ERROR(400001, "请求字段参数校验失败"),
|
||||
FIELD_FORMAT_ERROR(400002, "请求参数格式错误"),
|
||||
FIELD_MISSING_ERROR(400003, "请求参数不完整或不对"),
|
||||
REQUEST_BODY_MISSING(400004, "需要请求体"),
|
||||
|
||||
ACCOUNT_CLIENT_ERROR(400000, "用户端错误"),
|
||||
|
||||
ACCOUNT_REGISTER_ERROR(400100, "用户注册异常"),
|
||||
ACCOUNT_EXISTS(400101, "用户名已存在"),
|
||||
ACCOUNT_ERROR(400102, "用户名校验错误"),
|
||||
ACCOUNT_CONTAINS_SENSITIVE(400103, "用户名包含敏感词 "),
|
||||
ACCOUNT_CONTAINS_SPECIAL(400104, "用户名包含特殊字符"),
|
||||
PASSWORD_ERROR(400105, "密码校验错误"),
|
||||
PASSWORD_TOO_SHORT(400106, "密码长度不够"),
|
||||
PASSWORD_TOO_WEAK(400107, "密码强度不够"),
|
||||
VCODE_ERROR(400108, "校验码错误"),
|
||||
SMS_VCODE_ERROR(400109, "短信校验码错误"),
|
||||
EMAIL_VCODE_ERROR(400110, "邮件校验码错误"),
|
||||
ID_NUMBER_FORMAT_ERROR(400111, "身份证号格式错误"),
|
||||
PHONE_NUMBER_FORMAT_ERROR(400112, "手机号格式错误"),
|
||||
ADDRESS_ERROR(400113, "地址错误"),
|
||||
EMAIL_FORMAT_ERROR(400114, "邮箱格式错误"),
|
||||
|
||||
ACCOUNT_LOGIN_EXCEPTION(400200, "用户登录异常"),
|
||||
NOT_ACCOUNT(400201, "用户名不存在"),
|
||||
ACCOUNT_IS_LOCKED(400202, "用户被锁定,请联系管理人员"),
|
||||
ACCOUNT_HAS_EXPIRED(400203, "用户已作废"),
|
||||
USERNAME_OR_PASSWORD_WRONG(400204, "用户名或密码错误"),
|
||||
WRONG_PASSWORD(400205, "密码错误"),
|
||||
WRONG_PASSWORDS_OVER_LIMIT(400206, "用户输入密码错误次数超限"),
|
||||
ACCOUNT_LOGIN_EXPIRED(400207, "用户登录已过期"),
|
||||
ACCOUNT_VCODE_ERROR(400208, "用户验证码错误"),
|
||||
WRONG_VCODE_OVER_LIMIT(400209, "用户验证码错误次数超限"),
|
||||
NO_AUTHORIZED(400210, "用户未获得第三方登录授权 "),
|
||||
NO_SESSION(400211, "登录信息失效"),
|
||||
CREDENTIALS_EXPIRED(400212, "登录凭证已过期,请重新登录"),
|
||||
CREDENTIALS_EXCEPTION(400213, "登录凭证异常,请重新登录"),
|
||||
ID_NUMBER_ERROR(400214, "身份证号错误"),
|
||||
PHONE_NUMBER_ERROR(400215, "手机号错误"),
|
||||
EMAIL_ERROR(400216, "邮箱错误"),
|
||||
|
||||
ACCOUNT_ACCESS_EXCEPTION(400300, "用户访问权限异常"),
|
||||
ACCESS_UNAUTHORIZED(400301, "访问未授权"),
|
||||
NOT_TOKEN(400302, "未提供token"),
|
||||
TOKEN_INVALID(400303, "token无效"),
|
||||
TOKEN_TIMEOUT(400304, "token已过期"),
|
||||
TOKEN_BE_REPLACED(400305, "token已被顶下线"),
|
||||
TOKEN_KICK_OUT(400306, "token已被踢下线"),
|
||||
TOKEN_OTHER(400307, "token异常"),
|
||||
|
||||
PARAMETER_ERROR(400400, "用户请求参数错误"),
|
||||
NOT_PARAMETER(400401, "请求必填参数为空"),
|
||||
PARAMETER_VERIFICATION_ERROR(400402, "请求参数指定校验失败"),
|
||||
PARAMETER_FORMAT_ERROR(400403, "请求参数格式错误"),
|
||||
INCOMPLETE_PARAMETERS(400404, "请求参数不完整"),
|
||||
NOT_REQUEST_BODY(400405, "无请求体"),
|
||||
NOT_ID(400406, "id为空"),
|
||||
NOT_ORGCODE(400407, "机构编码为空"),
|
||||
NOT_SITECODE(400408, "站点编码为空"),
|
||||
|
||||
PERMISSION_MANAGEMENT_EXCEPTION(400500, "权限管理异常"),
|
||||
NOT_ORG(400501, "机构不存在"),
|
||||
ORG_HAS_SUBORG(400502, "该机构存在下级机构,无法删除"),
|
||||
ORGNAME_EXISTS(400503, "机构名已存在"),
|
||||
ORG_LINKED_USER(400504, "机构关联用户,无法删除"),
|
||||
ROLENAME_EXISTS(400505, "角色名已存在"),
|
||||
ROLE_LINKED_USER(400506, "角色关联用户,无法删除"),
|
||||
NOT_ROLE(400507, "角色为空"),
|
||||
NOT_PERMISSION(400508, "权限为空"),
|
||||
|
||||
REQUEST_SERVICE_EXCEPTION(400600, "用户请求服务异常"),
|
||||
REQUEST_COUNT_OVER_LIMIT(400601, "请求次数超出限制 "),
|
||||
REQUEST_CONCURRENCY_COUNT_OVER_LIMIT(400602, "请求并发数超出限制 "),
|
||||
WEBSOCKET_CONNECTION_EXCEPTION(400603, "WebSocket 连接异常"),
|
||||
WEBSOCKET_CONNECTION_DISCONNECTED(400604, "WebSocket 连接断开"),
|
||||
USER_REPEATED_REQUEST(400605, "用户重复请求"),
|
||||
|
||||
GATEWAY_ERROR(400700, "网关异常"),
|
||||
GATEWAY_NOT_FOUND_SERVICE(400701, "服务未找到"),
|
||||
GATEWAY_CONNECT_TIME_OUT(400702, "网关超时"),
|
||||
UPLOAD_FILE_SIZE_LIMIT(400703, "上传文件大小超过限制"),
|
||||
DUPLICATE_PRIMARY_KEY(400704, "唯一键冲突");
|
||||
|
||||
/**
|
||||
* 错误码
|
||||
*/
|
||||
private final Integer code;
|
||||
|
||||
/**
|
||||
* 错误消息
|
||||
*/
|
||||
private final String msg;
|
||||
|
||||
ResponseCode(int code, String msg) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.metalloop.common.exception.model;
|
||||
|
||||
import lombok.Data;
|
||||
//
|
||||
//import javax.validation.constraints.NotNull;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* API 错误日志
|
||||
*
|
||||
* @author zhaowenhao
|
||||
*/
|
||||
@Data
|
||||
public class ApiErrorLog {
|
||||
|
||||
/**
|
||||
* 链路编号
|
||||
*/
|
||||
private String traceId;
|
||||
/**
|
||||
* 账号编号
|
||||
*/
|
||||
private Long userId;
|
||||
/**
|
||||
* 用户类型
|
||||
*/
|
||||
private Integer userType;
|
||||
/**
|
||||
* 应用名
|
||||
*/
|
||||
// @NotNull(message = "应用名不能为空")
|
||||
private String applicationName;
|
||||
|
||||
/**
|
||||
* 请求方法名
|
||||
*/
|
||||
// @NotNull(message = "http 请求方法不能为空")
|
||||
private String requestMethod;
|
||||
/**
|
||||
* 访问地址
|
||||
*/
|
||||
// @NotNull(message = "访问地址不能为空")
|
||||
private String requestUrl;
|
||||
/**
|
||||
* 请求参数
|
||||
*/
|
||||
// @NotNull(message = "请求参数不能为空")
|
||||
private String requestParams;
|
||||
/**
|
||||
* 用户 IP
|
||||
*/
|
||||
// @NotNull(message = "ip 不能为空")
|
||||
private String userIp;
|
||||
/**
|
||||
* 浏览器 UA
|
||||
*/
|
||||
// @NotNull(message = "User-Agent 不能为空")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 异常时间
|
||||
*/
|
||||
// @NotNull(message = "异常时间不能为空")
|
||||
private LocalDateTime exceptionTime;
|
||||
/**
|
||||
* 异常名
|
||||
*/
|
||||
// @NotNull(message = "异常名不能为空")
|
||||
private String exceptionName;
|
||||
/**
|
||||
* 异常发生的类全名
|
||||
*/
|
||||
// @NotNull(message = "异常发生的类全名不能为空")
|
||||
private String exceptionClassName;
|
||||
/**
|
||||
* 异常发生的类文件
|
||||
*/
|
||||
// @NotNull(message = "异常发生的类文件不能为空")
|
||||
private String exceptionFileName;
|
||||
/**
|
||||
* 异常发生的方法名
|
||||
*/
|
||||
// @NotNull(message = "异常发生的方法名不能为空")
|
||||
private String exceptionMethodName;
|
||||
/**
|
||||
* 异常发生的方法所在行
|
||||
*/
|
||||
// @NotNull(message = "异常发生的方法所在行不能为空")
|
||||
private Integer exceptionLineNumber;
|
||||
/**
|
||||
* 异常的栈轨迹异常的栈轨迹
|
||||
*/
|
||||
// @NotNull(message = "异常的栈轨迹不能为空")
|
||||
private String exceptionStackTrace;
|
||||
/**
|
||||
* 异常导致的根消息
|
||||
*/
|
||||
// @NotNull(message = "异常导致的根消息不能为空")
|
||||
private String exceptionRootCauseMessage;
|
||||
/**
|
||||
* 异常导致的消息
|
||||
*/
|
||||
// @NotNull(message = "异常导致的消息不能为空")
|
||||
private String exceptionMessage;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.metalloop.common.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.metalloop.common.exception.code.ResponseCode;
|
||||
import com.metalloop.common.exception.BusinessException;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 响应包装体
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2022-12-27
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Result<T> implements Serializable {
|
||||
|
||||
private T data;
|
||||
private Integer code;
|
||||
private String msg;
|
||||
|
||||
public static <T> Result<T> ok() {
|
||||
return of(null, ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg());
|
||||
}
|
||||
|
||||
public static <T> Result<T> ok(T data) {
|
||||
return of(data, ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg());
|
||||
}
|
||||
|
||||
public static <T> Result<T> ok(T data, String msg) {
|
||||
return of(data, ResponseCode.SUCCESS.getCode(), msg);
|
||||
}
|
||||
|
||||
public static <T> Result<T> fail() {
|
||||
return of(null, ResponseCode.FAIL.getCode(), ResponseCode.FAIL.getMsg());
|
||||
}
|
||||
|
||||
public static <T> Result<T> fail(String msg) {
|
||||
return of(null, ResponseCode.FAIL.getCode(), msg);
|
||||
}
|
||||
|
||||
public static <T> Result<T> fail(int code, String msg) {
|
||||
return of(null, code, msg);
|
||||
}
|
||||
|
||||
public static <T> Result<T> fail(ResponseCode responseCode) {
|
||||
return of(null, responseCode.getCode(), responseCode.getMsg());
|
||||
}
|
||||
|
||||
public static <T> Result<T> of(T data, int code, String msg) {
|
||||
return new Result<>(data, code, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否成功
|
||||
*
|
||||
* @param code 状态码
|
||||
* @return 是否成功
|
||||
*/
|
||||
public static boolean isSuccess(Integer code) {
|
||||
return Objects.equals(code, ResponseCode.SUCCESS.getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否成功
|
||||
*
|
||||
* @return 是否成功
|
||||
*/
|
||||
@JsonIgnore
|
||||
public boolean isSuccess() {
|
||||
return isSuccess(code);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isError() {
|
||||
return !isSuccess();
|
||||
}
|
||||
|
||||
// ========= 和 Exception 异常体系集成 =========
|
||||
|
||||
/**
|
||||
* 判断是否有异常。如果有,则抛出 {@link BusinessException} 异常
|
||||
*/
|
||||
public void checkError() throws BusinessException {
|
||||
if (isSuccess()) {
|
||||
return;
|
||||
}
|
||||
// 业务异常
|
||||
throw new BusinessException(code, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否有异常。如果有,则抛出 {@link BusinessException} 异常
|
||||
* 如果没有,则返回 {@link #data} 数据
|
||||
*/
|
||||
@JsonIgnore
|
||||
public T getCheckedData() {
|
||||
checkError();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.metalloop.common.redis;
|
||||
|
||||
import com.metalloop.common.redis.properties.CacheManagerProperties;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.interceptor.KeyGenerator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
|
||||
import org.springframework.data.redis.cache.RedisCacheManager;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Redis 配置类,键强制为string序列化,值默认使用json序列化,同时支持java序列化、string序列化
|
||||
* 根据需求自行选择
|
||||
* 缓存默认启用
|
||||
*
|
||||
* @author zhaowenhao
|
||||
*/
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
@EnableConfigurationProperties({RedisProperties.class, CacheManagerProperties.class})
|
||||
@EnableCaching
|
||||
@Component
|
||||
public class RedisAutoConfigure {
|
||||
@Autowired
|
||||
private CacheManagerProperties cacheManagerProperties;
|
||||
|
||||
@Bean
|
||||
public RedisSerializer<String> redisKeyStringSerializer() {
|
||||
return RedisSerializer.string();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisSerializer<Object> redisValueJsonSerializer() {
|
||||
return RedisSerializer.json();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RedisSerializer<Object> redisValueJavaSerializer() {
|
||||
return RedisSerializer.java();
|
||||
}
|
||||
|
||||
/**
|
||||
* string-json RedisTemplate配置,默认使用
|
||||
*
|
||||
* @param factory RedisConnectionFactory
|
||||
* @param redisKeyStringSerializer RedisSerializer
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory, RedisSerializer<String> redisKeyStringSerializer, RedisSerializer<Object> redisValueJsonSerializer) {
|
||||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||
redisTemplate.setConnectionFactory(factory);
|
||||
|
||||
redisTemplate.setDefaultSerializer(redisValueJsonSerializer);
|
||||
redisTemplate.setKeySerializer(redisKeyStringSerializer);
|
||||
redisTemplate.setHashKeySerializer(redisKeyStringSerializer);
|
||||
redisTemplate.afterPropertiesSet();
|
||||
return redisTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* string-string RedisTemplate配置
|
||||
*
|
||||
* @param redisConnectionFactory RedisConnectionFactory
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
StringRedisTemplate template = new StringRedisTemplate();
|
||||
template.setConnectionFactory(redisConnectionFactory);
|
||||
return template;
|
||||
}
|
||||
|
||||
/**
|
||||
* string-java RedisTemplate配置
|
||||
*
|
||||
* @param factory RedisConnectionFactory
|
||||
* @param redisKeyStringSerializer RedisSerializer
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(name = "javaRedisTemplate")
|
||||
public RedisTemplate<String, Object> javaRedisTemplate(RedisConnectionFactory factory, RedisSerializer<String> redisKeyStringSerializer, RedisSerializer<Object> redisValueJavaSerializer) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(factory);
|
||||
|
||||
template.setDefaultSerializer(redisValueJavaSerializer);
|
||||
template.setKeySerializer(redisKeyStringSerializer);
|
||||
template.setHashKeySerializer(redisKeyStringSerializer);
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
|
||||
@Bean(name = "cacheManager")
|
||||
@Primary
|
||||
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, RedisSerializer<String> redisKeyStringSerializer, RedisSerializer<Object> redisValueJsonSerializer) {
|
||||
RedisCacheConfiguration difConf = getDefConf(redisKeyStringSerializer, redisValueJsonSerializer).entryTtl(Duration.ofHours(1));
|
||||
|
||||
// 自定义的缓存过期时间配置
|
||||
int configSize = cacheManagerProperties.getConfigs() == null ? 0 : cacheManagerProperties.getConfigs().size();
|
||||
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(configSize);
|
||||
if (configSize > 0) {
|
||||
cacheManagerProperties.getConfigs().forEach(e -> {
|
||||
RedisCacheConfiguration conf = getDefConf(redisKeyStringSerializer, redisValueJsonSerializer).entryTtl(Duration.ofSeconds(e.getSecond()));
|
||||
redisCacheConfigurationMap.put(e.getKey(), conf);
|
||||
});
|
||||
}
|
||||
|
||||
return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(difConf).withInitialCacheConfigurations(redisCacheConfigurationMap).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public KeyGenerator keyGenerator() {
|
||||
return (target, method, objects) -> {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(target.getClass().getName());
|
||||
sb.append(":").append(method.getName()).append(":");
|
||||
for (Object obj : objects) {
|
||||
sb.append(obj.toString());
|
||||
}
|
||||
return sb.toString();
|
||||
};
|
||||
}
|
||||
|
||||
private RedisCacheConfiguration getDefConf(RedisSerializer<String> redisKeyStringSerializer, RedisSerializer<Object> redisValueJsonSerializer) {
|
||||
return RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues().computePrefixWith(cacheName -> "cache".concat(":").concat(cacheName).concat(":")).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisKeyStringSerializer)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisValueJsonSerializer));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.metalloop.common.redis.lock;
|
||||
|
||||
|
||||
import com.metalloop.common.constant.CommonConstant;
|
||||
import com.metalloop.common.core.lock.DistributedLock;
|
||||
import com.metalloop.common.core.lock.ZLock;
|
||||
import com.metalloop.common.exception.LockException;
|
||||
import org.redisson.api.RLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* redisson分布式锁实现,基本锁功能的抽象实现
|
||||
* 本接口能满足绝大部分的需求,高级的锁功能,请自行扩展或直接使用原生api
|
||||
*
|
||||
* @author zhaowenhao
|
||||
*/
|
||||
@Component
|
||||
public class RedissonDistributedLock implements DistributedLock {
|
||||
@Autowired
|
||||
private RedissonClient redisson;
|
||||
|
||||
private ZLock getLock(String key, boolean isFair) {
|
||||
RLock lock;
|
||||
if (isFair) {
|
||||
lock = redisson.getFairLock(CommonConstant.LOCK_KEY_PREFIX + ":" + key);
|
||||
} else {
|
||||
lock = redisson.getLock(CommonConstant.LOCK_KEY_PREFIX + ":" + key);
|
||||
}
|
||||
return new ZLock(lock, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZLock lock(String key, long leaseTime, TimeUnit unit, boolean isFair) {
|
||||
ZLock zLock = getLock(key, isFair);
|
||||
RLock lock = (RLock) zLock.getLock();
|
||||
lock.lock(leaseTime, unit);
|
||||
return zLock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZLock tryLock(String key, long waitTime, long leaseTime, TimeUnit unit, boolean isFair) throws InterruptedException {
|
||||
ZLock zLock = getLock(key, isFair);
|
||||
RLock lock = (RLock) zLock.getLock();
|
||||
if (lock.tryLock(waitTime, leaseTime, unit)) {
|
||||
return zLock;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlock(Object lock) {
|
||||
if (lock != null) {
|
||||
if (lock instanceof RLock) {
|
||||
RLock rLock = (RLock) lock;
|
||||
if (rLock.isLocked()) {
|
||||
rLock.unlock();
|
||||
}
|
||||
} else {
|
||||
throw new LockException("requires RLock type");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.metalloop.common.redis.properties;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author zlt
|
||||
* @date 2019/1/6
|
||||
*/
|
||||
@Setter
|
||||
@Getter
|
||||
@ConfigurationProperties(prefix = "tocc.cache-manager")
|
||||
public class CacheManagerProperties {
|
||||
private List<CacheConfig> configs;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
public static class CacheConfig {
|
||||
/**
|
||||
* cache key
|
||||
*/
|
||||
private String key;
|
||||
/**
|
||||
* 过期时间,sec
|
||||
*/
|
||||
private long second = 60;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.metalloop.common.redis.properties;
|
||||
|
||||
import com.metalloop.common.core.lock.DistributedLock;
|
||||
import com.metalloop.common.redis.lock.RedissonDistributedLock;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(RedissonClient.class)
|
||||
@ConditionalOnProperty(
|
||||
prefix = "tocc.lock",
|
||||
name = "lockerType",
|
||||
havingValue = "REDIS",
|
||||
matchIfMissing = true
|
||||
)
|
||||
public class RedissonLockAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(DistributedLock.class)
|
||||
public DistributedLock distributedLock() {
|
||||
return new RedissonDistributedLock();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
package com.metalloop.common.redis.template;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.data.redis.connection.RedisClusterNode;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.RedisServerCommands;
|
||||
import org.springframework.data.redis.connection.RedisStringCommands;
|
||||
import org.springframework.data.redis.core.*;
|
||||
import org.springframework.data.redis.core.types.Expiration;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.SerializationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Redis Repository
|
||||
* redis 基本操作 可扩展,基本够用了
|
||||
*
|
||||
* <p>基于 Spring Boot 3.4.x(Spring Data Redis 3.4.x)API 编写。</p>
|
||||
*
|
||||
* @author zhaowenhao
|
||||
*/
|
||||
@Slf4j
|
||||
public class RedisRepository {
|
||||
|
||||
/**
|
||||
* Spring Redis Template
|
||||
*/
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
public RedisRepository(RedisTemplate<String, Object> redisTemplate) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取链接工厂
|
||||
*/
|
||||
public RedisConnectionFactory getConnectionFactory() {
|
||||
return this.redisTemplate.getConnectionFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 RedisTemplate 对象
|
||||
*/
|
||||
public RedisTemplate<String, Object> getRedisTemplate() {
|
||||
return redisTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空DB
|
||||
*
|
||||
* @param node redis 节点
|
||||
*/
|
||||
public void flushDB(RedisClusterNode node) {
|
||||
this.redisTemplate.opsForCluster().flushDb(node);
|
||||
}
|
||||
|
||||
// ======================== 写入类操作 ========================
|
||||
|
||||
/**
|
||||
* 添加到带有过期时间的缓存(byte[] 版本,直接写原始字节,不做任何序列化包装)
|
||||
*
|
||||
* <p>需要写原始字节,无法迁移到 {@code opsForValue()},故保留底层调用。
|
||||
* Spring Data Redis 3.4.x 中 {@code RedisConnection#set} 已标记废弃,
|
||||
* 但尚无等价的带自定义序列化器的高层 API,故显式抑制警告。</p>
|
||||
*
|
||||
* @param key redis 主键
|
||||
* @param value 值
|
||||
* @param time 过期时间(单位秒)
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public void setExpire(final byte[] key, final byte[] value, final long time) {
|
||||
redisTemplate.execute((RedisCallback<Boolean>) connection ->
|
||||
connection.set(key, value,
|
||||
Expiration.seconds(time),
|
||||
RedisStringCommands.SetOption.upsert()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到带有过期时间的缓存(推荐,使用模板自身的序列化器)
|
||||
*
|
||||
* @param key redis 主键
|
||||
* @param value 值
|
||||
* @param time 过期时间
|
||||
* @param timeUnit 过期时间单位
|
||||
*/
|
||||
public void setExpire(final String key, final Object value, final long time, final TimeUnit timeUnit) {
|
||||
redisTemplate.opsForValue().set(key, value, time, timeUnit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到带有过期时间的缓存(默认单位为秒)
|
||||
*
|
||||
* @param key redis 主键
|
||||
* @param value 值
|
||||
* @param time 过期时间(单位秒)
|
||||
*/
|
||||
public void setExpire(final String key, final Object value, final long time) {
|
||||
this.setExpire(key, value, time, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到带有过期时间的缓存(支持自定义 valueSerializer)
|
||||
*
|
||||
* <p>因需要独立的 {@code valueSerializer},无法迁移到 {@code opsForValue()} 高层 API,
|
||||
* 故保留底层调用并抑制弃用警告。</p>
|
||||
*
|
||||
* @param key redis 主键
|
||||
* @param value 值
|
||||
* @param time 过期时间
|
||||
* @param timeUnit 过期时间单位
|
||||
* @param valueSerializer 自定义值序列化器
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public void setExpire(final String key, final Object value, final long time,
|
||||
final TimeUnit timeUnit, RedisSerializer<Object> valueSerializer) {
|
||||
byte[] rawKey = rawKey(key);
|
||||
byte[] rawValue = rawValue(value, valueSerializer);
|
||||
redisTemplate.execute((RedisCallback<Boolean>) connection ->
|
||||
connection.set(rawKey, rawValue,
|
||||
Expiration.from(time, timeUnit),
|
||||
RedisStringCommands.SetOption.upsert()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性添加数组到带过期时间的缓存(使用 Pipeline,减少网络往返)
|
||||
*
|
||||
* @param keys redis 主键数组
|
||||
* @param values 值数组
|
||||
* @param time 过期时间(单位秒)
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public void setExpire(final String[] keys, final Object[] values, final long time) {
|
||||
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
|
||||
RedisSerializer<Object> ks = keySerializer();
|
||||
RedisSerializer<Object> vs = valueSerializer();
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
connection.set(Objects.requireNonNull(ks.serialize(keys[i])), Objects.requireNonNull(vs.serialize(values[i])),
|
||||
Expiration.seconds(time),
|
||||
RedisStringCommands.SetOption.upsert());
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性添加数组到缓存(使用 Pipeline,减少网络往返)
|
||||
*
|
||||
* @param keys 主键数组
|
||||
* @param values 值数组
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public void set(final String[] keys, final Object[] values) {
|
||||
redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
|
||||
RedisSerializer<Object> ks = keySerializer();
|
||||
RedisSerializer<Object> vs = valueSerializer();
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
connection.set(Objects.requireNonNull(ks.serialize(keys[i])), Objects.requireNonNull(vs.serialize(values[i])));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加到缓存(无过期时间)
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
*/
|
||||
public void set(final String key, final Object value) {
|
||||
redisTemplate.opsForValue().set(key, value);
|
||||
}
|
||||
|
||||
// ======================== 读取类操作 ========================
|
||||
|
||||
/**
|
||||
* 查询在以 keyPattern 开头的所有 key
|
||||
*
|
||||
* <p>注意:底层使用 KEYS 命令,生产环境数据量大时会阻塞 Redis,请谨慎使用。</p>
|
||||
*
|
||||
* @param keyPattern the key pattern (会自动追加 "*")
|
||||
* @return the set
|
||||
*/
|
||||
public Set<String> keys(final String keyPattern) {
|
||||
return redisTemplate.keys(keyPattern + "*");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 key 获取原始字节
|
||||
*
|
||||
* @param key the key
|
||||
* @return the byte[]
|
||||
*/
|
||||
public byte[] get(final byte[] key) {
|
||||
return redisTemplate.execute((RedisCallback<byte[]>) connection -> connection.get(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 key 获取对象
|
||||
*
|
||||
* @param key the key
|
||||
* @return the object
|
||||
*/
|
||||
public Object get(final String key) {
|
||||
return redisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取旧值并设置新值(原子操作,对应 Redis 的 SET key value GET 语义)
|
||||
*
|
||||
* <p>使用模板的 valueSerializer,请确保其与调用方期望一致。</p>
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
* @return 旧值,若不存在返回 null
|
||||
*/
|
||||
public String getAndSet(final String key, String value) {
|
||||
if (StringUtils.isEmpty(key)) {
|
||||
log.error("非法入参");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Object oldValue = redisTemplate.opsForValue().getAndSet(key, value);
|
||||
return oldValue == null ? null : oldValue.toString();
|
||||
} catch (Exception e) {
|
||||
log.error("redisTemplate操作异常", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 key 获取对象(支持自定义 valueSerializer)
|
||||
*
|
||||
* @param key the key
|
||||
* @param valueSerializer 序列化器
|
||||
* @return the object
|
||||
*/
|
||||
public Object get(final String key, RedisSerializer<Object> valueSerializer) {
|
||||
byte[] rawKey = rawKey(key);
|
||||
return redisTemplate.execute((RedisCallback<Object>) connection ->
|
||||
deserializeValue(connection.stringCommands().get(rawKey), valueSerializer));
|
||||
}
|
||||
|
||||
// ======================== Set 操作 ========================
|
||||
|
||||
/**
|
||||
* 缓存 Set
|
||||
*
|
||||
* @param key 缓存键值
|
||||
* @param dataSet 缓存的数据
|
||||
* @return 缓存数据的对象
|
||||
*/
|
||||
public <T> BoundSetOperations<String, Object> setCacheSet(final String key, final Set<T> dataSet) {
|
||||
BoundSetOperations<String, Object> setOperation = redisTemplate.boundSetOps(key);
|
||||
Iterator<T> it = dataSet.iterator();
|
||||
while (it.hasNext()) {
|
||||
setOperation.add(it.next());
|
||||
}
|
||||
return setOperation;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得缓存的 set
|
||||
*
|
||||
* @param key the key
|
||||
* @return the set
|
||||
*/
|
||||
public Set<Object> getCacheSet(final String key) {
|
||||
return redisTemplate.opsForSet().members(key);
|
||||
}
|
||||
|
||||
// ======================== Hash 操作 ========================
|
||||
|
||||
/**
|
||||
* Ops for hash hash operations.
|
||||
*
|
||||
* @return the hash operations
|
||||
*/
|
||||
public HashOperations<String, String, Object> opsForHash() {
|
||||
return redisTemplate.opsForHash();
|
||||
}
|
||||
|
||||
/**
|
||||
* 对 HashMap 设置值
|
||||
*
|
||||
* @param key the key
|
||||
* @param hashKey the hash key
|
||||
* @param hashValue the hash value
|
||||
*/
|
||||
public void putHashValue(String key, String hashKey, Object hashValue) {
|
||||
opsForHash().put(key, hashKey, hashValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个 field 对应的值
|
||||
*
|
||||
* @param key the key
|
||||
* @param hashKey the hash key
|
||||
* @return the hash values
|
||||
*/
|
||||
public Object getHashValues(String key, String hashKey) {
|
||||
return opsForHash().get(key, hashKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 key 值删除 hash 字段
|
||||
*
|
||||
* @param key the key
|
||||
* @param hashKeys the hash keys
|
||||
*/
|
||||
public void delHashValues(String key, Object... hashKeys) {
|
||||
opsForHash().delete(key, hashKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* key 对应的整个 map
|
||||
*
|
||||
* @param key the key
|
||||
* @return the hash value
|
||||
*/
|
||||
public Map<String, Object> getHashValue(String key) {
|
||||
return opsForHash().entries(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量添加 hash
|
||||
*
|
||||
* @param key the key
|
||||
* @param map the map
|
||||
*/
|
||||
public void putHashValues(String key, Map<String, Object> map) {
|
||||
opsForHash().putAll(key, map);
|
||||
}
|
||||
|
||||
// ======================== 通用操作 ========================
|
||||
|
||||
/**
|
||||
* 集合数量
|
||||
*
|
||||
* @return the long
|
||||
*/
|
||||
public long dbSize() {
|
||||
Long size = redisTemplate.execute(RedisServerCommands::dbSize);
|
||||
return size == null ? 0L : size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空 redis 存储的数据
|
||||
*
|
||||
* @return the string
|
||||
*/
|
||||
public String flushDB() {
|
||||
return redisTemplate.execute((RedisCallback<String>) connection -> {
|
||||
connection.flushDb();
|
||||
return "ok";
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断某个主键是否存在
|
||||
*
|
||||
* @param key the key
|
||||
* @return the boolean
|
||||
*/
|
||||
public boolean exists(final String key) {
|
||||
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除 key
|
||||
*
|
||||
* @param keys the keys
|
||||
* @return 实际删除的数量
|
||||
*/
|
||||
public long del(final String... keys) {
|
||||
if (keys == null || keys.length == 0) {
|
||||
return 0L;
|
||||
}
|
||||
Long count = redisTemplate.delete(Arrays.asList(keys));
|
||||
return count == null ? 0L : count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对某个主键对应的值加一, value 值必须是全数字的字符串
|
||||
*
|
||||
* @param key the key
|
||||
* @return the long
|
||||
*/
|
||||
public long incr(final String key) {
|
||||
Long value = redisTemplate.opsForValue().increment(key);
|
||||
return value == null ? 0L : value;
|
||||
}
|
||||
|
||||
// ======================== List 操作 ========================
|
||||
|
||||
/**
|
||||
* redis List 引擎
|
||||
*
|
||||
* @return the list operations
|
||||
*/
|
||||
public ListOperations<String, Object> opsForList() {
|
||||
return redisTemplate.opsForList();
|
||||
}
|
||||
|
||||
/**
|
||||
* redis List 数据结构 : 将一个或多个值 value 插入到列表 key 的表头
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
* @return the long
|
||||
*/
|
||||
public Long leftPush(String key, Object value) {
|
||||
return opsForList().leftPush(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* redis List 数据结构 : 移除并返回列表 key 的头元素
|
||||
*
|
||||
* @param key the key
|
||||
* @return the object
|
||||
*/
|
||||
public Object leftPop(String key) {
|
||||
return opsForList().leftPop(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* redis List 数据结构 : 将一个或多个值 value 插入到列表 key 的表尾(最右边)。
|
||||
*
|
||||
* @param key the key
|
||||
* @param value the value
|
||||
* @return the long
|
||||
*/
|
||||
public Long in(String key, Object value) {
|
||||
return opsForList().rightPush(key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* redis List 数据结构 : 移除并返回列表 key 的末尾元素
|
||||
*
|
||||
* @param key the key
|
||||
* @return the object
|
||||
*/
|
||||
public Object rightPop(String key) {
|
||||
return opsForList().rightPop(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* redis List 数据结构 : 返回列表 key 的长度
|
||||
*
|
||||
* @param key the key
|
||||
* @return the long
|
||||
*/
|
||||
public Long length(String key) {
|
||||
return opsForList().size(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* redis List 数据结构 : 根据参数 i 的值,移除列表中与参数 value 相等的元素
|
||||
*
|
||||
* @param key the key
|
||||
* @param i the index
|
||||
* @param value the value
|
||||
*/
|
||||
public void remove(String key, long i, Object value) {
|
||||
opsForList().remove(key, i, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* redis List 数据结构 : 将列表 key 下标为 index 的元素的值设置为 value
|
||||
*
|
||||
* @param key the key
|
||||
* @param index the index
|
||||
* @param value the value
|
||||
*/
|
||||
public void set(String key, long index, Object value) {
|
||||
opsForList().set(key, index, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* redis List 数据结构 : 返回列表 key 中指定区间内的元素
|
||||
*
|
||||
* @param key the key
|
||||
* @param start the start
|
||||
* @param end the end
|
||||
* @return the list
|
||||
*/
|
||||
public List<Object> getList(String key, int start, int end) {
|
||||
return opsForList().range(key, start, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* redis List 数据结构 : 返回列表 key 中指定区间内的元素(自定义 valueSerializer)
|
||||
*
|
||||
* @param key the key
|
||||
* @param start the start
|
||||
* @param end the end
|
||||
* @param valueSerializer 序列化器
|
||||
* @return the list
|
||||
*/
|
||||
public List<Object> getList(String key, int start, int end, RedisSerializer<Object> valueSerializer) {
|
||||
byte[] rawKey = rawKey(key);
|
||||
return redisTemplate.execute((RedisCallback<List<Object>>) connection ->
|
||||
deserializeValues(connection.listCommands().lRange(rawKey, start, end), valueSerializer));
|
||||
}
|
||||
|
||||
/**
|
||||
* redis List 数据结构 : 批量存储
|
||||
*
|
||||
* @param key the key
|
||||
* @param list the list
|
||||
* @return the long
|
||||
*/
|
||||
public Long leftPushAll(String key, List<String> list) {
|
||||
return opsForList().leftPushAll(key, list);
|
||||
}
|
||||
|
||||
/**
|
||||
* redis List 数据结构 : 将值 value 插入到列表 key 当中,位于值 index 之前或之后,默认之后。
|
||||
*
|
||||
* @param key the key
|
||||
* @param index the index
|
||||
* @param value the value
|
||||
*/
|
||||
public void insert(String key, long index, Object value) {
|
||||
opsForList().set(key, index, value);
|
||||
}
|
||||
|
||||
// ======================== 私有工具方法 ========================
|
||||
|
||||
/**
|
||||
* 获取 key 序列化器(统一类型转换,避免每次强转告警)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private RedisSerializer<Object> keySerializer() {
|
||||
return (RedisSerializer<Object>) redisTemplate.getKeySerializer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 value 序列化器(统一类型转换,避免每次强转告警)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private RedisSerializer<Object> valueSerializer() {
|
||||
return (RedisSerializer<Object>) redisTemplate.getValueSerializer();
|
||||
}
|
||||
|
||||
private byte[] rawKey(Object key) {
|
||||
Assert.notNull(key, "non null key required");
|
||||
if (key instanceof byte[]) {
|
||||
return (byte[]) key;
|
||||
}
|
||||
return keySerializer().serialize(key);
|
||||
}
|
||||
|
||||
private byte[] rawValue(Object value, RedisSerializer valueSerializer) {
|
||||
if (value instanceof byte[]) {
|
||||
return (byte[]) value;
|
||||
}
|
||||
return valueSerializer.serialize(value);
|
||||
}
|
||||
|
||||
private List<Object> deserializeValues(List<byte[]> rawValues, RedisSerializer<Object> valueSerializer) {
|
||||
if (valueSerializer == null) {
|
||||
return (List<Object>) (List<?>) rawValues;
|
||||
}
|
||||
return SerializationUtils.deserialize(rawValues, valueSerializer);
|
||||
}
|
||||
|
||||
private Object deserializeValue(byte[] value, RedisSerializer<Object> valueSerializer) {
|
||||
if (valueSerializer == null) {
|
||||
return value;
|
||||
}
|
||||
return valueSerializer.deserialize(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.metalloop.common.service;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.metalloop.common.core.lock.DistributedLock;
|
||||
import com.metalloop.common.core.lock.LockCondition;
|
||||
import com.metalloop.common.utils.QueryWrapperBuilder;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* service接口父类
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2022-12-10
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public interface ISuperService<T> extends IService<T> {
|
||||
|
||||
/**
|
||||
* 使用分布式锁确保保存操作的幂等性。该方法接受一个或多个 LockCondition 对象,对每个条件进行唯一性检查。
|
||||
* 所有的条件都必须满足(即对应的记录在数据库中不存在)才会保存实体,否则抛出 IdempotencyException 异常。
|
||||
*
|
||||
* @param entity 需要保存的实体对象
|
||||
* @param locker 分布式锁对象
|
||||
* @param lockConditions 一个或多个锁条件,每个条件对应一个唯一性检查
|
||||
* @return 如果实体保存成功则返回 true,否则抛出异常
|
||||
* @throws LockException 如果无法获取锁或锁等待超时则抛出此异常
|
||||
* @throws IdempotencyException 如果任何唯一性检查失败则抛出此异常
|
||||
*/
|
||||
boolean saveIdempotency(T entity, DistributedLock locker, LockCondition<T>... lockConditions);
|
||||
|
||||
/**
|
||||
* 以幂等方式根据实体的ID进行更新。此方法使用分布式锁和锁定条件,以确保操作的幂等性和线程安全性。
|
||||
*
|
||||
* @param entity 需要更新的实体。
|
||||
* @param locker 分布式锁。
|
||||
* @param lockConditions 在更新之前需要检查的锁定条件。
|
||||
* 每个条件包括一个锁定键,一个用于检查现有数据的查询包装器,
|
||||
* 以及在存在重复数据时抛出异常的消息。
|
||||
* @return 如果更新操作成功,则返回true;否则返回false。
|
||||
* @throws LockException 如果分布式锁为null或无法获取。
|
||||
* @throws IdempotencyException 如果存在重复数据或在更新操作中发生其他异常。
|
||||
*/
|
||||
boolean updateByIdIdempotency(T entity, DistributedLock locker, LockCondition<T>... lockConditions);
|
||||
|
||||
/**
|
||||
* 这个方法提供了一个并发安全的方式来保存或更新给定的实体。
|
||||
* 如果实体已经存在,那么它会被更新,否则,它将会被保存。
|
||||
* <p>
|
||||
* 这个方法使用了分布式锁来确保并发安全,并且在执行前会检查是否存在满足条件的记录,从而确保操作的幂等性。
|
||||
* 锁和条件检查的行为可以通过 {@link LockCondition} 对象进行配置。
|
||||
*
|
||||
* @param entity 要保存或更新的实体
|
||||
* @param lock 分布式锁对象,用于确保并发安全
|
||||
* @param lockConditions 锁条件对象数组,定义了锁和条件检查的行为
|
||||
* @return 如果操作成功,则返回 true,否则返回 false
|
||||
* @throws IdempotencyException 如果存在满足条件的记录,或者找不到有效的 {@link TableId} 字段
|
||||
*/
|
||||
boolean saveOrUpdateIdempotency(T entity, DistributedLock lock, LockCondition<T>... lockConditions);
|
||||
|
||||
/**
|
||||
* 根据查询对象获取对象
|
||||
*
|
||||
* @param query 查询对象
|
||||
* @param orQueryArray OR嵌套查询对象数组,如果有的话
|
||||
* @param <Q> 查询对象类型
|
||||
* @return 结果对象
|
||||
*/
|
||||
default <Q> T get(Q query, Q... orQueryArray) {
|
||||
return getOne(getWrapper(query, orQueryArray), Boolean.FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据查询对象获取列表
|
||||
*
|
||||
* @param <Q> 查询对象类型
|
||||
* @param query 查询对象
|
||||
* @param orQueryArray OR嵌套查询对象数组,如果有的话
|
||||
* @return 结果列表
|
||||
*/
|
||||
default <Q> List<T> list(Q query, Q... orQueryArray){
|
||||
return list(getWrapper(query, orQueryArray));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据查询对象获取列表,并执行分组,返回Map,适用于确定分组后只有一条数据的情况,否则保留最后一条
|
||||
*
|
||||
* @param keyMapper 生成键的映射函数
|
||||
* @param query 查询对象
|
||||
* @param orQueryArray OR嵌套查询对象数组,如果有的话
|
||||
* @param <Q> 查询对象类型
|
||||
* @param <K> 返回的Map中的key的类型
|
||||
* @return 结果Map
|
||||
*/
|
||||
default <Q, K> Map<K, T> toMap(Function<T, K> keyMapper, Q query, Q... orQueryArray) {
|
||||
List<T> list = list(getWrapper(query, orQueryArray));
|
||||
return list.stream()
|
||||
.collect(Collectors.toMap(keyMapper, Function.identity(), (oldValue, newValue) -> newValue));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据查询对象获取列表并执行分组,返回Map,每个键都有一个对象列表
|
||||
*
|
||||
* @param keyMapper 生成键的映射函数
|
||||
* @param query 查询对象
|
||||
* @param orQueryArray OR嵌套查询对象数组,如果有的话
|
||||
* @param <Q> 查询对象类型
|
||||
* @param <K> 返回的Map中的key的类型
|
||||
* @return 结果Map,其值是对象列表
|
||||
*/
|
||||
default <Q, K> Map<K, List<T>> toGroup(Function<T, K> keyMapper, Q query, Q... orQueryArray) {
|
||||
List<T> list = list(getWrapper(query, orQueryArray));
|
||||
return list.stream()
|
||||
.collect(Collectors.groupingBy(keyMapper));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计数查询
|
||||
*
|
||||
* @param query 查询对象
|
||||
* @param orQueryArray OR嵌套查询对象数组,如果有的话
|
||||
* @param <Q> 查询对象类型
|
||||
* @return 计数结果
|
||||
*/
|
||||
default <Q> long count(Q query, Q... orQueryArray) {
|
||||
return count(getWrapper(query, orQueryArray));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param query 查询对象
|
||||
* @param orQueryArray OR嵌套查询对象数组,如果有的话
|
||||
* @param <Q> 查询对象类型
|
||||
* @param <E> 分页对象类型
|
||||
* @return 分页对象
|
||||
*/
|
||||
default <Q, E extends IPage<T>> E page(E page, Q query, Q... orQueryArray) {
|
||||
return page(page, getWrapper(query, orQueryArray));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据查询对象修改记录
|
||||
*
|
||||
* @param entity 实体对象
|
||||
* @param query 查询对象
|
||||
* @param orQueryArray OR嵌套查询对象数组,如果有的话
|
||||
* @param <Q> 查询对象类型
|
||||
* @return 是否成功
|
||||
*/
|
||||
default <Q> boolean update(T entity, Q query, Q... orQueryArray) {
|
||||
return update(entity, getWrapper(query, orQueryArray));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据查询对象删除记录
|
||||
*
|
||||
* @param query 查询对象
|
||||
* @param orQueryArray OR嵌套查询对象数组,如果有的话
|
||||
* @param <Q> 查询对象类型
|
||||
* @return 是否成功
|
||||
*/
|
||||
default <Q> boolean remove(Q query, Q... orQueryArray) {
|
||||
return remove(getWrapper(query, orQueryArray));
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部方法根据查询对象获取查询条件
|
||||
*
|
||||
* @param query 查询对象
|
||||
* @param orQueryArray OR嵌套查询对象数组,如果有的话
|
||||
* @return 查询条件
|
||||
*/
|
||||
default <Q> QueryWrapper<T> getWrapper(Q query, Q... orQueryArray) {
|
||||
return QueryWrapperBuilder.build(query, orQueryArray);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.metalloop.common.service.impl;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.metalloop.common.core.lock.DistributedLock;
|
||||
import com.metalloop.common.core.lock.LockCondition;
|
||||
import com.metalloop.common.core.lock.ZLock;
|
||||
import com.metalloop.common.exception.IdempotencyException;
|
||||
import com.metalloop.common.exception.LockException;
|
||||
import com.metalloop.common.service.ISuperService;
|
||||
import com.metalloop.modules.auth.model.po.SysPermission;
|
||||
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* service实现父类
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2022-12-10
|
||||
* <p>
|
||||
*/
|
||||
public abstract class SuperServiceImpl<M extends BaseMapper<T>, T> extends ServiceImpl<M, T> implements ISuperService<T> {
|
||||
|
||||
@SafeVarargs
|
||||
@Override
|
||||
public final boolean saveIdempotency(T entity, DistributedLock locker, LockCondition<T>... lockConditions) {
|
||||
if (locker == null) {
|
||||
throw new LockException("DistributedLock is null");
|
||||
}
|
||||
|
||||
List<ZLock> locks = new ArrayList<>();
|
||||
try {
|
||||
checkRepeat(locker, locks, lockConditions);
|
||||
return super.save(entity);
|
||||
} catch (Exception e) {
|
||||
throw new IdempotencyException(e.getMessage());
|
||||
} finally {
|
||||
for (ZLock lock : locks) {
|
||||
try {
|
||||
lock.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
@Override
|
||||
public final boolean updateByIdIdempotency(T entity, DistributedLock locker, LockCondition<T>... lockConditions) {
|
||||
if (locker == null) {
|
||||
throw new LockException("DistributedLock is null");
|
||||
}
|
||||
|
||||
List<ZLock> locks = new ArrayList<>();
|
||||
try {
|
||||
checkRepeat(locker, locks, lockConditions);
|
||||
return super.updateById(entity);
|
||||
} catch (Exception e) {
|
||||
throw new IdempotencyException(e.getMessage());
|
||||
} finally {
|
||||
for (ZLock lock : locks) {
|
||||
try {
|
||||
lock.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRepeat(DistributedLock locker, List<ZLock> locks, LockCondition<T>[] lockConditions) throws Exception {
|
||||
for (LockCondition<T> condition : lockConditions) {
|
||||
if (condition.isEnabled()) {
|
||||
ZLock lock = locker.tryLock(condition.getLockKey(), 10, 60, TimeUnit.SECONDS);
|
||||
if (lock == null) {
|
||||
throw new LockException("锁等待超时");
|
||||
}
|
||||
locks.add(lock);
|
||||
long count = super.count(condition.getQueryWrapper());
|
||||
if (count > 0) {
|
||||
throw new IdempotencyException(condition.getMsg());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
@Override
|
||||
public final boolean saveOrUpdateIdempotency(T entity, DistributedLock lock, LockCondition<T>... lockConditions) {
|
||||
if (null != entity) {
|
||||
Class<?> cls = entity.getClass();
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(cls);
|
||||
if (null != tableInfo && CharSequenceUtil.isNotEmpty(tableInfo.getKeyProperty())) {
|
||||
Object idVal = ReflectionKit.getFieldValue(entity, tableInfo.getKeyProperty());
|
||||
if (StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal))) {
|
||||
return this.saveIdempotency(entity, lock, lockConditions);
|
||||
} else {
|
||||
return updateByIdIdempotency(entity, lock, lockConditions);
|
||||
}
|
||||
} else {
|
||||
throw new IdempotencyException("找不到有效的@TableId字段.");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.metalloop.common.utils;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* @author lifan
|
||||
* @date 2024/3/26 10:26
|
||||
*/
|
||||
public class IPHelper {
|
||||
|
||||
private static final String UNKNOWN = "unknown";
|
||||
|
||||
/**
|
||||
* 获取用户的真实ip地址 如果有多个就取第一个
|
||||
*
|
||||
* @param request http请求
|
||||
* @return 客户端ip
|
||||
*/
|
||||
public static String getIpAddr(HttpServletRequest request) {
|
||||
|
||||
if (request == null) {
|
||||
return null;
|
||||
}
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
|
||||
if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || UNKNOWN.equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
String[] ips = ip.split(",");
|
||||
|
||||
return ips[0].trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将IPV6转化为IPV4 否则采用原来的值
|
||||
*
|
||||
* @param ipvAddress ip地址
|
||||
* @return 转化后的地址
|
||||
*/
|
||||
public static String convertIPv6ToIPv4(String ipvAddress) {
|
||||
try {
|
||||
InetAddress inet6Address = InetAddress.getByName(ipvAddress);
|
||||
if (inet6Address instanceof Inet6Address inet6) {
|
||||
// 如果是IPv6地址
|
||||
byte[] ipv4Bytes = new byte[4];
|
||||
System.arraycopy(inet6.getAddress(), 12, ipv4Bytes, 0, 4);
|
||||
InetAddress ipv4Address = InetAddress.getByAddress(ipv4Bytes);
|
||||
return ipv4Address.getHostAddress();
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return ipvAddress;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package com.metalloop.common.utils;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.core.conditions.interfaces.Compare;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.metalloop.common.enums.Direction;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeanWrapperImpl;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus QueryWrapper 条件构造器,从查询对象中构建查询条件
|
||||
* 条件构造遵循以下约定:
|
||||
* <p>
|
||||
* 1.解析规则
|
||||
* <p>
|
||||
* 1.1.查询对象字段值非空(不是null和空字符串)才会加入查询条件
|
||||
* <p>
|
||||
* 1.2.查询对象字段对应的数据库字段,通过对应注解 @TableField(value = "client_id") 的value提取
|
||||
* <p>
|
||||
* 1.3.查询对象字段值类型是 Direction,用order by片段,如 Direction detectionTimeOrder = Direction.DESC 会转换为 order by detection_time desc
|
||||
* <p>
|
||||
* 1.4.查询对象字段值类型是 {@link Collection},用in片段,如果以not开头或结尾,使用not in片段如 List<> notSiteCodes 会转换为 site_code not in (?,?,?),否则使用in片段如 List<> siteCodes 会转换为 site_code in (?,?,?)。另请注意:如果集合不为 null 但元素数量为零,不是不加入查询条件,而是加入一个不可能的值,如 in () 会报错,所以会加入 in ('__UNLIKELY_VALUE_300552__'),这样可以避免报错。同时保持字面的含义,即选择了 0 个值,查询结果也为 0 个
|
||||
* <p>
|
||||
* 1.5.查询对象字段名以start|lower|min|from开头或结尾,表示范围的下限,用>=片段,如 detectionTimeStart 会转换为 detection_time >= ?
|
||||
* <p>
|
||||
* 1.6.查询对象字段名以end|upper|max|to开头或结尾,表示范围的上限,用<=片段,如 detectionTimeEnd 会转换为 detection_time <= ?
|
||||
* <p>
|
||||
* 1.7.查询对象字段名以startEc|lowerEc|minEc|fromEc开头或结尾,Ec约定为‘exclusive’的缩写,表示范围的下限,但是不包含当前值,用>片段,如 detectionTimeStartEc 会转换为 detection_time > ?
|
||||
* <p>
|
||||
* 1.8.查询对象字段名以endEc|upperEc|maxEc|toEc开头或结尾,Ec约定为‘exclusive’的缩写,表示范围的上限,但是不包含当前值,用<片段,如 detectionTimeEndEc 会转换为 detection_time < ?
|
||||
* <p>
|
||||
* 1.9.prefix|suffix开头或结尾,表示前缀或后缀查询,用like片段,如 vehPlatePrefix 会转换为 veh_plate like ?% 或 vehPlateSuffix 会转换为 veh_plate like %?
|
||||
* <p>
|
||||
* 1.10.查询对象字段名以notLike|notContains|notMatches|notMatch开头或结尾,表示否定模糊查询,用not like片段,如 vehPlateNotLike 会转换为 veh_plate not like %?%
|
||||
* <p>
|
||||
* 1.11.查询对象字段名以like|contains|matches|match开头或结尾,表示模糊查询,用like片段,如 vehPlateLike 会转换为 veh_plate like %?%
|
||||
* <p>
|
||||
* 1.12.查询对象字段名以fulltext开头或结尾,表示全文检索查询,用MATCH() AGAINST()片段,如 vehPlateFullText 会转换为 MATCH(veh_plate) AGAINST(?)
|
||||
* <p>
|
||||
* 1.13.查询对象字段名以not开头或结尾,表示否定查询,用not片段,如 vehPlateNot 会转换为 veh_plate != ?
|
||||
* <p>
|
||||
* 1.14.其他情况表示精确查询,用=片段,如busiNum 会转换为busi_num = ?
|
||||
* <p>
|
||||
* 2.OR嵌套查询列表
|
||||
* <p>
|
||||
* 需要构建OR嵌套查询列表,解析规则同上,解析结果为嵌套查询,最终将转换为 and (site_code in (?,?,?) or detection_time >= ? or ...)
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2022-12-27
|
||||
*/
|
||||
@SuppressWarnings({"AlibabaMethodTooLong", "DuplicatedCode"})
|
||||
public class QueryWrapperBuilder {
|
||||
|
||||
/**
|
||||
* Utility classes should not have a public or default constructor.
|
||||
*/
|
||||
private QueryWrapperBuilder() {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
|
||||
/**
|
||||
* 不可能的值,用于处理集合为空的情况
|
||||
*/
|
||||
public static final String UNLIKELY_VALUE = "__UNLIKELY_VALUE_300552__";
|
||||
|
||||
/**
|
||||
* 范围开始(不包含当前值)正则表达式
|
||||
*/
|
||||
public static final Pattern EXCLUSIVE_RANGE_START_PATTERN = Pattern.compile("(.+(start|lower|min|from)ec)|((start|lower|min|from)ec.+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 范围结束(不包含当前值)正则表达式
|
||||
*/
|
||||
public static final Pattern EXCLUSIVE_RANGE_END_PATTERN = Pattern.compile("(.+(end|upper|max|to)ec)|((end|upper|max|to)ec.+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 范围开始正则表达式
|
||||
*/
|
||||
public static final Pattern RANGE_START_PATTERN = Pattern.compile("(.+(start|lower|min|from))|((start|lower|min|from).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 范围结束正则表达式
|
||||
*/
|
||||
public static final Pattern RANGE_END_PATTERN = Pattern.compile("(.+(end|upper|max|to))|((end|upper|max|to).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 前缀查询
|
||||
*/
|
||||
public static final Pattern PREFIX_PATTERN = Pattern.compile("(.+(prefix))|((prefix).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 后缀查询
|
||||
*/
|
||||
public static final Pattern SUFFIX_PATTERN = Pattern.compile("(.+(suffix))|((suffix).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 模糊查询
|
||||
*/
|
||||
public static final Pattern LIKE_PATTERN = Pattern.compile("(.+(like|contains|matches|match))|((like|contains|matches|match).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 否定模糊查询
|
||||
*/
|
||||
public static final Pattern NOT_LIKE_PATTERN = Pattern.compile("(.+(not)(like|contains|matches|match))|((not)(like|contains|matches|match).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 全文检索查询
|
||||
*/
|
||||
public static final Pattern FULL_TEXT_SEARCH_PATTERN = Pattern.compile("(.+(fulltext))|((fulltext).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 不等于查询
|
||||
*/
|
||||
public static final Pattern NOT_PATTERN = Pattern.compile("(.+(not))|((not).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
/**
|
||||
* 特定JSON值查询
|
||||
*/
|
||||
public static final Pattern JSON_CONTAINS_PATTERN = Pattern.compile("(.+(jsonContains))|((jsonContains).+)", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@FunctionalInterface
|
||||
public interface QueryStrategy {
|
||||
default void apply(QueryWrapper<?> queryWrapper, String column, Object value, boolean isOr) {
|
||||
if (isOr) {
|
||||
queryWrapper.or();
|
||||
}
|
||||
apply(queryWrapper, column, value);
|
||||
}
|
||||
|
||||
void apply(QueryWrapper<?> wrapper, String column, Object value);
|
||||
}
|
||||
|
||||
// region 封装正则表达式Map和策略Map
|
||||
private static final Map<String, QueryStrategy> STRATEGY_MAP = new HashMap<>();
|
||||
private static final Map<String, Pattern> PATTERN_MAP = new HashMap<>();
|
||||
|
||||
public static final String RANGE_START_PATTERN_KEY = "RANGE_START_PATTERN";
|
||||
|
||||
public static final String RANGE_END_PATTERN_KEY = "RANGE_END_PATTERN";
|
||||
|
||||
public static final String EXCLUSIVE_RANGE_START_PATTERN_KEY = "EXCLUSIVE_RANGE_START_PATTERN";
|
||||
|
||||
public static final String EXCLUSIVE_RANGE_END_PATTERN_KEY = "EXCLUSIVE_RANGE_END_PATTERN";
|
||||
|
||||
public static final String PREFIX_PATTERN_KEY = "PREFIX_PATTERN";
|
||||
|
||||
public static final String SUFFIX_PATTERN_KEY = "SUFFIX_PATTERN";
|
||||
|
||||
public static final String NOT_LIKE_PATTERN_KEY = "NOT_LIKE_PATTERN";
|
||||
|
||||
public static final String LIKE_PATTERN_KEY = "LIKE_PATTERN";
|
||||
|
||||
public static final String FULL_TEXT_SEARCH_PATTERN_KEY = "FULL_TEXT_SEARCH_PATTERN";
|
||||
|
||||
public static final String NOT_PATTERN_KEY = "NOT_PATTERN";
|
||||
|
||||
public static final String JSON_CONTAINS_PATTERN_KEY = "JSON_CONTAINS_PATTERN";
|
||||
|
||||
static {
|
||||
PATTERN_MAP.put(RANGE_START_PATTERN_KEY, RANGE_START_PATTERN);
|
||||
PATTERN_MAP.put(RANGE_END_PATTERN_KEY, RANGE_END_PATTERN);
|
||||
PATTERN_MAP.put(EXCLUSIVE_RANGE_START_PATTERN_KEY, EXCLUSIVE_RANGE_START_PATTERN);
|
||||
PATTERN_MAP.put(EXCLUSIVE_RANGE_END_PATTERN_KEY, EXCLUSIVE_RANGE_END_PATTERN);
|
||||
PATTERN_MAP.put(PREFIX_PATTERN_KEY, PREFIX_PATTERN);
|
||||
PATTERN_MAP.put(SUFFIX_PATTERN_KEY, SUFFIX_PATTERN);
|
||||
PATTERN_MAP.put(NOT_LIKE_PATTERN_KEY, NOT_LIKE_PATTERN);
|
||||
PATTERN_MAP.put(LIKE_PATTERN_KEY, LIKE_PATTERN);
|
||||
PATTERN_MAP.put(FULL_TEXT_SEARCH_PATTERN_KEY, FULL_TEXT_SEARCH_PATTERN);
|
||||
PATTERN_MAP.put(NOT_PATTERN_KEY, NOT_PATTERN);
|
||||
|
||||
|
||||
STRATEGY_MAP.put(RANGE_START_PATTERN_KEY, Compare::ge);
|
||||
STRATEGY_MAP.put(RANGE_END_PATTERN_KEY, Compare::le);
|
||||
STRATEGY_MAP.put(EXCLUSIVE_RANGE_START_PATTERN_KEY, Compare::gt);
|
||||
STRATEGY_MAP.put(EXCLUSIVE_RANGE_END_PATTERN_KEY, Compare::lt);
|
||||
STRATEGY_MAP.put(PREFIX_PATTERN_KEY, Compare::likeRight);
|
||||
STRATEGY_MAP.put(SUFFIX_PATTERN_KEY, Compare::likeLeft);
|
||||
STRATEGY_MAP.put(NOT_LIKE_PATTERN_KEY, Compare::notLike);
|
||||
STRATEGY_MAP.put(LIKE_PATTERN_KEY, Compare::like);
|
||||
STRATEGY_MAP.put(FULL_TEXT_SEARCH_PATTERN_KEY, (queryWrapper, column, value) -> queryWrapper.apply("MATCH(" + column + ") AGAINST({0})", value));
|
||||
STRATEGY_MAP.put(NOT_PATTERN_KEY, Compare::ne);
|
||||
STRATEGY_MAP.put(JSON_CONTAINS_PATTERN_KEY, (queryWrapper, column, value) ->
|
||||
queryWrapper.apply("JSON_CONTAINS(" + column + ", CAST({0} AS JSON))", value)
|
||||
);
|
||||
}
|
||||
// endregion
|
||||
|
||||
@SafeVarargs
|
||||
public static <Q, T> QueryWrapper<T> build(Q andQuery, Q... orQueryArray) {
|
||||
if (andQuery == null) return null;
|
||||
|
||||
QueryWrapper<T> queryWrapper = new QueryWrapper<>();
|
||||
applyQuery(queryWrapper, andQuery, false);
|
||||
|
||||
Optional.ofNullable(orQueryArray).ifPresent(array -> Arrays.stream(array)
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(orQuery -> queryWrapper.and(qw -> applyQuery(qw, orQuery, true))));
|
||||
|
||||
return queryWrapper;
|
||||
}
|
||||
|
||||
private static <Q, T> void applyQuery(QueryWrapper<T> queryWrapper, Q query, boolean isOr) {
|
||||
Class<?> aClass = query.getClass();
|
||||
PropertyDescriptor[] propertyDescriptors = getPropertyDescriptors(aClass);
|
||||
final BeanWrapperImpl wrapper = new BeanWrapperImpl(query);
|
||||
|
||||
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
|
||||
String column = getColumn(aClass, propertyDescriptor);
|
||||
String propertyName = propertyDescriptor.getName();
|
||||
Object propertyValue = wrapper.getPropertyValue(propertyName);
|
||||
// 先处理特殊值:排序标志和集合
|
||||
if (propertyValue instanceof Direction) {
|
||||
applyOrder(queryWrapper, column, propertyValue);
|
||||
} else if (propertyValue instanceof Collection<?>) {
|
||||
// 如果集合为空,需要特殊处理为一个无意义的值,否则会导致 in () 报错
|
||||
if (((Collection<?>) propertyValue).isEmpty()) {
|
||||
propertyValue = Collections.singletonList(UNLIKELY_VALUE);
|
||||
}
|
||||
applyCollection(queryWrapper, column, propertyName, (Collection<?>) propertyValue);
|
||||
} else if (propertyValue != null && CharSequenceUtil.isNotBlank(propertyValue.toString())) {
|
||||
applyStrategy(queryWrapper, column, propertyName, propertyValue, isOr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void applyOrder(QueryWrapper<T> queryWrapper, String column, Object value) {
|
||||
if (Direction.ASC.equals(value)) {
|
||||
queryWrapper.orderByAsc(column);
|
||||
} else {
|
||||
queryWrapper.orderByDesc(column);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void applyCollection(QueryWrapper<T> queryWrapper, String column, String propertyName, Collection<?> values) {
|
||||
if (PATTERN_MAP.get(NOT_PATTERN_KEY).matcher(propertyName).matches()) {
|
||||
queryWrapper.notIn(column, values);
|
||||
} else {
|
||||
queryWrapper.in(column, values);
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> void applyStrategy(QueryWrapper<T> queryWrapper, String column, String propertyName, Object propertyValue, boolean isOr) {
|
||||
Optional<QueryStrategy> matchedStrategy = findMatchingStrategy(propertyName);
|
||||
if (matchedStrategy.isPresent() && !propertyName.equalsIgnoreCase(CharSequenceUtil.toCamelCase(column))) {
|
||||
matchedStrategy.get().apply(queryWrapper, column, propertyValue, isOr);
|
||||
} else {
|
||||
applyDefaultStrategy(queryWrapper, column, propertyValue, isOr);
|
||||
}
|
||||
}
|
||||
|
||||
private static Optional<QueryStrategy> findMatchingStrategy(String propertyName) {
|
||||
return STRATEGY_MAP.entrySet().stream()
|
||||
.filter(entry -> {
|
||||
Pattern pattern = PATTERN_MAP.get(entry.getKey());
|
||||
return pattern != null && pattern.matcher(propertyName).matches();
|
||||
})
|
||||
.findFirst()
|
||||
.map(Map.Entry::getValue);
|
||||
}
|
||||
|
||||
private static <T> void applyDefaultStrategy(QueryWrapper<T> queryWrapper, String column, Object value, boolean isOr) {
|
||||
if (isOr) {
|
||||
queryWrapper.or().eq(column, value);
|
||||
} else {
|
||||
queryWrapper.eq(column, value);
|
||||
}
|
||||
}
|
||||
|
||||
// region 处理缓存的Class的PropertyDescriptor和column的映射关系
|
||||
|
||||
static final ConcurrentMap<Class<?>, CachedColumnResults> CLASS_CACHED_COLUMN_RESULTS_MAP =
|
||||
new ConcurrentHashMap<>(128);
|
||||
private static final PropertyDescriptor[] EMPTY_PROPERTY_DESCRIPTOR_ARRAY = {};
|
||||
|
||||
public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws BeansException {
|
||||
return forClass(clazz).getPropertyDescriptors();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getColumn(Class<?> clazz, PropertyDescriptor pd) throws BeansException {
|
||||
return forClass(clazz).getColumn(pd);
|
||||
}
|
||||
|
||||
static CachedColumnResults forClass(Class<?> beanClass) throws BeansException {
|
||||
CachedColumnResults results = CLASS_CACHED_COLUMN_RESULTS_MAP.get(beanClass);
|
||||
if (results != null) {
|
||||
return results;
|
||||
}
|
||||
|
||||
results = new CachedColumnResults(beanClass);
|
||||
|
||||
CachedColumnResults existing = CLASS_CACHED_COLUMN_RESULTS_MAP.putIfAbsent(beanClass, results);
|
||||
return (existing != null ? existing : results);
|
||||
}
|
||||
|
||||
public static final class CachedColumnResults {
|
||||
|
||||
private final Map<PropertyDescriptor, String> pdToColumnMap;
|
||||
|
||||
private CachedColumnResults(Class<?> beanClass) throws BeansException {
|
||||
pdToColumnMap = Arrays.stream(beanClass.getDeclaredFields())
|
||||
.filter(field -> field.isAnnotationPresent(TableField.class))
|
||||
.collect(Collectors.toMap(field -> BeanUtils.getPropertyDescriptor(beanClass, field.getName()),
|
||||
field -> field.getAnnotation(TableField.class).value()));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getColumn(PropertyDescriptor pd) {
|
||||
return pdToColumnMap.get(pd);
|
||||
}
|
||||
|
||||
PropertyDescriptor[] getPropertyDescriptors() {
|
||||
return this.pdToColumnMap.keySet().toArray(EMPTY_PROPERTY_DESCRIPTOR_ARRAY);
|
||||
}
|
||||
|
||||
}
|
||||
// endregion
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.metalloop.common.utils;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* RSA加解密工具类
|
||||
*
|
||||
* @author zlt
|
||||
* @date 2019/7/16
|
||||
*/
|
||||
public class RsaUtils {
|
||||
/**
|
||||
* 默认"RSA"="RSA/ECB/PKCS1Padding"
|
||||
*/
|
||||
private static final String CIPHER_INSTANCE = "RSA/ECB/PKCS1Padding";
|
||||
|
||||
/**
|
||||
* 公钥加密
|
||||
*
|
||||
* @param content 要加密的内容
|
||||
* @param publicKey 公钥
|
||||
*/
|
||||
public static String encrypt(String content, PublicKey publicKey) {
|
||||
try {
|
||||
return Base64.getEncoder().encodeToString(encrypt(content.getBytes(), publicKey));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公钥加密
|
||||
*
|
||||
* @param content 要加密的内容
|
||||
* @param publicKey 公钥
|
||||
*/
|
||||
public static byte[] encrypt(byte[] content, PublicKey publicKey) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(CIPHER_INSTANCE);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
return cipher.doFinal(content);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 私钥解密
|
||||
*
|
||||
* @param content 要解密的内容
|
||||
* @param privateKey 私钥
|
||||
*/
|
||||
public static String decrypt(String content, PrivateKey privateKey) throws Exception {
|
||||
return new String(decrypt(Base64.getDecoder().decode(content), privateKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* 私钥解密
|
||||
*
|
||||
* @param content 要解密的内容
|
||||
* @param privateKey 私钥
|
||||
*/
|
||||
public static byte[] decrypt(byte[] content, PrivateKey privateKey) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance(CIPHER_INSTANCE);
|
||||
cipher.init(Cipher.DECRYPT_MODE, privateKey);
|
||||
return cipher.doFinal(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* String转公钥PublicKey
|
||||
*
|
||||
* @param key 公钥字符
|
||||
*/
|
||||
public static RSAPublicKey getPublicKey(String key) throws Exception {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(key);
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
return (RSAPublicKey) keyFactory.generatePublic(keySpec);
|
||||
}
|
||||
|
||||
/**
|
||||
* String转私钥PrivateKey
|
||||
*
|
||||
* @param key 私钥字符
|
||||
*/
|
||||
public static PrivateKey getPrivateKey(String key) throws Exception {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(key);
|
||||
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
return keyFactory.generatePrivate(keySpec);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.metalloop.common.utils;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* spring获取bean工具类
|
||||
*
|
||||
* @author 作者 owen E-mail: 624191343@qq.com
|
||||
*/
|
||||
@Component
|
||||
@Order(0)
|
||||
public class SpringUtil implements ApplicationContextAware {
|
||||
|
||||
private static ApplicationContext applicationContext = null;
|
||||
|
||||
public static <T> T getBean(Class<T> cla) {
|
||||
return applicationContext.getBean(cla);
|
||||
}
|
||||
|
||||
public static <T> T getBean(String name, Class<T> cal) {
|
||||
return applicationContext.getBean(name, cal);
|
||||
}
|
||||
|
||||
public static String getProperty(String key) {
|
||||
return applicationContext.getBean(Environment.class).getProperty(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) {
|
||||
SpringUtil.applicationContext = applicationContext;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.metalloop.common.utils.json;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* JSON 工具类
|
||||
*
|
||||
* @author zhaowenhao
|
||||
*/
|
||||
@UtilityClass
|
||||
@Slf4j
|
||||
public class JsonUtils {
|
||||
|
||||
private static ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
static {
|
||||
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||
objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 objectMapper 属性
|
||||
* <p>
|
||||
* 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean
|
||||
*
|
||||
* @param objectMapper ObjectMapper 对象
|
||||
*/
|
||||
public static void init(ObjectMapper objectMapper) {
|
||||
JsonUtils.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static String toJsonString(Object object) {
|
||||
return objectMapper.writeValueAsString(object);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static byte[] toJsonByte(Object object) {
|
||||
return objectMapper.writeValueAsBytes(object);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static String toJsonPrettyString(Object object) {
|
||||
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
|
||||
}
|
||||
|
||||
public static <T> T parseObject(String text, Class<T> clazz) {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(text, clazz);
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串解析成指定类型的对象
|
||||
* 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
|
||||
* 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
|
||||
*
|
||||
* @param text 字符串
|
||||
* @param clazz 类型
|
||||
* @return 对象
|
||||
*/
|
||||
public static <T> T parseObject2(String text, Class<T> clazz) {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
return JSONUtil.toBean(text, clazz);
|
||||
}
|
||||
|
||||
public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
|
||||
if (ArrayUtil.isEmpty(bytes)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(bytes, clazz);
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", bytes, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> T parseObject(String text, TypeReference<T> typeReference) {
|
||||
try {
|
||||
return objectMapper.readValue(text, typeReference);
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static <T> List<T> parseArray(String text, Class<T> clazz) {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static JsonNode parseTree(String text) {
|
||||
try {
|
||||
return objectMapper.readTree(text);
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static JsonNode parseTree(byte[] text) {
|
||||
try {
|
||||
return objectMapper.readTree(text);
|
||||
} catch (IOException e) {
|
||||
log.error("json parse err,json:{}", text, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isJson(String text) {
|
||||
return JSONUtil.isTypeJSON(text);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.metalloop.framework.satoken;
|
||||
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.router.SaRouter;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class SaTokenInterceptorConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册 Sa-Token 全局拦截器
|
||||
registry.addInterceptor(new SaInterceptor(handler -> {
|
||||
// 排除登录、注册、验证码等公开接口
|
||||
SaRouter.match("/api/**")
|
||||
.notMatch("/api/auth/login")
|
||||
.notMatch("/api/auth/register")
|
||||
.notMatch("/api/auth/captcha")
|
||||
.notMatch("/api/public/**")
|
||||
.check(r -> StpUtil.checkLogin());
|
||||
})).addPathPatterns("/api/**");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.metalloop.framework.satoken;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
//import com.metalloop.modules.permission.service.PermissionService;
|
||||
//import com.metalloop.modules.permission.service.RoleService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Sa-Token 权限数据源实现
|
||||
* 框架会自动调用此接口获取当前登录用户的角色和权限码
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
// private final RoleService roleService;
|
||||
// private final PermissionService permissionService;
|
||||
|
||||
@Override
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
// 根据用户 ID 获取该用户所有权限码(code 字段)
|
||||
Long userId = Long.parseLong(loginId.toString());
|
||||
// return permissionService.listPermissionCodesByUserId(userId);
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
// 根据用户 ID 获取该用户所有角色名称
|
||||
Long userId = Long.parseLong(loginId.toString());
|
||||
// return roleService.listRoleNamesByUserId(userId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+34
@@ -0,0 +1,34 @@
|
||||
package com.metalloop.framework.satoken.admin.config;
|
||||
|
||||
import cn.dev33.satoken.config.SaTokenConfig;
|
||||
import com.metalloop.framework.satoken.admin.StpAdminUtil;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
||||
/**
|
||||
* Admin Sa-Token 配置类,目标是提供一个支持配置的开箱即用的StpAdminUtil
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-11-17
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(SaTokenAdminProperties.class)
|
||||
public class SaTokenAdminConfiguration {
|
||||
private final SaTokenAdminProperties saTokenAdminProperties;
|
||||
|
||||
public SaTokenAdminConfiguration(SaTokenAdminProperties saTokenAdminProperties) {
|
||||
this.saTokenAdminProperties = saTokenAdminProperties;
|
||||
}
|
||||
|
||||
// 使用注入的 SaTokenAdminProperties 来配置 StpUtil 或其他组件
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
SaTokenConfig config = new SaTokenConfig();
|
||||
// 拷贝属性
|
||||
BeanUtils.copyProperties(saTokenAdminProperties, config);
|
||||
StpAdminUtil.getStpLogic().setConfig(config);
|
||||
}
|
||||
}
|
||||
+561
@@ -0,0 +1,561 @@
|
||||
package com.metalloop.framework.satoken.admin.config;
|
||||
|
||||
import cn.dev33.satoken.config.SaCookieConfig;
|
||||
import cn.dev33.satoken.config.SaSignConfig;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
import lombok.Getter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 复制自 Sa-Token cn.dev33.satoken.config.SaTokenConfig,升级版本时需要注意重新复制,目的是为了使用两套配置,
|
||||
* 但是又不能影响原框架的注入问题
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-11-20
|
||||
*/
|
||||
@Getter
|
||||
@ConfigurationProperties(prefix = "sa-token.admin")
|
||||
public class SaTokenAdminProperties implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -6541180061782004705L;
|
||||
|
||||
/**
|
||||
* token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀)
|
||||
*/
|
||||
private String tokenName = "satoken";
|
||||
|
||||
/**
|
||||
* token 有效期(单位:秒) 默认30天,-1 代表永久有效
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* token 有效期(单位:秒) 默认30天,-1 代表永久有效
|
||||
*/
|
||||
private long timeout = 60 * 60 * 24 * 30;
|
||||
|
||||
/**
|
||||
* token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
|
||||
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
|
||||
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
|
||||
*/
|
||||
private long activeTimeout = -1;
|
||||
|
||||
/**
|
||||
* 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
|
||||
*/
|
||||
private Boolean dynamicActiveTimeout = false;
|
||||
|
||||
/**
|
||||
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
||||
*/
|
||||
private Boolean isConcurrent = true;
|
||||
|
||||
/**
|
||||
* 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token)
|
||||
*/
|
||||
private Boolean isShare = true;
|
||||
|
||||
/**
|
||||
* 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
|
||||
*/
|
||||
private int maxLoginCount = 12;
|
||||
|
||||
/**
|
||||
* 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
|
||||
*/
|
||||
private int maxTryTimes = 12;
|
||||
|
||||
/**
|
||||
* 是否尝试从请求体里读取 token
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否尝试从请求体里读取 token
|
||||
*/
|
||||
private Boolean isReadBody = true;
|
||||
|
||||
/**
|
||||
* 是否尝试从 header 里读取 token
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否尝试从 header 里读取 token
|
||||
*/
|
||||
private Boolean isReadHeader = true;
|
||||
|
||||
/**
|
||||
* 是否尝试从 cookie 里读取 token
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否尝试从 cookie 里读取 token
|
||||
*/
|
||||
private Boolean isReadCookie = true;
|
||||
|
||||
/**
|
||||
* 是否在登录后将 token 写入到响应头
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否在登录后将 token 写入到响应头
|
||||
*/
|
||||
private Boolean isWriteHeader = false;
|
||||
|
||||
/**
|
||||
* token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
||||
*/
|
||||
private String tokenStyle = "uuid";
|
||||
|
||||
/**
|
||||
* 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
|
||||
*/
|
||||
private int dataRefreshPeriod = 30;
|
||||
|
||||
/**
|
||||
* 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
|
||||
*/
|
||||
private Boolean tokenSessionCheckLogin = true;
|
||||
|
||||
/**
|
||||
* 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
|
||||
*/
|
||||
private Boolean autoRenew = true;
|
||||
|
||||
/**
|
||||
* token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
|
||||
*/
|
||||
private String tokenPrefix;
|
||||
|
||||
/**
|
||||
* 是否在初始化配置时在控制台打印版本字符画
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否在初始化配置时在控制台打印版本字符画
|
||||
*/
|
||||
private Boolean isPrint = true;
|
||||
|
||||
/**
|
||||
* 是否打印操作日志
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否打印操作日志
|
||||
*/
|
||||
private Boolean isLog = false;
|
||||
|
||||
/**
|
||||
* 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
|
||||
*/
|
||||
private String logLevel = "trace";
|
||||
|
||||
/**
|
||||
* 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
|
||||
*/
|
||||
private int logLevelInt = 1;
|
||||
|
||||
/**
|
||||
* 是否打印彩色日志
|
||||
* -- GETTER --
|
||||
* 获取:是否打印彩色日志
|
||||
* <p>
|
||||
* isColorLog 是否打印彩色日志
|
||||
*/
|
||||
private Boolean isColorLog = null;
|
||||
|
||||
/**
|
||||
* jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
|
||||
*/
|
||||
private String jwtSecretKey;
|
||||
|
||||
/**
|
||||
* Http Basic 认证的默认账号和密码
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* Http Basic 认证的默认账号和密码
|
||||
*/
|
||||
private String basic = "";
|
||||
|
||||
/**
|
||||
* 配置当前项目的网络访问地址
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 配置当前项目的网络访问地址
|
||||
*/
|
||||
private String currDomain;
|
||||
|
||||
/**
|
||||
* Same-Token 的有效期 (单位: 秒)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* Same-Token 的有效期 (单位: 秒)
|
||||
*/
|
||||
private long sameTokenTimeout = 60 * 60 * 24;
|
||||
|
||||
/**
|
||||
* 是否校验 Same-Token(部分rpc插件有效)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否校验Same-Token(部分rpc插件有效)
|
||||
*/
|
||||
private Boolean checkSameToken = false;
|
||||
|
||||
/**
|
||||
* Cookie配置对象
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* Cookie 全局配置对象
|
||||
*/
|
||||
public SaCookieConfig cookie = new SaCookieConfig();
|
||||
|
||||
/**
|
||||
* API 签名配置对象
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* API 签名全局配置对象
|
||||
*/
|
||||
public SaSignConfig sign = new SaSignConfig();
|
||||
|
||||
|
||||
/**
|
||||
* @param tokenName token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setTokenName(String tokenName) {
|
||||
this.tokenName = tokenName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param timeout token 有效期(单位:秒) 默认30天,-1 代表永久有效
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setTimeout(long timeout) {
|
||||
this.timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param activeTimeout token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
|
||||
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setActiveTimeout(long activeTimeout) {
|
||||
this.activeTimeout = activeTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dynamicActiveTimeout 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setDynamicActiveTimeout(Boolean dynamicActiveTimeout) {
|
||||
this.dynamicActiveTimeout = dynamicActiveTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isConcurrent 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setIsConcurrent(Boolean isConcurrent) {
|
||||
this.isConcurrent = isConcurrent;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isShare 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setIsShare(Boolean isShare) {
|
||||
this.isShare = isShare;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxLoginCount 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setMaxLoginCount(int maxLoginCount) {
|
||||
this.maxLoginCount = maxLoginCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxTryTimes 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setMaxTryTimes(int maxTryTimes) {
|
||||
this.maxTryTimes = maxTryTimes;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isReadBody 是否尝试从请求体里读取 token
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setIsReadBody(Boolean isReadBody) {
|
||||
this.isReadBody = isReadBody;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isReadHeader 是否尝试从 header 里读取 token
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setIsReadHeader(Boolean isReadHeader) {
|
||||
this.isReadHeader = isReadHeader;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isReadCookie 是否尝试从 cookie 里读取 token
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setIsReadCookie(Boolean isReadCookie) {
|
||||
this.isReadCookie = isReadCookie;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isWriteHeader 是否在登录后将 token 写入到响应头
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setIsWriteHeader(Boolean isWriteHeader) {
|
||||
this.isWriteHeader = isWriteHeader;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tokenStyle token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setTokenStyle(String tokenStyle) {
|
||||
this.tokenStyle = tokenStyle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dataRefreshPeriod 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setDataRefreshPeriod(int dataRefreshPeriod) {
|
||||
this.dataRefreshPeriod = dataRefreshPeriod;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tokenSessionCheckLogin 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setTokenSessionCheckLogin(Boolean tokenSessionCheckLogin) {
|
||||
this.tokenSessionCheckLogin = tokenSessionCheckLogin;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param autoRenew 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setAutoRenew(Boolean autoRenew) {
|
||||
this.autoRenew = autoRenew;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tokenPrefix token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setTokenPrefix(String tokenPrefix) {
|
||||
this.tokenPrefix = tokenPrefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isPrint 是否在初始化配置时在控制台打印版本字符画
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setIsPrint(Boolean isPrint) {
|
||||
this.isPrint = isPrint;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isLog 是否打印操作日志
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setIsLog(Boolean isLog) {
|
||||
this.isLog = isLog;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param logLevel 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setLogLevel(String logLevel) {
|
||||
this.logLevel = logLevel;
|
||||
this.logLevelInt = SaFoxUtil.translateLogLevelToInt(logLevel);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param logLevelInt 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setLogLevelInt(int logLevelInt) {
|
||||
this.logLevelInt = logLevelInt;
|
||||
this.logLevel = SaFoxUtil.translateLogLevelToString(logLevelInt);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置:是否打印彩色日志
|
||||
*
|
||||
* @param isColorLog 是否打印彩色日志
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setIsColorLog(Boolean isColorLog) {
|
||||
this.isColorLog = isColorLog;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param jwtSecretKey jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setJwtSecretKey(String jwtSecretKey) {
|
||||
this.jwtSecretKey = jwtSecretKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param basic Http Basic 认证的默认账号和密码
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setBasic(String basic) {
|
||||
this.basic = basic;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param currDomain 配置当前项目的网络访问地址
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setCurrDomain(String currDomain) {
|
||||
this.currDomain = currDomain;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sameTokenTimeout Same-Token 的有效期 (单位: 秒)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setSameTokenTimeout(long sameTokenTimeout) {
|
||||
this.sameTokenTimeout = sameTokenTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param checkSameToken 是否校验Same-Token(部分rpc插件有效)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setCheckSameToken(Boolean checkSameToken) {
|
||||
this.checkSameToken = checkSameToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cookie Cookie 全局配置对象
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setCookie(SaCookieConfig cookie) {
|
||||
this.cookie = cookie;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sign API 签名全局配置对象
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenAdminProperties setSign(SaSignConfig sign) {
|
||||
this.sign = sign;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SaTokenConfig ["
|
||||
+ "tokenName=" + tokenName
|
||||
+ ", timeout=" + timeout
|
||||
+ ", activeTimeout=" + activeTimeout
|
||||
+ ", dynamicActiveTimeout=" + dynamicActiveTimeout
|
||||
+ ", isConcurrent=" + isConcurrent
|
||||
+ ", isShare=" + isShare
|
||||
+ ", maxLoginCount=" + maxLoginCount
|
||||
+ ", maxTryTimes=" + maxTryTimes
|
||||
+ ", isReadBody=" + isReadBody
|
||||
+ ", isReadHeader=" + isReadHeader
|
||||
+ ", isReadCookie=" + isReadCookie
|
||||
+ ", isWriteHeader=" + isWriteHeader
|
||||
+ ", tokenStyle=" + tokenStyle
|
||||
+ ", dataRefreshPeriod=" + dataRefreshPeriod
|
||||
+ ", tokenSessionCheckLogin=" + tokenSessionCheckLogin
|
||||
+ ", autoRenew=" + autoRenew
|
||||
+ ", tokenPrefix=" + tokenPrefix
|
||||
+ ", isPrint=" + isPrint
|
||||
+ ", isLog=" + isLog
|
||||
+ ", logLevel=" + logLevel
|
||||
+ ", logLevelInt=" + logLevelInt
|
||||
+ ", isColorLog=" + isColorLog
|
||||
+ ", jwtSecretKey=" + jwtSecretKey
|
||||
+ ", basic=" + basic
|
||||
+ ", currDomain=" + currDomain
|
||||
+ ", sameTokenTimeout=" + sameTokenTimeout
|
||||
+ ", checkSameToken=" + checkSameToken
|
||||
+ ", cookie=" + cookie
|
||||
+ ", sign=" + sign
|
||||
+ "]";
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+34
@@ -0,0 +1,34 @@
|
||||
package com.metalloop.framework.satoken.self.config;
|
||||
|
||||
import cn.dev33.satoken.config.SaTokenConfig;
|
||||
import com.metalloop.framework.satoken.self.StpSelfUtil;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
||||
/**
|
||||
* 自助服务终端 Sa-Token 配置类,目标是提供一个支持配置的开箱即用的StpSelfUtil
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-11-17
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(SaTokenSelfProperties.class)
|
||||
public class SaTokenSelfConfiguration {
|
||||
private final SaTokenSelfProperties saTokenSelfProperties;
|
||||
|
||||
public SaTokenSelfConfiguration(SaTokenSelfProperties saTokenSelfProperties) {
|
||||
this.saTokenSelfProperties = saTokenSelfProperties;
|
||||
}
|
||||
|
||||
// 使用注入的 SaTokenSelfProperties 来配置 StpUtil 或其他组件
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
SaTokenConfig config = new SaTokenConfig();
|
||||
// 拷贝属性
|
||||
BeanUtils.copyProperties(saTokenSelfProperties, config);
|
||||
StpSelfUtil.getStpLogic().setConfig(config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
package com.metalloop.framework.satoken.self.config;
|
||||
|
||||
import cn.dev33.satoken.config.SaCookieConfig;
|
||||
import cn.dev33.satoken.config.SaSignConfig;
|
||||
import cn.dev33.satoken.util.SaFoxUtil;
|
||||
import lombok.Getter;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 复制自 Sa-Token cn.dev33.satoken.config.SaTokenConfig,升级版本时需要注意重新复制,目的是为了使用两套配置,
|
||||
* 但是又不能影响原框架的注入问题
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-11-20
|
||||
*/
|
||||
@Getter
|
||||
@ConfigurationProperties(prefix = "sa-token.self")
|
||||
public class SaTokenSelfProperties implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = -6541180061782004705L;
|
||||
|
||||
/**
|
||||
* token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀)
|
||||
*/
|
||||
private String tokenName = "satoken";
|
||||
|
||||
/**
|
||||
* token 有效期(单位:秒) 默认30天,-1 代表永久有效
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* token 有效期(单位:秒) 默认30天,-1 代表永久有效
|
||||
*/
|
||||
private long timeout = 60 * 60 * 24 * 30;
|
||||
|
||||
/**
|
||||
* token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
|
||||
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
|
||||
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
|
||||
*/
|
||||
private long activeTimeout = -1;
|
||||
|
||||
/**
|
||||
* 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
|
||||
*/
|
||||
private Boolean dynamicActiveTimeout = false;
|
||||
|
||||
/**
|
||||
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
||||
*/
|
||||
private Boolean isConcurrent = true;
|
||||
|
||||
/**
|
||||
* 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token)
|
||||
*/
|
||||
private Boolean isShare = true;
|
||||
|
||||
/**
|
||||
* 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
|
||||
*/
|
||||
private int maxLoginCount = 12;
|
||||
|
||||
/**
|
||||
* 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
|
||||
*/
|
||||
private int maxTryTimes = 12;
|
||||
|
||||
/**
|
||||
* 是否尝试从请求体里读取 token
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否尝试从请求体里读取 token
|
||||
*/
|
||||
private Boolean isReadBody = true;
|
||||
|
||||
/**
|
||||
* 是否尝试从 header 里读取 token
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否尝试从 header 里读取 token
|
||||
*/
|
||||
private Boolean isReadHeader = true;
|
||||
|
||||
/**
|
||||
* 是否尝试从 cookie 里读取 token
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否尝试从 cookie 里读取 token
|
||||
*/
|
||||
private Boolean isReadCookie = true;
|
||||
|
||||
/**
|
||||
* 是否在登录后将 token 写入到响应头
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否在登录后将 token 写入到响应头
|
||||
*/
|
||||
private Boolean isWriteHeader = false;
|
||||
|
||||
/**
|
||||
* token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
||||
*/
|
||||
private String tokenStyle = "uuid";
|
||||
|
||||
/**
|
||||
* 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
|
||||
*/
|
||||
private int dataRefreshPeriod = 30;
|
||||
|
||||
/**
|
||||
* 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
|
||||
*/
|
||||
private Boolean tokenSessionCheckLogin = true;
|
||||
|
||||
/**
|
||||
* 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
|
||||
*/
|
||||
private Boolean autoRenew = true;
|
||||
|
||||
/**
|
||||
* token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
|
||||
*/
|
||||
private String tokenPrefix;
|
||||
|
||||
/**
|
||||
* 是否在初始化配置时在控制台打印版本字符画
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否在初始化配置时在控制台打印版本字符画
|
||||
*/
|
||||
private Boolean isPrint = true;
|
||||
|
||||
/**
|
||||
* 是否打印操作日志
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否打印操作日志
|
||||
*/
|
||||
private Boolean isLog = false;
|
||||
|
||||
/**
|
||||
* 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
|
||||
*/
|
||||
private String logLevel = "trace";
|
||||
|
||||
/**
|
||||
* 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
|
||||
*/
|
||||
private int logLevelInt = 1;
|
||||
|
||||
/**
|
||||
* 是否打印彩色日志
|
||||
* -- GETTER --
|
||||
* 获取:是否打印彩色日志
|
||||
* <p>
|
||||
* isColorLog 是否打印彩色日志
|
||||
*/
|
||||
private Boolean isColorLog = null;
|
||||
|
||||
/**
|
||||
* jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
|
||||
*/
|
||||
private String jwtSecretKey;
|
||||
|
||||
/**
|
||||
* Http Basic 认证的默认账号和密码
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* Http Basic 认证的默认账号和密码
|
||||
*/
|
||||
private String basic = "";
|
||||
|
||||
/**
|
||||
* 配置当前项目的网络访问地址
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 配置当前项目的网络访问地址
|
||||
*/
|
||||
private String currDomain;
|
||||
|
||||
/**
|
||||
* Same-Token 的有效期 (单位: 秒)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* Same-Token 的有效期 (单位: 秒)
|
||||
*/
|
||||
private long sameTokenTimeout = 60 * 60 * 24;
|
||||
|
||||
/**
|
||||
* 是否校验 Same-Token(部分rpc插件有效)
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* 是否校验Same-Token(部分rpc插件有效)
|
||||
*/
|
||||
private Boolean checkSameToken = false;
|
||||
|
||||
/**
|
||||
* Cookie配置对象
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* Cookie 全局配置对象
|
||||
*/
|
||||
public SaCookieConfig cookie = new SaCookieConfig();
|
||||
|
||||
/**
|
||||
* API 签名配置对象
|
||||
* -- GETTER --
|
||||
* <p>
|
||||
* API 签名全局配置对象
|
||||
*/
|
||||
public SaSignConfig sign = new SaSignConfig();
|
||||
|
||||
|
||||
/**
|
||||
* @param tokenName token 名称 (同时也是: cookie 名称、提交 token 时参数的名称、存储 token 时的 key 前缀)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setTokenName(String tokenName) {
|
||||
this.tokenName = tokenName;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param timeout token 有效期(单位:秒) 默认30天,-1 代表永久有效
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setTimeout(long timeout) {
|
||||
this.timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param activeTimeout token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
|
||||
* (例如可以设置为 1800 代表 30 分钟内无操作就冻结)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setActiveTimeout(long activeTimeout) {
|
||||
this.activeTimeout = activeTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dynamicActiveTimeout 是否启用动态 activeTimeout 功能,如不需要请设置为 false,节省缓存请求次数
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setDynamicActiveTimeout(Boolean dynamicActiveTimeout) {
|
||||
this.dynamicActiveTimeout = dynamicActiveTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isConcurrent 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setIsConcurrent(Boolean isConcurrent) {
|
||||
this.isConcurrent = isConcurrent;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isShare 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个token, 为 false 时每次登录新建一个 token)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setIsShare(Boolean isShare) {
|
||||
this.isShare = isShare;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxLoginCount 同一账号最大登录数量,-1代表不限 (只有在 isConcurrent=true, isShare=false 时此配置项才有意义)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setMaxLoginCount(int maxLoginCount) {
|
||||
this.maxLoginCount = maxLoginCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param maxTryTimes 在每次创建 token 时的最高循环次数,用于保证 token 唯一性(-1=不循环尝试,直接使用)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setMaxTryTimes(int maxTryTimes) {
|
||||
this.maxTryTimes = maxTryTimes;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isReadBody 是否尝试从请求体里读取 token
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setIsReadBody(Boolean isReadBody) {
|
||||
this.isReadBody = isReadBody;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isReadHeader 是否尝试从 header 里读取 token
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setIsReadHeader(Boolean isReadHeader) {
|
||||
this.isReadHeader = isReadHeader;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isReadCookie 是否尝试从 cookie 里读取 token
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setIsReadCookie(Boolean isReadCookie) {
|
||||
this.isReadCookie = isReadCookie;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isWriteHeader 是否在登录后将 token 写入到响应头
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setIsWriteHeader(Boolean isWriteHeader) {
|
||||
this.isWriteHeader = isWriteHeader;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tokenStyle token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setTokenStyle(String tokenStyle) {
|
||||
this.tokenStyle = tokenStyle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dataRefreshPeriod 默认 SaTokenDao 实现类中,每次清理过期数据间隔的时间(单位: 秒),默认值30秒,设置为 -1 代表不启动定时清理
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setDataRefreshPeriod(int dataRefreshPeriod) {
|
||||
this.dataRefreshPeriod = dataRefreshPeriod;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tokenSessionCheckLogin 获取 Token-Session 时是否必须登录(如果配置为true,会在每次获取 getTokenSession() 时校验当前是否登录)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setTokenSessionCheckLogin(Boolean tokenSessionCheckLogin) {
|
||||
this.tokenSessionCheckLogin = tokenSessionCheckLogin;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param autoRenew 是否打开自动续签 activeTimeout (如果此值为 true, 框架会在每次直接或间接调用 getLoginId() 时进行一次过期检查与续签操作)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setAutoRenew(Boolean autoRenew) {
|
||||
this.autoRenew = autoRenew;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param tokenPrefix token 前缀, 前端提交 token 时应该填写的固定前缀,格式样例(satoken: Bearer xxxx-xxxx-xxxx-xxxx)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setTokenPrefix(String tokenPrefix) {
|
||||
this.tokenPrefix = tokenPrefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isPrint 是否在初始化配置时在控制台打印版本字符画
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setIsPrint(Boolean isPrint) {
|
||||
this.isPrint = isPrint;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param isLog 是否打印操作日志
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setIsLog(Boolean isLog) {
|
||||
this.isLog = isLog;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param logLevel 日志等级(trace、debug、info、warn、error、fatal),此值与 logLevelInt 联动
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setLogLevel(String logLevel) {
|
||||
this.logLevel = logLevel;
|
||||
this.logLevelInt = SaFoxUtil.translateLogLevelToInt(logLevel);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param logLevelInt 日志等级 int 值(1=trace、2=debug、3=info、4=warn、5=error、6=fatal),此值与 logLevel 联动
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setLogLevelInt(int logLevelInt) {
|
||||
this.logLevelInt = logLevelInt;
|
||||
this.logLevel = SaFoxUtil.translateLogLevelToString(logLevelInt);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置:是否打印彩色日志
|
||||
*
|
||||
* @param isColorLog 是否打印彩色日志
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setIsColorLog(Boolean isColorLog) {
|
||||
this.isColorLog = isColorLog;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param jwtSecretKey jwt秘钥(只有集成 jwt 相关模块时此参数才会生效)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setJwtSecretKey(String jwtSecretKey) {
|
||||
this.jwtSecretKey = jwtSecretKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param basic Http Basic 认证的默认账号和密码
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setBasic(String basic) {
|
||||
this.basic = basic;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param currDomain 配置当前项目的网络访问地址
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setCurrDomain(String currDomain) {
|
||||
this.currDomain = currDomain;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sameTokenTimeout Same-Token 的有效期 (单位: 秒)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setSameTokenTimeout(long sameTokenTimeout) {
|
||||
this.sameTokenTimeout = sameTokenTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param checkSameToken 是否校验Same-Token(部分rpc插件有效)
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setCheckSameToken(Boolean checkSameToken) {
|
||||
this.checkSameToken = checkSameToken;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cookie Cookie 全局配置对象
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setCookie(SaCookieConfig cookie) {
|
||||
this.cookie = cookie;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sign API 签名全局配置对象
|
||||
* 对象自身
|
||||
*/
|
||||
public SaTokenSelfProperties setSign(SaSignConfig sign) {
|
||||
this.sign = sign;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SaTokenConfig ["
|
||||
+ "tokenName=" + tokenName
|
||||
+ ", timeout=" + timeout
|
||||
+ ", activeTimeout=" + activeTimeout
|
||||
+ ", dynamicActiveTimeout=" + dynamicActiveTimeout
|
||||
+ ", isConcurrent=" + isConcurrent
|
||||
+ ", isShare=" + isShare
|
||||
+ ", maxLoginCount=" + maxLoginCount
|
||||
+ ", maxTryTimes=" + maxTryTimes
|
||||
+ ", isReadBody=" + isReadBody
|
||||
+ ", isReadHeader=" + isReadHeader
|
||||
+ ", isReadCookie=" + isReadCookie
|
||||
+ ", isWriteHeader=" + isWriteHeader
|
||||
+ ", tokenStyle=" + tokenStyle
|
||||
+ ", dataRefreshPeriod=" + dataRefreshPeriod
|
||||
+ ", tokenSessionCheckLogin=" + tokenSessionCheckLogin
|
||||
+ ", autoRenew=" + autoRenew
|
||||
+ ", tokenPrefix=" + tokenPrefix
|
||||
+ ", isPrint=" + isPrint
|
||||
+ ", isLog=" + isLog
|
||||
+ ", logLevel=" + logLevel
|
||||
+ ", logLevelInt=" + logLevelInt
|
||||
+ ", isColorLog=" + isColorLog
|
||||
+ ", jwtSecretKey=" + jwtSecretKey
|
||||
+ ", basic=" + basic
|
||||
+ ", currDomain=" + currDomain
|
||||
+ ", sameTokenTimeout=" + sameTokenTimeout
|
||||
+ ", checkSameToken=" + checkSameToken
|
||||
+ ", cookie=" + cookie
|
||||
+ ", sign=" + sign
|
||||
+ "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.metalloop.framework.swagger;
|
||||
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Springdoc OpenAPI 配置类
|
||||
* 用于定制 API 文档信息及全局安全认证方案
|
||||
*/
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
// 设置 API 基本信息
|
||||
.info(new Info()
|
||||
.title("有色再生资源交易平台 API")
|
||||
.version("1.0")
|
||||
.description("METALLOOP 后端接口文档")
|
||||
)
|
||||
// 声明全局安全要求(在 Swagger UI 中显示 Authorize 按钮)
|
||||
.addSecurityItem(new SecurityRequirement().addList("Sa-Token"))
|
||||
// 定义安全方案(API Key 方式)
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("Sa-Token",
|
||||
new SecurityScheme()
|
||||
.type(SecurityScheme.Type.APIKEY)
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.name("metalloop-token") // 与 Sa-Token 配置的 token-name 保持一致
|
||||
.description("Sa-Token 认证令牌,登录后获取")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package com.metalloop.modules.auth.controller;
|
||||
|
||||
import cn.dev33.satoken.session.SaSession;
|
||||
import com.metalloop.common.auth.Authentication;
|
||||
import com.metalloop.common.auth.handler.LoginSuccessHandler;
|
||||
import com.metalloop.common.auth.handler.LogoutSuccessHandler;
|
||||
import com.metalloop.common.auth.handler.PasswordCheckFailureHandler;
|
||||
import com.metalloop.common.auth.handler.VerifyCodeHandler;
|
||||
import com.metalloop.common.auth.model.AuthenticationMiniAppRequest;
|
||||
import com.metalloop.common.auth.password.PasswordDecryptor;
|
||||
import com.metalloop.common.auth.sso.SsoCodeService;
|
||||
import com.metalloop.common.auth.sso.model.SsoUserInfo;
|
||||
import com.metalloop.common.auth.userdetails.UserDetails;
|
||||
import com.metalloop.common.auth.userdetails.UserDetailsService;
|
||||
import com.metalloop.common.constant.SecurityConstants;
|
||||
import com.metalloop.common.enums.LoginType;
|
||||
import com.metalloop.common.exception.*;
|
||||
import com.metalloop.common.model.Result;
|
||||
import com.metalloop.common.utils.IPHelper;
|
||||
import com.metalloop.modules.auth.model.req.AuthenticationRequest;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 认证授权端点
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public class AuthController {
|
||||
|
||||
private final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private final LoginType loginType;
|
||||
|
||||
private final UserDetailsService userDetailsService;
|
||||
|
||||
private final LoginSuccessHandler loginSuccessHandler;
|
||||
private final LogoutSuccessHandler logoutSuccessHandler;
|
||||
|
||||
private final PasswordDecryptor passwordDecryptor;
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
private final VerifyCodeHandler verifyCodeHandler;
|
||||
|
||||
private final PasswordCheckFailureHandler passwordCheckFailureHandler;
|
||||
|
||||
/**
|
||||
* 单点登陆授权码服务
|
||||
*/
|
||||
private SsoCodeService ssoCodeService;
|
||||
|
||||
/**
|
||||
* 单点登陆授权码超时时间,单位:秒
|
||||
*/
|
||||
private int ssoCodeTimeOut = 300;
|
||||
|
||||
public void initSso(SsoCodeService ssoCodeService, int ssoCodeTimeOut) {
|
||||
this.ssoCodeService = ssoCodeService;
|
||||
this.ssoCodeTimeOut = ssoCodeTimeOut;
|
||||
}
|
||||
|
||||
public AuthController(LoginType loginType, UserDetailsService userDetailsService, LoginSuccessHandler loginSuccessHandler,
|
||||
LogoutSuccessHandler logoutSuccessHandler, PasswordDecryptor passwordDecryptor, PasswordEncoder passwordEncoder
|
||||
, VerifyCodeHandler verifyCodeHandler, PasswordCheckFailureHandler passwordCheckFailureHandler) {
|
||||
this.loginType = loginType;
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.loginSuccessHandler = loginSuccessHandler;
|
||||
this.logoutSuccessHandler = logoutSuccessHandler;
|
||||
this.passwordDecryptor = passwordDecryptor;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.verifyCodeHandler = verifyCodeHandler;
|
||||
this.passwordCheckFailureHandler = passwordCheckFailureHandler;
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
public Object getVerifyCode(@RequestBody @Validated AuthenticationRequest request, HttpServletRequest httpServletRequest) {
|
||||
// 用户名、密码校验
|
||||
UserDetails userDetails = loginVerify(request, httpServletRequest);
|
||||
if (verifyCodeHandler.createVerifyCode(userDetails.getPhoneNum())) {
|
||||
return Result.ok("发送验证码成功");
|
||||
} else {
|
||||
return Result.fail("发送验证码失败");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ResponseBody
|
||||
public Object login(@RequestBody @Validated AuthenticationRequest request, HttpServletRequest httpServletRequest) {
|
||||
|
||||
// 用户名、密码校验
|
||||
UserDetails userDetails = loginVerify(request, httpServletRequest);
|
||||
|
||||
// 在sa-token中进行登录操作
|
||||
loginType.getStpLogic().login(userDetails.getUserId());
|
||||
String accessToken = loginType.getStpLogic().getTokenValue();
|
||||
SaSession session = loginType.getStpLogic().getSession();
|
||||
|
||||
// 将登录成功的认证信息封装至session中
|
||||
Authentication authentication = Authentication.withUserDetails(userDetails).accessToken(accessToken).build();
|
||||
session.set(SecurityConstants.SESSION_KEY_AUTHENTICATION, authentication);
|
||||
|
||||
// 登录成功响应结果处理器
|
||||
return loginSuccessHandler.onSuccess(authentication);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 用户名、密码校验
|
||||
*/
|
||||
private UserDetails loginVerify(AuthenticationRequest request, HttpServletRequest servletRequest) {
|
||||
String username = request.getUsername();
|
||||
String password = request.getPassword();
|
||||
|
||||
// 获取用户ip
|
||||
String ip = IPHelper.getIpAddr(servletRequest);
|
||||
|
||||
// 从 UserDetailsService 加载用户并验证用户名和密码
|
||||
UserDetails userDetails;
|
||||
try {
|
||||
userDetails = userDetailsService.loadUserByUsername(username);
|
||||
} catch (UsernameNotFoundException ex) {
|
||||
this.logger.debug("未找到用户: '" + username + "'");
|
||||
throw new BadCredentialsException("用户名或密码错误");
|
||||
}
|
||||
if (userDetails == null) {
|
||||
this.logger.debug("未找到用户: '" + username + "'");
|
||||
throw new BadCredentialsException("用户名或密码错误");
|
||||
}
|
||||
|
||||
// 验证用户账号状态
|
||||
if (!userDetails.isAccountNonLocked()) {
|
||||
this.logger.debug("账号被锁定");
|
||||
passwordCheckFailureHandler.checkAccountLockStatus(ip, userDetails);
|
||||
}
|
||||
if (!userDetails.isEnabled()) {
|
||||
this.logger.debug("账号被禁用");
|
||||
throw new DisabledException("账号被禁用");
|
||||
}
|
||||
if (!userDetails.isAccountNonExpired()) {
|
||||
this.logger.debug("账号已过期");
|
||||
throw new AccountExpiredException("账号已过期");
|
||||
}
|
||||
if (!userDetails.isCredentialsNonExpired()) {
|
||||
this.logger.debug("密码已过期");
|
||||
throw new CredentialsExpiredException("密码已过期");
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
String presentedPassword = password;
|
||||
|
||||
// 密码解密
|
||||
if (passwordDecryptor != null) {
|
||||
presentedPassword = passwordDecryptor.decrypt(presentedPassword);
|
||||
}
|
||||
|
||||
// 密码匹配
|
||||
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
|
||||
logger.debug("密码错误");
|
||||
throw new BadCredentialsException("用户名或密码错误");
|
||||
}
|
||||
|
||||
return userDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户名、密码校验
|
||||
*/
|
||||
private UserDetails loginVerifyForMiniApp(AuthenticationMiniAppRequest request, HttpServletRequest servletRequest) {
|
||||
String username = request.getUsername();
|
||||
String password = request.getPassword();
|
||||
|
||||
// 获取用户ip
|
||||
String ip = IPHelper.getIpAddr(servletRequest);
|
||||
|
||||
// 从 UserDetailsService 加载用户并验证用户名和密码
|
||||
UserDetails userDetails;
|
||||
try {
|
||||
userDetails = userDetailsService.loadUserByUsername(username);
|
||||
} catch (UsernameNotFoundException ex) {
|
||||
this.logger.debug("未找到用户: '" + username + "'");
|
||||
throw new BadCredentialsException("用户名或密码错误");
|
||||
}
|
||||
if (userDetails == null) {
|
||||
this.logger.debug("未找到用户: '" + username + "'");
|
||||
throw new BadCredentialsException("用户名或密码错误");
|
||||
}
|
||||
|
||||
// 验证用户账号状态
|
||||
if (!userDetails.isAccountNonLocked()) {
|
||||
this.logger.debug("账号被锁定");
|
||||
passwordCheckFailureHandler.checkAccountLockStatus(ip, userDetails);
|
||||
}
|
||||
if (!userDetails.isEnabled()) {
|
||||
this.logger.debug("账号被禁用");
|
||||
throw new DisabledException("账号被禁用");
|
||||
}
|
||||
if (!userDetails.isAccountNonExpired()) {
|
||||
this.logger.debug("账号已过期");
|
||||
throw new AccountExpiredException("账号已过期");
|
||||
}
|
||||
if (!userDetails.isCredentialsNonExpired()) {
|
||||
this.logger.debug("密码已过期");
|
||||
throw new CredentialsExpiredException("密码已过期");
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
String presentedPassword = password;
|
||||
|
||||
// 密码解密
|
||||
if (passwordDecryptor != null) {
|
||||
presentedPassword = passwordDecryptor.decrypt(presentedPassword);
|
||||
}
|
||||
|
||||
// 密码匹配
|
||||
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
|
||||
logger.debug("密码错误");
|
||||
throw new BadCredentialsException("用户名或密码错误");
|
||||
}
|
||||
|
||||
return userDetails;
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
public Object logout() {
|
||||
Object o = loginType.getStpLogic().getSession().get(SecurityConstants.SESSION_KEY_AUTHENTICATION);
|
||||
Authentication authentication = null;
|
||||
if (o instanceof Authentication) {
|
||||
authentication = (Authentication) o;
|
||||
}
|
||||
loginType.getStpLogic().logout();
|
||||
// 登出成功响应结果处理器
|
||||
return logoutSuccessHandler.onSuccess(authentication);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
public Object miniAppLogout() {
|
||||
Object o = loginType.getStpLogic().getSession().get(SecurityConstants.SESSION_KEY_AUTHENTICATION);
|
||||
Authentication authentication = null;
|
||||
if (o instanceof Authentication) {
|
||||
authentication = (Authentication) o;
|
||||
}
|
||||
loginType.getStpLogic().logout();
|
||||
// 登出成功响应结果处理器
|
||||
return logoutSuccessHandler.onSuccess(authentication);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
public Object getSsoCode() {
|
||||
// 验证当前会话是否有效
|
||||
if (loginType.getStpLogic().isLogin()) {
|
||||
Object o = loginType.getStpLogic().getSession().get(SecurityConstants.SESSION_KEY_AUTHENTICATION);
|
||||
Authentication authentication = null;
|
||||
if (o instanceof Authentication) {
|
||||
authentication = (Authentication) o;
|
||||
}
|
||||
if (authentication != null) {
|
||||
// 签发授权码
|
||||
String authorizationCode = ssoCodeService.generateCode(authentication.getUserId(), ssoCodeTimeOut);
|
||||
return Result.ok(authorizationCode);
|
||||
}
|
||||
}
|
||||
return Result.fail("当前会话无效");
|
||||
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
public Object getSsoUserInfo(@RequestParam("code") String code) {
|
||||
// 验证授权码并获取绑定的用户信息
|
||||
SsoUserInfo userInfo = ssoCodeService.validateCode(code);
|
||||
|
||||
// 登录成功响应结果处理器
|
||||
return Result.ok(userInfo);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.metalloop.modules.auth.controller;
|
||||
|
||||
import com.metalloop.common.auth.encrypt.KeyProperties;
|
||||
import com.metalloop.common.auth.encrypt.KeyStoreKeyFactory;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 获取公钥端点
|
||||
*
|
||||
* @author zhaowenhao
|
||||
* @since 2023-03-19
|
||||
*/
|
||||
public class RsaPublicKeyController {
|
||||
|
||||
private final KeyProperties keyProperties;
|
||||
|
||||
public RsaPublicKeyController(KeyProperties keyProperties) {
|
||||
this.keyProperties = keyProperties;
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
public String getPublicKey() {
|
||||
if (keyProperties != null) {
|
||||
KeyPair keyPair = new KeyStoreKeyFactory(keyProperties.getKeyStore().getLocation(), keyProperties.getKeyStore().getSecret().toCharArray()).getKeyPair(keyProperties.getKeyStore().getAlias());
|
||||
PublicKey publicKey = keyPair.getPublic();
|
||||
RSAPublicKey rsaPublicKey = (RSAPublicKey) publicKey;
|
||||
byte[] publicKeyBytes = rsaPublicKey.getEncoded();
|
||||
return Base64.getEncoder().encodeToString(publicKeyBytes);
|
||||
}
|
||||
return "获取失败";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.metalloop.modules.auth.converter;
|
||||
|
||||
import com.metalloop.modules.auth.model.req.SysEnterpriseQueryReqDto;
|
||||
import com.metalloop.modules.auth.model.req.SysEnterpriseReqDto;
|
||||
import com.metalloop.modules.auth.model.resp.SysEnterpriseRespDto;
|
||||
import com.metalloop.modules.auth.model.po.SysEnterprise;
|
||||
import com.metalloop.modules.auth.model.query.SysEnterpriseQuery;
|
||||
import org.mapstruct.Builder;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 企业信息主转换器
|
||||
*
|
||||
* @author yy
|
||||
* @since 2026-09-10
|
||||
*/
|
||||
@SuppressWarnings("UnmappedTargetProperties")
|
||||
@Mapper(builder = @Builder(disableBuilder = true))
|
||||
public interface SysEnterpriseConverter {
|
||||
|
||||
SysEnterpriseConverter INSTANCE = Mappers.getMapper(SysEnterpriseConverter.class);
|
||||
|
||||
/**
|
||||
* 实体转换为响应 DTO
|
||||
*
|
||||
* @param sysEnterprise 实体
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysEnterpriseRespDto toRespDto(SysEnterprise sysEnterprise);
|
||||
|
||||
/**
|
||||
* 实体列表转换为响应 DTO 列表
|
||||
*
|
||||
* @param sysEnterprises 实体列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysEnterpriseRespDto> toRespDto(List<SysEnterprise> sysEnterprises);
|
||||
|
||||
/**
|
||||
* 请求 DTO 转换为实体
|
||||
*
|
||||
* @param sysEnterpriseReqDto 请求 DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysEnterprise fromReqDto(SysEnterpriseReqDto sysEnterpriseReqDto);
|
||||
|
||||
/**
|
||||
* 请求 DTO 列表转换为实体列表
|
||||
*
|
||||
* @param sysEnterpriseReqDtos 请求 DTO 列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysEnterprise> fromReqDto(List<SysEnterpriseReqDto> sysEnterpriseReqDtos);
|
||||
|
||||
/**
|
||||
* 查询请求 DTO 转换为查询对象
|
||||
*
|
||||
* @param sysEnterpriseQueryReqDto 查询请求DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysEnterpriseQuery fromQueryReqDtoToQuery(SysEnterpriseQueryReqDto sysEnterpriseQueryReqDto);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.metalloop.modules.auth.converter;
|
||||
|
||||
import com.metalloop.modules.auth.model.req.SysEnterpriseImageQueryReqDto;
|
||||
import com.metalloop.modules.auth.model.req.SysEnterpriseImageReqDto;
|
||||
import com.metalloop.modules.auth.model.resp.SysEnterpriseImageRespDto;
|
||||
import com.metalloop.modules.auth.model.po.SysEnterpriseImage;
|
||||
import com.metalloop.modules.auth.model.query.SysEnterpriseImageQuery;
|
||||
import org.mapstruct.Builder;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 企业图片附件转换器
|
||||
*
|
||||
* @author yy
|
||||
* @since 2026-09-10
|
||||
*/
|
||||
@SuppressWarnings("UnmappedTargetProperties")
|
||||
@Mapper(builder = @Builder(disableBuilder = true))
|
||||
public interface SysEnterpriseImageConverter {
|
||||
|
||||
SysEnterpriseImageConverter INSTANCE = Mappers.getMapper(SysEnterpriseImageConverter.class);
|
||||
|
||||
/**
|
||||
* 实体转换为响应 DTO
|
||||
*
|
||||
* @param sysEnterpriseImage 实体
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysEnterpriseImageRespDto toRespDto(SysEnterpriseImage sysEnterpriseImage);
|
||||
|
||||
/**
|
||||
* 实体列表转换为响应 DTO 列表
|
||||
*
|
||||
* @param sysEnterpriseImages 实体列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysEnterpriseImageRespDto> toRespDto(List<SysEnterpriseImage> sysEnterpriseImages);
|
||||
|
||||
/**
|
||||
* 请求 DTO 转换为实体
|
||||
*
|
||||
* @param sysEnterpriseImageReqDto 请求 DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysEnterpriseImage fromReqDto(SysEnterpriseImageReqDto sysEnterpriseImageReqDto);
|
||||
|
||||
/**
|
||||
* 请求 DTO 列表转换为实体列表
|
||||
*
|
||||
* @param sysEnterpriseImageReqDtos 请求 DTO 列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysEnterpriseImage> fromReqDto(List<SysEnterpriseImageReqDto> sysEnterpriseImageReqDtos);
|
||||
|
||||
/**
|
||||
* 查询请求 DTO 转换为查询对象
|
||||
*
|
||||
* @param sysEnterpriseImageQueryReqDto 查询请求DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysEnterpriseImageQuery fromQueryReqDtoToQuery(SysEnterpriseImageQueryReqDto sysEnterpriseImageQueryReqDto);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.metalloop.modules.auth.converter;
|
||||
|
||||
import com.metalloop.modules.auth.model.req.SysPermissionQueryReqDto;
|
||||
import com.metalloop.modules.auth.model.req.SysPermissionReqDto;
|
||||
import com.metalloop.modules.auth.model.resp.SysPermissionRespDto;
|
||||
import com.metalloop.modules.auth.model.po.SysPermission;
|
||||
import com.metalloop.modules.auth.model.query.SysPermissionQuery;
|
||||
import org.mapstruct.Builder;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 权限转换器
|
||||
*
|
||||
* @author yy
|
||||
* @since 2026-09-10
|
||||
*/
|
||||
@SuppressWarnings("UnmappedTargetProperties")
|
||||
@Mapper(builder = @Builder(disableBuilder = true))
|
||||
public interface SysPermissionConverter {
|
||||
|
||||
SysPermissionConverter INSTANCE = Mappers.getMapper(SysPermissionConverter.class);
|
||||
|
||||
/**
|
||||
* 实体转换为响应 DTO
|
||||
*
|
||||
* @param sysPermission 实体
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysPermissionRespDto toRespDto(SysPermission sysPermission);
|
||||
|
||||
/**
|
||||
* 实体列表转换为响应 DTO 列表
|
||||
*
|
||||
* @param sysPermissions 实体列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysPermissionRespDto> toRespDto(List<SysPermission> sysPermissions);
|
||||
|
||||
/**
|
||||
* 请求 DTO 转换为实体
|
||||
*
|
||||
* @param sysPermissionReqDto 请求 DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysPermission fromReqDto(SysPermissionReqDto sysPermissionReqDto);
|
||||
|
||||
/**
|
||||
* 请求 DTO 列表转换为实体列表
|
||||
*
|
||||
* @param sysPermissionReqDtos 请求 DTO 列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysPermission> fromReqDto(List<SysPermissionReqDto> sysPermissionReqDtos);
|
||||
|
||||
/**
|
||||
* 查询请求 DTO 转换为查询对象
|
||||
*
|
||||
* @param sysPermissionQueryReqDto 查询请求DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysPermissionQuery fromQueryReqDtoToQuery(SysPermissionQueryReqDto sysPermissionQueryReqDto);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.metalloop.modules.auth.converter;
|
||||
|
||||
import com.metalloop.modules.auth.model.req.SysRoleQueryReqDto;
|
||||
import com.metalloop.modules.auth.model.req.SysRoleReqDto;
|
||||
import com.metalloop.modules.auth.model.resp.SysRoleRespDto;
|
||||
import com.metalloop.modules.auth.model.po.SysRole;
|
||||
import com.metalloop.modules.auth.model.query.SysRoleQuery;
|
||||
import org.mapstruct.Builder;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色转换器
|
||||
*
|
||||
* @author yy
|
||||
* @since 2026-09-10
|
||||
*/
|
||||
@SuppressWarnings("UnmappedTargetProperties")
|
||||
@Mapper(builder = @Builder(disableBuilder = true))
|
||||
public interface SysRoleConverter {
|
||||
|
||||
SysRoleConverter INSTANCE = Mappers.getMapper(SysRoleConverter.class);
|
||||
|
||||
/**
|
||||
* 实体转换为响应 DTO
|
||||
*
|
||||
* @param sysRole 实体
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysRoleRespDto toRespDto(SysRole sysRole);
|
||||
|
||||
/**
|
||||
* 实体列表转换为响应 DTO 列表
|
||||
*
|
||||
* @param sysRoles 实体列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysRoleRespDto> toRespDto(List<SysRole> sysRoles);
|
||||
|
||||
/**
|
||||
* 请求 DTO 转换为实体
|
||||
*
|
||||
* @param sysRoleReqDto 请求 DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysRole fromReqDto(SysRoleReqDto sysRoleReqDto);
|
||||
|
||||
/**
|
||||
* 请求 DTO 列表转换为实体列表
|
||||
*
|
||||
* @param sysRoleReqDtos 请求 DTO 列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysRole> fromReqDto(List<SysRoleReqDto> sysRoleReqDtos);
|
||||
|
||||
/**
|
||||
* 查询请求 DTO 转换为查询对象
|
||||
*
|
||||
* @param sysRoleQueryReqDto 查询请求DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysRoleQuery fromQueryReqDtoToQuery(SysRoleQueryReqDto sysRoleQueryReqDto);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.metalloop.modules.auth.converter;
|
||||
|
||||
import com.metalloop.modules.auth.model.req.SysRolePermissionQueryReqDto;
|
||||
import com.metalloop.modules.auth.model.req.SysRolePermissionReqDto;
|
||||
import com.metalloop.modules.auth.model.resp.SysRolePermissionRespDto;
|
||||
import com.metalloop.modules.auth.model.po.SysRolePermission;
|
||||
import com.metalloop.modules.auth.model.query.SysRolePermissionQuery;
|
||||
import org.mapstruct.Builder;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色-权限关联转换器
|
||||
*
|
||||
* @author yy
|
||||
* @since 2026-09-10
|
||||
*/
|
||||
@SuppressWarnings("UnmappedTargetProperties")
|
||||
@Mapper(builder = @Builder(disableBuilder = true))
|
||||
public interface SysRolePermissionConverter {
|
||||
|
||||
SysRolePermissionConverter INSTANCE = Mappers.getMapper(SysRolePermissionConverter.class);
|
||||
|
||||
/**
|
||||
* 实体转换为响应 DTO
|
||||
*
|
||||
* @param sysRolePermission 实体
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysRolePermissionRespDto toRespDto(SysRolePermission sysRolePermission);
|
||||
|
||||
/**
|
||||
* 实体列表转换为响应 DTO 列表
|
||||
*
|
||||
* @param sysRolePermissions 实体列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysRolePermissionRespDto> toRespDto(List<SysRolePermission> sysRolePermissions);
|
||||
|
||||
/**
|
||||
* 请求 DTO 转换为实体
|
||||
*
|
||||
* @param sysRolePermissionReqDto 请求 DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysRolePermission fromReqDto(SysRolePermissionReqDto sysRolePermissionReqDto);
|
||||
|
||||
/**
|
||||
* 请求 DTO 列表转换为实体列表
|
||||
*
|
||||
* @param sysRolePermissionReqDtos 请求 DTO 列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysRolePermission> fromReqDto(List<SysRolePermissionReqDto> sysRolePermissionReqDtos);
|
||||
|
||||
/**
|
||||
* 查询请求 DTO 转换为查询对象
|
||||
*
|
||||
* @param sysRolePermissionQueryReqDto 查询请求DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysRolePermissionQuery fromQueryReqDtoToQuery(SysRolePermissionQueryReqDto sysRolePermissionQueryReqDto);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.metalloop.modules.auth.converter;
|
||||
|
||||
import com.metalloop.modules.auth.model.req.SysUserQueryReqDto;
|
||||
import com.metalloop.modules.auth.model.req.SysUserReqDto;
|
||||
import com.metalloop.modules.auth.model.resp.SysUserRespDto;
|
||||
import com.metalloop.modules.auth.model.po.SysUser;
|
||||
import com.metalloop.modules.auth.model.query.SysUserQuery;
|
||||
import org.mapstruct.Builder;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户转换器
|
||||
*
|
||||
* @author yy
|
||||
* @since 2026-09-10
|
||||
*/
|
||||
@SuppressWarnings("UnmappedTargetProperties")
|
||||
@Mapper(builder = @Builder(disableBuilder = true))
|
||||
public interface SysUserConverter {
|
||||
|
||||
SysUserConverter INSTANCE = Mappers.getMapper(SysUserConverter.class);
|
||||
|
||||
/**
|
||||
* 实体转换为响应 DTO
|
||||
*
|
||||
* @param sysUser 实体
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysUserRespDto toRespDto(SysUser sysUser);
|
||||
|
||||
/**
|
||||
* 实体列表转换为响应 DTO 列表
|
||||
*
|
||||
* @param sysUsers 实体列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysUserRespDto> toRespDto(List<SysUser> sysUsers);
|
||||
|
||||
/**
|
||||
* 请求 DTO 转换为实体
|
||||
*
|
||||
* @param sysUserReqDto 请求 DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysUser fromReqDto(SysUserReqDto sysUserReqDto);
|
||||
|
||||
/**
|
||||
* 请求 DTO 列表转换为实体列表
|
||||
*
|
||||
* @param sysUserReqDtos 请求 DTO 列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysUser> fromReqDto(List<SysUserReqDto> sysUserReqDtos);
|
||||
|
||||
/**
|
||||
* 查询请求 DTO 转换为查询对象
|
||||
*
|
||||
* @param sysUserQueryReqDto 查询请求DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysUserQuery fromQueryReqDtoToQuery(SysUserQueryReqDto sysUserQueryReqDto);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.metalloop.modules.auth.converter;
|
||||
|
||||
import com.metalloop.modules.auth.model.req.SysUserRoleQueryReqDto;
|
||||
import com.metalloop.modules.auth.model.req.SysUserRoleReqDto;
|
||||
import com.metalloop.modules.auth.model.resp.SysUserRoleRespDto;
|
||||
import com.metalloop.modules.auth.model.po.SysUserRole;
|
||||
import com.metalloop.modules.auth.model.query.SysUserRoleQuery;
|
||||
import org.mapstruct.Builder;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户与角色关联转换器
|
||||
*
|
||||
* @author yy
|
||||
* @since 2026-09-10
|
||||
*/
|
||||
@SuppressWarnings("UnmappedTargetProperties")
|
||||
@Mapper(builder = @Builder(disableBuilder = true))
|
||||
public interface SysUserRoleConverter {
|
||||
|
||||
SysUserRoleConverter INSTANCE = Mappers.getMapper(SysUserRoleConverter.class);
|
||||
|
||||
/**
|
||||
* 实体转换为响应 DTO
|
||||
*
|
||||
* @param sysUserRole 实体
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysUserRoleRespDto toRespDto(SysUserRole sysUserRole);
|
||||
|
||||
/**
|
||||
* 实体列表转换为响应 DTO 列表
|
||||
*
|
||||
* @param sysUserRoles 实体列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysUserRoleRespDto> toRespDto(List<SysUserRole> sysUserRoles);
|
||||
|
||||
/**
|
||||
* 请求 DTO 转换为实体
|
||||
*
|
||||
* @param sysUserRoleReqDto 请求 DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysUserRole fromReqDto(SysUserRoleReqDto sysUserRoleReqDto);
|
||||
|
||||
/**
|
||||
* 请求 DTO 列表转换为实体列表
|
||||
*
|
||||
* @param sysUserRoleReqDtos 请求 DTO 列表
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
List<SysUserRole> fromReqDto(List<SysUserRoleReqDto> sysUserRoleReqDtos);
|
||||
|
||||
/**
|
||||
* 查询请求 DTO 转换为查询对象
|
||||
*
|
||||
* @param sysUserRoleQueryReqDto 查询请求DTO
|
||||
* @return 转换后的数据
|
||||
*/
|
||||
SysUserRoleQuery fromQueryReqDtoToQuery(SysUserRoleQueryReqDto sysUserRoleQueryReqDto);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.metalloop.modules.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.metalloop.modules.auth.model.po.SysEnterpriseImage;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 企业图片附件 Mapper
|
||||
*
|
||||
* @author yy
|
||||
* @since 2026-09-10
|
||||
*/
|
||||
@Mapper
|
||||
public interface SysEnterpriseImageMapper extends BaseMapper<SysEnterpriseImage> {
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user