Initial commit
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
|
||||
# CI/CD
|
||||
.github
|
||||
|
||||
# IDE
|
||||
.idea
|
||||
.vscode
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# Maven
|
||||
target/
|
||||
!target/*.jar
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Documentation
|
||||
README.md
|
||||
docs/
|
||||
|
||||
# Docker
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
docker-compose.yml
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# Eclipse
|
||||
.settings/
|
||||
.project
|
||||
.classpath
|
||||
.factorypath
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
|
||||
# VS Code
|
||||
.vscode/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# Application properties with sensitive data
|
||||
application-local.yml
|
||||
application-local.properties
|
||||
|
||||
# Maven cache (CI/CD)
|
||||
.m2/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# 多阶段构建 Dockerfile
|
||||
# 阶段1: 构建
|
||||
FROM openjdk:21
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY target/auth-backend-1.0.0.jar /app/
|
||||
|
||||
EXPOSE 9001
|
||||
|
||||
# 启动应用
|
||||
CMD ["java", "-jar", "auth-backend-1.0.0.jar"]
|
||||
@@ -0,0 +1,173 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.7</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.kurihada</groupId>
|
||||
<artifactId>auth-backend</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>auth-system</name>
|
||||
<description>权限管理系统</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<jjwt.version>0.12.3</jjwt.version>
|
||||
<mybatis-plus.version>3.5.14</mybatis-plus.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-bom</artifactId>
|
||||
<version>${mybatis-plus.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Mail -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Thymeleaf (用于邮件模板) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lettuce (Redis客户端) -->
|
||||
<dependency>
|
||||
<groupId>io.lettuce</groupId>
|
||||
<artifactId>lettuce-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Spring Boot Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot AOP -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL Driver -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>${jjwt.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Knife4j (Swagger UI Enhancement) -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
|
||||
<version>4.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Security Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.kurihada.auth;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 权限系统主应用类
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class AuthSystemApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AuthSystemApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.kurihada.auth.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 限流注解
|
||||
* 用于标记需要限流的接口
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface RateLimit {
|
||||
|
||||
/**
|
||||
* 限流的键前缀,用于区分不同的限流规则
|
||||
* 例如: "login", "register", "api"
|
||||
*/
|
||||
String key() default "rate_limit";
|
||||
|
||||
/**
|
||||
* 时间窗口,单位:秒
|
||||
* 默认 60 秒
|
||||
*/
|
||||
int window() default 60;
|
||||
|
||||
/**
|
||||
* 时间窗口内允许的最大请求次数
|
||||
* 默认 5 次
|
||||
*/
|
||||
int maxRequests() default 5;
|
||||
|
||||
/**
|
||||
* 限流维度类型
|
||||
* IP: 基于IP地址限流
|
||||
* USER: 基于用户名限流
|
||||
* IP_AND_USER: 同时基于IP和用户名限流
|
||||
*/
|
||||
LimitType limitType() default LimitType.IP;
|
||||
|
||||
/**
|
||||
* 限流提示消息
|
||||
*/
|
||||
String message() default "请求过于频繁,请稍后再试";
|
||||
|
||||
/**
|
||||
* 限流维度类型枚举
|
||||
*/
|
||||
enum LimitType {
|
||||
/**
|
||||
* 基于IP地址限流
|
||||
*/
|
||||
IP,
|
||||
|
||||
/**
|
||||
* 基于用户名限流
|
||||
*/
|
||||
USER,
|
||||
|
||||
/**
|
||||
* 同时基于IP和用户名限流
|
||||
*/
|
||||
IP_AND_USER
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package com.kurihada.auth.annotation;
|
||||
|
||||
import com.kurihada.auth.validation.PasswordValidator;
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 密码复杂度验证注解
|
||||
* 验证密码是否满足复杂度要求
|
||||
*/
|
||||
@Target({ElementType.FIELD, ElementType.PARAMETER})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = PasswordValidator.class)
|
||||
@Documented
|
||||
public @interface ValidPassword {
|
||||
|
||||
/**
|
||||
* 默认错误消息
|
||||
*/
|
||||
String message() default "密码不符合复杂度要求";
|
||||
|
||||
/**
|
||||
* 分组
|
||||
*/
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
/**
|
||||
* 载荷
|
||||
*/
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
||||
/**
|
||||
* 最小长度(默认8位)
|
||||
*/
|
||||
int minLength() default 8;
|
||||
|
||||
/**
|
||||
* 最大长度(默认32位)
|
||||
*/
|
||||
int maxLength() default 32;
|
||||
|
||||
/**
|
||||
* 是否要求包含小写字母(默认true)
|
||||
*/
|
||||
boolean requireLowercase() default true;
|
||||
|
||||
/**
|
||||
* 是否要求包含大写字母(默认true)
|
||||
*/
|
||||
boolean requireUppercase() default true;
|
||||
|
||||
/**
|
||||
* 是否要求包含数字(默认true)
|
||||
*/
|
||||
boolean requireDigit() default true;
|
||||
|
||||
/**
|
||||
* 是否要求包含特殊字符(默认true)
|
||||
*/
|
||||
boolean requireSpecialChar() default true;
|
||||
|
||||
/**
|
||||
* 允许的特殊字符(默认常用特殊字符)
|
||||
*/
|
||||
String specialChars() default "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||||
|
||||
/**
|
||||
* 是否禁止包含用户名(默认true)
|
||||
*/
|
||||
boolean forbidUsername() default true;
|
||||
|
||||
/**
|
||||
* 是否禁止常见弱密码(默认true)
|
||||
*/
|
||||
boolean forbidCommonPasswords() default true;
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.kurihada.auth.aspect;
|
||||
|
||||
import com.kurihada.auth.annotation.RateLimit;
|
||||
import com.kurihada.auth.component.RedisRateLimiter;
|
||||
import com.kurihada.auth.exception.RateLimitException;
|
||||
import com.kurihada.auth.util.IpUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* 限流切面
|
||||
* 拦截带有 @RateLimit 注解的方法,执行限流检查
|
||||
*/
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RateLimitAspect {
|
||||
|
||||
private final RedisRateLimiter rateLimiter;
|
||||
|
||||
@Around("@annotation(com.kurihada.auth.annotation.RateLimit)")
|
||||
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
|
||||
|
||||
if (rateLimit == null) {
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
// 获取请求信息
|
||||
HttpServletRequest request = getRequest();
|
||||
if (request == null) {
|
||||
log.warn("无法获取 HttpServletRequest,跳过限流检查");
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
// 构建限流键
|
||||
String limitKey = buildLimitKey(rateLimit, request, point.getArgs());
|
||||
|
||||
// 执行限流检查
|
||||
RedisRateLimiter.RateLimitResult result = rateLimiter.tryAcquire(
|
||||
limitKey,
|
||||
rateLimit.window(),
|
||||
rateLimit.maxRequests()
|
||||
);
|
||||
|
||||
if (!result.isAllowed()) {
|
||||
String errorMessage = String.format("%s,请在 %d 秒后重试",
|
||||
rateLimit.message(), result.getWaitTime());
|
||||
log.warn("限流触发 - 方法: {}, Key: {}, 等待时间: {}秒",
|
||||
method.getName(), limitKey, result.getWaitTime());
|
||||
throw new RateLimitException(errorMessage, result.getWaitTime());
|
||||
}
|
||||
|
||||
log.debug("限流检查通过 - 方法: {}, Key: {}, 剩余次数: {}",
|
||||
method.getName(), limitKey, result.getRemaining());
|
||||
|
||||
return point.proceed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建限流键
|
||||
*/
|
||||
private String buildLimitKey(RateLimit rateLimit, HttpServletRequest request, Object[] args) {
|
||||
StringBuilder keyBuilder = new StringBuilder("rate_limit:");
|
||||
keyBuilder.append(rateLimit.key()).append(":");
|
||||
|
||||
switch (rateLimit.limitType()) {
|
||||
case IP:
|
||||
String ip = IpUtil.getIpAddress(request);
|
||||
keyBuilder.append("ip:").append(ip);
|
||||
break;
|
||||
|
||||
case USER:
|
||||
String username = extractUsername(args);
|
||||
if (username != null) {
|
||||
keyBuilder.append("user:").append(username);
|
||||
} else {
|
||||
// 如果无法获取用户名,降级为IP限流
|
||||
log.warn("无法提取用户名,降级为IP限流");
|
||||
String fallbackIp = IpUtil.getIpAddress(request);
|
||||
keyBuilder.append("ip:").append(fallbackIp);
|
||||
}
|
||||
break;
|
||||
|
||||
case IP_AND_USER:
|
||||
String ipAddr = IpUtil.getIpAddress(request);
|
||||
String user = extractUsername(args);
|
||||
if (user != null) {
|
||||
keyBuilder.append("ip_user:").append(ipAddr).append(":").append(user);
|
||||
} else {
|
||||
// 如果无法获取用户名,只使用IP
|
||||
keyBuilder.append("ip:").append(ipAddr);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return keyBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从方法参数中提取用户名
|
||||
* 支持 LoginRequest、RegisterRequest 等包含 username 字段的对象
|
||||
*/
|
||||
private String extractUsername(Object[] args) {
|
||||
if (args == null || args.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Object arg : args) {
|
||||
if (arg == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试通过反射获取 username 字段
|
||||
Method getUsername = arg.getClass().getMethod("getUsername");
|
||||
Object username = getUsername.invoke(arg);
|
||||
if (username instanceof String) {
|
||||
return (String) username;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略异常,继续尝试下一个参数
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 HTTP 请求
|
||||
*/
|
||||
private HttpServletRequest getRequest() {
|
||||
ServletRequestAttributes attributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
return attributes != null ? attributes.getRequest() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package com.kurihada.auth.component;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.script.RedisScript;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Redis 实现的限流器
|
||||
* 使用滑动窗口算法
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RedisRateLimiter {
|
||||
|
||||
private final RedisTemplate<String, String> redisTemplate;
|
||||
|
||||
/**
|
||||
* 滑动窗口限流算法的 Lua 脚本
|
||||
* 使用 Sorted Set 实现,score 为时间戳
|
||||
*/
|
||||
private static final String RATE_LIMIT_SCRIPT = """
|
||||
local key = KEYS[1]
|
||||
local window = tonumber(ARGV[1])
|
||||
local maxRequests = tonumber(ARGV[2])
|
||||
local currentTime = tonumber(ARGV[3])
|
||||
|
||||
-- 移除窗口之外的记录
|
||||
redis.call('ZREMRANGEBYSCORE', key, 0, currentTime - window * 1000)
|
||||
|
||||
-- 获取当前窗口内的请求数
|
||||
local currentRequests = redis.call('ZCARD', key)
|
||||
|
||||
if currentRequests < maxRequests then
|
||||
-- 允许请求,添加当前时间戳
|
||||
redis.call('ZADD', key, currentTime, currentTime)
|
||||
-- 设置过期时间(窗口时间 + 1秒缓冲)
|
||||
redis.call('EXPIRE', key, window + 1)
|
||||
return {1, maxRequests - currentRequests - 1}
|
||||
else
|
||||
-- 拒绝请求,返回最早请求的时间,用于计算需要等待的时间
|
||||
local oldestTime = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')[2]
|
||||
local remainingTime = math.ceil((tonumber(oldestTime) + window * 1000 - currentTime) / 1000)
|
||||
return {0, remainingTime}
|
||||
end
|
||||
""";
|
||||
|
||||
/**
|
||||
* 检查是否允许请求
|
||||
*
|
||||
* @param key 限流键
|
||||
* @param window 时间窗口(秒)
|
||||
* @param maxRequests 最大请求数
|
||||
* @return 如果允许请求返回 true,否则返回 false
|
||||
*/
|
||||
public boolean isAllowed(String key, int window, int maxRequests) {
|
||||
RateLimitResult result = tryAcquire(key, window, maxRequests);
|
||||
return result.isAllowed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取请求许可
|
||||
*
|
||||
* @param key 限流键
|
||||
* @param window 时间窗口(秒)
|
||||
* @param maxRequests 最大请求数
|
||||
* @return 限流结果
|
||||
*/
|
||||
public RateLimitResult tryAcquire(String key, int window, int maxRequests) {
|
||||
try {
|
||||
long currentTime = Instant.now().toEpochMilli();
|
||||
|
||||
RedisScript<List> script = RedisScript.of(RATE_LIMIT_SCRIPT, List.class);
|
||||
List<Long> result = (List<Long>) redisTemplate.execute(
|
||||
script,
|
||||
Collections.singletonList(key),
|
||||
String.valueOf(window),
|
||||
String.valueOf(maxRequests),
|
||||
String.valueOf(currentTime)
|
||||
);
|
||||
|
||||
if (result == null || result.size() != 2) {
|
||||
log.error("限流脚本执行异常,返回结果为空或格式错误");
|
||||
// 降级处理:允许请求通过
|
||||
return new RateLimitResult(true, 0, 0);
|
||||
}
|
||||
|
||||
boolean allowed = result.get(0) == 1;
|
||||
int remaining = result.get(1).intValue();
|
||||
|
||||
if (allowed) {
|
||||
log.debug("限流检查通过,key: {}, 剩余请求数: {}", key, remaining);
|
||||
return new RateLimitResult(true, remaining, 0);
|
||||
} else {
|
||||
log.warn("触发限流,key: {}, 需等待: {}秒", key, remaining);
|
||||
return new RateLimitResult(false, 0, remaining);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("限流检查异常,key: {}", key, e);
|
||||
// 降级处理:允许请求通过,避免因限流组件故障影响正常业务
|
||||
return new RateLimitResult(true, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置限流计数
|
||||
*
|
||||
* @param key 限流键
|
||||
*/
|
||||
public void reset(String key) {
|
||||
try {
|
||||
redisTemplate.delete(key);
|
||||
log.info("重置限流计数,key: {}", key);
|
||||
} catch (Exception e) {
|
||||
log.error("重置限流计数失败,key: {}", key, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前窗口内的请求数
|
||||
*
|
||||
* @param key 限流键
|
||||
* @return 请求数
|
||||
*/
|
||||
public long getCurrentRequests(String key) {
|
||||
try {
|
||||
Long count = redisTemplate.opsForZSet().zCard(key);
|
||||
return count != null ? count : 0;
|
||||
} catch (Exception e) {
|
||||
log.error("获取当前请求数失败,key: {}", key, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 限流结果
|
||||
*/
|
||||
public static class RateLimitResult {
|
||||
private final boolean allowed;
|
||||
private final int remaining;
|
||||
private final int waitTime;
|
||||
|
||||
public RateLimitResult(boolean allowed, int remaining, int waitTime) {
|
||||
this.allowed = allowed;
|
||||
this.remaining = remaining;
|
||||
this.waitTime = waitTime;
|
||||
}
|
||||
|
||||
public boolean isAllowed() {
|
||||
return allowed;
|
||||
}
|
||||
|
||||
public int getRemaining() {
|
||||
return remaining;
|
||||
}
|
||||
|
||||
public int getWaitTime() {
|
||||
return waitTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.kurihada.auth.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.AsyncConfigurer;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* 异步配置
|
||||
* 用于审计日志等异步任务
|
||||
*/
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig implements AsyncConfigurer {
|
||||
|
||||
@Override
|
||||
public Executor getAsyncExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(2);
|
||||
executor.setMaxPoolSize(5);
|
||||
executor.setQueueCapacity(100);
|
||||
executor.setThreadNamePrefix("async-audit-");
|
||||
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||
executor.setAwaitTerminationSeconds(60);
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.kurihada.auth.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
/**
|
||||
* 开发环境跨域配置(允许所有域名)
|
||||
*/
|
||||
@Configuration
|
||||
@Profile({"default", "dev"}) // 仅在开发环境生效
|
||||
public class CorsConfig {
|
||||
|
||||
@Bean
|
||||
public CorsFilter corsFilter() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
|
||||
// 允许所有域名跨域(生产环境应该指定具体域名)
|
||||
config.addAllowedOriginPattern("*");
|
||||
|
||||
// 允许所有请求头
|
||||
config.addAllowedHeader("*");
|
||||
|
||||
// 允许所有请求方法
|
||||
config.addAllowedMethod("*");
|
||||
|
||||
// 允许发送Cookie
|
||||
config.setAllowCredentials(true);
|
||||
|
||||
// 预检请求的有效期,单位为秒
|
||||
config.setMaxAge(3600L);
|
||||
|
||||
// 允许的响应头
|
||||
config.addExposedHeader("Authorization");
|
||||
config.addExposedHeader("Content-Type");
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.kurihada.auth.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.Components;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Knife4j API文档配置
|
||||
*/
|
||||
@Configuration
|
||||
public class Knife4jConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("权限管理系统 API")
|
||||
.description("基于Spring Boot 3 + MyBatis Plus + JWT的权限管理系统接口文档")
|
||||
.version("1.0.0")
|
||||
.contact(new Contact()
|
||||
.name("Auth System")
|
||||
.email("support@example.com"))
|
||||
.license(new License()
|
||||
.name("Apache 2.0")
|
||||
.url("https://www.apache.org/licenses/LICENSE-2.0")))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("Bearer",
|
||||
new SecurityScheme()
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.description("请输入JWT Token,格式:Bearer {token}")))
|
||||
.addSecurityItem(new SecurityRequirement().addList("Bearer"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.kurihada.auth.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import com.kurihada.auth.context.TenantContext;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@MapperScan("com.kurihada.auth.mapper")
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* 添加分页插件和租户插件
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
|
||||
// 租户插件(必须在分页插件之前)
|
||||
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler() {
|
||||
@Override
|
||||
public Expression getTenantId() {
|
||||
// 从上下文中获取租户ID
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
if (tenantId == null) {
|
||||
return null;
|
||||
}
|
||||
return new LongValue(tenantId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTenantIdColumn() {
|
||||
// 租户字段名
|
||||
return "tenant_id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ignoreTable(String tableName) {
|
||||
// 忽略的表(不进行租户隔离)
|
||||
// tenants 表本身不需要租户隔离
|
||||
// permissions 表允许为 null(公共权限)
|
||||
return "tenants".equalsIgnoreCase(tableName);
|
||||
}
|
||||
}));
|
||||
|
||||
// 分页插件(必须在租户插件之后)
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.kurihada.auth.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.CorsFilter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 生产环境跨域配置(限制允许的域名)
|
||||
*/
|
||||
@Configuration
|
||||
@Profile("prod") // 仅在生产环境生效
|
||||
public class ProdCorsConfig {
|
||||
|
||||
@Bean
|
||||
public CorsFilter prodCorsFilter() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
|
||||
// 生产环境:只允许指定的域名跨域
|
||||
config.setAllowedOrigins(Arrays.asList(
|
||||
"https://your-frontend-domain.com",
|
||||
"https://www.your-frontend-domain.com",
|
||||
"https://admin.your-frontend-domain.com"
|
||||
));
|
||||
|
||||
// 允许的请求头
|
||||
config.setAllowedHeaders(Arrays.asList(
|
||||
"Authorization",
|
||||
"Content-Type",
|
||||
"Accept",
|
||||
"X-Requested-With"
|
||||
));
|
||||
|
||||
// 允许的请求方法
|
||||
config.setAllowedMethods(Arrays.asList(
|
||||
"GET",
|
||||
"POST",
|
||||
"PUT",
|
||||
"DELETE",
|
||||
"OPTIONS"
|
||||
));
|
||||
|
||||
// 允许发送Cookie
|
||||
config.setAllowCredentials(true);
|
||||
|
||||
// 预检请求的有效期
|
||||
config.setMaxAge(3600L);
|
||||
|
||||
// 允许的响应头
|
||||
config.setExposedHeaders(Arrays.asList(
|
||||
"Authorization",
|
||||
"Content-Type"
|
||||
));
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
|
||||
return new CorsFilter(source);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.kurihada.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis 配置类
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
|
||||
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
|
||||
serializer.setObjectMapper(mapper);
|
||||
|
||||
// 使用StringRedisSerializer来序列化和反序列化redis的key值
|
||||
StringRedisSerializer stringSerializer = new StringRedisSerializer();
|
||||
|
||||
// key采用String的序列化方式
|
||||
template.setKeySerializer(stringSerializer);
|
||||
// hash的key也采用String的序列化方式
|
||||
template.setHashKeySerializer(stringSerializer);
|
||||
// value序列化方式采用jackson
|
||||
template.setValueSerializer(serializer);
|
||||
// hash的value序列化方式采用jackson
|
||||
template.setHashValueSerializer(serializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.kurihada.auth.config;
|
||||
|
||||
import com.kurihada.auth.security.JwtAccessDeniedHandler;
|
||||
import com.kurihada.auth.security.JwtAuthenticationEntryPoint;
|
||||
import com.kurihada.auth.security.JwtAuthenticationFilter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
/**
|
||||
* Spring Security配置类
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final UserDetailsService userDetailsService;
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
|
||||
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
|
||||
|
||||
/**
|
||||
* 配置安全过滤器链
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 禁用CSRF
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
// 启用CORS(使用默认配置,会自动使用CorsFilter)
|
||||
.cors(cors -> {})
|
||||
// 配置异常处理
|
||||
.exceptionHandling(exception -> exception
|
||||
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
|
||||
.accessDeniedHandler(jwtAccessDeniedHandler)
|
||||
)
|
||||
// 配置会话管理 - 无状态
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
)
|
||||
// 配置授权规则
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// 允许匿名访问的接口
|
||||
.requestMatchers("/api/v1/auth/login", "/api/v1/auth/register",
|
||||
"/api/v1/auth/forgot-password", "/api/v1/auth/reset-password",
|
||||
"/api/v1/auth/verify-email", "/api/v1/auth/resend-verification").permitAll()
|
||||
// Knife4j 文档接口
|
||||
.requestMatchers("/doc.html", "/webjars/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll()
|
||||
// 其他接口需要认证
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
// 配置认证提供者
|
||||
.authenticationProvider(authenticationProvider())
|
||||
// 添加JWT过滤器
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码编码器
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证提供者
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.kurihada.auth.config;
|
||||
|
||||
import com.kurihada.auth.interceptor.TenantInterceptor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC 配置
|
||||
*/
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final TenantInterceptor tenantInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(tenantInterceptor)
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/api/auth/login",
|
||||
"/api/auth/register",
|
||||
"/doc.html",
|
||||
"/swagger-resources/**",
|
||||
"/webjars/**",
|
||||
"/v3/api-docs/**"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.kurihada.auth.constant;
|
||||
|
||||
/**
|
||||
* 缓存常量
|
||||
*/
|
||||
public class CacheConstants {
|
||||
|
||||
/**
|
||||
* 缓存键前缀
|
||||
*/
|
||||
public static final String CACHE_PREFIX = "auth:";
|
||||
|
||||
/**
|
||||
* 用户权限缓存键前缀
|
||||
* 格式: auth:user:permissions:{userId}:{tenantId}
|
||||
*/
|
||||
public static final String USER_PERMISSIONS_KEY = CACHE_PREFIX + "user:permissions:";
|
||||
|
||||
/**
|
||||
* 用户角色缓存键前缀
|
||||
* 格式: auth:user:roles:{userId}:{tenantId}
|
||||
*/
|
||||
public static final String USER_ROLES_KEY = CACHE_PREFIX + "user:roles:";
|
||||
|
||||
/**
|
||||
* 角色权限缓存键前缀
|
||||
* 格式: auth:role:permissions:{roleId}:{tenantId}
|
||||
*/
|
||||
public static final String ROLE_PERMISSIONS_KEY = CACHE_PREFIX + "role:permissions:";
|
||||
|
||||
/**
|
||||
* 租户所有用户权限缓存键前缀
|
||||
* 格式: auth:tenant:users:{tenantId}
|
||||
*/
|
||||
public static final String TENANT_USERS_KEY = CACHE_PREFIX + "tenant:users:";
|
||||
|
||||
/**
|
||||
* 用户信息缓存键前缀
|
||||
* 格式: auth:user:info:{userId}
|
||||
*/
|
||||
public static final String USER_INFO_KEY = CACHE_PREFIX + "user:info:";
|
||||
|
||||
/**
|
||||
* 权限缓存过期时间(秒)- 默认2小时
|
||||
*/
|
||||
public static final long PERMISSION_CACHE_TTL = 7200L;
|
||||
|
||||
/**
|
||||
* 用户信息缓存过期时间(秒)- 默认30分钟
|
||||
*/
|
||||
public static final long USER_INFO_CACHE_TTL = 1800L;
|
||||
|
||||
/**
|
||||
* 角色权限缓存过期时间(秒)- 默认1小时
|
||||
*/
|
||||
public static final long ROLE_PERMISSION_CACHE_TTL = 3600L;
|
||||
|
||||
/**
|
||||
* 构建用户权限缓存键
|
||||
*/
|
||||
public static String buildUserPermissionKey(Long userId, Long tenantId) {
|
||||
return USER_PERMISSIONS_KEY + userId + ":" + tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用户角色缓存键
|
||||
*/
|
||||
public static String buildUserRoleKey(Long userId, Long tenantId) {
|
||||
return USER_ROLES_KEY + userId + ":" + tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建角色权限缓存键
|
||||
*/
|
||||
public static String buildRolePermissionKey(Long roleId, Long tenantId) {
|
||||
return ROLE_PERMISSIONS_KEY + roleId + ":" + tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建租户用户集合缓存键
|
||||
*/
|
||||
public static String buildTenantUsersKey(Long tenantId) {
|
||||
return TENANT_USERS_KEY + tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用户信息缓存键
|
||||
*/
|
||||
public static String buildUserInfoKey(Long userId) {
|
||||
return USER_INFO_KEY + userId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.kurihada.auth.context;
|
||||
|
||||
/**
|
||||
* 租户上下文 - 使用ThreadLocal存储当前租户ID
|
||||
*/
|
||||
public class TenantContext {
|
||||
|
||||
private static final ThreadLocal<Long> CURRENT_TENANT = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 设置当前租户ID
|
||||
*/
|
||||
public static void setTenantId(Long tenantId) {
|
||||
CURRENT_TENANT.set(tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前租户ID
|
||||
*/
|
||||
public static Long getTenantId() {
|
||||
return CURRENT_TENANT.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除当前租户ID
|
||||
*/
|
||||
public static void clear() {
|
||||
CURRENT_TENANT.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.kurihada.auth.controller;
|
||||
|
||||
import com.kurihada.auth.dto.AuditLogQueryRequest;
|
||||
import com.kurihada.auth.dto.AuditLogResponse;
|
||||
import com.kurihada.auth.dto.PageResult;
|
||||
import com.kurihada.auth.dto.Result;
|
||||
import com.kurihada.auth.service.AuditLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 审计日志控制器
|
||||
*/
|
||||
@Tag(name = "审计日志管理", description = "审计日志查询相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/audit-logs")
|
||||
@RequiredArgsConstructor
|
||||
@SecurityRequirement(name = "Bearer")
|
||||
public class AuditLogController {
|
||||
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
/**
|
||||
* 分页查询审计日志
|
||||
*/
|
||||
@Operation(
|
||||
summary = "分页查询审计日志",
|
||||
description = "根据条件查询审计日志,支持按用户、操作类型、时间范围等筛选。需要 ADMIN 或 AUDIT_VIEWER 权限"
|
||||
)
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'AUDIT_VIEWER')")
|
||||
public Result<PageResult<AuditLogResponse>> queryAuditLogs(
|
||||
@Valid @ModelAttribute AuditLogQueryRequest request) {
|
||||
PageResult<AuditLogResponse> result = auditLogService.queryAuditLogs(request);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取审计日志详情
|
||||
*/
|
||||
@Operation(
|
||||
summary = "获取审计日志详情",
|
||||
description = "根据日志ID查询审计日志详细信息。需要 ADMIN 或 AUDIT_VIEWER 权限"
|
||||
)
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'AUDIT_VIEWER')")
|
||||
public Result<AuditLogResponse> getAuditLogById(
|
||||
@Parameter(description = "审计日志ID") @PathVariable Long id) {
|
||||
AuditLogResponse response = auditLogService.getAuditLogById(id);
|
||||
return Result.success(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package com.kurihada.auth.controller;
|
||||
|
||||
import com.kurihada.auth.annotation.RateLimit;
|
||||
import com.kurihada.auth.dto.*;
|
||||
import com.kurihada.auth.entity.User;
|
||||
import com.kurihada.auth.mapper.UserMapper;
|
||||
import com.kurihada.auth.service.AuthService;
|
||||
import com.kurihada.auth.util.JwtUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
*/
|
||||
@Tag(name = "认证管理", description = "用户登录、注册相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
|
||||
private final AuthService authService;
|
||||
private final JwtUtil jwtUtil;
|
||||
private final UserMapper userMapper;
|
||||
|
||||
@Value("${jwt.header}")
|
||||
private String tokenHeader;
|
||||
|
||||
@Value("${jwt.prefix}")
|
||||
private String tokenPrefix;
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
@Operation(summary = "用户登录", description = "通过用户名和密码登录,返回JWT Token")
|
||||
@PostMapping("/login")
|
||||
@RateLimit(
|
||||
key = "login",
|
||||
window = 60,
|
||||
maxRequests = 5,
|
||||
limitType = RateLimit.LimitType.IP_AND_USER,
|
||||
message = "登录请求过于频繁,请稍后再试"
|
||||
)
|
||||
public Result<LoginResponse> login(@Valid @RequestBody LoginRequest request, HttpServletRequest httpRequest) {
|
||||
LoginResponse response = authService.login(request, httpRequest);
|
||||
return Result.success("登录成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@Operation(summary = "用户注册", description = "注册新用户账号")
|
||||
@PostMapping("/register")
|
||||
@RateLimit(
|
||||
key = "register",
|
||||
window = 300,
|
||||
maxRequests = 3,
|
||||
limitType = RateLimit.LimitType.IP,
|
||||
message = "注册请求过于频繁,请稍后再试"
|
||||
)
|
||||
public Result<Void> register(@Valid @RequestBody RegisterRequest request, HttpServletRequest httpRequest) {
|
||||
authService.register(request, httpRequest);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Access Token
|
||||
*/
|
||||
@Operation(summary = "刷新Token", description = "使用 Refresh Token 获取新的 Access Token")
|
||||
@PostMapping("/refresh")
|
||||
@RateLimit(
|
||||
key = "refresh",
|
||||
window = 60,
|
||||
maxRequests = 10,
|
||||
limitType = RateLimit.LimitType.IP,
|
||||
message = "刷新Token请求过于频繁,请稍后再试"
|
||||
)
|
||||
public Result<LoginResponse> refreshToken(@Valid @RequestBody RefreshTokenRequest request) {
|
||||
LoginResponse response = authService.refreshAccessToken(request.getRefreshToken());
|
||||
return Result.success("Token刷新成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户退出登录
|
||||
*/
|
||||
@Operation(summary = "退出登录", description = "退出登录,使当前 Token 失效")
|
||||
@PostMapping("/logout")
|
||||
public Result<Void> logout(HttpServletRequest request) {
|
||||
// 从请求中获取 token
|
||||
String token = getJwtFromRequest(request);
|
||||
|
||||
Long userId = null;
|
||||
Long tenantId = null;
|
||||
|
||||
// 从 token 中解析用户信息
|
||||
if (token != null) {
|
||||
try {
|
||||
// 从 token 获取用户名和租户ID
|
||||
String username = jwtUtil.getUsernameFromToken(token);
|
||||
tenantId = jwtUtil.getTenantIdFromToken(token);
|
||||
|
||||
// 根据用户名查询用户获取用户ID
|
||||
if (username != null) {
|
||||
User user = userMapper.selectUserWithRolesAndPermissions(username);
|
||||
if (user != null) {
|
||||
userId = user.getId();
|
||||
// 如果token中没有tenantId,使用用户的tenantId
|
||||
if (tenantId == null) {
|
||||
tenantId = user.getTenantId();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Token 解析失败,仍然尝试将其加入黑名单
|
||||
}
|
||||
}
|
||||
|
||||
authService.logout(token, userId, tenantId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码 - 发送重置邮件
|
||||
*/
|
||||
@Operation(summary = "忘记密码", description = "发送密码重置邮件到用户邮箱")
|
||||
@PostMapping("/forgot-password")
|
||||
@RateLimit(
|
||||
key = "forgot-password",
|
||||
window = 300,
|
||||
maxRequests = 3,
|
||||
limitType = RateLimit.LimitType.IP,
|
||||
message = "请求过于频繁,请稍后再试"
|
||||
)
|
||||
public Result<String> forgotPassword(@Valid @RequestBody ForgotPasswordRequest request) {
|
||||
authService.forgotPassword(request);
|
||||
return Result.success("如果该邮箱已注册,我们已向其发送密码重置邮件");
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*/
|
||||
@Operation(summary = "重置密码", description = "通过重置令牌设置新密码")
|
||||
@PostMapping("/reset-password")
|
||||
@RateLimit(
|
||||
key = "reset-password",
|
||||
window = 60,
|
||||
maxRequests = 5,
|
||||
limitType = RateLimit.LimitType.IP,
|
||||
message = "请求过于频繁,请稍后再试"
|
||||
)
|
||||
public Result<String> resetPassword(@Valid @RequestBody ResetPasswordRequest request) {
|
||||
authService.resetPassword(request);
|
||||
return Result.success("密码重置成功,请使用新密码登录");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱
|
||||
*/
|
||||
@Operation(summary = "验证邮箱", description = "通过验证令牌验证用户邮箱")
|
||||
@PostMapping("/verify-email")
|
||||
@RateLimit(
|
||||
key = "verify-email",
|
||||
window = 60,
|
||||
maxRequests = 10,
|
||||
message = "请求过于频繁,请稍后再试"
|
||||
)
|
||||
public Result<String> verifyEmail(@Valid @RequestBody com.kurihada.auth.dto.VerifyEmailRequest request) {
|
||||
authService.verifyEmail(request.getToken(), request.getTenantId());
|
||||
return Result.success("邮箱验证成功!现在可以登录了");
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新发送验证邮件
|
||||
*/
|
||||
@Operation(summary = "重新发送验证邮件", description = "为指定邮箱重新发送验证邮件")
|
||||
@PostMapping("/resend-verification")
|
||||
@RateLimit(
|
||||
key = "resend-verification",
|
||||
window = 300,
|
||||
maxRequests = 3,
|
||||
message = "请求过于频繁,请稍后再试"
|
||||
)
|
||||
public Result<String> resendVerification(@Valid @RequestBody com.kurihada.auth.dto.ResendVerificationRequest request) {
|
||||
authService.resendVerificationEmail(request.getEmail(), request.getTenantId());
|
||||
return Result.success("验证邮件已发送,请检查您的邮箱");
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取JWT token
|
||||
*/
|
||||
private String getJwtFromRequest(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader(tokenHeader);
|
||||
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(tokenPrefix + " ")) {
|
||||
return bearerToken.substring(7);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.kurihada.auth.controller;
|
||||
|
||||
import com.kurihada.auth.dto.*;
|
||||
import com.kurihada.auth.service.PermissionService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 权限控制器
|
||||
*/
|
||||
@Tag(name = "权限管理", description = "权限信息管理相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/permissions")
|
||||
@RequiredArgsConstructor
|
||||
@SecurityRequirement(name = "Bearer")
|
||||
public class PermissionController {
|
||||
|
||||
private final PermissionService permissionService;
|
||||
|
||||
/**
|
||||
* 分页查询权限
|
||||
*/
|
||||
@Operation(summary = "分页查询权限", description = "分页查询权限列表,支持搜索和排序")
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
|
||||
public Result<PageResult<PermissionDTO>> getPermissionsPage(@Valid @ModelAttribute PageRequest pageRequest) {
|
||||
PageResult<PermissionDTO> result = permissionService.getPermissionsPage(pageRequest);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取权限
|
||||
*/
|
||||
@Operation(summary = "根据ID获取权限", description = "根据权限ID查询权限详细信息")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
|
||||
public Result<PermissionDTO> getPermissionById(
|
||||
@Parameter(description = "权限ID") @PathVariable Long id) {
|
||||
PermissionDTO permission = permissionService.getPermissionById(id);
|
||||
return Result.success(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新权限
|
||||
*/
|
||||
@Operation(summary = "创建权限", description = "创建新的权限(仅管理员)")
|
||||
@PostMapping
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<PermissionDTO> createPermission(
|
||||
@Valid @RequestBody CreatePermissionRequest request) {
|
||||
PermissionDTO permission = permissionService.createPermission(request);
|
||||
return Result.success(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新权限
|
||||
*/
|
||||
@Operation(summary = "更新权限", description = "更新指定权限信息(仅管理员)")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<PermissionDTO> updatePermission(
|
||||
@Parameter(description = "权限ID") @PathVariable Long id,
|
||||
@Valid @RequestBody UpdatePermissionRequest request) {
|
||||
PermissionDTO permission = permissionService.updatePermission(id, request);
|
||||
return Result.success(permission);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除权限
|
||||
*/
|
||||
@Operation(summary = "删除权限", description = "删除指定权限(仅管理员)")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<Void> deletePermission(
|
||||
@Parameter(description = "权限ID") @PathVariable Long id) {
|
||||
permissionService.deletePermission(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换权限状态
|
||||
*/
|
||||
@Operation(summary = "切换权限状态", description = "启用或禁用指定权限(仅管理员)")
|
||||
@PutMapping("/{id}/toggle-status")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<Void> togglePermissionStatus(
|
||||
@Parameter(description = "权限ID") @PathVariable Long id) {
|
||||
permissionService.togglePermissionStatus(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限树形结构
|
||||
*/
|
||||
@Operation(summary = "获取权限树", description = "获取权限的树形结构(用于菜单展示)")
|
||||
@GetMapping("/tree")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
|
||||
public Result<List<PermissionTreeNode>> getPermissionTree() {
|
||||
List<PermissionTreeNode> tree = permissionService.getPermissionTree();
|
||||
return Result.success(tree);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.kurihada.auth.controller;
|
||||
|
||||
import com.kurihada.auth.dto.*;
|
||||
import com.kurihada.auth.service.RolePermissionService;
|
||||
import com.kurihada.auth.service.RoleService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色控制器
|
||||
*/
|
||||
@Tag(name = "角色管理", description = "角色信息管理相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/roles")
|
||||
@RequiredArgsConstructor
|
||||
@SecurityRequirement(name = "Bearer")
|
||||
public class RoleController {
|
||||
|
||||
private final RoleService roleService;
|
||||
private final RolePermissionService rolePermissionService;
|
||||
|
||||
/**
|
||||
* 分页查询角色
|
||||
*/
|
||||
@Operation(summary = "分页查询角色", description = "分页查询角色列表,支持搜索和排序")
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
|
||||
public Result<PageResult<RoleDTO>> getRolesPage(@Valid @ModelAttribute PageRequest pageRequest) {
|
||||
PageResult<RoleDTO> result = roleService.getRolesPage(pageRequest);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取角色
|
||||
*/
|
||||
@Operation(summary = "根据ID获取角色", description = "根据角色ID查询角色详细信息")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
|
||||
public Result<RoleDTO> getRoleById(
|
||||
@Parameter(description = "角色ID") @PathVariable Long id) {
|
||||
RoleDTO role = roleService.getRoleById(id);
|
||||
return Result.success(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新角色
|
||||
*/
|
||||
@Operation(summary = "创建角色", description = "创建新的角色(仅管理员)")
|
||||
@PostMapping
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<RoleDTO> createRole(
|
||||
@Valid @RequestBody CreateRoleRequest request) {
|
||||
RoleDTO role = roleService.createRole(request);
|
||||
return Result.success(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*/
|
||||
@Operation(summary = "更新角色", description = "更新指定角色信息(仅管理员)")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<RoleDTO> updateRole(
|
||||
@Parameter(description = "角色ID") @PathVariable Long id,
|
||||
@Valid @RequestBody UpdateRoleRequest request) {
|
||||
RoleDTO role = roleService.updateRole(id, request);
|
||||
return Result.success(role);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
@Operation(summary = "删除角色", description = "删除指定角色(仅管理员)")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<Void> deleteRole(
|
||||
@Parameter(description = "角色ID") @PathVariable Long id) {
|
||||
roleService.deleteRole(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换角色状态
|
||||
*/
|
||||
@Operation(summary = "切换角色状态", description = "启用或禁用指定角色(仅管理员)")
|
||||
@PutMapping("/{id}/toggle-status")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<Void> toggleRoleStatus(
|
||||
@Parameter(description = "角色ID") @PathVariable Long id) {
|
||||
roleService.toggleRoleStatus(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色的所有权限
|
||||
*/
|
||||
@Operation(summary = "获取角色权限", description = "获取指定角色的所有权限列表")
|
||||
@GetMapping("/{id}/permissions")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
|
||||
public Result<List<PermissionDTO>> getRolePermissions(
|
||||
@Parameter(description = "角色ID") @PathVariable Long id) {
|
||||
List<com.kurihada.auth.entity.Permission> permissions = rolePermissionService.getPermissionsByRoleId(id);
|
||||
List<PermissionDTO> permissionDTOs = permissions.stream()
|
||||
.map(permission -> PermissionDTO.builder()
|
||||
.id(permission.getId())
|
||||
.name(permission.getName())
|
||||
.code(permission.getCode())
|
||||
.tenantId(permission.getTenantId())
|
||||
.type(permission.getType())
|
||||
.resource(permission.getResource())
|
||||
.parentId(permission.getParentId())
|
||||
.sort(permission.getSort())
|
||||
.status(permission.getStatus())
|
||||
.createTime(permission.getCreateTime())
|
||||
.updateTime(permission.getUpdateTime())
|
||||
.build())
|
||||
.toList();
|
||||
return Result.success(permissionDTOs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为角色分配权限(批量)
|
||||
*/
|
||||
@Operation(summary = "分配权限", description = "为指定角色分配权限(批量,仅管理员)")
|
||||
@PostMapping("/{id}/permissions")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<Void> assignPermissionsToRole(
|
||||
@Parameter(description = "角色ID") @PathVariable Long id,
|
||||
@Valid @RequestBody AssignPermissionsRequest request) {
|
||||
rolePermissionService.assignPermissionsToRole(id, request.getPermissionIds());
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除角色的指定权限
|
||||
*/
|
||||
@Operation(summary = "移除权限", description = "移除角色的指定权限(仅管理员)")
|
||||
@DeleteMapping("/{roleId}/permissions/{permissionId}")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<Void> removePermissionFromRole(
|
||||
@Parameter(description = "角色ID") @PathVariable Long roleId,
|
||||
@Parameter(description = "权限ID") @PathVariable Long permissionId) {
|
||||
rolePermissionService.removePermissionFromRole(roleId, permissionId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新角色的权限(替换)
|
||||
*/
|
||||
@Operation(summary = "更新角色权限", description = "替换角色的所有权限(仅管理员)")
|
||||
@PutMapping("/{id}/permissions")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<Void> updateRolePermissions(
|
||||
@Parameter(description = "角色ID") @PathVariable Long id,
|
||||
@Valid @RequestBody AssignPermissionsRequest request) {
|
||||
rolePermissionService.updateRolePermissions(id, request.getPermissionIds());
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.kurihada.auth.controller;
|
||||
|
||||
import com.kurihada.auth.dto.Result;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 测试控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/test")
|
||||
public class TestController {
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
public Result<String> health() {
|
||||
return Result.success("服务运行正常");
|
||||
}
|
||||
|
||||
/**
|
||||
* 需要认证的接口
|
||||
*/
|
||||
@GetMapping("/protected")
|
||||
public Result<String> protectedEndpoint() {
|
||||
return Result.success("您已通过认证");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package com.kurihada.auth.controller;
|
||||
|
||||
import com.kurihada.auth.dto.*;
|
||||
import com.kurihada.auth.service.UserRoleService;
|
||||
import com.kurihada.auth.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户控制器
|
||||
*/
|
||||
@Tag(name = "用户管理", description = "用户信息管理相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/users")
|
||||
@RequiredArgsConstructor
|
||||
@SecurityRequirement(name = "Bearer")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
private final UserRoleService userRoleService;
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*/
|
||||
@Operation(summary = "获取当前用户信息", description = "获取当前登录用户的详细信息")
|
||||
@GetMapping("/current")
|
||||
public Result<UserDTO> getCurrentUser() {
|
||||
UserDTO user = userService.getCurrentUser();
|
||||
return Result.success(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前用户信息
|
||||
*/
|
||||
@Operation(summary = "更新当前用户信息", description = "当前登录用户更新自己的基本信息")
|
||||
@PutMapping("/current")
|
||||
public Result<UserDTO> updateCurrentUser(@Valid @RequestBody UpdateUserRequest request) {
|
||||
UserDTO user = userService.updateCurrentUser(request);
|
||||
return Result.success(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户信息
|
||||
*/
|
||||
@Operation(summary = "根据ID获取用户", description = "根据用户ID查询用户详细信息")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
|
||||
public Result<UserDTO> getUserById(
|
||||
@Parameter(description = "用户ID") @PathVariable Long id) {
|
||||
UserDTO user = userService.getUserById(id);
|
||||
return Result.success(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
*/
|
||||
@Operation(summary = "分页查询用户", description = "分页查询用户列表,支持搜索和排序(仅管理员)")
|
||||
@GetMapping
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<PageResult<UserDTO>> getUsersPage(@Valid @ModelAttribute PageRequest pageRequest) {
|
||||
PageResult<UserDTO> result = userService.getUsersPage(pageRequest);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
@Operation(summary = "删除用户", description = "逻辑删除指定用户(仅管理员)")
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<Void> deleteUser(
|
||||
@Parameter(description = "用户ID") @PathVariable Long id) {
|
||||
userService.deleteUser(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
@Operation(summary = "更新用户信息", description = "更新指定用户的基本信息(仅管理员)")
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<UserDTO> updateUser(
|
||||
@Parameter(description = "用户ID") @PathVariable Long id,
|
||||
@Valid @RequestBody UpdateUserRequest request) {
|
||||
UserDTO user = userService.updateUser(id, request);
|
||||
return Result.success(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用用户
|
||||
*/
|
||||
@Operation(summary = "切换用户状态", description = "启用或禁用指定用户(仅管理员)")
|
||||
@PutMapping("/{id}/toggle-status")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<Void> toggleUserStatus(
|
||||
@Parameter(description = "用户ID") @PathVariable Long id) {
|
||||
userService.toggleUserStatus(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有角色
|
||||
*/
|
||||
@Operation(summary = "获取用户角色", description = "获取指定用户的所有角色列表")
|
||||
@GetMapping("/{id}/roles")
|
||||
@PreAuthorize("hasAnyRole('ADMIN', 'USER')")
|
||||
public Result<List<RoleDTO>> getUserRoles(
|
||||
@Parameter(description = "用户ID") @PathVariable Long id) {
|
||||
List<com.kurihada.auth.entity.Role> roles = userRoleService.getRolesByUserId(id);
|
||||
List<RoleDTO> roleDTOs = roles.stream()
|
||||
.map(role -> RoleDTO.builder()
|
||||
.id(role.getId())
|
||||
.name(role.getName())
|
||||
.code(role.getCode())
|
||||
.tenantId(role.getTenantId())
|
||||
.description(role.getDescription())
|
||||
.status(role.getStatus())
|
||||
.createTime(role.getCreateTime())
|
||||
.updateTime(role.getUpdateTime())
|
||||
.build())
|
||||
.toList();
|
||||
return Result.success(roleDTOs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为用户分配角色(批量)
|
||||
*/
|
||||
@Operation(summary = "分配角色", description = "为指定用户分配角色(批量,仅管理员)")
|
||||
@PostMapping("/{id}/roles")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<Void> assignRolesToUser(
|
||||
@Parameter(description = "用户ID") @PathVariable Long id,
|
||||
@Valid @RequestBody AssignRolesRequest request) {
|
||||
userRoleService.assignRolesToUser(id, request.getRoleIds());
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除用户的指定角色
|
||||
*/
|
||||
@Operation(summary = "移除角色", description = "移除用户的指定角色(仅管理员)")
|
||||
@DeleteMapping("/{userId}/roles/{roleId}")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<Void> removeRoleFromUser(
|
||||
@Parameter(description = "用户ID") @PathVariable Long userId,
|
||||
@Parameter(description = "角色ID") @PathVariable Long roleId) {
|
||||
userRoleService.removeRoleFromUser(userId, roleId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户的角色(替换)
|
||||
*/
|
||||
@Operation(summary = "更新用户角色", description = "替换用户的所有角色(仅管理员)")
|
||||
@PutMapping("/{id}/roles")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public Result<Void> updateUserRoles(
|
||||
@Parameter(description = "用户ID") @PathVariable Long id,
|
||||
@Valid @RequestBody AssignRolesRequest request) {
|
||||
userRoleService.updateUserRoles(id, request.getRoleIds());
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改当前用户密码
|
||||
*/
|
||||
@Operation(summary = "修改密码", description = "当前用户修改自己的密码")
|
||||
@PutMapping("/password")
|
||||
public Result<Void> changePassword(@Valid @RequestBody ChangePasswordRequest request) {
|
||||
userService.changePassword(request);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的权限列表
|
||||
*/
|
||||
@Operation(summary = "获取当前用户权限", description = "获取当前登录用户的所有权限(包括通过角色继承的)")
|
||||
@GetMapping("/permissions")
|
||||
public Result<List<PermissionDTO>> getCurrentUserPermissions() {
|
||||
List<PermissionDTO> permissions = userService.getCurrentUserPermissions();
|
||||
return Result.success(permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的菜单树
|
||||
*/
|
||||
@Operation(summary = "获取当前用户菜单", description = "获取当前登录用户可访问的菜单树形结构")
|
||||
@GetMapping("/menus")
|
||||
public Result<List<PermissionTreeNode>> getCurrentUserMenus() {
|
||||
List<PermissionTreeNode> menus = userService.getCurrentUserMenus();
|
||||
return Result.success(menus);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分配权限请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "分配权限请求")
|
||||
public class AssignPermissionsRequest {
|
||||
|
||||
@Schema(description = "权限ID列表", example = "[1, 2, 3]")
|
||||
@NotEmpty(message = "权限ID列表不能为空")
|
||||
private List<Long> permissionIds;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分配角色请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "分配角色请求")
|
||||
public class AssignRolesRequest {
|
||||
|
||||
@Schema(description = "角色ID列表", example = "[1, 2, 3]")
|
||||
@NotEmpty(message = "角色ID列表不能为空")
|
||||
private List<Long> roleIds;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 审计日志查询请求
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class AuditLogQueryRequest extends PageRequest {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户名(模糊查询)
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 操作类型
|
||||
*/
|
||||
private String actionType;
|
||||
|
||||
/**
|
||||
* 资源类型
|
||||
*/
|
||||
private String resourceType;
|
||||
|
||||
/**
|
||||
* 资源ID
|
||||
*/
|
||||
private String resourceId;
|
||||
|
||||
/**
|
||||
* 操作结果(SUCCESS, FAILURE)
|
||||
*/
|
||||
private String result;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime startDate;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime endDate;
|
||||
|
||||
/**
|
||||
* IP地址(精确匹配)
|
||||
*/
|
||||
private String ipAddress;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 审计日志响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AuditLogResponse {
|
||||
|
||||
/**
|
||||
* 日志ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 操作类型
|
||||
*/
|
||||
private String actionType;
|
||||
|
||||
/**
|
||||
* 操作用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 操作用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 目标资源类型
|
||||
*/
|
||||
private String resourceType;
|
||||
|
||||
/**
|
||||
* 目标资源ID
|
||||
*/
|
||||
private String resourceId;
|
||||
|
||||
/**
|
||||
* 操作详情
|
||||
*/
|
||||
private String details;
|
||||
|
||||
/**
|
||||
* 操作结果(SUCCESS, FAILURE)
|
||||
*/
|
||||
private String result;
|
||||
|
||||
/**
|
||||
* 错误信息(如果失败)
|
||||
*/
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 请求IP
|
||||
*/
|
||||
private String ipAddress;
|
||||
|
||||
/**
|
||||
* User Agent
|
||||
*/
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import com.kurihada.auth.annotation.ValidPassword;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 修改密码请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "修改密码请求")
|
||||
public class ChangePasswordRequest {
|
||||
|
||||
@Schema(description = "旧密码", example = "OldPass123!")
|
||||
@NotBlank(message = "旧密码不能为空")
|
||||
private String oldPassword;
|
||||
|
||||
@Schema(description = "新密码", example = "NewPass123!")
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
@ValidPassword
|
||||
private String newPassword;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 创建权限请求
|
||||
*/
|
||||
@Data
|
||||
public class CreatePermissionRequest {
|
||||
|
||||
@NotBlank(message = "权限名称不能为空")
|
||||
@Size(max = 50, message = "权限名称长度不能超过50个字符")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "权限代码不能为空")
|
||||
@Size(max = 100, message = "权限代码长度不能超过100个字符")
|
||||
private String code;
|
||||
|
||||
@Size(max = 255, message = "权限描述长度不能超过255个字符")
|
||||
private String description;
|
||||
|
||||
@Size(max = 20, message = "权限类型长度不能超过20个字符")
|
||||
private String type;
|
||||
|
||||
@Size(max = 255, message = "资源路径长度不能超过255个字符")
|
||||
private String resource;
|
||||
|
||||
private Long parentId;
|
||||
|
||||
private Integer sort = 0;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 创建角色请求
|
||||
*/
|
||||
@Data
|
||||
public class CreateRoleRequest {
|
||||
|
||||
@NotBlank(message = "角色名称不能为空")
|
||||
@Size(max = 50, message = "角色名称长度不能超过50个字符")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "角色代码不能为空")
|
||||
@Size(max = 50, message = "角色代码长度不能超过50个字符")
|
||||
private String code;
|
||||
|
||||
@Size(max = 255, message = "角色描述长度不能超过255个字符")
|
||||
private String description;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 忘记密码请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class ForgotPasswordRequest {
|
||||
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
@NotNull(message = "租户ID不能为空")
|
||||
private Long tenantId;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class LoginRequest {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 租户代码(可选)
|
||||
* 如果不提供,系统会按以下优先级查找:
|
||||
* 1. 请求头 X-Tenant-Code
|
||||
* 2. 子域名(如 tenant1.example.com)
|
||||
* 3. 使用默认租户
|
||||
*/
|
||||
private String tenantCode;
|
||||
|
||||
/**
|
||||
* 租户ID(可选,一般使用 tenantCode)
|
||||
*/
|
||||
private Long tenantId;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 登录响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LoginResponse {
|
||||
|
||||
private String token;
|
||||
|
||||
private String refreshToken;
|
||||
|
||||
private String tokenType = "Bearer";
|
||||
|
||||
private Long userId;
|
||||
|
||||
private String username;
|
||||
|
||||
private String realName;
|
||||
|
||||
private Set<String> roles;
|
||||
|
||||
private Set<String> permissions;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 分页请求参数
|
||||
*/
|
||||
@Data
|
||||
public class PageRequest {
|
||||
|
||||
/**
|
||||
* 当前页码(从1开始)
|
||||
*/
|
||||
@Min(value = 1, message = "页码必须大于0")
|
||||
private Integer page = 1;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
@Min(value = 1, message = "每页大小必须大于0")
|
||||
@Max(value = 100, message = "每页大小不能超过100")
|
||||
private Integer size = 10;
|
||||
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
private String sortBy;
|
||||
|
||||
/**
|
||||
* 排序方向(asc/desc)
|
||||
*/
|
||||
private String sortDirection = "desc";
|
||||
|
||||
/**
|
||||
* 搜索关键词
|
||||
*/
|
||||
private String keyword;
|
||||
|
||||
/**
|
||||
* 获取 MyBatis Plus 的偏移量
|
||||
*/
|
||||
public long getOffset() {
|
||||
return (long) (page - 1) * size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取限制数量
|
||||
*/
|
||||
public long getLimit() {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页响应结果
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PageResult<T> {
|
||||
|
||||
/**
|
||||
* 数据列表
|
||||
*/
|
||||
private List<T> records;
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
private Long total;
|
||||
|
||||
/**
|
||||
* 当前页码
|
||||
*/
|
||||
private Integer page;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
private Integer size;
|
||||
|
||||
/**
|
||||
* 总页数
|
||||
*/
|
||||
private Integer totalPages;
|
||||
|
||||
/**
|
||||
* 是否有下一页
|
||||
*/
|
||||
private Boolean hasNext;
|
||||
|
||||
/**
|
||||
* 是否有上一页
|
||||
*/
|
||||
private Boolean hasPrevious;
|
||||
|
||||
/**
|
||||
* 创建分页结果
|
||||
*/
|
||||
public static <T> PageResult<T> of(List<T> records, Long total, Integer page, Integer size) {
|
||||
int totalPages = (int) Math.ceil((double) total / size);
|
||||
return PageResult.<T>builder()
|
||||
.records(records)
|
||||
.total(total)
|
||||
.page(page)
|
||||
.size(size)
|
||||
.totalPages(totalPages)
|
||||
.hasNext(page < totalPages)
|
||||
.hasPrevious(page > 1)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 权限信息DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PermissionDTO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String code;
|
||||
|
||||
private Long tenantId;
|
||||
|
||||
private String description;
|
||||
|
||||
private String type;
|
||||
|
||||
private String resource;
|
||||
|
||||
private Long parentId;
|
||||
|
||||
private Integer sort;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 权限树节点DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PermissionTreeNode {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String code;
|
||||
|
||||
private Long tenantId;
|
||||
|
||||
private String type;
|
||||
|
||||
private String resource;
|
||||
|
||||
private Long parentId;
|
||||
|
||||
private Integer sort;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
@Builder.Default
|
||||
private List<PermissionTreeNode> children = new ArrayList<>();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 刷新Token请求
|
||||
*/
|
||||
@Data
|
||||
public class RefreshTokenRequest {
|
||||
|
||||
@NotBlank(message = "Refresh Token不能为空")
|
||||
private String refreshToken;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import com.kurihada.auth.annotation.ValidPassword;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 注册请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class RegisterRequest {
|
||||
|
||||
@NotBlank(message = "用户名不能为空")
|
||||
@Size(min = 3, max = 50, message = "用户名长度必须在3-50个字符之间")
|
||||
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@ValidPassword(
|
||||
minLength = 8,
|
||||
maxLength = 32,
|
||||
requireLowercase = true,
|
||||
requireUppercase = true,
|
||||
requireDigit = true,
|
||||
requireSpecialChar = true,
|
||||
forbidCommonPasswords = true
|
||||
)
|
||||
private String password;
|
||||
|
||||
@NotBlank(message = "确认密码不能为空")
|
||||
private String confirmPassword;
|
||||
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
private String realName;
|
||||
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 租户代码(可选)
|
||||
* 如果不提供,系统会按以下优先级查找:
|
||||
* 1. 请求头 X-Tenant-Code
|
||||
* 2. 子域名(如 tenant1.example.com)
|
||||
* 3. 使用默认租户
|
||||
*/
|
||||
private String tenantCode;
|
||||
|
||||
/**
|
||||
* 租户ID(可选,一般使用 tenantCode)
|
||||
*/
|
||||
private Long tenantId;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 重新发送验证邮件请求
|
||||
*/
|
||||
@Data
|
||||
public class ResendVerificationRequest {
|
||||
|
||||
@NotBlank(message = "邮箱不能为空")
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
@NotNull(message = "租户ID不能为空")
|
||||
private Long tenantId;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 重置密码请求DTO
|
||||
*/
|
||||
@Data
|
||||
public class ResetPasswordRequest {
|
||||
|
||||
@NotBlank(message = "重置令牌不能为空")
|
||||
private String token;
|
||||
|
||||
@NotNull(message = "租户ID不能为空")
|
||||
private Long tenantId;
|
||||
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
@Size(min = 8, max = 32, message = "密码长度必须在8-32位之间")
|
||||
@Pattern(
|
||||
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-=\\[\\]{}|;:,.<>?]).+$",
|
||||
message = "密码必须包含大小写字母、数字和特殊字符"
|
||||
)
|
||||
private String newPassword;
|
||||
|
||||
@NotBlank(message = "确认密码不能为空")
|
||||
private String confirmPassword;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 统一响应结果
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class Result<T> {
|
||||
|
||||
/**
|
||||
* 响应码
|
||||
*/
|
||||
private Integer code;
|
||||
|
||||
/**
|
||||
* 响应消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 响应数据
|
||||
*/
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 成功响应
|
||||
*/
|
||||
public static <T> Result<T> success(T data) {
|
||||
return new Result<>(200, "操作成功", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应(无数据)
|
||||
*/
|
||||
public static <T> Result<T> success() {
|
||||
return new Result<>(200, "操作成功", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功响应(自定义消息)
|
||||
*/
|
||||
public static <T> Result<T> success(String message, T data) {
|
||||
return new Result<>(200, message, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应
|
||||
*/
|
||||
public static <T> Result<T> error(String message) {
|
||||
return new Result<>(500, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败响应(自定义响应码)
|
||||
*/
|
||||
public static <T> Result<T> error(Integer code, String message) {
|
||||
return new Result<>(code, message, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 角色信息DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RoleDTO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
private String code;
|
||||
|
||||
private Long tenantId;
|
||||
|
||||
private String description;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
private Set<String> permissions;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 更新权限请求
|
||||
*/
|
||||
@Data
|
||||
public class UpdatePermissionRequest {
|
||||
|
||||
@NotBlank(message = "权限名称不能为空")
|
||||
@Size(max = 50, message = "权限名称长度不能超过50个字符")
|
||||
private String name;
|
||||
|
||||
@Size(max = 255, message = "权限描述长度不能超过255个字符")
|
||||
private String description;
|
||||
|
||||
@Size(max = 20, message = "权限类型长度不能超过20个字符")
|
||||
private String type;
|
||||
|
||||
@Size(max = 255, message = "资源路径长度不能超过255个字符")
|
||||
private String resource;
|
||||
|
||||
private Long parentId;
|
||||
|
||||
private Integer sort;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 更新角色请求
|
||||
*/
|
||||
@Data
|
||||
public class UpdateRoleRequest {
|
||||
|
||||
@NotBlank(message = "角色名称不能为空")
|
||||
@Size(max = 50, message = "角色名称长度不能超过50个字符")
|
||||
private String name;
|
||||
|
||||
@Size(max = 255, message = "角色描述长度不能超过255个字符")
|
||||
private String description;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 更新用户信息请求DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "更新用户信息请求")
|
||||
public class UpdateUserRequest {
|
||||
|
||||
@Schema(description = "真实姓名", example = "张三")
|
||||
@Size(max = 100, message = "真实姓名长度不能超过100个字符")
|
||||
private String realName;
|
||||
|
||||
@Schema(description = "邮箱", example = "user@example.com")
|
||||
@Email(message = "邮箱格式不正确")
|
||||
private String email;
|
||||
|
||||
@Schema(description = "手机号", example = "13800138000")
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
private String phone;
|
||||
|
||||
@Schema(description = "性别:0-未知,1-男,2-女", example = "1")
|
||||
private Integer gender;
|
||||
|
||||
@Schema(description = "头像URL", example = "https://example.com/avatar.jpg")
|
||||
private String avatar;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 用户信息DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserDTO {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
|
||||
private String realName;
|
||||
|
||||
private String email;
|
||||
|
||||
private String phone;
|
||||
|
||||
private Integer gender;
|
||||
|
||||
private String avatar;
|
||||
|
||||
private Integer status;
|
||||
|
||||
private LocalDateTime lastLoginTime;
|
||||
|
||||
private LocalDateTime createTime;
|
||||
|
||||
private Set<String> roles;
|
||||
|
||||
private Set<String> permissions;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 用户权限缓存DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class UserPermissionCache implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 角色代码列表(初始化为空列表,避免 null)
|
||||
*/
|
||||
private List<String> roleCodes = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 角色名称列表(初始化为空列表,避免 null)
|
||||
*/
|
||||
private List<String> roleNames = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 权限代码集合(初始化为空集合,避免 null)
|
||||
*/
|
||||
private Set<String> permissionCodes = new HashSet<>();
|
||||
|
||||
/**
|
||||
* 权限资源集合(API路径,初始化为空集合,避免 null)
|
||||
*/
|
||||
private Set<String> resources = new HashSet<>();
|
||||
|
||||
/**
|
||||
* 缓存时间戳
|
||||
*/
|
||||
private Long cacheTime;
|
||||
|
||||
/**
|
||||
* 检查是否拥有某个权限
|
||||
*/
|
||||
public boolean hasPermission(String permissionCode) {
|
||||
return permissionCodes != null && permissionCodes.contains(permissionCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否拥有某个角色
|
||||
*/
|
||||
public boolean hasRole(String roleCode) {
|
||||
return roleCodes != null && roleCodes.contains(roleCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有权限访问某个资源
|
||||
*/
|
||||
public boolean hasResource(String resource) {
|
||||
return resources != null && resources.contains(resource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.kurihada.auth.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 邮箱验证请求
|
||||
*/
|
||||
@Data
|
||||
public class VerifyEmailRequest {
|
||||
|
||||
@NotBlank(message = "验证令牌不能为空")
|
||||
private String token;
|
||||
|
||||
@NotNull(message = "租户ID不能为空")
|
||||
private Long tenantId;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.kurihada.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 审计日志实体
|
||||
* 用于记录系统中的重要操作
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("audit_log")
|
||||
public class AuditLog {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 操作类型(LOGIN, LOGOUT, CREATE_USER, UPDATE_USER, DELETE_USER, ASSIGN_ROLE, etc.)
|
||||
*/
|
||||
@TableField("action_type")
|
||||
private String actionType;
|
||||
|
||||
/**
|
||||
* 操作用户ID
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 操作用户名
|
||||
*/
|
||||
@TableField("username")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 目标资源类型(USER, ROLE, PERMISSION)
|
||||
*/
|
||||
@TableField("resource_type")
|
||||
private String resourceType;
|
||||
|
||||
/**
|
||||
* 目标资源ID
|
||||
*/
|
||||
@TableField("resource_id")
|
||||
private String resourceId;
|
||||
|
||||
/**
|
||||
* 操作详情
|
||||
*/
|
||||
@TableField("details")
|
||||
private String details;
|
||||
|
||||
/**
|
||||
* 操作结果(SUCCESS, FAILURE)
|
||||
*/
|
||||
@TableField("result")
|
||||
private String result;
|
||||
|
||||
/**
|
||||
* 错误信息(如果失败)
|
||||
*/
|
||||
@TableField("error_message")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 请求IP
|
||||
*/
|
||||
@TableField("ip_address")
|
||||
private String ipAddress;
|
||||
|
||||
/**
|
||||
* User Agent
|
||||
*/
|
||||
@TableField("user_agent")
|
||||
private String userAgent;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.kurihada.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 权限实体类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("permissions")
|
||||
public class Permission {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 权限名称
|
||||
*/
|
||||
@TableField("name")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 权限代码
|
||||
*/
|
||||
@TableField("code")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 租户ID(null表示公共权限)
|
||||
*/
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 权限描述
|
||||
*/
|
||||
@TableField("description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 权限类型(如:menu, button, api等)
|
||||
*/
|
||||
@TableField("type")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 资源路径
|
||||
*/
|
||||
@TableField("resource")
|
||||
private String resource;
|
||||
|
||||
/**
|
||||
* 父权限ID
|
||||
*/
|
||||
@TableField("parent_id")
|
||||
private Long parentId;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
@TableField("sort")
|
||||
private Integer sort;
|
||||
|
||||
/**
|
||||
* 状态(0:禁用,1:启用)
|
||||
*/
|
||||
@TableField("status")
|
||||
private Integer status = 1;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.kurihada.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Refresh Token 实体类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("refresh_tokens")
|
||||
public class RefreshToken {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* Refresh Token
|
||||
*/
|
||||
@TableField("token")
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
@TableField("expire_time")
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
/**
|
||||
* 是否已使用(已使用的token不能再次刷新)
|
||||
*/
|
||||
@TableField("used")
|
||||
private Boolean used = false;
|
||||
|
||||
/**
|
||||
* 是否删除(0:未删除,1:已删除)
|
||||
*/
|
||||
@TableLogic
|
||||
@TableField("deleted")
|
||||
private Integer deleted = 0;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.kurihada.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色实体类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("roles")
|
||||
public class Role {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 角色名称
|
||||
*/
|
||||
@TableField("name")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 角色代码
|
||||
*/
|
||||
@TableField("code")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 角色描述
|
||||
*/
|
||||
@TableField("description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 状态(0:禁用,1:启用)
|
||||
*/
|
||||
@TableField("status")
|
||||
private Integer status = 1;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 角色拥有的权限集合(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private List<Permission> permissions;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.kurihada.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 角色权限关联实体类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("role_permissions")
|
||||
public class RolePermission {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 角色ID
|
||||
*/
|
||||
@TableField("role_id")
|
||||
private Long roleId;
|
||||
|
||||
/**
|
||||
* 权限ID
|
||||
*/
|
||||
@TableField("permission_id")
|
||||
private Long permissionId;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 角色信息(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private Role role;
|
||||
|
||||
/**
|
||||
* 权限信息(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private Permission permission;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.kurihada.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 租户实体类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("tenants")
|
||||
public class Tenant {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 租户代码(唯一标识)
|
||||
*/
|
||||
@TableField("code")
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 租户名称
|
||||
*/
|
||||
@TableField("name")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 联系人
|
||||
*/
|
||||
@TableField("contact_name")
|
||||
private String contactName;
|
||||
|
||||
/**
|
||||
* 联系电话
|
||||
*/
|
||||
@TableField("contact_phone")
|
||||
private String contactPhone;
|
||||
|
||||
/**
|
||||
* 联系邮箱
|
||||
*/
|
||||
@TableField("contact_email")
|
||||
private String contactEmail;
|
||||
|
||||
/**
|
||||
* 状态(0:禁用,1:启用)
|
||||
*/
|
||||
@TableField("status")
|
||||
private Integer status = 1;
|
||||
|
||||
/**
|
||||
* 是否删除(0:未删除,1:已删除)
|
||||
*/
|
||||
@TableLogic
|
||||
@TableField("deleted")
|
||||
private Integer deleted = 0;
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
@TableField("expire_time")
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.kurihada.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户实体类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("users")
|
||||
public class User {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@TableField("username")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码(加密后)
|
||||
*/
|
||||
@TableField("password")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 真实姓名
|
||||
*/
|
||||
@TableField("real_name")
|
||||
private String realName;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@TableField("email")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@TableField("phone")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 性别(0:女,1:男,2:未知)
|
||||
*/
|
||||
@TableField("gender")
|
||||
private Integer gender;
|
||||
|
||||
/**
|
||||
* 头像
|
||||
*/
|
||||
@TableField("avatar")
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 状态(0:禁用,1:启用)
|
||||
*/
|
||||
@TableField("status")
|
||||
private Integer status = 1;
|
||||
|
||||
/**
|
||||
* 是否删除(0:未删除,1:已删除)
|
||||
*/
|
||||
@TableLogic
|
||||
@TableField("deleted")
|
||||
private Integer deleted = 0;
|
||||
|
||||
/**
|
||||
* 邮箱是否已验证(0:未验证,1:已验证)
|
||||
*/
|
||||
@TableField("email_verified")
|
||||
private Integer emailVerified = 0;
|
||||
|
||||
/**
|
||||
* 邮箱验证时间
|
||||
*/
|
||||
@TableField("email_verified_at")
|
||||
private LocalDateTime emailVerifiedAt;
|
||||
|
||||
/**
|
||||
* 登录失败次数
|
||||
*/
|
||||
@TableField("login_failed_count")
|
||||
private Integer loginFailedCount = 0;
|
||||
|
||||
/**
|
||||
* 账户锁定时间
|
||||
*/
|
||||
@TableField("account_locked_at")
|
||||
private LocalDateTime accountLockedAt;
|
||||
|
||||
/**
|
||||
* 账户解锁时间
|
||||
*/
|
||||
@TableField("account_unlock_at")
|
||||
private LocalDateTime accountUnlockAt;
|
||||
|
||||
/**
|
||||
* 最后登录时间
|
||||
*/
|
||||
@TableField("last_login_time")
|
||||
private LocalDateTime lastLoginTime;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 用户拥有的角色集合(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private List<Role> roles;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.kurihada.auth.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 用户角色关联实体类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("user_roles")
|
||||
public class UserRole {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@TableField("user_id")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 角色ID
|
||||
*/
|
||||
@TableField("role_id")
|
||||
private Long roleId;
|
||||
|
||||
/**
|
||||
* 租户ID
|
||||
*/
|
||||
@TableField("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 用户信息(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private User user;
|
||||
|
||||
/**
|
||||
* 角色信息(非数据库字段)
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private Role role;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.kurihada.auth.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 业务异常类
|
||||
*/
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
|
||||
private final Integer code;
|
||||
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
this.code = 500;
|
||||
}
|
||||
|
||||
public BusinessException(Integer code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public BusinessException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = 500;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.kurihada.auth.exception;
|
||||
|
||||
import com.kurihada.auth.dto.Result;
|
||||
import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
*/
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public Result<Object> handleBusinessException(BusinessException e) {
|
||||
log.error("业务异常: {}", e.getMessage());
|
||||
return Result.error(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理限流异常
|
||||
*/
|
||||
@ExceptionHandler(RateLimitException.class)
|
||||
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
|
||||
public Result<Object> handleRateLimitException(RateLimitException e) {
|
||||
log.warn("触发限流: {}, 需等待: {}秒", e.getMessage(), e.getRemainingTime());
|
||||
return Result.error(429, e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数校验异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Object> handleValidationException(MethodArgumentNotValidException e) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining(", "));
|
||||
log.error("参数校验异常: {}", message);
|
||||
return Result.error(400, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理绑定异常
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Object> handleBindException(BindException e) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining(", "));
|
||||
log.error("参数绑定异常: {}", message);
|
||||
return Result.error(400, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Spring Security认证异常
|
||||
*/
|
||||
@ExceptionHandler(BadCredentialsException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public Result<Object> handleBadCredentialsException(BadCredentialsException e) {
|
||||
log.error("认证失败: {}", e.getMessage());
|
||||
return Result.error(401, "用户名或密码错误");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Spring Security权限异常
|
||||
*/
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public Result<Object> handleAccessDeniedException(AccessDeniedException e) {
|
||||
log.error("权限不足: {}", e.getMessage());
|
||||
return Result.error(403, "权限不足,访问被拒绝");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理数据库唯一键冲突异常
|
||||
*/
|
||||
@ExceptionHandler(DuplicateKeyException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Object> handleDuplicateKeyException(DuplicateKeyException e) {
|
||||
log.error("数据库唯一键冲突: {}", e.getMessage());
|
||||
return Result.error(400, "数据已存在,请勿重复添加");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理数据访问异常
|
||||
*/
|
||||
@ExceptionHandler(DataAccessException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Result<Object> handleDataAccessException(DataAccessException e) {
|
||||
log.error("数据访问异常: ", e);
|
||||
return Result.error("数据访问失败,请稍后重试");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理MyBatis Plus异常
|
||||
*/
|
||||
@ExceptionHandler(MybatisPlusException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Result<Object> handleMybatisPlusException(MybatisPlusException e) {
|
||||
log.error("MyBatis Plus异常: ", e);
|
||||
return Result.error("数据操作失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求方法不支持异常
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
|
||||
public Result<Object> handleMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
|
||||
log.error("请求方法不支持: {}", e.getMessage());
|
||||
return Result.error(405, "不支持的请求方法: " + e.getMethod());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求参数缺失异常
|
||||
*/
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Object> handleMissingParameterException(MissingServletRequestParameterException e) {
|
||||
log.error("缺少请求参数: {}", e.getMessage());
|
||||
return Result.error(400, "缺少必要参数: " + e.getParameterName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数类型不匹配异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Object> handleTypeMismatchException(MethodArgumentTypeMismatchException e) {
|
||||
log.error("参数类型不匹配: {}", e.getMessage());
|
||||
return Result.error(400, "参数类型错误: " + e.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理JSON解析异常
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Result<Object> handleJsonParseException(HttpMessageNotReadableException e) {
|
||||
log.error("JSON解析异常: {}", e.getMessage());
|
||||
return Result.error(400, "请求数据格式错误");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理404异常
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public Result<Object> handleNotFoundException(NoHandlerFoundException e) {
|
||||
log.error("请求路径不存在: {}", e.getRequestURL());
|
||||
return Result.error(404, "请求的资源不存在");
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理其他异常
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Result<Object> handleException(Exception e) {
|
||||
log.error("系统异常: ", e);
|
||||
return Result.error("系统异常,请联系管理员");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.kurihada.auth.exception;
|
||||
|
||||
/**
|
||||
* 限流异常
|
||||
*/
|
||||
public class RateLimitException extends RuntimeException {
|
||||
|
||||
private final int remainingTime;
|
||||
|
||||
public RateLimitException(String message) {
|
||||
super(message);
|
||||
this.remainingTime = 0;
|
||||
}
|
||||
|
||||
public RateLimitException(String message, int remainingTime) {
|
||||
super(message);
|
||||
this.remainingTime = remainingTime;
|
||||
}
|
||||
|
||||
public int getRemainingTime() {
|
||||
return remainingTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.kurihada.auth.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* MyBatis Plus 自动填充处理器
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MyMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
/**
|
||||
* 插入时自动填充
|
||||
*/
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
log.debug("开始插入填充...");
|
||||
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
|
||||
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新时自动填充
|
||||
*/
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
log.debug("开始更新填充...");
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.kurihada.auth.interceptor;
|
||||
|
||||
import com.kurihada.auth.context.TenantContext;
|
||||
import com.kurihada.auth.util.JwtUtil;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
/**
|
||||
* 租户拦截器 - 从请求中提取租户ID并设置到TenantContext
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TenantInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
@Value("${jwt.header}")
|
||||
private String tokenHeader;
|
||||
|
||||
@Value("${jwt.prefix}")
|
||||
private String tokenPrefix;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
String jwt = getJwtFromRequest(request);
|
||||
|
||||
if (StringUtils.hasText(jwt) && jwtUtil.validateToken(jwt)) {
|
||||
// 从JWT的claims中获取tenantId
|
||||
Long tenantId = jwtUtil.getTenantIdFromToken(jwt);
|
||||
if (tenantId != null) {
|
||||
TenantContext.setTenantId(tenantId);
|
||||
log.debug("设置当前租户ID: {}", tenantId);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
|
||||
// 请求处理完成后清除租户上下文
|
||||
TenantContext.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||
// 确保清除租户上下文(异常情况下也要清除)
|
||||
TenantContext.clear();
|
||||
}
|
||||
|
||||
private String getJwtFromRequest(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader(tokenHeader);
|
||||
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(tokenPrefix + " ")) {
|
||||
return bearerToken.substring(7);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.kurihada.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kurihada.auth.entity.AuditLog;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 审计日志 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface AuditLogMapper extends BaseMapper<AuditLog> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.kurihada.auth.mapper;
|
||||
|
||||
import com.kurihada.auth.entity.Permission;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 权限Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface PermissionMapper extends BaseMapper<Permission> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.kurihada.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kurihada.auth.entity.RefreshToken;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* Refresh Token Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RefreshTokenMapper extends BaseMapper<RefreshToken> {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.kurihada.auth.mapper;
|
||||
|
||||
import com.kurihada.auth.entity.Role;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 角色Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface RoleMapper extends BaseMapper<Role> {
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.kurihada.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kurihada.auth.entity.RolePermission;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色权限关联Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface RolePermissionMapper extends BaseMapper<RolePermission> {
|
||||
|
||||
/**
|
||||
* 根据角色ID查询权限ID列表
|
||||
*/
|
||||
@Select("SELECT permission_id FROM role_permissions WHERE role_id = #{roleId}")
|
||||
List<Long> selectPermissionIdsByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 根据权限ID查询角色ID列表
|
||||
*/
|
||||
@Select("SELECT role_id FROM role_permissions WHERE permission_id = #{permissionId}")
|
||||
List<Long> selectRoleIdsByPermissionId(Long permissionId);
|
||||
|
||||
/**
|
||||
* 为角色分配权限
|
||||
*/
|
||||
@Insert("INSERT INTO role_permissions(role_id, permission_id) VALUES(#{roleId}, #{permissionId})")
|
||||
int insert(Long roleId, Long permissionId);
|
||||
|
||||
/**
|
||||
* 批量为角色分配权限
|
||||
*/
|
||||
@Insert("<script>" +
|
||||
"INSERT INTO role_permissions(role_id, permission_id) VALUES " +
|
||||
"<foreach collection='permissionIds' item='permissionId' separator=','>" +
|
||||
"(#{roleId}, #{permissionId})" +
|
||||
"</foreach>" +
|
||||
"</script>")
|
||||
int batchInsert(Long roleId, List<Long> permissionIds);
|
||||
|
||||
/**
|
||||
* 删除角色的所有权限
|
||||
*/
|
||||
@Delete("DELETE FROM role_permissions WHERE role_id = #{roleId}")
|
||||
int deleteByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 删除权限的所有角色关联
|
||||
*/
|
||||
@Delete("DELETE FROM role_permissions WHERE permission_id = #{permissionId}")
|
||||
int deleteByPermissionId(Long permissionId);
|
||||
|
||||
/**
|
||||
* 删除指定角色的指定权限
|
||||
*/
|
||||
@Delete("DELETE FROM role_permissions WHERE role_id = #{roleId} AND permission_id = #{permissionId}")
|
||||
int deleteByRoleIdAndPermissionId(Long roleId, Long permissionId);
|
||||
|
||||
/**
|
||||
* 检查角色是否拥有指定权限
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM role_permissions WHERE role_id = #{roleId} AND permission_id = #{permissionId}")
|
||||
int existsByRoleIdAndPermissionId(Long roleId, Long permissionId);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.kurihada.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kurihada.auth.entity.Tenant;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 租户Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface TenantMapper extends BaseMapper<Tenant> {
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.kurihada.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
|
||||
import com.kurihada.auth.entity.User;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
/**
|
||||
* 用户Mapper接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserMapper extends BaseMapper<User> {
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户(包含角色和权限,一次查询)
|
||||
* 忽略租户过滤器,因为登录时还没有租户上下文
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
User selectUserWithRolesAndPermissions(@Param("username") String username);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询用户(包含角色和权限,一次查询)
|
||||
* 忽略租户过滤器,用于刷新token等场景
|
||||
*/
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
User selectUserWithRolesAndPermissionsById(@Param("userId") Long userId);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.kurihada.auth.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.kurihada.auth.entity.UserRole;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户角色关联Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface UserRoleMapper extends BaseMapper<UserRole> {
|
||||
|
||||
/**
|
||||
* 根据用户ID查询角色ID列表
|
||||
*/
|
||||
@Select("SELECT role_id FROM user_roles WHERE user_id = #{userId}")
|
||||
List<Long> selectRoleIdsByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 根据角色ID查询用户ID列表
|
||||
*/
|
||||
@Select("SELECT user_id FROM user_roles WHERE role_id = #{roleId}")
|
||||
List<Long> selectUserIdsByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 为用户分配角色
|
||||
*/
|
||||
@Insert("INSERT INTO user_roles(user_id, role_id) VALUES(#{userId}, #{roleId})")
|
||||
int insert(Long userId, Long roleId);
|
||||
|
||||
/**
|
||||
* 批量为用户分配角色
|
||||
*/
|
||||
@Insert("<script>" +
|
||||
"INSERT INTO user_roles(user_id, role_id) VALUES " +
|
||||
"<foreach collection='roleIds' item='roleId' separator=','>" +
|
||||
"(#{userId}, #{roleId})" +
|
||||
"</foreach>" +
|
||||
"</script>")
|
||||
int batchInsert(Long userId, List<Long> roleIds);
|
||||
|
||||
/**
|
||||
* 批量为角色分配用户
|
||||
*/
|
||||
@Insert("<script>" +
|
||||
"INSERT INTO user_roles(user_id, role_id) VALUES " +
|
||||
"<foreach collection='userIds' item='userId' separator=','>" +
|
||||
"(#{userId}, #{roleId})" +
|
||||
"</foreach>" +
|
||||
"</script>")
|
||||
int batchInsertUsers(Long roleId, List<Long> userIds);
|
||||
|
||||
/**
|
||||
* 删除用户的所有角色
|
||||
*/
|
||||
@Delete("DELETE FROM user_roles WHERE user_id = #{userId}")
|
||||
int deleteByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 删除角色的所有用户关联
|
||||
*/
|
||||
@Delete("DELETE FROM user_roles WHERE role_id = #{roleId}")
|
||||
int deleteByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 删除指定用户的指定角色
|
||||
*/
|
||||
@Delete("DELETE FROM user_roles WHERE user_id = #{userId} AND role_id = #{roleId}")
|
||||
int deleteByUserIdAndRoleId(Long userId, Long roleId);
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有指定角色
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM user_roles WHERE user_id = #{userId} AND role_id = #{roleId}")
|
||||
int existsByUserIdAndRoleId(Long userId, Long roleId);
|
||||
|
||||
/**
|
||||
* 统计角色的用户数量
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM user_roles WHERE role_id = #{roleId}")
|
||||
int countUsersByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 统计用户的角色数量
|
||||
*/
|
||||
@Select("SELECT COUNT(*) FROM user_roles WHERE user_id = #{userId}")
|
||||
int countRolesByUserId(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.kurihada.auth.security;
|
||||
|
||||
import com.kurihada.auth.dto.Result;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* JWT访问拒绝处理器 - 处理授权失败
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AccessDeniedException accessDeniedException) throws IOException, ServletException {
|
||||
log.error("访问被拒绝: {}", accessDeniedException.getMessage());
|
||||
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
|
||||
Result<Object> result = Result.error(403, "权限不足,访问被拒绝");
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
response.getWriter().write(objectMapper.writeValueAsString(result));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.kurihada.auth.security;
|
||||
|
||||
import com.kurihada.auth.dto.Result;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* JWT认证入口点 - 处理认证失败
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException authException) throws IOException, ServletException {
|
||||
log.error("认证失败: {}", authException.getMessage());
|
||||
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
|
||||
Result<Object> result = Result.error(401, "未授权,请先登录");
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
response.getWriter().write(objectMapper.writeValueAsString(result));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.kurihada.auth.security;
|
||||
|
||||
import com.kurihada.auth.context.TenantContext;
|
||||
import com.kurihada.auth.service.PermissionCacheService;
|
||||
import com.kurihada.auth.util.JwtUtil;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* JWT认证过滤器
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
private final UserDetailsService userDetailsService;
|
||||
private final com.kurihada.auth.service.TokenBlacklistService tokenBlacklistService;
|
||||
private final PermissionCacheService permissionCacheService;
|
||||
|
||||
@Value("${jwt.header}")
|
||||
private String tokenHeader;
|
||||
|
||||
@Value("${jwt.prefix}")
|
||||
private String tokenPrefix;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
try {
|
||||
String jwt = getJwtFromRequest(request);
|
||||
|
||||
// 检查 token 是否有效且不在黑名单中
|
||||
if (StringUtils.hasText(jwt) && jwtUtil.validateToken(jwt) && !tokenBlacklistService.isBlacklisted(jwt)) {
|
||||
String username = jwtUtil.getUsernameFromToken(jwt);
|
||||
Long userId = jwtUtil.getUserIdFromToken(jwt);
|
||||
Long tenantId = jwtUtil.getTenantIdFromToken(jwt);
|
||||
|
||||
// 设置租户上下文
|
||||
if (tenantId != null) {
|
||||
TenantContext.setTenantId(tenantId);
|
||||
}
|
||||
|
||||
UserDetails userDetails = null;
|
||||
|
||||
// 优先从缓存构建 UserDetails(避免数据库查询)
|
||||
if (userId != null && tenantId != null) {
|
||||
userDetails = permissionCacheService.buildUserDetailsFromCache(userId, tenantId, username);
|
||||
if (userDetails != null) {
|
||||
logger.debug("从缓存构建 UserDetails 成功: userId=" + userId + ", tenantId=" + tenantId);
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存未命中,回退到数据库查询
|
||||
if (userDetails == null) {
|
||||
logger.debug("缓存未命中,从数据库加载 UserDetails: username=" + username);
|
||||
userDetails = userDetailsService.loadUserByUsername(username);
|
||||
}
|
||||
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
userDetails,
|
||||
null,
|
||||
userDetails.getAuthorities()
|
||||
);
|
||||
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("无法设置用户认证: {}", e);
|
||||
}
|
||||
|
||||
// 调用下一个过滤器
|
||||
filterChain.doFilter(request, response);
|
||||
|
||||
// CRITICAL: 请求处理完成后清理租户上下文,防止 ThreadLocal 内存泄露
|
||||
// 注意:必须在 filterChain.doFilter() 之后执行,确保整个请求链都能访问到租户信息
|
||||
// SecurityContext 不需要手动清理,Spring Security 会自动处理
|
||||
TenantContext.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取JWT token
|
||||
*/
|
||||
private String getJwtFromRequest(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader(tokenHeader);
|
||||
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(tokenPrefix + " ")) {
|
||||
return bearerToken.substring(7);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.kurihada.auth.security;
|
||||
|
||||
import com.kurihada.auth.entity.Permission;
|
||||
import com.kurihada.auth.entity.Role;
|
||||
import com.kurihada.auth.entity.User;
|
||||
import com.kurihada.auth.mapper.PermissionMapper;
|
||||
import com.kurihada.auth.mapper.RoleMapper;
|
||||
import com.kurihada.auth.mapper.RolePermissionMapper;
|
||||
import com.kurihada.auth.mapper.UserMapper;
|
||||
import com.kurihada.auth.mapper.UserRoleMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* UserDetailsService实现类
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final RoleMapper roleMapper;
|
||||
private final PermissionMapper permissionMapper;
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
private final RolePermissionMapper rolePermissionMapper;
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
// 使用优化后的联表查询,一次性加载用户、角色和权限
|
||||
User user = userMapper.selectUserWithRolesAndPermissions(username);
|
||||
|
||||
if (user == null) {
|
||||
throw new UsernameNotFoundException("用户不存在: " + username);
|
||||
}
|
||||
|
||||
if (user.getStatus() == 0) {
|
||||
throw new UsernameNotFoundException("用户已被禁用: " + username);
|
||||
}
|
||||
|
||||
return buildUserDetails(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建UserDetails对象
|
||||
*/
|
||||
private UserDetails buildUserDetails(User user) {
|
||||
Collection<? extends GrantedAuthority> authorities = getAuthorities(user);
|
||||
|
||||
return org.springframework.security.core.userdetails.User.builder()
|
||||
.username(user.getUsername())
|
||||
.password(user.getPassword())
|
||||
.authorities(authorities)
|
||||
.accountExpired(false)
|
||||
.accountLocked(user.getStatus() == 0)
|
||||
.credentialsExpired(false)
|
||||
.disabled(user.getDeleted() == 1)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限集合
|
||||
*/
|
||||
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
|
||||
Set<GrantedAuthority> authorities = new HashSet<>();
|
||||
|
||||
// 添加角色权限
|
||||
if (user.getRoles() != null) {
|
||||
for (Role role : user.getRoles()) {
|
||||
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getCode()));
|
||||
|
||||
// 添加角色对应的权限
|
||||
if (role.getPermissions() != null) {
|
||||
for (Permission permission : role.getPermissions()) {
|
||||
authorities.add(new SimpleGrantedAuthority(permission.getCode()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return authorities;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import com.kurihada.auth.entity.User;
|
||||
|
||||
/**
|
||||
* 账户锁定服务接口
|
||||
* 功能:管理账户登录失败次数和锁定状态
|
||||
*/
|
||||
public interface AccountLockService {
|
||||
|
||||
/**
|
||||
* 检查账户是否被锁定
|
||||
*
|
||||
* @param user 用户对象
|
||||
* @return true-已锁定,false-未锁定
|
||||
*/
|
||||
boolean isAccountLocked(User user);
|
||||
|
||||
/**
|
||||
* 记录登录失败
|
||||
* 如果失败次数达到阈值,则锁定账户
|
||||
*
|
||||
* @param user 用户对象
|
||||
*/
|
||||
void recordLoginFailure(User user);
|
||||
|
||||
/**
|
||||
* 重置登录失败次数
|
||||
* 在登录成功后调用
|
||||
*
|
||||
* @param user 用户对象
|
||||
*/
|
||||
void resetLoginFailureCount(User user);
|
||||
|
||||
/**
|
||||
* 解锁账户
|
||||
* 手动解锁或自动解锁时调用
|
||||
*
|
||||
* @param user 用户对象
|
||||
*/
|
||||
void unlockAccount(User user);
|
||||
|
||||
/**
|
||||
* 获取账户剩余锁定时间(秒)
|
||||
*
|
||||
* @param user 用户对象
|
||||
* @return 剩余锁定时间(秒),如果未锁定则返回0
|
||||
*/
|
||||
long getRemainingLockTime(User user);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import com.kurihada.auth.dto.AuditLogQueryRequest;
|
||||
import com.kurihada.auth.dto.AuditLogResponse;
|
||||
import com.kurihada.auth.dto.PageResult;
|
||||
import com.kurihada.auth.entity.AuditLog;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 审计日志服务接口
|
||||
*/
|
||||
public interface AuditLogService {
|
||||
|
||||
/**
|
||||
* 记录审计日志
|
||||
*
|
||||
* @param auditLog 审计日志对象
|
||||
*/
|
||||
void log(AuditLog auditLog);
|
||||
|
||||
/**
|
||||
* 记录成功的操作
|
||||
*
|
||||
* @param actionType 操作类型
|
||||
* @param userId 用户ID
|
||||
* @param username 用户名
|
||||
* @param tenantId 租户ID
|
||||
* @param resourceType 资源类型
|
||||
* @param resourceId 资源ID
|
||||
* @param details 操作详情
|
||||
* @param request HTTP 请求对象
|
||||
*/
|
||||
void logSuccess(String actionType, Long userId, String username, Long tenantId,
|
||||
String resourceType, String resourceId, String details, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 记录失败的操作
|
||||
*
|
||||
* @param actionType 操作类型
|
||||
* @param userId 用户ID
|
||||
* @param username 用户名
|
||||
* @param tenantId 租户ID
|
||||
* @param resourceType 资源类型
|
||||
* @param resourceId 资源ID
|
||||
* @param details 操作详情
|
||||
* @param errorMessage 错误信息
|
||||
* @param request HTTP 请求对象
|
||||
*/
|
||||
void logFailure(String actionType, Long userId, String username, Long tenantId,
|
||||
String resourceType, String resourceId, String details,
|
||||
String errorMessage, HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 分页查询审计日志
|
||||
*
|
||||
* @param request 查询请求参数
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<AuditLogResponse> queryAuditLogs(AuditLogQueryRequest request);
|
||||
|
||||
/**
|
||||
* 根据ID获取审计日志详情
|
||||
*
|
||||
* @param id 日志ID
|
||||
* @return 审计日志详情
|
||||
*/
|
||||
AuditLogResponse getAuditLogById(Long id);
|
||||
}
|
||||
@@ -0,0 +1,632 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import com.kurihada.auth.dto.ForgotPasswordRequest;
|
||||
import com.kurihada.auth.dto.LoginRequest;
|
||||
import com.kurihada.auth.dto.LoginResponse;
|
||||
import com.kurihada.auth.dto.RegisterRequest;
|
||||
import com.kurihada.auth.dto.ResetPasswordRequest;
|
||||
import com.kurihada.auth.entity.Permission;
|
||||
import com.kurihada.auth.entity.RefreshToken;
|
||||
import com.kurihada.auth.entity.Role;
|
||||
import com.kurihada.auth.entity.User;
|
||||
import com.kurihada.auth.exception.BusinessException;
|
||||
import com.kurihada.auth.mapper.PermissionMapper;
|
||||
import com.kurihada.auth.mapper.RoleMapper;
|
||||
import com.kurihada.auth.mapper.RolePermissionMapper;
|
||||
import com.kurihada.auth.mapper.UserMapper;
|
||||
import com.kurihada.auth.mapper.UserRoleMapper;
|
||||
import com.kurihada.auth.util.JwtUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 认证服务类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AuthService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final RoleMapper roleMapper;
|
||||
private final PermissionMapper permissionMapper;
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
private final RolePermissionMapper rolePermissionMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtUtil jwtUtil;
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final RefreshTokenService refreshTokenService;
|
||||
private final TokenBlacklistService tokenBlacklistService;
|
||||
private final com.kurihada.auth.util.TenantResolver tenantResolver;
|
||||
private final PermissionCacheService permissionCacheService;
|
||||
private final com.kurihada.auth.service.UserRoleService userRoleService;
|
||||
private final EmailService emailService;
|
||||
private final PasswordResetTokenService passwordResetTokenService;
|
||||
private final EmailVerificationTokenService emailVerificationTokenService;
|
||||
private final AccountLockService accountLockService;
|
||||
|
||||
@Value("${registration.auto-assign-default-role:true}")
|
||||
private boolean autoAssignDefaultRole;
|
||||
|
||||
@Value("${registration.default-role-code:USER}")
|
||||
private String defaultRoleCode;
|
||||
|
||||
@Value("${registration.require-email-verification:true}")
|
||||
private boolean requireEmailVerification;
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*/
|
||||
@Transactional
|
||||
public LoginResponse login(LoginRequest request, jakarta.servlet.http.HttpServletRequest httpRequest) {
|
||||
// 解析租户ID
|
||||
Long tenantId = tenantResolver.resolveTenantId(
|
||||
httpRequest,
|
||||
request.getTenantId(),
|
||||
request.getTenantCode()
|
||||
);
|
||||
log.info("用户 {} 登录,租户ID: {}", request.getUsername(), tenantId);
|
||||
|
||||
// 先查询用户信息以检查账户锁定状态
|
||||
User user = userMapper.selectUserWithRolesAndPermissions(request.getUsername());
|
||||
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户名或密码错误");
|
||||
}
|
||||
|
||||
// 验证用户是否属于指定租户
|
||||
if (!user.getTenantId().equals(tenantId)) {
|
||||
log.warn("用户 {} 不属于租户 {}, 实际租户: {}", request.getUsername(), tenantId, user.getTenantId());
|
||||
throw new BusinessException("用户名或密码错误"); // 不暴露租户信息
|
||||
}
|
||||
|
||||
// 检查账户是否被锁定
|
||||
if (accountLockService.isAccountLocked(user)) {
|
||||
long remainingSeconds = accountLockService.getRemainingLockTime(user);
|
||||
long remainingMinutes = remainingSeconds / 60;
|
||||
log.warn("用户 {} 账户已被锁定,剩余时间: {} 分钟", request.getUsername(), remainingMinutes);
|
||||
throw new BusinessException(String.format("账户已被锁定,请在 %d 分钟后重试", remainingMinutes));
|
||||
}
|
||||
|
||||
// 进行密码认证
|
||||
Authentication authentication;
|
||||
try {
|
||||
authentication = authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
request.getUsername(),
|
||||
request.getPassword()
|
||||
)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
// 认证失败,记录失败次数
|
||||
accountLockService.recordLoginFailure(user);
|
||||
log.warn("用户 {} 登录失败: {}", request.getUsername(), e.getMessage());
|
||||
|
||||
// 检查是否刚刚被锁定
|
||||
if (accountLockService.isAccountLocked(user)) {
|
||||
long remainingMinutes = accountLockService.getRemainingLockTime(user) / 60;
|
||||
throw new BusinessException(String.format("登录失败次数过多,账户已被锁定 %d 分钟", remainingMinutes));
|
||||
}
|
||||
|
||||
throw new BusinessException("用户名或密码错误");
|
||||
}
|
||||
|
||||
// 检查邮箱是否已验证(如果启用了邮箱验证功能)
|
||||
if (requireEmailVerification && user.getEmail() != null && user.getEmailVerified() == 0) {
|
||||
log.warn("用户 {} 尝试登录但邮箱未验证", request.getUsername());
|
||||
throw new BusinessException("请先验证您的邮箱地址后再登录。如未收到验证邮件,可申请重新发送。");
|
||||
}
|
||||
|
||||
// 登录成功,重置失败次数
|
||||
accountLockService.resetLoginFailureCount(user);
|
||||
|
||||
// 更新最后登录时间
|
||||
user.setLastLoginTime(LocalDateTime.now());
|
||||
userMapper.updateById(user);
|
||||
|
||||
// 缓存用户权限(登录时预热缓存)
|
||||
permissionCacheService.cacheUserPermissions(user.getId(), user.getTenantId(), user);
|
||||
log.debug("已缓存用户权限: userId={}, tenantId={}", user.getId(), user.getTenantId());
|
||||
|
||||
// 生成JWT token,包含租户ID和用户ID
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", user.getId());
|
||||
if (user.getTenantId() != null) {
|
||||
claims.put("tenantId", user.getTenantId());
|
||||
}
|
||||
String token = jwtUtil.generateToken(authentication.getName(), claims);
|
||||
|
||||
// 生成 Refresh Token
|
||||
String refreshToken = refreshTokenService.createRefreshToken(user);
|
||||
|
||||
// 构建响应
|
||||
return LoginResponse.builder()
|
||||
.token(token)
|
||||
.refreshToken(refreshToken)
|
||||
.tokenType("Bearer")
|
||||
.userId(user.getId())
|
||||
.username(user.getUsername())
|
||||
.realName(user.getRealName())
|
||||
.roles(getRoleCodes(user))
|
||||
.permissions(getPermissionCodes(user))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*/
|
||||
@Transactional
|
||||
public void register(RegisterRequest request, jakarta.servlet.http.HttpServletRequest httpRequest) {
|
||||
// 解析租户ID
|
||||
Long tenantId = tenantResolver.resolveTenantId(
|
||||
httpRequest,
|
||||
request.getTenantId(),
|
||||
request.getTenantCode()
|
||||
);
|
||||
// 验证用户名是否已存在
|
||||
Long count = userMapper.selectCount(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, request.getUsername()));
|
||||
if (count > 0) {
|
||||
throw new BusinessException("用户名已存在");
|
||||
}
|
||||
|
||||
// 验证邮箱是否已存在
|
||||
if (request.getEmail() != null) {
|
||||
Long emailCount = userMapper.selectCount(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getEmail, request.getEmail()));
|
||||
if (emailCount > 0) {
|
||||
throw new BusinessException("邮箱已被使用");
|
||||
}
|
||||
}
|
||||
|
||||
// 验证两次密码是否一致
|
||||
if (!request.getPassword().equals(request.getConfirmPassword())) {
|
||||
throw new BusinessException("两次密码输入不一致");
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
User user = new User();
|
||||
user.setUsername(request.getUsername());
|
||||
user.setPassword(passwordEncoder.encode(request.getPassword()));
|
||||
user.setEmail(request.getEmail());
|
||||
user.setRealName(request.getRealName());
|
||||
user.setPhone(request.getPhone());
|
||||
user.setStatus(1);
|
||||
user.setDeleted(0);
|
||||
|
||||
// 设置租户ID
|
||||
user.setTenantId(tenantId);
|
||||
|
||||
userMapper.insert(user);
|
||||
log.info("用户注册成功: {}, 租户ID: {}", request.getUsername(), tenantId);
|
||||
|
||||
// 自动分配默认角色
|
||||
assignDefaultRole(user.getId(), tenantId);
|
||||
|
||||
// 发送邮箱验证邮件(如果启用且有邮箱)
|
||||
if (requireEmailVerification && user.getEmail() != null) {
|
||||
sendVerificationEmail(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为新注册用户分配默认角色
|
||||
*/
|
||||
private void assignDefaultRole(Long userId, Long tenantId) {
|
||||
// 检查是否启用自动分配
|
||||
if (!autoAssignDefaultRole) {
|
||||
log.debug("自动分配默认角色功能已禁用");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 查找默认角色
|
||||
Role defaultRole = roleMapper.selectOne(
|
||||
new LambdaQueryWrapper<Role>()
|
||||
.eq(Role::getCode, defaultRoleCode)
|
||||
.eq(Role::getTenantId, tenantId)
|
||||
);
|
||||
|
||||
if (defaultRole != null) {
|
||||
// 分配角色给用户
|
||||
boolean assigned = userRoleService.assignRoleToUser(userId, defaultRole.getId());
|
||||
if (assigned) {
|
||||
log.info("已为新用户自动分配默认角色: userId={}, roleId={}, roleCode={}, tenantId={}",
|
||||
userId, defaultRole.getId(), defaultRoleCode, tenantId);
|
||||
}
|
||||
} else {
|
||||
log.warn("未找到默认角色 {},无法为新用户分配角色: userId={}, tenantId={}",
|
||||
defaultRoleCode, userId, tenantId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 分配角色失败不影响注册流程,只记录日志
|
||||
log.error("为新用户分配默认角色失败: userId={}, tenantId={}, roleCode={}, error={}",
|
||||
userId, tenantId, defaultRoleCode, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色代码集合
|
||||
*/
|
||||
// private Set<String> getRoleCodes(User user) {
|
||||
// if (user.getRoles() == null) {
|
||||
// return Set.of();
|
||||
// }
|
||||
// return user.getRoles().stream()
|
||||
// .map(Role::getCode)
|
||||
// .collect(Collectors.toSet());
|
||||
// }
|
||||
|
||||
/**
|
||||
* 刷新 Access Token
|
||||
*/
|
||||
@Transactional
|
||||
public LoginResponse refreshAccessToken(String refreshTokenStr) {
|
||||
// 验证 Refresh Token
|
||||
RefreshToken refreshToken = refreshTokenService.validateRefreshToken(refreshTokenStr);
|
||||
|
||||
// 获取用户信息
|
||||
User user = userMapper.selectUserWithRolesAndPermissionsById(refreshToken.getUserId());
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
if (user.getStatus() == 0) {
|
||||
throw new BusinessException("用户已被禁用");
|
||||
}
|
||||
|
||||
// 标记旧的 Refresh Token 为已使用
|
||||
refreshTokenService.markTokenAsUsed(refreshToken.getId());
|
||||
|
||||
// 缓存用户权限(刷新token时更新缓存)
|
||||
permissionCacheService.cacheUserPermissions(user.getId(), user.getTenantId(), user);
|
||||
|
||||
// 生成新的 Access Token
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
if (user.getTenantId() != null) {
|
||||
claims.put("tenantId", user.getTenantId());
|
||||
}
|
||||
String newAccessToken = jwtUtil.generateToken(user.getUsername(), claims);
|
||||
|
||||
// 生成新的 Refresh Token
|
||||
String newRefreshToken = refreshTokenService.createRefreshToken(user);
|
||||
|
||||
// 构建响应
|
||||
return LoginResponse.builder()
|
||||
.token(newAccessToken)
|
||||
.refreshToken(newRefreshToken)
|
||||
.tokenType("Bearer")
|
||||
.userId(user.getId())
|
||||
.username(user.getUsername())
|
||||
.realName(user.getRealName())
|
||||
.roles(getRoleCodes(user))
|
||||
.permissions(getPermissionCodes(user))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户退出登录
|
||||
*/
|
||||
@Transactional
|
||||
public void logout(String token, Long userId, Long tenantId) {
|
||||
// 将 Access Token 加入黑名单
|
||||
if (token != null) {
|
||||
tokenBlacklistService.addToBlacklist(token);
|
||||
}
|
||||
|
||||
// 删除用户的所有 Refresh Token
|
||||
if (userId != null) {
|
||||
refreshTokenService.deleteUserTokens(userId);
|
||||
}
|
||||
|
||||
// 清除用户权限缓存
|
||||
if (userId != null && tenantId != null) {
|
||||
permissionCacheService.evictUserPermissions(userId, tenantId);
|
||||
log.debug("已清除用户权限缓存: userId={}, tenantId={}", userId, tenantId);
|
||||
}
|
||||
|
||||
log.info("用户 {} 已退出登录", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色代码集合
|
||||
*/
|
||||
private Set<String> getRoleCodes(User user) {
|
||||
if (user.getRoles() == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return user.getRoles().stream()
|
||||
.map(Role::getCode)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限代码集合
|
||||
*/
|
||||
private Set<String> getPermissionCodes(User user) {
|
||||
if (user.getRoles() == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return user.getRoles().stream()
|
||||
.flatMap(role -> role.getPermissions() != null ? role.getPermissions().stream() : java.util.stream.Stream.empty())
|
||||
.map(Permission::getCode)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码 - 发送重置邮件
|
||||
*
|
||||
* @param request 忘记密码请求
|
||||
*/
|
||||
public void forgotPassword(ForgotPasswordRequest request) {
|
||||
String email = request.getEmail();
|
||||
Long tenantId = request.getTenantId();
|
||||
|
||||
log.info("Processing forgot password request for email: {} in tenant: {}", email, tenantId);
|
||||
|
||||
try {
|
||||
// 设置租户上下文,让MyBatis Plus自动添加租户隔离条件
|
||||
com.kurihada.auth.context.TenantContext.setTenantId(tenantId);
|
||||
|
||||
// 查询用户是否存在(租户ID由插件自动添加)
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getEmail, email);
|
||||
queryWrapper.eq(User::getDeleted, 0); // 未删除的用户
|
||||
User user = userMapper.selectOne(queryWrapper);
|
||||
|
||||
// 无论用户是否存在,都返回相同的消息(安全考虑,防止邮箱枚举)
|
||||
if (user == null) {
|
||||
log.warn("Password reset requested for non-existent email: {} in tenant: {}", email, tenantId);
|
||||
// 不抛出异常,避免暴露用户是否存在
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (user.getStatus() != 1) {
|
||||
log.warn("Password reset requested for disabled user: {} in tenant: {}", email, tenantId);
|
||||
// 不抛出异常,避免暴露用户状态
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成重置令牌(将邮箱和租户ID组合存储)
|
||||
String resetToken = passwordResetTokenService.generateResetToken(email, tenantId);
|
||||
|
||||
// 构建重置链接
|
||||
String resetUrl = "http://localhost:5173/reset-password"; // TODO: 从配置文件读取
|
||||
|
||||
// 发送重置邮件
|
||||
try {
|
||||
emailService.sendPasswordResetEmail(email, user.getUsername(), resetToken, resetUrl);
|
||||
log.info("Password reset email sent successfully to: {} in tenant: {}", email, tenantId);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send password reset email to: {} in tenant: {}", email, tenantId, e);
|
||||
throw new BusinessException("发送重置邮件失败,请稍后重试");
|
||||
}
|
||||
} finally {
|
||||
// 清除租户上下文,避免影响其他请求
|
||||
com.kurihada.auth.context.TenantContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
*
|
||||
* @param request 重置密码请求
|
||||
*/
|
||||
@Transactional
|
||||
public void resetPassword(ResetPasswordRequest request) {
|
||||
Long tenantId = request.getTenantId();
|
||||
log.info("Processing password reset request for tenant: {}", tenantId);
|
||||
|
||||
// 验证两次密码是否一致
|
||||
if (!request.getNewPassword().equals(request.getConfirmPassword())) {
|
||||
throw new BusinessException("两次输入的密码不一致");
|
||||
}
|
||||
|
||||
// 验证令牌并获取邮箱
|
||||
String email = passwordResetTokenService.validateTokenAndGetEmail(request.getToken(), tenantId);
|
||||
if (email == null) {
|
||||
throw new BusinessException("重置令牌无效或已过期");
|
||||
}
|
||||
|
||||
try {
|
||||
// 设置租户上下文,让MyBatis Plus自动添加租户隔离条件
|
||||
com.kurihada.auth.context.TenantContext.setTenantId(tenantId);
|
||||
|
||||
// 查询用户(租户ID由插件自动添加)
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getEmail, email);
|
||||
queryWrapper.eq(User::getDeleted, 0);
|
||||
User user = userMapper.selectOne(queryWrapper);
|
||||
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
if (user.getStatus() != 1) {
|
||||
throw new BusinessException("用户账号已被禁用");
|
||||
}
|
||||
|
||||
// 记录用户信息用于调试
|
||||
log.debug("Reset password for user - ID: {}, Username: {}, TenantId: {}",
|
||||
user.getId(), user.getUsername(), user.getTenantId());
|
||||
|
||||
// 更新密码
|
||||
user.setPassword(passwordEncoder.encode(request.getNewPassword()));
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
userMapper.updateById(user);
|
||||
|
||||
// 使令牌失效
|
||||
passwordResetTokenService.invalidateToken(request.getToken());
|
||||
|
||||
// 清除该用户的所有权限缓存
|
||||
permissionCacheService.evictUserPermissions(user.getId(), user.getTenantId());
|
||||
log.info("User permission cache evicted for userId: {}, tenantId: {}",
|
||||
user.getId(), user.getTenantId());
|
||||
|
||||
// 可选:将该用户的所有已登录token加入黑名单,强制重新登录
|
||||
// 这里暂不实现,允许用户在密码重置后继续使用旧token直到过期
|
||||
|
||||
log.info("Password reset successfully for user: {} in tenant: {}", user.getUsername(), tenantId);
|
||||
} finally {
|
||||
// 清除租户上下文,避免影响其他请求
|
||||
com.kurihada.auth.context.TenantContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮箱验证邮件
|
||||
*
|
||||
* @param user 用户对象
|
||||
*/
|
||||
private void sendVerificationEmail(User user) {
|
||||
try {
|
||||
// 生成验证令牌
|
||||
String verificationToken = emailVerificationTokenService.generateVerificationToken(
|
||||
user.getEmail(),
|
||||
user.getId(),
|
||||
user.getTenantId()
|
||||
);
|
||||
|
||||
// 构建验证链接
|
||||
String verificationUrl = "http://localhost:5173/verify-email"; // TODO: 从配置文件读取
|
||||
|
||||
// 发送验证邮件
|
||||
emailService.sendEmailVerification(
|
||||
user.getEmail(),
|
||||
user.getUsername(),
|
||||
verificationToken,
|
||||
verificationUrl
|
||||
);
|
||||
|
||||
log.info("Email verification sent to: {} (userId: {}, tenantId: {})",
|
||||
user.getEmail(), user.getId(), user.getTenantId());
|
||||
} catch (Exception e) {
|
||||
// 发送邮件失败不影响注册流程,只记录日志
|
||||
log.error("Failed to send verification email to: {} (userId: {})",
|
||||
user.getEmail(), user.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱
|
||||
*
|
||||
* @param token 验证令牌
|
||||
* @param tenantId 租户ID
|
||||
*/
|
||||
@Transactional
|
||||
public void verifyEmail(String token, Long tenantId) {
|
||||
// 验证令牌并获取用户信息
|
||||
Map<String, Object> tokenData = emailVerificationTokenService.validateTokenAndGetUserInfo(token);
|
||||
if (tokenData == null) {
|
||||
throw new BusinessException("验证令牌无效或已过期");
|
||||
}
|
||||
|
||||
Long userId = convertToLong(tokenData.get("userId"));
|
||||
Long storedTenantId = convertToLong(tokenData.get("tenantId"));
|
||||
String email = (String) tokenData.get("email");
|
||||
|
||||
// 验证租户ID是否匹配
|
||||
if (!tenantId.equals(storedTenantId)) {
|
||||
log.warn("Tenant ID mismatch for email verification. Expected: {}, Got: {}", storedTenantId, tenantId);
|
||||
throw new BusinessException("验证令牌无效");
|
||||
}
|
||||
|
||||
try {
|
||||
// 设置租户上下文
|
||||
com.kurihada.auth.context.TenantContext.setTenantId(tenantId);
|
||||
|
||||
// 查询用户
|
||||
User user = userMapper.selectById(userId);
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
// 检查邮箱是否匹配
|
||||
if (!email.equals(user.getEmail())) {
|
||||
throw new BusinessException("验证令牌与用户邮箱不匹配");
|
||||
}
|
||||
|
||||
// 检查是否已验证
|
||||
if (user.getEmailVerified() == 1) {
|
||||
log.info("Email already verified for user: {} (userId: {})", user.getUsername(), userId);
|
||||
return; // 已验证,直接返回
|
||||
}
|
||||
|
||||
// 更新用户邮箱验证状态
|
||||
user.setEmailVerified(1);
|
||||
user.setEmailVerifiedAt(LocalDateTime.now());
|
||||
userMapper.updateById(user);
|
||||
|
||||
// 使令牌失效
|
||||
emailVerificationTokenService.invalidateToken(token);
|
||||
|
||||
log.info("Email verified successfully for user: {} (userId: {}, email: {})",
|
||||
user.getUsername(), userId, email);
|
||||
} finally {
|
||||
com.kurihada.auth.context.TenantContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新发送验证邮件
|
||||
*
|
||||
* @param email 用户邮箱
|
||||
* @param tenantId 租户ID
|
||||
*/
|
||||
public void resendVerificationEmail(String email, Long tenantId) {
|
||||
try {
|
||||
com.kurihada.auth.context.TenantContext.setTenantId(tenantId);
|
||||
|
||||
// 查询用户
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getEmail, email);
|
||||
queryWrapper.eq(User::getDeleted, 0);
|
||||
User user = userMapper.selectOne(queryWrapper);
|
||||
|
||||
if (user == null) {
|
||||
// 为了安全,不暴露用户是否存在
|
||||
log.warn("Resend verification requested for non-existent email: {} in tenant: {}", email, tenantId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已验证
|
||||
if (user.getEmailVerified() == 1) {
|
||||
throw new BusinessException("邮箱已验证,无需重新发送");
|
||||
}
|
||||
|
||||
// 发送验证邮件
|
||||
sendVerificationEmail(user);
|
||||
|
||||
log.info("Verification email resent to: {} (userId: {})", email, user.getId());
|
||||
} finally {
|
||||
com.kurihada.auth.context.TenantContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助方法:将 Object 转换为 Long
|
||||
*/
|
||||
private Long convertToLong(Object value) {
|
||||
if (value instanceof Integer) {
|
||||
return ((Integer) value).longValue();
|
||||
} else if (value instanceof Long) {
|
||||
return (Long) value;
|
||||
}
|
||||
throw new BusinessException("Invalid token data");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
|
||||
/**
|
||||
* 邮件服务
|
||||
* 负责发送各类邮件通知
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EmailService {
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
private final SpringTemplateEngine templateEngine;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String from;
|
||||
|
||||
/**
|
||||
* 发送密码重置邮件
|
||||
*
|
||||
* @param to 收件人邮箱
|
||||
* @param username 用户名
|
||||
* @param resetToken 重置令牌
|
||||
* @param resetUrl 重置链接
|
||||
*/
|
||||
@Async
|
||||
public void sendPasswordResetEmail(String to, String username, String resetToken, String resetUrl) {
|
||||
try {
|
||||
Context context = new Context();
|
||||
context.setVariable("username", username);
|
||||
context.setVariable("resetUrl", resetUrl + "?token=" + resetToken);
|
||||
context.setVariable("validMinutes", 60); // 1小时
|
||||
|
||||
String htmlContent = templateEngine.process("password-reset-email", context);
|
||||
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
|
||||
helper.setFrom(from);
|
||||
helper.setTo(to);
|
||||
helper.setSubject("密码重置请求 - Auth System");
|
||||
helper.setText(htmlContent, true);
|
||||
|
||||
mailSender.send(message);
|
||||
log.info("Password reset email sent to: {}", to);
|
||||
} catch (MessagingException e) {
|
||||
log.error("Failed to send password reset email to: {}", to, e);
|
||||
throw new RuntimeException("发送密码重置邮件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮箱验证邮件
|
||||
*
|
||||
* @param to 收件人邮箱
|
||||
* @param username 用户名
|
||||
* @param verificationToken 验证令牌
|
||||
* @param verificationUrl 验证链接
|
||||
*/
|
||||
@Async
|
||||
public void sendEmailVerification(String to, String username, String verificationToken, String verificationUrl) {
|
||||
try {
|
||||
Context context = new Context();
|
||||
context.setVariable("username", username);
|
||||
context.setVariable("verificationUrl", verificationUrl + "?token=" + verificationToken);
|
||||
context.setVariable("validHours", 24); // 24小时
|
||||
|
||||
String htmlContent = templateEngine.process("email-verification", context);
|
||||
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
|
||||
helper.setFrom(from);
|
||||
helper.setTo(to);
|
||||
helper.setSubject("验证您的邮箱 - Auth System");
|
||||
helper.setText(htmlContent, true);
|
||||
|
||||
mailSender.send(message);
|
||||
log.info("Email verification sent to: {}", to);
|
||||
} catch (MessagingException e) {
|
||||
log.error("Failed to send email verification to: {}", to, e);
|
||||
throw new RuntimeException("发送验证邮件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送纯文本邮件(备用方案)
|
||||
*
|
||||
* @param to 收件人
|
||||
* @param subject 主题
|
||||
* @param content 内容
|
||||
*/
|
||||
@Async
|
||||
public void sendSimpleEmail(String to, String subject, String content) {
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
|
||||
helper.setFrom(from);
|
||||
helper.setTo(to);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(content, false);
|
||||
|
||||
mailSender.send(message);
|
||||
log.info("Simple email sent to: {}", to);
|
||||
} catch (MessagingException e) {
|
||||
log.error("Failed to send simple email to: {}", to, e);
|
||||
throw new RuntimeException("发送邮件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送HTML邮件
|
||||
*
|
||||
* @param to 收件人
|
||||
* @param subject 主题
|
||||
* @param htmlContent HTML内容
|
||||
*/
|
||||
@Async
|
||||
public void sendHtmlEmail(String to, String subject, String htmlContent) {
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
|
||||
helper.setFrom(from);
|
||||
helper.setTo(to);
|
||||
helper.setSubject(subject);
|
||||
helper.setText(htmlContent, true);
|
||||
|
||||
mailSender.send(message);
|
||||
log.info("HTML email sent to: {}", to);
|
||||
} catch (MessagingException e) {
|
||||
log.error("Failed to send HTML email to: {}", to, e);
|
||||
throw new RuntimeException("发送HTML邮件失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 邮箱验证Token服务
|
||||
* 负责生成、验证和管理邮箱验证令牌
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EmailVerificationTokenService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Value("${email.verification-token-expiration:86400}") // 默认24小时
|
||||
private long tokenExpirationSeconds;
|
||||
|
||||
private static final String VERIFICATION_TOKEN_PREFIX = "email_verification:";
|
||||
private static final int TOKEN_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* 生成邮箱验证令牌
|
||||
*
|
||||
* @param email 用户邮箱
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @return 验证令牌
|
||||
*/
|
||||
public String generateVerificationToken(String email, Long userId, Long tenantId) {
|
||||
// 生成安全的随机令牌
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] bytes = new byte[TOKEN_LENGTH];
|
||||
random.nextBytes(bytes);
|
||||
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
||||
|
||||
// 存储到Redis,key为token,value为包含email、userId和tenantId的Map
|
||||
String key = VERIFICATION_TOKEN_PREFIX + token;
|
||||
Map<String, Object> tokenData = new HashMap<>();
|
||||
tokenData.put("email", email);
|
||||
tokenData.put("userId", userId);
|
||||
tokenData.put("tenantId", tenantId);
|
||||
|
||||
redisTemplate.opsForValue().set(key, tokenData, tokenExpirationSeconds, TimeUnit.SECONDS);
|
||||
|
||||
log.info("Generated email verification token for email: {} (userId: {}, tenantId: {})",
|
||||
email, userId, tenantId);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证令牌并获取用户信息
|
||||
*
|
||||
* @param token 验证令牌
|
||||
* @return 包含email、userId和tenantId的Map,如果令牌无效则返回null
|
||||
*/
|
||||
public Map<String, Object> validateTokenAndGetUserInfo(String token) {
|
||||
String key = VERIFICATION_TOKEN_PREFIX + token;
|
||||
Object tokenDataObj = redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (tokenDataObj == null) {
|
||||
log.warn("Invalid or expired email verification token");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> tokenData = (Map<String, Object>) tokenDataObj;
|
||||
|
||||
log.debug("Validated email verification token for email: {}", tokenData.get("email"));
|
||||
return tokenData;
|
||||
} catch (ClassCastException e) {
|
||||
log.error("Error parsing token data", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使令牌失效(用于邮箱验证成功后)
|
||||
*
|
||||
* @param token 验证令牌
|
||||
*/
|
||||
public void invalidateToken(String token) {
|
||||
String key = VERIFICATION_TOKEN_PREFIX + token;
|
||||
redisTemplate.delete(key);
|
||||
log.info("Invalidated email verification token");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有未使用的验证令牌
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return true表示存在活跃令牌
|
||||
*/
|
||||
public boolean hasActiveVerificationToken(Long userId) {
|
||||
// 简化实现:直接返回false,允许重新发送验证邮件
|
||||
// 实际应用中可以通过用户ID索引来查询
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 密码重置Token服务
|
||||
* 负责生成、验证和管理密码重置令牌
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PasswordResetTokenService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Value("${password.reset-token-expiration:3600}")
|
||||
private long tokenExpirationSeconds;
|
||||
|
||||
private static final String RESET_TOKEN_PREFIX = "password_reset:";
|
||||
private static final int TOKEN_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* 生成密码重置令牌
|
||||
*
|
||||
* @param email 用户邮箱
|
||||
* @param tenantId 租户ID
|
||||
* @return 重置令牌
|
||||
*/
|
||||
public String generateResetToken(String email, Long tenantId) {
|
||||
// 生成安全的随机令牌
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] bytes = new byte[TOKEN_LENGTH];
|
||||
random.nextBytes(bytes);
|
||||
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
||||
|
||||
// 存储到Redis,key为token,value为包含email和tenantId的Map
|
||||
String key = RESET_TOKEN_PREFIX + token;
|
||||
Map<String, Object> tokenData = new HashMap<>();
|
||||
tokenData.put("email", email);
|
||||
tokenData.put("tenantId", tenantId);
|
||||
|
||||
redisTemplate.opsForValue().set(key, tokenData, tokenExpirationSeconds, TimeUnit.SECONDS);
|
||||
|
||||
log.info("Generated password reset token for email: {} in tenant: {}", email, tenantId);
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证重置令牌并获取关联的邮箱
|
||||
*
|
||||
* @param token 重置令牌
|
||||
* @param tenantId 租户ID(用于验证租户匹配)
|
||||
* @return 关联的邮箱,如果令牌无效或租户不匹配则返回null
|
||||
*/
|
||||
public String validateTokenAndGetEmail(String token, Long tenantId) {
|
||||
String key = RESET_TOKEN_PREFIX + token;
|
||||
Object tokenDataObj = redisTemplate.opsForValue().get(key);
|
||||
|
||||
if (tokenDataObj == null) {
|
||||
log.warn("Invalid or expired password reset token");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> tokenData = (Map<String, Object>) tokenDataObj;
|
||||
|
||||
String email = (String) tokenData.get("email");
|
||||
Object storedTenantIdObj = tokenData.get("tenantId");
|
||||
|
||||
// 将存储的 tenantId 转换为 Long 类型
|
||||
Long storedTenantId = null;
|
||||
if (storedTenantIdObj instanceof Integer) {
|
||||
storedTenantId = ((Integer) storedTenantIdObj).longValue();
|
||||
} else if (storedTenantIdObj instanceof Long) {
|
||||
storedTenantId = (Long) storedTenantIdObj;
|
||||
}
|
||||
|
||||
// 验证租户ID是否匹配
|
||||
if (!tenantId.equals(storedTenantId)) {
|
||||
log.warn("Tenant ID mismatch for password reset token. Expected: {}, Got: {}",
|
||||
storedTenantId, tenantId);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug("Validated password reset token for email: {} in tenant: {}", email, tenantId);
|
||||
return email;
|
||||
} catch (ClassCastException e) {
|
||||
log.error("Error parsing token data", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使令牌失效(用于密码重置成功后)
|
||||
*
|
||||
* @param token 重置令牌
|
||||
*/
|
||||
public void invalidateToken(String token) {
|
||||
String key = RESET_TOKEN_PREFIX + token;
|
||||
redisTemplate.delete(key);
|
||||
log.info("Invalidated password reset token");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱在指定租户下是否有活跃的重置令牌
|
||||
*
|
||||
* @param email 用户邮箱
|
||||
* @param tenantId 租户ID
|
||||
* @return true表示存在活跃令牌
|
||||
*/
|
||||
public boolean hasActiveResetToken(String email, Long tenantId) {
|
||||
// 这里可以通过扫描Redis键来实现,但为了性能考虑,
|
||||
// 我们可以在生成token时额外存储一个反向映射
|
||||
// 简化实现:直接返回false,允许生成新令牌
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import com.kurihada.auth.dto.UserPermissionCache;
|
||||
import com.kurihada.auth.entity.User;
|
||||
|
||||
/**
|
||||
* 权限缓存服务接口
|
||||
*/
|
||||
public interface PermissionCacheService {
|
||||
|
||||
/**
|
||||
* 获取用户权限缓存
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @return 用户权限缓存
|
||||
*/
|
||||
UserPermissionCache getUserPermissions(Long userId, Long tenantId);
|
||||
|
||||
/**
|
||||
* 缓存用户权限
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @param user 用户对象(包含角色和权限)
|
||||
*/
|
||||
void cacheUserPermissions(Long userId, Long tenantId, User user);
|
||||
|
||||
/**
|
||||
* 删除用户权限缓存
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
*/
|
||||
void evictUserPermissions(Long userId, Long tenantId);
|
||||
|
||||
/**
|
||||
* 删除租户下所有用户的权限缓存
|
||||
* @param tenantId 租户ID
|
||||
*/
|
||||
void evictTenantPermissions(Long tenantId);
|
||||
|
||||
/**
|
||||
* 删除拥有指定角色的所有用户的权限缓存
|
||||
* @param roleId 角色ID
|
||||
* @param tenantId 租户ID
|
||||
*/
|
||||
void evictRoleRelatedPermissions(Long roleId, Long tenantId);
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有指定权限
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @param permissionCode 权限代码
|
||||
* @return true-有权限,false-无权限
|
||||
*/
|
||||
boolean hasPermission(Long userId, Long tenantId, String permissionCode);
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有指定角色
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @param roleCode 角色代码
|
||||
* @return true-有角色,false-无角色
|
||||
*/
|
||||
boolean hasRole(Long userId, Long tenantId, String roleCode);
|
||||
|
||||
/**
|
||||
* 检查用户是否有权访问指定资源
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @param resource 资源路径
|
||||
* @return true-有权限,false-无权限
|
||||
*/
|
||||
boolean hasResource(Long userId, Long tenantId, String resource);
|
||||
|
||||
/**
|
||||
* 预热用户权限缓存
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
*/
|
||||
void warmUpUserPermissions(Long userId, Long tenantId);
|
||||
|
||||
/**
|
||||
* 清空所有权限缓存
|
||||
*/
|
||||
void clearAllPermissionCache();
|
||||
|
||||
/**
|
||||
* 从缓存构建 UserDetails(用于 JWT 认证过滤器)
|
||||
* @param userId 用户ID
|
||||
* @param tenantId 租户ID
|
||||
* @param username 用户名
|
||||
* @return UserDetails 对象,如果缓存未命中则返回null
|
||||
*/
|
||||
org.springframework.security.core.userdetails.UserDetails buildUserDetailsFromCache(Long userId, Long tenantId, String username);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import com.kurihada.auth.dto.CreatePermissionRequest;
|
||||
import com.kurihada.auth.dto.PageRequest;
|
||||
import com.kurihada.auth.dto.PageResult;
|
||||
import com.kurihada.auth.dto.PermissionDTO;
|
||||
import com.kurihada.auth.dto.PermissionTreeNode;
|
||||
import com.kurihada.auth.dto.UpdatePermissionRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 权限服务接口
|
||||
*/
|
||||
public interface PermissionService {
|
||||
|
||||
/**
|
||||
* 分页查询权限
|
||||
*/
|
||||
PageResult<PermissionDTO> getPermissionsPage(PageRequest pageRequest);
|
||||
|
||||
/**
|
||||
* 根据ID获取权限
|
||||
*/
|
||||
PermissionDTO getPermissionById(Long id);
|
||||
|
||||
/**
|
||||
* 创建新权限
|
||||
*/
|
||||
PermissionDTO createPermission(CreatePermissionRequest request);
|
||||
|
||||
/**
|
||||
* 更新权限
|
||||
*/
|
||||
PermissionDTO updatePermission(Long id, UpdatePermissionRequest request);
|
||||
|
||||
/**
|
||||
* 删除权限
|
||||
*/
|
||||
void deletePermission(Long id);
|
||||
|
||||
/**
|
||||
* 切换权限状态
|
||||
*/
|
||||
void togglePermissionStatus(Long id);
|
||||
|
||||
/**
|
||||
* 获取权限树形结构
|
||||
*/
|
||||
List<PermissionTreeNode> getPermissionTree();
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.kurihada.auth.entity.RefreshToken;
|
||||
import com.kurihada.auth.entity.User;
|
||||
import com.kurihada.auth.exception.BusinessException;
|
||||
import com.kurihada.auth.mapper.RefreshTokenMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Refresh Token 服务
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RefreshTokenService {
|
||||
|
||||
private final RefreshTokenMapper refreshTokenMapper;
|
||||
|
||||
@Value("${jwt.refresh-expiration}")
|
||||
private Long refreshExpiration;
|
||||
|
||||
/**
|
||||
* 创建 Refresh Token
|
||||
*/
|
||||
@Transactional
|
||||
public String createRefreshToken(User user) {
|
||||
// 生成唯一的 refresh token
|
||||
String token = UUID.randomUUID().toString().replace("-", "");
|
||||
|
||||
RefreshToken refreshToken = new RefreshToken();
|
||||
refreshToken.setUserId(user.getId());
|
||||
refreshToken.setToken(token);
|
||||
refreshToken.setTenantId(user.getTenantId());
|
||||
refreshToken.setExpireTime(LocalDateTime.now().plusSeconds(refreshExpiration / 1000));
|
||||
refreshToken.setUsed(false);
|
||||
|
||||
refreshTokenMapper.insert(refreshToken);
|
||||
|
||||
log.info("为用户 {} 创建 Refresh Token", user.getUsername());
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Refresh Token
|
||||
*/
|
||||
public RefreshToken validateRefreshToken(String token) {
|
||||
LambdaQueryWrapper<RefreshToken> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(RefreshToken::getToken, token)
|
||||
.eq(RefreshToken::getUsed, false)
|
||||
.gt(RefreshToken::getExpireTime, LocalDateTime.now());
|
||||
|
||||
RefreshToken refreshToken = refreshTokenMapper.selectOne(queryWrapper);
|
||||
|
||||
if (refreshToken == null) {
|
||||
throw new BusinessException("Refresh Token 无效或已过期");
|
||||
}
|
||||
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记 Refresh Token 为已使用
|
||||
*/
|
||||
@Transactional
|
||||
public void markTokenAsUsed(Long tokenId) {
|
||||
RefreshToken refreshToken = refreshTokenMapper.selectById(tokenId);
|
||||
if (refreshToken != null) {
|
||||
refreshToken.setUsed(true);
|
||||
refreshTokenMapper.updateById(refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户的所有 Refresh Token(用于退出登录)
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteUserTokens(Long userId) {
|
||||
LambdaQueryWrapper<RefreshToken> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(RefreshToken::getUserId, userId);
|
||||
refreshTokenMapper.delete(queryWrapper);
|
||||
log.info("删除用户 {} 的所有 Refresh Token", userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的 Refresh Token(定时任务)
|
||||
*/
|
||||
@Transactional
|
||||
public void cleanExpiredTokens() {
|
||||
LambdaQueryWrapper<RefreshToken> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.lt(RefreshToken::getExpireTime, LocalDateTime.now());
|
||||
int count = refreshTokenMapper.delete(queryWrapper);
|
||||
log.info("清理了 {} 个过期的 Refresh Token", count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kurihada.auth.entity.Permission;
|
||||
import com.kurihada.auth.entity.RolePermission;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色权限关联服务接口
|
||||
*/
|
||||
public interface RolePermissionService extends IService<RolePermission> {
|
||||
|
||||
/**
|
||||
* 为角色分配权限
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @param permissionId 权限ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean assignPermissionToRole(Long roleId, Long permissionId);
|
||||
|
||||
/**
|
||||
* 批量为角色分配权限
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @param permissionIds 权限ID列表
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean assignPermissionsToRole(Long roleId, List<Long> permissionIds);
|
||||
|
||||
/**
|
||||
* 移除角色的指定权限
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @param permissionId 权限ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean removePermissionFromRole(Long roleId, Long permissionId);
|
||||
|
||||
/**
|
||||
* 移除角色的所有权限
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean removeAllPermissionsFromRole(Long roleId);
|
||||
|
||||
/**
|
||||
* 移除权限的所有角色关联
|
||||
*
|
||||
* @param permissionId 权限ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean removeAllRolesFromPermission(Long permissionId);
|
||||
|
||||
/**
|
||||
* 获取角色的所有权限ID列表
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @return 权限ID列表
|
||||
*/
|
||||
List<Long> getPermissionIdsByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 获取角色的所有权限详情列表
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @return 权限详情列表
|
||||
*/
|
||||
List<Permission> getPermissionsByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 获取拥有指定权限的所有角色ID列表
|
||||
*
|
||||
* @param permissionId 权限ID
|
||||
* @return 角色ID列表
|
||||
*/
|
||||
List<Long> getRoleIdsByPermissionId(Long permissionId);
|
||||
|
||||
/**
|
||||
* 检查角色是否拥有指定权限
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @param permissionId 权限ID
|
||||
* @return 是否拥有
|
||||
*/
|
||||
boolean hasPermission(Long roleId, Long permissionId);
|
||||
|
||||
/**
|
||||
* 更新角色的权限(先删除所有旧权限,再添加新权限)
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @param permissionIds 新的权限ID列表
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateRolePermissions(Long roleId, List<Long> permissionIds);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import com.kurihada.auth.dto.CreateRoleRequest;
|
||||
import com.kurihada.auth.dto.PageRequest;
|
||||
import com.kurihada.auth.dto.PageResult;
|
||||
import com.kurihada.auth.dto.RoleDTO;
|
||||
import com.kurihada.auth.dto.UpdateRoleRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 角色服务接口
|
||||
*/
|
||||
public interface RoleService {
|
||||
|
||||
/**
|
||||
* 分页查询角色
|
||||
*/
|
||||
PageResult<RoleDTO> getRolesPage(PageRequest pageRequest);
|
||||
|
||||
/**
|
||||
* 根据ID获取角色
|
||||
*/
|
||||
RoleDTO getRoleById(Long id);
|
||||
|
||||
/**
|
||||
* 创建新角色
|
||||
*/
|
||||
RoleDTO createRole(CreateRoleRequest request);
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
*/
|
||||
RoleDTO updateRole(Long id, UpdateRoleRequest request);
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
void deleteRole(Long id);
|
||||
|
||||
/**
|
||||
* 切换角色状态
|
||||
*/
|
||||
void toggleRoleStatus(Long id);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import com.kurihada.auth.util.JwtUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Token 黑名单服务
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TokenBlacklistService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
private static final String BLACKLIST_PREFIX = "token:blacklist:";
|
||||
|
||||
/**
|
||||
* 将 token 加入黑名单
|
||||
*/
|
||||
public void addToBlacklist(String token) {
|
||||
try {
|
||||
// 获取 token 的过期时间
|
||||
Date expirationDate = jwtUtil.getExpirationDateFromToken(token);
|
||||
long expiration = expirationDate.getTime() - System.currentTimeMillis();
|
||||
|
||||
if (expiration > 0) {
|
||||
// 将 token 加入黑名单,过期时间与 token 本身的过期时间一致
|
||||
String key = BLACKLIST_PREFIX + token;
|
||||
redisTemplate.opsForValue().set(key, "1", expiration, TimeUnit.MILLISECONDS);
|
||||
log.info("Token 已加入黑名单,剩余有效期: {} 毫秒", expiration);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("添加 token 到黑名单失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 token 是否在黑名单中
|
||||
*/
|
||||
public boolean isBlacklisted(String token) {
|
||||
try {
|
||||
String key = BLACKLIST_PREFIX + token;
|
||||
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
|
||||
} catch (Exception e) {
|
||||
log.error("检查 token 黑名单状态失败: {}", e.getMessage());
|
||||
// 发生异常时,为了安全起见,返回 false(允许通过)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从黑名单中移除 token(通常不需要,因为会自动过期)
|
||||
*/
|
||||
public void removeFromBlacklist(String token) {
|
||||
try {
|
||||
String key = BLACKLIST_PREFIX + token;
|
||||
redisTemplate.delete(key);
|
||||
log.info("Token 已从黑名单中移除");
|
||||
} catch (Exception e) {
|
||||
log.error("从黑名单移除 token 失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.kurihada.auth.entity.Role;
|
||||
import com.kurihada.auth.entity.User;
|
||||
import com.kurihada.auth.entity.UserRole;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户角色关联服务接口
|
||||
*/
|
||||
public interface UserRoleService extends IService<UserRole> {
|
||||
|
||||
/**
|
||||
* 为用户分配角色
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param roleId 角色ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean assignRoleToUser(Long userId, Long roleId);
|
||||
|
||||
/**
|
||||
* 批量为用户分配角色
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param roleIds 角色ID列表
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean assignRolesToUser(Long userId, List<Long> roleIds);
|
||||
|
||||
/**
|
||||
* 批量为角色分配用户
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @param userIds 用户ID列表
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean assignUsersToRole(Long roleId, List<Long> userIds);
|
||||
|
||||
/**
|
||||
* 移除用户的指定角色
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param roleId 角色ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean removeRoleFromUser(Long userId, Long roleId);
|
||||
|
||||
/**
|
||||
* 移除用户的所有角色
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean removeAllRolesFromUser(Long userId);
|
||||
|
||||
/**
|
||||
* 移除角色的所有用户关联
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean removeAllUsersFromRole(Long roleId);
|
||||
|
||||
/**
|
||||
* 获取用户的所有角色ID列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 角色ID列表
|
||||
*/
|
||||
List<Long> getRoleIdsByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 获取用户的所有角色详情列表
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 角色详情列表
|
||||
*/
|
||||
List<Role> getRolesByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 获取拥有指定角色的所有用户ID列表
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @return 用户ID列表
|
||||
*/
|
||||
List<Long> getUserIdsByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 获取拥有指定角色的所有用户详情列表
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @return 用户详情列表
|
||||
*/
|
||||
List<User> getUsersByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有指定角色
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param roleId 角色ID
|
||||
* @return 是否拥有
|
||||
*/
|
||||
boolean hasRole(Long userId, Long roleId);
|
||||
|
||||
/**
|
||||
* 检查用户是否拥有指定角色代码
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param roleCode 角色代码
|
||||
* @return 是否拥有
|
||||
*/
|
||||
boolean hasRoleByCode(Long userId, String roleCode);
|
||||
|
||||
/**
|
||||
* 更新用户的角色(先删除所有旧角色,再添加新角色)
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param roleIds 新的角色ID列表
|
||||
* @return 是否成功
|
||||
*/
|
||||
boolean updateUserRoles(Long userId, List<Long> roleIds);
|
||||
|
||||
/**
|
||||
* 统计角色的用户数量
|
||||
*
|
||||
* @param roleId 角色ID
|
||||
* @return 用户数量
|
||||
*/
|
||||
int countUsersByRoleId(Long roleId);
|
||||
|
||||
/**
|
||||
* 统计用户的角色数量
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @return 角色数量
|
||||
*/
|
||||
int countRolesByUserId(Long userId);
|
||||
}
|
||||
@@ -0,0 +1,466 @@
|
||||
package com.kurihada.auth.service;
|
||||
|
||||
import com.kurihada.auth.dto.ChangePasswordRequest;
|
||||
import com.kurihada.auth.dto.PageRequest;
|
||||
import com.kurihada.auth.dto.PageResult;
|
||||
import com.kurihada.auth.dto.PermissionDTO;
|
||||
import com.kurihada.auth.dto.PermissionTreeNode;
|
||||
import com.kurihada.auth.dto.UserDTO;
|
||||
import com.kurihada.auth.dto.UpdateUserRequest;
|
||||
import com.kurihada.auth.entity.Permission;
|
||||
import com.kurihada.auth.entity.Role;
|
||||
import com.kurihada.auth.entity.User;
|
||||
import com.kurihada.auth.exception.BusinessException;
|
||||
import com.kurihada.auth.mapper.PermissionMapper;
|
||||
import com.kurihada.auth.mapper.RoleMapper;
|
||||
import com.kurihada.auth.mapper.RolePermissionMapper;
|
||||
import com.kurihada.auth.mapper.UserMapper;
|
||||
import com.kurihada.auth.mapper.UserRoleMapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户服务类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
private final RoleMapper roleMapper;
|
||||
private final PermissionMapper permissionMapper;
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
private final RolePermissionMapper rolePermissionMapper;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final PermissionCacheService permissionCacheService;
|
||||
|
||||
/**
|
||||
* 获取当前登录用户信息
|
||||
*/
|
||||
public UserDTO getCurrentUser() {
|
||||
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
// 使用优化后的联表查询
|
||||
User user = userMapper.selectUserWithRolesAndPermissions(username);
|
||||
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
return convertToDTO(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户信息
|
||||
*/
|
||||
public UserDTO getUserById(Long id) {
|
||||
// 使用优化后的联表查询
|
||||
User user = userMapper.selectUserWithRolesAndPermissionsById(id);
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
return convertToDTO(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询用户列表
|
||||
*/
|
||||
public PageResult<UserDTO> getUsersPage(PageRequest pageRequest) {
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(User::getDeleted, 0);
|
||||
|
||||
// 搜索条件
|
||||
if (StringUtils.hasText(pageRequest.getKeyword())) {
|
||||
queryWrapper.and(wrapper -> wrapper
|
||||
.like(User::getUsername, pageRequest.getKeyword())
|
||||
.or()
|
||||
.like(User::getRealName, pageRequest.getKeyword())
|
||||
.or()
|
||||
.like(User::getEmail, pageRequest.getKeyword())
|
||||
.or()
|
||||
.like(User::getPhone, pageRequest.getKeyword())
|
||||
);
|
||||
}
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(pageRequest.getSortBy())) {
|
||||
boolean isAsc = "asc".equalsIgnoreCase(pageRequest.getSortDirection());
|
||||
switch (pageRequest.getSortBy()) {
|
||||
case "username" -> queryWrapper.orderBy(true, isAsc, User::getUsername);
|
||||
case "realName" -> queryWrapper.orderBy(true, isAsc, User::getRealName);
|
||||
case "email" -> queryWrapper.orderBy(true, isAsc, User::getEmail);
|
||||
case "status" -> queryWrapper.orderBy(true, isAsc, User::getStatus);
|
||||
case "createTime" -> queryWrapper.orderBy(true, isAsc, User::getCreateTime);
|
||||
case "lastLoginTime" -> queryWrapper.orderBy(true, isAsc, User::getLastLoginTime);
|
||||
default -> queryWrapper.orderByDesc(User::getCreateTime);
|
||||
}
|
||||
} else {
|
||||
queryWrapper.orderByDesc(User::getCreateTime);
|
||||
}
|
||||
|
||||
// 查询总数
|
||||
long total = userMapper.selectCount(queryWrapper);
|
||||
|
||||
// 查询分页数据
|
||||
queryWrapper.last("LIMIT " + pageRequest.getOffset() + ", " + pageRequest.getLimit());
|
||||
List<User> users = userMapper.selectList(queryWrapper);
|
||||
|
||||
// 转换为DTO
|
||||
List<UserDTO> userDTOs = users.stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResult.of(userDTOs, total, pageRequest.getPage(), pageRequest.getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户(逻辑删除)
|
||||
*/
|
||||
public void deleteUser(Long id) {
|
||||
User user = userMapper.selectById(id);
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
// MyBatis Plus的逻辑删除会自动处理
|
||||
userMapper.deleteById(id);
|
||||
log.info("用户已删除: {}", user.getUsername());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户信息(管理员操作)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public UserDTO updateUser(Long id, com.kurihada.auth.dto.UpdateUserRequest request) {
|
||||
User user = userMapper.selectById(id);
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
if (request.getRealName() != null) {
|
||||
user.setRealName(request.getRealName());
|
||||
}
|
||||
if (request.getEmail() != null) {
|
||||
// 检查邮箱是否已被其他用户使用
|
||||
Long count = userMapper.selectCount(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getEmail, request.getEmail())
|
||||
.ne(User::getId, id));
|
||||
if (count > 0) {
|
||||
throw new BusinessException("邮箱已被其他用户使用");
|
||||
}
|
||||
user.setEmail(request.getEmail());
|
||||
}
|
||||
if (request.getPhone() != null) {
|
||||
// 检查手机号是否已被其他用户使用
|
||||
Long count = userMapper.selectCount(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getPhone, request.getPhone())
|
||||
.ne(User::getId, id));
|
||||
if (count > 0) {
|
||||
throw new BusinessException("手机号已被其他用户使用");
|
||||
}
|
||||
user.setPhone(request.getPhone());
|
||||
}
|
||||
if (request.getGender() != null) {
|
||||
user.setGender(request.getGender());
|
||||
}
|
||||
if (request.getAvatar() != null) {
|
||||
user.setAvatar(request.getAvatar());
|
||||
}
|
||||
|
||||
userMapper.updateById(user);
|
||||
log.info("用户信息已更新: {}", user.getUsername());
|
||||
|
||||
// 返回更新后的用户信息
|
||||
User updatedUser = userMapper.selectUserWithRolesAndPermissions(user.getUsername());
|
||||
return convertToDTO(updatedUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前用户信息(用户自己操作)
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public UserDTO updateCurrentUser(com.kurihada.auth.dto.UpdateUserRequest request) {
|
||||
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, username));
|
||||
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
// 更新用户信息
|
||||
if (request.getRealName() != null) {
|
||||
user.setRealName(request.getRealName());
|
||||
}
|
||||
if (request.getEmail() != null) {
|
||||
// 检查邮箱是否已被其他用户使用
|
||||
Long count = userMapper.selectCount(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getEmail, request.getEmail())
|
||||
.ne(User::getId, user.getId()));
|
||||
if (count > 0) {
|
||||
throw new BusinessException("邮箱已被其他用户使用");
|
||||
}
|
||||
user.setEmail(request.getEmail());
|
||||
}
|
||||
if (request.getPhone() != null) {
|
||||
// 检查手机号是否已被其他用户使用
|
||||
Long count = userMapper.selectCount(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getPhone, request.getPhone())
|
||||
.ne(User::getId, user.getId()));
|
||||
if (count > 0) {
|
||||
throw new BusinessException("手机号已被其他用户使用");
|
||||
}
|
||||
user.setPhone(request.getPhone());
|
||||
}
|
||||
if (request.getGender() != null) {
|
||||
user.setGender(request.getGender());
|
||||
}
|
||||
if (request.getAvatar() != null) {
|
||||
user.setAvatar(request.getAvatar());
|
||||
}
|
||||
|
||||
userMapper.updateById(user);
|
||||
log.info("用户 {} 更新了自己的信息", user.getUsername());
|
||||
|
||||
// 返回更新后的用户信息
|
||||
User updatedUser = userMapper.selectUserWithRolesAndPermissions(user.getUsername());
|
||||
return convertToDTO(updatedUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用用户
|
||||
*/
|
||||
public void toggleUserStatus(Long id) {
|
||||
User user = userMapper.selectById(id);
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
user.setStatus(user.getStatus() == 1 ? 0 : 1);
|
||||
userMapper.updateById(user);
|
||||
log.info("用户状态已更新: {}, 新状态: {}", user.getUsername(), user.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为DTO
|
||||
*/
|
||||
private UserDTO convertToDTO(User user) {
|
||||
// 角色和权限已经通过联表查询加载,无需再次查询
|
||||
return UserDTO.builder()
|
||||
.id(user.getId())
|
||||
.username(user.getUsername())
|
||||
.realName(user.getRealName())
|
||||
.email(user.getEmail())
|
||||
.phone(user.getPhone())
|
||||
.gender(user.getGender())
|
||||
.avatar(user.getAvatar())
|
||||
.status(user.getStatus())
|
||||
.lastLoginTime(user.getLastLoginTime())
|
||||
.createTime(user.getCreateTime())
|
||||
.roles(getRoleCodes(user))
|
||||
.permissions(getPermissionCodes(user))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户角色代码集合
|
||||
*/
|
||||
private Set<String> getRoleCodes(User user) {
|
||||
if (user.getRoles() == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return user.getRoles().stream()
|
||||
.map(Role::getCode)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户权限代码集合
|
||||
*/
|
||||
private Set<String> getPermissionCodes(User user) {
|
||||
if (user.getRoles() == null) {
|
||||
return Set.of();
|
||||
}
|
||||
return user.getRoles().stream()
|
||||
.flatMap(role -> role.getPermissions() != null ? role.getPermissions().stream() : java.util.stream.Stream.empty())
|
||||
.map(Permission::getCode)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改当前用户密码
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void changePassword(ChangePasswordRequest request) {
|
||||
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
|
||||
.eq(User::getUsername, username));
|
||||
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
if (!passwordEncoder.matches(request.getOldPassword(), user.getPassword())) {
|
||||
throw new BusinessException("旧密码不正确");
|
||||
}
|
||||
|
||||
// 验证新密码不能与旧密码相同
|
||||
if (request.getOldPassword().equals(request.getNewPassword())) {
|
||||
throw new BusinessException("新密码不能与旧密码相同");
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.setPassword(passwordEncoder.encode(request.getNewPassword()));
|
||||
userMapper.updateById(user);
|
||||
log.info("用户 {} 已修改密码", username);
|
||||
|
||||
// 清除用户权限缓存(可选,因为密码修改不影响权限)
|
||||
if (user.getTenantId() != null) {
|
||||
permissionCacheService.evictUserPermissions(user.getId(), user.getTenantId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的所有权限
|
||||
*/
|
||||
public List<PermissionDTO> getCurrentUserPermissions() {
|
||||
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
User user = userMapper.selectUserWithRolesAndPermissions(username);
|
||||
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
// 获取用户所有角色的权限
|
||||
Set<Long> permissionIds = new HashSet<>();
|
||||
if (user.getRoles() != null) {
|
||||
for (Role role : user.getRoles()) {
|
||||
List<Long> rolePermissionIds = rolePermissionMapper.selectPermissionIdsByRoleId(role.getId());
|
||||
permissionIds.addAll(rolePermissionIds);
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// 查询权限详情
|
||||
List<Permission> permissions = permissionMapper.selectBatchIds(permissionIds);
|
||||
|
||||
return permissions.stream()
|
||||
.filter(p -> p.getStatus() == 1) // 只返回启用的权限
|
||||
.map(this::convertPermissionToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的菜单树
|
||||
*/
|
||||
public List<PermissionTreeNode> getCurrentUserMenus() {
|
||||
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
User user = userMapper.selectUserWithRolesAndPermissions(username);
|
||||
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
// 获取用户所有角色的权限
|
||||
Set<Long> permissionIds = new HashSet<>();
|
||||
if (user.getRoles() != null) {
|
||||
for (Role role : user.getRoles()) {
|
||||
List<Long> rolePermissionIds = rolePermissionMapper.selectPermissionIdsByRoleId(role.getId());
|
||||
permissionIds.addAll(rolePermissionIds);
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
// 查询权限详情,只查询菜单类型的权限
|
||||
List<Permission> permissions = permissionMapper.selectList(
|
||||
new LambdaQueryWrapper<Permission>()
|
||||
.in(Permission::getId, permissionIds)
|
||||
.eq(Permission::getType, "menu")
|
||||
.eq(Permission::getStatus, 1)
|
||||
.orderByAsc(Permission::getSort)
|
||||
);
|
||||
|
||||
// 构建树形结构
|
||||
Map<Long, PermissionTreeNode> nodeMap = new HashMap<>();
|
||||
for (Permission permission : permissions) {
|
||||
PermissionTreeNode node = convertPermissionToTreeNode(permission);
|
||||
nodeMap.put(permission.getId(), node);
|
||||
}
|
||||
|
||||
// 构建树
|
||||
List<PermissionTreeNode> rootNodes = new ArrayList<>();
|
||||
for (PermissionTreeNode node : nodeMap.values()) {
|
||||
if (node.getParentId() == null || node.getParentId() == 0) {
|
||||
rootNodes.add(node);
|
||||
} else {
|
||||
PermissionTreeNode parent = nodeMap.get(node.getParentId());
|
||||
if (parent != null) {
|
||||
parent.getChildren().add(node);
|
||||
} else {
|
||||
rootNodes.add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换权限为DTO
|
||||
*/
|
||||
private PermissionDTO convertPermissionToDTO(Permission permission) {
|
||||
return PermissionDTO.builder()
|
||||
.id(permission.getId())
|
||||
.name(permission.getName())
|
||||
.code(permission.getCode())
|
||||
.tenantId(permission.getTenantId())
|
||||
.description(permission.getDescription())
|
||||
.type(permission.getType())
|
||||
.resource(permission.getResource())
|
||||
.parentId(permission.getParentId())
|
||||
.sort(permission.getSort())
|
||||
.status(permission.getStatus())
|
||||
.createTime(permission.getCreateTime())
|
||||
.updateTime(permission.getUpdateTime())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换权限为树节点
|
||||
*/
|
||||
private PermissionTreeNode convertPermissionToTreeNode(Permission permission) {
|
||||
return PermissionTreeNode.builder()
|
||||
.id(permission.getId())
|
||||
.name(permission.getName())
|
||||
.code(permission.getCode())
|
||||
.tenantId(permission.getTenantId())
|
||||
.type(permission.getType())
|
||||
.resource(permission.getResource())
|
||||
.parentId(permission.getParentId())
|
||||
.sort(permission.getSort())
|
||||
.status(permission.getStatus())
|
||||
.createTime(permission.getCreateTime())
|
||||
.updateTime(permission.getUpdateTime())
|
||||
.children(new ArrayList<>())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.kurihada.auth.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.kurihada.auth.entity.User;
|
||||
import com.kurihada.auth.mapper.UserMapper;
|
||||
import com.kurihada.auth.service.AccountLockService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
/**
|
||||
* 账户锁定服务实现类
|
||||
* 功能:管理账户登录失败次数和锁定状态
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AccountLockServiceImpl implements AccountLockService {
|
||||
|
||||
private final UserMapper userMapper;
|
||||
|
||||
/**
|
||||
* 最大登录失败次数(默认5次)
|
||||
*/
|
||||
@Value("${account-lock.max-failed-attempts:5}")
|
||||
private int maxFailedAttempts;
|
||||
|
||||
/**
|
||||
* 账户锁定时长(分钟,默认30分钟)
|
||||
*/
|
||||
@Value("${account-lock.lock-duration-minutes:30}")
|
||||
private int lockDurationMinutes;
|
||||
|
||||
@Override
|
||||
public boolean isAccountLocked(User user) {
|
||||
if (user.getAccountLockedAt() == null || user.getAccountUnlockAt() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime unlockTime = user.getAccountUnlockAt();
|
||||
|
||||
// 如果当前时间超过解锁时间,自动解锁
|
||||
if (now.isAfter(unlockTime)) {
|
||||
log.info("账户锁定时间已过,自动解锁用户: {}", user.getUsername());
|
||||
unlockAccount(user);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void recordLoginFailure(User user) {
|
||||
int currentFailedCount = user.getLoginFailedCount() != null ? user.getLoginFailedCount() : 0;
|
||||
int newFailedCount = currentFailedCount + 1;
|
||||
|
||||
log.warn("用户 {} 登录失败,失败次数: {}/{}", user.getUsername(), newFailedCount, maxFailedAttempts);
|
||||
|
||||
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
|
||||
updateWrapper.eq(User::getId, user.getId());
|
||||
|
||||
if (newFailedCount >= maxFailedAttempts) {
|
||||
// 达到最大失败次数,锁定账户
|
||||
LocalDateTime lockedAt = LocalDateTime.now();
|
||||
LocalDateTime unlockAt = lockedAt.plusMinutes(lockDurationMinutes);
|
||||
|
||||
updateWrapper.set(User::getLoginFailedCount, newFailedCount)
|
||||
.set(User::getAccountLockedAt, lockedAt)
|
||||
.set(User::getAccountUnlockAt, unlockAt);
|
||||
|
||||
log.warn("用户 {} 登录失败次数达到 {} 次,账户已锁定,解锁时间: {}",
|
||||
user.getUsername(), maxFailedAttempts, unlockAt);
|
||||
} else {
|
||||
// 只增加失败次数
|
||||
updateWrapper.set(User::getLoginFailedCount, newFailedCount);
|
||||
}
|
||||
|
||||
userMapper.update(null, updateWrapper);
|
||||
|
||||
// 更新当前对象的状态
|
||||
user.setLoginFailedCount(newFailedCount);
|
||||
if (newFailedCount >= maxFailedAttempts) {
|
||||
user.setAccountLockedAt(LocalDateTime.now());
|
||||
user.setAccountUnlockAt(LocalDateTime.now().plusMinutes(lockDurationMinutes));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void resetLoginFailureCount(User user) {
|
||||
if (user.getLoginFailedCount() != null && user.getLoginFailedCount() > 0) {
|
||||
log.info("重置用户 {} 的登录失败次数", user.getUsername());
|
||||
|
||||
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
|
||||
updateWrapper.eq(User::getId, user.getId())
|
||||
.set(User::getLoginFailedCount, 0)
|
||||
.set(User::getAccountLockedAt, null)
|
||||
.set(User::getAccountUnlockAt, null);
|
||||
|
||||
userMapper.update(null, updateWrapper);
|
||||
|
||||
// 更新当前对象的状态
|
||||
user.setLoginFailedCount(0);
|
||||
user.setAccountLockedAt(null);
|
||||
user.setAccountUnlockAt(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void unlockAccount(User user) {
|
||||
log.info("解锁用户账户: {}", user.getUsername());
|
||||
|
||||
LambdaUpdateWrapper<User> updateWrapper = new LambdaUpdateWrapper<>();
|
||||
updateWrapper.eq(User::getId, user.getId())
|
||||
.set(User::getLoginFailedCount, 0)
|
||||
.set(User::getAccountLockedAt, null)
|
||||
.set(User::getAccountUnlockAt, null);
|
||||
|
||||
userMapper.update(null, updateWrapper);
|
||||
|
||||
// 更新当前对象的状态
|
||||
user.setLoginFailedCount(0);
|
||||
user.setAccountLockedAt(null);
|
||||
user.setAccountUnlockAt(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRemainingLockTime(User user) {
|
||||
if (!isAccountLocked(user)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime unlockTime = user.getAccountUnlockAt();
|
||||
|
||||
return ChronoUnit.SECONDS.between(now, unlockTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package com.kurihada.auth.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.kurihada.auth.dto.AuditLogQueryRequest;
|
||||
import com.kurihada.auth.dto.AuditLogResponse;
|
||||
import com.kurihada.auth.dto.PageResult;
|
||||
import com.kurihada.auth.entity.AuditLog;
|
||||
import com.kurihada.auth.exception.BusinessException;
|
||||
import com.kurihada.auth.mapper.AuditLogMapper;
|
||||
import com.kurihada.auth.service.AuditLogService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 审计日志服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AuditLogServiceImpl implements AuditLogService {
|
||||
|
||||
@Autowired
|
||||
private AuditLogMapper auditLogMapper;
|
||||
|
||||
@Override
|
||||
@Async // 异步记录日志,不影响主流程性能
|
||||
public void log(AuditLog auditLog) {
|
||||
try {
|
||||
auditLogMapper.insert(auditLog);
|
||||
log.debug("审计日志记录成功: {}", auditLog);
|
||||
} catch (Exception e) {
|
||||
// 记录日志失败不应该影响主业务流程
|
||||
log.error("审计日志记录失败: {}", auditLog, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logSuccess(String actionType, Long userId, String username, Long tenantId,
|
||||
String resourceType, String resourceId, String details,
|
||||
HttpServletRequest request) {
|
||||
AuditLog auditLog = buildAuditLog(actionType, userId, username, tenantId,
|
||||
resourceType, resourceId, details, "SUCCESS", null, request);
|
||||
log(auditLog);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logFailure(String actionType, Long userId, String username, Long tenantId,
|
||||
String resourceType, String resourceId, String details,
|
||||
String errorMessage, HttpServletRequest request) {
|
||||
AuditLog auditLog = buildAuditLog(actionType, userId, username, tenantId,
|
||||
resourceType, resourceId, details, "FAILURE", errorMessage, request);
|
||||
log(auditLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建审计日志对象
|
||||
*/
|
||||
private AuditLog buildAuditLog(String actionType, Long userId, String username,
|
||||
Long tenantId, String resourceType, String resourceId,
|
||||
String details, String result, String errorMessage,
|
||||
HttpServletRequest request) {
|
||||
return AuditLog.builder()
|
||||
.actionType(actionType)
|
||||
.userId(userId)
|
||||
.username(username)
|
||||
.tenantId(tenantId)
|
||||
.resourceType(resourceType)
|
||||
.resourceId(resourceId)
|
||||
.details(details)
|
||||
.result(result)
|
||||
.errorMessage(errorMessage)
|
||||
.ipAddress(getClientIp(request))
|
||||
.userAgent(request != null ? request.getHeader("User-Agent") : null)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端真实IP地址
|
||||
*/
|
||||
private String getClientIp(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.getHeader("HTTP_CLIENT_IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
|
||||
// 对于多级代理,取第一个IP
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<AuditLogResponse> queryAuditLogs(AuditLogQueryRequest request) {
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapper<AuditLog> queryWrapper = new LambdaQueryWrapper<>();
|
||||
|
||||
// 用户ID
|
||||
if (request.getUserId() != null) {
|
||||
queryWrapper.eq(AuditLog::getUserId, request.getUserId());
|
||||
}
|
||||
|
||||
// 用户名(模糊查询)
|
||||
if (StringUtils.isNotBlank(request.getUsername())) {
|
||||
queryWrapper.like(AuditLog::getUsername, request.getUsername());
|
||||
}
|
||||
|
||||
// 操作类型
|
||||
if (StringUtils.isNotBlank(request.getActionType())) {
|
||||
queryWrapper.eq(AuditLog::getActionType, request.getActionType());
|
||||
}
|
||||
|
||||
// 资源类型
|
||||
if (StringUtils.isNotBlank(request.getResourceType())) {
|
||||
queryWrapper.eq(AuditLog::getResourceType, request.getResourceType());
|
||||
}
|
||||
|
||||
// 资源ID
|
||||
if (StringUtils.isNotBlank(request.getResourceId())) {
|
||||
queryWrapper.eq(AuditLog::getResourceId, request.getResourceId());
|
||||
}
|
||||
|
||||
// 操作结果
|
||||
if (StringUtils.isNotBlank(request.getResult())) {
|
||||
queryWrapper.eq(AuditLog::getResult, request.getResult());
|
||||
}
|
||||
|
||||
// 租户ID
|
||||
if (request.getTenantId() != null) {
|
||||
queryWrapper.eq(AuditLog::getTenantId, request.getTenantId());
|
||||
}
|
||||
|
||||
// IP地址
|
||||
if (StringUtils.isNotBlank(request.getIpAddress())) {
|
||||
queryWrapper.eq(AuditLog::getIpAddress, request.getIpAddress());
|
||||
}
|
||||
|
||||
// 时间范围查询
|
||||
if (request.getStartDate() != null) {
|
||||
queryWrapper.ge(AuditLog::getCreateTime, request.getStartDate());
|
||||
}
|
||||
if (request.getEndDate() != null) {
|
||||
queryWrapper.le(AuditLog::getCreateTime, request.getEndDate());
|
||||
}
|
||||
|
||||
// 关键词搜索(在详情中搜索)
|
||||
if (StringUtils.isNotBlank(request.getKeyword())) {
|
||||
queryWrapper.like(AuditLog::getDetails, request.getKeyword());
|
||||
}
|
||||
|
||||
// 排序(默认按创建时间降序)
|
||||
if (StringUtils.isNotBlank(request.getSortBy())) {
|
||||
if ("desc".equalsIgnoreCase(request.getSortDirection())) {
|
||||
queryWrapper.orderByDesc(getColumnByField(request.getSortBy()));
|
||||
} else {
|
||||
queryWrapper.orderByAsc(getColumnByField(request.getSortBy()));
|
||||
}
|
||||
} else {
|
||||
queryWrapper.orderByDesc(AuditLog::getCreateTime);
|
||||
}
|
||||
|
||||
// 查询总数
|
||||
Long total = auditLogMapper.selectCount(queryWrapper);
|
||||
|
||||
// 分页查询
|
||||
queryWrapper.last("LIMIT " + request.getLimit() + " OFFSET " + request.getOffset());
|
||||
List<AuditLog> auditLogs = auditLogMapper.selectList(queryWrapper);
|
||||
|
||||
// 转换为DTO
|
||||
List<AuditLogResponse> responses = auditLogs.stream()
|
||||
.map(this::convertToResponse)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResult.of(responses, total, request.getPage(), request.getSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuditLogResponse getAuditLogById(Long id) {
|
||||
AuditLog auditLog = auditLogMapper.selectById(id);
|
||||
if (auditLog == null) {
|
||||
throw new BusinessException("审计日志不存在");
|
||||
}
|
||||
return convertToResponse(auditLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 AuditLog 实体转换为 AuditLogResponse DTO
|
||||
*/
|
||||
private AuditLogResponse convertToResponse(AuditLog auditLog) {
|
||||
AuditLogResponse response = new AuditLogResponse();
|
||||
BeanUtils.copyProperties(auditLog, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字段名获取排序列
|
||||
*/
|
||||
private com.baomidou.mybatisplus.core.toolkit.support.SFunction<AuditLog, ?> getColumnByField(String field) {
|
||||
return switch (field) {
|
||||
case "actionType" -> AuditLog::getActionType;
|
||||
case "userId" -> AuditLog::getUserId;
|
||||
case "username" -> AuditLog::getUsername;
|
||||
case "result" -> AuditLog::getResult;
|
||||
case "createTime" -> AuditLog::getCreateTime;
|
||||
default -> AuditLog::getCreateTime;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package com.kurihada.auth.service.impl;
|
||||
|
||||
import com.kurihada.auth.constant.CacheConstants;
|
||||
import com.kurihada.auth.dto.UserPermissionCache;
|
||||
import com.kurihada.auth.entity.Permission;
|
||||
import com.kurihada.auth.entity.Role;
|
||||
import com.kurihada.auth.entity.User;
|
||||
import com.kurihada.auth.mapper.UserMapper;
|
||||
import com.kurihada.auth.mapper.UserRoleMapper;
|
||||
import com.kurihada.auth.service.PermissionCacheService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 权限缓存服务实现
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PermissionCacheServiceImpl implements PermissionCacheService {
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final UserMapper userMapper;
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
|
||||
@Override
|
||||
public UserPermissionCache getUserPermissions(Long userId, Long tenantId) {
|
||||
String cacheKey = CacheConstants.buildUserPermissionKey(userId, tenantId);
|
||||
|
||||
// 尝试从缓存获取
|
||||
UserPermissionCache cache = (UserPermissionCache) redisTemplate.opsForValue().get(cacheKey);
|
||||
|
||||
if (cache != null) {
|
||||
log.debug("从缓存获取用户权限: userId={}, tenantId={}", userId, tenantId);
|
||||
return cache;
|
||||
}
|
||||
|
||||
log.debug("缓存未命中,从数据库加载用户权限: userId={}, tenantId={}", userId, tenantId);
|
||||
|
||||
// 缓存未命中,从数据库加载
|
||||
return loadAndCacheUserPermissions(userId, tenantId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cacheUserPermissions(Long userId, Long tenantId, User user) {
|
||||
if (user == null) {
|
||||
log.warn("用户对象为空,无法缓存权限: userId={}, tenantId={}", userId, tenantId);
|
||||
return;
|
||||
}
|
||||
|
||||
UserPermissionCache cache = buildUserPermissionCache(user, tenantId);
|
||||
String cacheKey = CacheConstants.buildUserPermissionKey(userId, tenantId);
|
||||
|
||||
redisTemplate.opsForValue().set(
|
||||
cacheKey,
|
||||
cache,
|
||||
CacheConstants.PERMISSION_CACHE_TTL,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
|
||||
// 将用户ID添加到租户用户集合中(用于批量删除)
|
||||
String tenantUsersKey = CacheConstants.buildTenantUsersKey(tenantId);
|
||||
redisTemplate.opsForSet().add(tenantUsersKey, userId);
|
||||
redisTemplate.expire(tenantUsersKey, CacheConstants.PERMISSION_CACHE_TTL, TimeUnit.SECONDS);
|
||||
|
||||
log.info("已缓存用户权限: userId={}, tenantId={}, roles={}, permissions={}",
|
||||
userId, tenantId, cache.getRoleCodes().size(), cache.getPermissionCodes().size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evictUserPermissions(Long userId, Long tenantId) {
|
||||
String cacheKey = CacheConstants.buildUserPermissionKey(userId, tenantId);
|
||||
Boolean deleted = redisTemplate.delete(cacheKey);
|
||||
|
||||
if (Boolean.TRUE.equals(deleted)) {
|
||||
log.info("已删除用户权限缓存: userId={}, tenantId={}", userId, tenantId);
|
||||
}
|
||||
|
||||
// 从租户用户集合中移除
|
||||
String tenantUsersKey = CacheConstants.buildTenantUsersKey(tenantId);
|
||||
redisTemplate.opsForSet().remove(tenantUsersKey, userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evictTenantPermissions(Long tenantId) {
|
||||
String tenantUsersKey = CacheConstants.buildTenantUsersKey(tenantId);
|
||||
|
||||
// 获取租户下所有用户ID
|
||||
Set<Object> userIds = redisTemplate.opsForSet().members(tenantUsersKey);
|
||||
|
||||
if (CollectionUtils.isEmpty(userIds)) {
|
||||
log.debug("租户下没有缓存的用户: tenantId={}", tenantId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 批量删除用户权限缓存
|
||||
int count = 0;
|
||||
for (Object userId : userIds) {
|
||||
if (userId instanceof Long) {
|
||||
String cacheKey = CacheConstants.buildUserPermissionKey((Long) userId, tenantId);
|
||||
Boolean deleted = redisTemplate.delete(cacheKey);
|
||||
if (Boolean.TRUE.equals(deleted)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除租户用户集合
|
||||
redisTemplate.delete(tenantUsersKey);
|
||||
|
||||
log.info("已删除租户下所有用户权限缓存: tenantId={}, count={}", tenantId, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void evictRoleRelatedPermissions(Long roleId, Long tenantId) {
|
||||
// 获取拥有该角色的所有用户ID(直接使用 Mapper 避免循环依赖)
|
||||
List<Long> userIds = userRoleMapper.selectUserIdsByRoleId(roleId);
|
||||
|
||||
if (CollectionUtils.isEmpty(userIds)) {
|
||||
log.debug("没有用户拥有该角色: roleId={}, tenantId={}", roleId, tenantId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 批量删除相关用户的权限缓存
|
||||
int count = 0;
|
||||
for (Long userId : userIds) {
|
||||
evictUserPermissions(userId, tenantId);
|
||||
count++;
|
||||
}
|
||||
|
||||
log.info("已删除角色关联的用户权限缓存: roleId={}, tenantId={}, userCount={}",
|
||||
roleId, tenantId, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(Long userId, Long tenantId, String permissionCode) {
|
||||
UserPermissionCache cache = getUserPermissions(userId, tenantId);
|
||||
return cache != null && cache.hasPermission(permissionCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRole(Long userId, Long tenantId, String roleCode) {
|
||||
UserPermissionCache cache = getUserPermissions(userId, tenantId);
|
||||
return cache != null && cache.hasRole(roleCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasResource(Long userId, Long tenantId, String resource) {
|
||||
UserPermissionCache cache = getUserPermissions(userId, tenantId);
|
||||
return cache != null && cache.hasResource(resource);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warmUpUserPermissions(Long userId, Long tenantId) {
|
||||
log.info("预热用户权限缓存: userId={}, tenantId={}", userId, tenantId);
|
||||
loadAndCacheUserPermissions(userId, tenantId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAllPermissionCache() {
|
||||
Set<String> keys = redisTemplate.keys(CacheConstants.USER_PERMISSIONS_KEY + "*");
|
||||
|
||||
if (!CollectionUtils.isEmpty(keys)) {
|
||||
Long deletedCount = redisTemplate.delete(keys);
|
||||
log.info("已清空所有权限缓存: count={}", deletedCount);
|
||||
}
|
||||
|
||||
// 清空租户用户集合
|
||||
Set<String> tenantKeys = redisTemplate.keys(CacheConstants.TENANT_USERS_KEY + "*");
|
||||
if (!CollectionUtils.isEmpty(tenantKeys)) {
|
||||
redisTemplate.delete(tenantKeys);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库加载并缓存用户权限
|
||||
*/
|
||||
private UserPermissionCache loadAndCacheUserPermissions(Long userId, Long tenantId) {
|
||||
// 从数据库查询用户及其角色、权限
|
||||
User user = userMapper.selectUserWithRolesAndPermissionsById(userId);
|
||||
|
||||
if (user == null) {
|
||||
log.warn("用户不存在: userId={}", userId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证租户
|
||||
if (!user.getTenantId().equals(tenantId)) {
|
||||
log.warn("用户租户不匹配: userId={}, userTenantId={}, requestTenantId={}",
|
||||
userId, user.getTenantId(), tenantId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建并缓存权限信息
|
||||
UserPermissionCache cache = buildUserPermissionCache(user, tenantId);
|
||||
String cacheKey = CacheConstants.buildUserPermissionKey(userId, tenantId);
|
||||
|
||||
redisTemplate.opsForValue().set(
|
||||
cacheKey,
|
||||
cache,
|
||||
CacheConstants.PERMISSION_CACHE_TTL,
|
||||
TimeUnit.SECONDS
|
||||
);
|
||||
|
||||
// 将用户ID添加到租户用户集合
|
||||
String tenantUsersKey = CacheConstants.buildTenantUsersKey(tenantId);
|
||||
redisTemplate.opsForSet().add(tenantUsersKey, userId);
|
||||
redisTemplate.expire(tenantUsersKey, CacheConstants.PERMISSION_CACHE_TTL, TimeUnit.SECONDS);
|
||||
|
||||
log.info("已从数据库加载并缓存用户权限: userId={}, tenantId={}", userId, tenantId);
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用户权限缓存对象
|
||||
*/
|
||||
private UserPermissionCache buildUserPermissionCache(User user, Long tenantId) {
|
||||
UserPermissionCache cache = new UserPermissionCache();
|
||||
cache.setUserId(user.getId());
|
||||
cache.setTenantId(tenantId);
|
||||
cache.setUsername(user.getUsername());
|
||||
cache.setCacheTime(System.currentTimeMillis());
|
||||
|
||||
// 初始化空集合,避免 null
|
||||
List<String> roleCodes = new java.util.ArrayList<>();
|
||||
List<String> roleNames = new java.util.ArrayList<>();
|
||||
Set<String> permissionCodes = new HashSet<>();
|
||||
Set<String> resources = new HashSet<>();
|
||||
|
||||
List<Role> roles = user.getRoles();
|
||||
if (!CollectionUtils.isEmpty(roles)) {
|
||||
// 提取角色代码和名称
|
||||
roleCodes = roles.stream()
|
||||
.map(Role::getCode)
|
||||
.collect(Collectors.toList());
|
||||
roleNames = roles.stream()
|
||||
.map(Role::getName)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 提取所有权限
|
||||
for (Role role : roles) {
|
||||
List<Permission> permissions = role.getPermissions();
|
||||
if (!CollectionUtils.isEmpty(permissions)) {
|
||||
permissionCodes.addAll(permissions.stream()
|
||||
.map(Permission::getCode)
|
||||
.collect(Collectors.toSet()));
|
||||
|
||||
// 提取资源路径
|
||||
resources.addAll(permissions.stream()
|
||||
.map(Permission::getResource)
|
||||
.filter(resource -> resource != null && !resource.isEmpty())
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置集合(即使为空也设置,避免返回 null)
|
||||
cache.setRoleCodes(roleCodes);
|
||||
cache.setRoleNames(roleNames);
|
||||
cache.setPermissionCodes(permissionCodes);
|
||||
cache.setResources(resources);
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.springframework.security.core.userdetails.UserDetails buildUserDetailsFromCache(Long userId, Long tenantId, String username) {
|
||||
// 从缓存获取用户权限
|
||||
UserPermissionCache cache = getUserPermissions(userId, tenantId);
|
||||
|
||||
if (cache == null) {
|
||||
log.debug("缓存未命中,无法构建 UserDetails: userId={}, tenantId={}", userId, tenantId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建权限列表(角色 + 权限)
|
||||
Set<org.springframework.security.core.GrantedAuthority> authorities = new HashSet<>();
|
||||
|
||||
// 添加角色权限(ROLE_ 前缀)
|
||||
if (cache.getRoleCodes() != null) {
|
||||
cache.getRoleCodes().forEach(roleCode ->
|
||||
authorities.add(new org.springframework.security.core.authority.SimpleGrantedAuthority("ROLE_" + roleCode))
|
||||
);
|
||||
}
|
||||
|
||||
// 添加资源权限
|
||||
if (cache.getPermissionCodes() != null) {
|
||||
cache.getPermissionCodes().forEach(permissionCode ->
|
||||
authorities.add(new org.springframework.security.core.authority.SimpleGrantedAuthority(permissionCode))
|
||||
);
|
||||
}
|
||||
|
||||
// 构建 UserDetails 对象(使用缓存中的数据,不需要密码)
|
||||
return org.springframework.security.core.userdetails.User.builder()
|
||||
.username(username)
|
||||
.password("") // JWT 认证不需要密码验证
|
||||
.authorities(authorities)
|
||||
.accountExpired(false)
|
||||
.accountLocked(false)
|
||||
.credentialsExpired(false)
|
||||
.disabled(false)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
package com.kurihada.auth.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.kurihada.auth.context.TenantContext;
|
||||
import com.kurihada.auth.dto.CreatePermissionRequest;
|
||||
import com.kurihada.auth.dto.PageRequest;
|
||||
import com.kurihada.auth.dto.PageResult;
|
||||
import com.kurihada.auth.dto.PermissionDTO;
|
||||
import com.kurihada.auth.dto.PermissionTreeNode;
|
||||
import com.kurihada.auth.dto.UpdatePermissionRequest;
|
||||
import com.kurihada.auth.entity.AuditLog;
|
||||
import com.kurihada.auth.entity.Permission;
|
||||
import com.kurihada.auth.entity.RolePermission;
|
||||
import com.kurihada.auth.entity.User;
|
||||
import com.kurihada.auth.exception.BusinessException;
|
||||
import com.kurihada.auth.mapper.PermissionMapper;
|
||||
import com.kurihada.auth.mapper.RolePermissionMapper;
|
||||
import com.kurihada.auth.mapper.UserMapper;
|
||||
import com.kurihada.auth.service.AuditLogService;
|
||||
import com.kurihada.auth.service.PermissionCacheService;
|
||||
import com.kurihada.auth.service.PermissionService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 权限服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class PermissionServiceImpl implements PermissionService {
|
||||
|
||||
private final PermissionMapper permissionMapper;
|
||||
private final RolePermissionMapper rolePermissionMapper;
|
||||
private final UserMapper userMapper;
|
||||
private final PermissionCacheService permissionCacheService;
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
// 系统内置权限,不允许删除
|
||||
private static final Set<String> SYSTEM_PERMISSIONS = new HashSet<>(Arrays.asList(
|
||||
"system:manage", "user:manage", "role:manage", "permission:manage"
|
||||
));
|
||||
|
||||
@Override
|
||||
public PageResult<PermissionDTO> getPermissionsPage(PageRequest pageRequest) {
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapper<Permission> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.and(wrapper -> wrapper
|
||||
.isNull(Permission::getTenantId)
|
||||
.or()
|
||||
.eq(tenantId != null, Permission::getTenantId, tenantId)
|
||||
);
|
||||
|
||||
// 搜索条件
|
||||
if (StringUtils.hasText(pageRequest.getKeyword())) {
|
||||
queryWrapper.and(wrapper -> wrapper
|
||||
.like(Permission::getName, pageRequest.getKeyword())
|
||||
.or()
|
||||
.like(Permission::getCode, pageRequest.getKeyword())
|
||||
.or()
|
||||
.like(Permission::getDescription, pageRequest.getKeyword())
|
||||
.or()
|
||||
.like(Permission::getResource, pageRequest.getKeyword())
|
||||
);
|
||||
}
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(pageRequest.getSortBy())) {
|
||||
boolean isAsc = "asc".equalsIgnoreCase(pageRequest.getSortDirection());
|
||||
switch (pageRequest.getSortBy()) {
|
||||
case "name" -> queryWrapper.orderBy(true, isAsc, Permission::getName);
|
||||
case "code" -> queryWrapper.orderBy(true, isAsc, Permission::getCode);
|
||||
case "type" -> queryWrapper.orderBy(true, isAsc, Permission::getType);
|
||||
case "sort" -> queryWrapper.orderBy(true, isAsc, Permission::getSort);
|
||||
case "status" -> queryWrapper.orderBy(true, isAsc, Permission::getStatus);
|
||||
case "createTime" -> queryWrapper.orderBy(true, isAsc, Permission::getCreateTime);
|
||||
default -> queryWrapper.orderByAsc(Permission::getSort).orderByDesc(Permission::getCreateTime);
|
||||
}
|
||||
} else {
|
||||
queryWrapper.orderByAsc(Permission::getSort).orderByDesc(Permission::getCreateTime);
|
||||
}
|
||||
|
||||
// 查询总数
|
||||
Long total = permissionMapper.selectCount(queryWrapper);
|
||||
|
||||
// 分页查询 - 使用 MyBatis Plus 的安全分页方法
|
||||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<Permission> page =
|
||||
new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(
|
||||
pageRequest.getPage(),
|
||||
pageRequest.getSize()
|
||||
);
|
||||
page.setSearchCount(false); // 已经查询过总数,避免重复查询
|
||||
|
||||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<Permission> result =
|
||||
permissionMapper.selectPage(page, queryWrapper);
|
||||
List<Permission> permissions = result.getRecords();
|
||||
|
||||
List<PermissionDTO> permissionDTOs = permissions.stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResult.of(permissionDTOs, total, pageRequest.getPage(), pageRequest.getSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PermissionDTO getPermissionById(Long id) {
|
||||
Permission permission = permissionMapper.selectById(id);
|
||||
if (permission == null) {
|
||||
throw new BusinessException("权限不存在");
|
||||
}
|
||||
|
||||
// 验证租户权限
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
if (permission.getTenantId() != null && tenantId != null && !tenantId.equals(permission.getTenantId())) {
|
||||
throw new BusinessException("无权访问该权限");
|
||||
}
|
||||
|
||||
return convertToDTO(permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PermissionDTO createPermission(CreatePermissionRequest request) {
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
User currentUser = getCurrentUser();
|
||||
|
||||
// 验证权限代码格式(小写字母、数字、冒号、下划线)
|
||||
if (!request.getCode().matches("^[a-z0-9:_]+$")) {
|
||||
auditLogPermission("CREATE_PERMISSION", null, "FAILURE", "权限代码格式不正确", request, currentUser, tenantId);
|
||||
throw new BusinessException("权限代码只能包含小写字母、数字、冒号和下划线");
|
||||
}
|
||||
|
||||
// 检查权限代码是否已存在
|
||||
Long count = permissionMapper.selectCount(new LambdaQueryWrapper<Permission>()
|
||||
.eq(Permission::getCode, request.getCode())
|
||||
.and(wrapper -> wrapper
|
||||
.isNull(Permission::getTenantId)
|
||||
.or()
|
||||
.eq(tenantId != null, Permission::getTenantId, tenantId)
|
||||
));
|
||||
|
||||
if (count > 0) {
|
||||
auditLogPermission("CREATE_PERMISSION", null, "FAILURE", "权限代码已存在", request, currentUser, tenantId);
|
||||
throw new BusinessException("权限代码已存在");
|
||||
}
|
||||
|
||||
// 创建权限
|
||||
Permission permission = new Permission();
|
||||
permission.setName(request.getName());
|
||||
permission.setCode(request.getCode());
|
||||
permission.setDescription(request.getDescription());
|
||||
permission.setType(request.getType());
|
||||
permission.setResource(request.getResource());
|
||||
permission.setParentId(request.getParentId());
|
||||
permission.setSort(request.getSort());
|
||||
permission.setTenantId(tenantId);
|
||||
permission.setStatus(1);
|
||||
|
||||
permissionMapper.insert(permission);
|
||||
log.info("权限创建成功: {}, ID: {}", permission.getName(), permission.getId());
|
||||
|
||||
// 记录审计日志
|
||||
auditLogPermission("CREATE_PERMISSION", permission.getId(), "SUCCESS", null, permission, currentUser, tenantId);
|
||||
|
||||
return convertToDTO(permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PermissionDTO updatePermission(Long id, UpdatePermissionRequest request) {
|
||||
Permission permission = permissionMapper.selectById(id);
|
||||
if (permission == null) {
|
||||
throw new BusinessException("权限不存在");
|
||||
}
|
||||
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
User currentUser = getCurrentUser();
|
||||
|
||||
// 验证租户权限(公共权限不能修改)
|
||||
if (permission.getTenantId() == null) {
|
||||
auditLogPermission("UPDATE_PERMISSION", id, "FAILURE", "不能修改系统公共权限", request, currentUser, tenantId);
|
||||
throw new BusinessException("不能修改系统公共权限");
|
||||
}
|
||||
|
||||
if (tenantId != null && !tenantId.equals(permission.getTenantId())) {
|
||||
auditLogPermission("UPDATE_PERMISSION", id, "FAILURE", "无权修改该权限", request, currentUser, tenantId);
|
||||
throw new BusinessException("无权修改该权限");
|
||||
}
|
||||
|
||||
String oldName = permission.getName();
|
||||
String oldDescription = permission.getDescription();
|
||||
|
||||
// 更新权限信息
|
||||
permission.setName(request.getName());
|
||||
permission.setDescription(request.getDescription());
|
||||
permission.setType(request.getType());
|
||||
permission.setResource(request.getResource());
|
||||
permission.setParentId(request.getParentId());
|
||||
if (request.getSort() != null) {
|
||||
permission.setSort(request.getSort());
|
||||
}
|
||||
|
||||
permissionMapper.updateById(permission);
|
||||
log.info("权限更新成功: {}, ID: {}", permission.getName(), permission.getId());
|
||||
|
||||
// 清理相关权限缓存
|
||||
permissionCacheService.clearAllPermissionCache();
|
||||
|
||||
// 记录审计日志
|
||||
String details = String.format("更新前: name=%s, description=%s; 更新后: name=%s, description=%s",
|
||||
oldName, oldDescription, permission.getName(), permission.getDescription());
|
||||
auditLogPermission("UPDATE_PERMISSION", id, "SUCCESS", details, permission, currentUser, tenantId);
|
||||
|
||||
return convertToDTO(permission);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deletePermission(Long id) {
|
||||
Permission permission = permissionMapper.selectById(id);
|
||||
if (permission == null) {
|
||||
throw new BusinessException("权限不存在");
|
||||
}
|
||||
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
User currentUser = getCurrentUser();
|
||||
|
||||
// 验证租户权限(公共权限不能删除)
|
||||
if (permission.getTenantId() == null) {
|
||||
auditLogPermission("DELETE_PERMISSION", id, "FAILURE", "不能删除系统公共权限", permission, currentUser, tenantId);
|
||||
throw new BusinessException("不能删除系统公共权限");
|
||||
}
|
||||
|
||||
if (tenantId != null && !tenantId.equals(permission.getTenantId())) {
|
||||
auditLogPermission("DELETE_PERMISSION", id, "FAILURE", "无权删除该权限", permission, currentUser, tenantId);
|
||||
throw new BusinessException("无权删除该权限");
|
||||
}
|
||||
|
||||
// 防止删除系统内置权限
|
||||
if (SYSTEM_PERMISSIONS.contains(permission.getCode())) {
|
||||
auditLogPermission("DELETE_PERMISSION", id, "FAILURE", "不能删除系统内置权限", permission, currentUser, tenantId);
|
||||
throw new BusinessException("不能删除系统内置权限: " + permission.getCode());
|
||||
}
|
||||
|
||||
// 检查是否有角色正在使用该权限
|
||||
Long roleCount = rolePermissionMapper.selectCount(new LambdaQueryWrapper<RolePermission>()
|
||||
.eq(RolePermission::getPermissionId, id));
|
||||
if (roleCount > 0) {
|
||||
String errorMsg = String.format("该权限正被 %d 个角色使用,无法删除", roleCount);
|
||||
auditLogPermission("DELETE_PERMISSION", id, "FAILURE", errorMsg, permission, currentUser, tenantId);
|
||||
throw new BusinessException(errorMsg);
|
||||
}
|
||||
|
||||
// 删除权限
|
||||
permissionMapper.deleteById(id);
|
||||
log.info("权限删除成功: {}, ID: {}", permission.getName(), permission.getId());
|
||||
|
||||
// 清理权限缓存
|
||||
permissionCacheService.clearAllPermissionCache();
|
||||
|
||||
// 记录审计日志
|
||||
auditLogPermission("DELETE_PERMISSION", id, "SUCCESS", null, permission, currentUser, tenantId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void togglePermissionStatus(Long id) {
|
||||
Permission permission = permissionMapper.selectById(id);
|
||||
if (permission == null) {
|
||||
throw new BusinessException("权限不存在");
|
||||
}
|
||||
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
User currentUser = getCurrentUser();
|
||||
|
||||
// 验证租户权限(公共权限不能修改状态)
|
||||
if (permission.getTenantId() == null) {
|
||||
auditLogPermission("TOGGLE_PERMISSION_STATUS", id, "FAILURE", "不能修改系统公共权限状态", permission, currentUser, tenantId);
|
||||
throw new BusinessException("不能修改系统公共权限状态");
|
||||
}
|
||||
|
||||
if (tenantId != null && !tenantId.equals(permission.getTenantId())) {
|
||||
auditLogPermission("TOGGLE_PERMISSION_STATUS", id, "FAILURE", "无权修改该权限", permission, currentUser, tenantId);
|
||||
throw new BusinessException("无权修改该权限");
|
||||
}
|
||||
|
||||
// 防止禁用系统内置权限
|
||||
if (SYSTEM_PERMISSIONS.contains(permission.getCode()) && permission.getStatus() == 1) {
|
||||
auditLogPermission("TOGGLE_PERMISSION_STATUS", id, "FAILURE", "不能禁用系统内置权限", permission, currentUser, tenantId);
|
||||
throw new BusinessException("不能禁用系统内置权限: " + permission.getCode());
|
||||
}
|
||||
|
||||
int oldStatus = permission.getStatus();
|
||||
int newStatus = oldStatus == 1 ? 0 : 1;
|
||||
permission.setStatus(newStatus);
|
||||
permissionMapper.updateById(permission);
|
||||
log.info("权限状态已更新: {}, 新状态: {}", permission.getName(), permission.getStatus());
|
||||
|
||||
// 清理权限缓存
|
||||
permissionCacheService.clearAllPermissionCache();
|
||||
|
||||
// 记录审计日志
|
||||
String details = String.format("状态从 %s 变更为 %s",
|
||||
oldStatus == 1 ? "启用" : "禁用",
|
||||
newStatus == 1 ? "启用" : "禁用");
|
||||
auditLogPermission("TOGGLE_PERMISSION_STATUS", id, "SUCCESS", details, permission, currentUser, tenantId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PermissionTreeNode> getPermissionTree() {
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
|
||||
// 查询所有权限(公共权限 + 当前租户权限)
|
||||
List<Permission> allPermissions = permissionMapper.selectList(
|
||||
new LambdaQueryWrapper<Permission>()
|
||||
.and(wrapper -> wrapper
|
||||
.isNull(Permission::getTenantId)
|
||||
.or()
|
||||
.eq(tenantId != null, Permission::getTenantId, tenantId)
|
||||
)
|
||||
.eq(Permission::getStatus, 1) // 只查询启用的权限
|
||||
.orderByAsc(Permission::getSort)
|
||||
.orderByDesc(Permission::getCreateTime)
|
||||
);
|
||||
|
||||
// 将所有权限转换为树节点,并建立 ID -> 节点的映射
|
||||
Map<Long, PermissionTreeNode> nodeMap = new HashMap<>();
|
||||
for (Permission permission : allPermissions) {
|
||||
PermissionTreeNode node = convertToTreeNode(permission);
|
||||
nodeMap.put(permission.getId(), node);
|
||||
}
|
||||
|
||||
// 构建树形结构
|
||||
List<PermissionTreeNode> rootNodes = new ArrayList<>();
|
||||
for (PermissionTreeNode node : nodeMap.values()) {
|
||||
if (node.getParentId() == null || node.getParentId() == 0) {
|
||||
// 根节点
|
||||
rootNodes.add(node);
|
||||
} else {
|
||||
// 子节点,添加到父节点的 children 中
|
||||
PermissionTreeNode parent = nodeMap.get(node.getParentId());
|
||||
if (parent != null) {
|
||||
parent.getChildren().add(node);
|
||||
} else {
|
||||
// 如果找不到父节点,作为根节点处理
|
||||
rootNodes.add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rootNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为DTO
|
||||
*/
|
||||
private PermissionDTO convertToDTO(Permission permission) {
|
||||
return PermissionDTO.builder()
|
||||
.id(permission.getId())
|
||||
.name(permission.getName())
|
||||
.code(permission.getCode())
|
||||
.tenantId(permission.getTenantId())
|
||||
.description(permission.getDescription())
|
||||
.type(permission.getType())
|
||||
.resource(permission.getResource())
|
||||
.parentId(permission.getParentId())
|
||||
.sort(permission.getSort())
|
||||
.status(permission.getStatus())
|
||||
.createTime(permission.getCreateTime())
|
||||
.updateTime(permission.getUpdateTime())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为树节点
|
||||
*/
|
||||
private PermissionTreeNode convertToTreeNode(Permission permission) {
|
||||
return PermissionTreeNode.builder()
|
||||
.id(permission.getId())
|
||||
.name(permission.getName())
|
||||
.code(permission.getCode())
|
||||
.tenantId(permission.getTenantId())
|
||||
.type(permission.getType())
|
||||
.resource(permission.getResource())
|
||||
.parentId(permission.getParentId())
|
||||
.sort(permission.getSort())
|
||||
.status(permission.getStatus())
|
||||
.createTime(permission.getCreateTime())
|
||||
.updateTime(permission.getUpdateTime())
|
||||
.children(new ArrayList<>())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户
|
||||
*/
|
||||
private User getCurrentUser() {
|
||||
try {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
String username = authentication.getName();
|
||||
if (!"anonymousUser".equals(username)) {
|
||||
return userMapper.selectUserWithRolesAndPermissions(username);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取当前用户信息失败: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP地址
|
||||
*/
|
||||
private String getClientIp() {
|
||||
try {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取客户端IP失败: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取User Agent
|
||||
*/
|
||||
private String getUserAgent() {
|
||||
try {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
return attributes.getRequest().getHeader("User-Agent");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取User-Agent失败: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录权限操作审计日志
|
||||
*/
|
||||
private void auditLogPermission(String actionType, Long permissionId, String result,
|
||||
String errorMessage, Object details, User currentUser, Long tenantId) {
|
||||
try {
|
||||
AuditLog auditLog = AuditLog.builder()
|
||||
.actionType(actionType)
|
||||
.userId(currentUser != null ? currentUser.getId() : null)
|
||||
.username(currentUser != null ? currentUser.getUsername() : "system")
|
||||
.tenantId(tenantId != null ? tenantId : (currentUser != null ? currentUser.getTenantId() : null))
|
||||
.resourceType("PERMISSION")
|
||||
.resourceId(permissionId != null ? String.valueOf(permissionId) : null)
|
||||
.details(details != null ? details.toString() : null)
|
||||
.result(result)
|
||||
.errorMessage(errorMessage)
|
||||
.ipAddress(getClientIp())
|
||||
.userAgent(getUserAgent())
|
||||
.build();
|
||||
|
||||
auditLogService.log(auditLog);
|
||||
} catch (Exception e) {
|
||||
log.error("记录审计日志失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package com.kurihada.auth.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kurihada.auth.entity.Permission;
|
||||
import com.kurihada.auth.entity.Role;
|
||||
import com.kurihada.auth.entity.RolePermission;
|
||||
import com.kurihada.auth.exception.BusinessException;
|
||||
import com.kurihada.auth.mapper.PermissionMapper;
|
||||
import com.kurihada.auth.mapper.RoleMapper;
|
||||
import com.kurihada.auth.mapper.RolePermissionMapper;
|
||||
import com.kurihada.auth.service.RolePermissionService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 角色权限关联服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RolePermissionServiceImpl extends ServiceImpl<RolePermissionMapper, RolePermission> implements RolePermissionService {
|
||||
|
||||
private final RolePermissionMapper rolePermissionMapper;
|
||||
private final RoleMapper roleMapper;
|
||||
private final PermissionMapper permissionMapper;
|
||||
private final com.kurihada.auth.service.PermissionCacheService permissionCacheService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean assignPermissionToRole(Long roleId, Long permissionId) {
|
||||
// 验证角色是否存在
|
||||
Role role = roleMapper.selectById(roleId);
|
||||
if (role == null) {
|
||||
throw new BusinessException("角色不存在");
|
||||
}
|
||||
|
||||
// 验证权限是否存在
|
||||
Permission permission = permissionMapper.selectById(permissionId);
|
||||
if (permission == null) {
|
||||
throw new BusinessException("权限不存在");
|
||||
}
|
||||
|
||||
// 验证租户一致性(权限的 tenantId 可能为 null,表示公共权限)
|
||||
if (permission.getTenantId() != null && !permission.getTenantId().equals(role.getTenantId())) {
|
||||
throw new BusinessException("权限和角色不属于同一租户,无法分配");
|
||||
}
|
||||
|
||||
// 检查是否已经存在该关联
|
||||
if (hasPermission(roleId, permissionId)) {
|
||||
log.warn("角色ID: {} 已经拥有权限ID: {}", roleId, permissionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 创建角色权限关联,包含租户ID
|
||||
RolePermission rolePermission = new RolePermission();
|
||||
rolePermission.setRoleId(roleId);
|
||||
rolePermission.setPermissionId(permissionId);
|
||||
rolePermission.setTenantId(role.getTenantId());
|
||||
|
||||
int result = rolePermissionMapper.insert(rolePermission);
|
||||
if (result > 0) {
|
||||
log.info("成功为角色ID: {} 分配权限ID: {},租户ID: {}", roleId, permissionId, role.getTenantId());
|
||||
|
||||
// 清除相关用户的权限缓存
|
||||
permissionCacheService.evictRoleRelatedPermissions(roleId, role.getTenantId());
|
||||
log.debug("已清除角色 {} 关联用户的权限缓存", roleId);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean assignPermissionsToRole(Long roleId, List<Long> permissionIds) {
|
||||
// 验证角色是否存在
|
||||
Role role = roleMapper.selectById(roleId);
|
||||
if (role == null) {
|
||||
throw new BusinessException("角色不存在");
|
||||
}
|
||||
|
||||
if (permissionIds == null || permissionIds.isEmpty()) {
|
||||
throw new BusinessException("权限ID列表不能为空");
|
||||
}
|
||||
|
||||
// 验证所有权限是否存在,并检查租户一致性
|
||||
List<Permission> permissions = permissionMapper.selectBatchIds(permissionIds);
|
||||
if (permissions.size() != permissionIds.size()) {
|
||||
throw new BusinessException("部分权限不存在");
|
||||
}
|
||||
|
||||
// 验证所有权限都属于同一租户或为公共权限
|
||||
boolean allValidTenant = permissions.stream()
|
||||
.allMatch(permission -> permission.getTenantId() == null ||
|
||||
permission.getTenantId().equals(role.getTenantId()));
|
||||
if (!allValidTenant) {
|
||||
throw new BusinessException("部分权限与角色不属于同一租户,无法分配");
|
||||
}
|
||||
|
||||
// 过滤掉已存在的关联
|
||||
List<Long> existingPermissionIds = getPermissionIdsByRoleId(roleId);
|
||||
List<Long> newPermissionIds = permissionIds.stream()
|
||||
.filter(pid -> !existingPermissionIds.contains(pid))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (newPermissionIds.isEmpty()) {
|
||||
log.warn("所有权限已经分配给角色ID: {}", roleId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 批量创建角色权限关联
|
||||
List<RolePermission> rolePermissions = newPermissionIds.stream()
|
||||
.map(permissionId -> {
|
||||
RolePermission rolePermission = new RolePermission();
|
||||
rolePermission.setRoleId(roleId);
|
||||
rolePermission.setPermissionId(permissionId);
|
||||
rolePermission.setTenantId(role.getTenantId());
|
||||
return rolePermission;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
boolean result = saveBatch(rolePermissions);
|
||||
if (result) {
|
||||
log.info("成功为角色ID: {} 批量分配 {} 个权限,租户ID: {}", roleId, rolePermissions.size(), role.getTenantId());
|
||||
|
||||
// 清除相关用户的权限缓存
|
||||
permissionCacheService.evictRoleRelatedPermissions(roleId, role.getTenantId());
|
||||
log.debug("已清除角色 {} 关联用户的权限缓存", roleId);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean removePermissionFromRole(Long roleId, Long permissionId) {
|
||||
// 获取角色信息以获取租户ID
|
||||
Role role = roleMapper.selectById(roleId);
|
||||
|
||||
int result = rolePermissionMapper.deleteByRoleIdAndPermissionId(roleId, permissionId);
|
||||
if (result > 0) {
|
||||
log.info("成功移除角色ID: {} 的权限ID: {}", roleId, permissionId);
|
||||
|
||||
// 清除相关用户的权限缓存
|
||||
if (role != null) {
|
||||
permissionCacheService.evictRoleRelatedPermissions(roleId, role.getTenantId());
|
||||
log.debug("已清除角色 {} 关联用户的权限缓存", roleId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
log.warn("角色ID: {} 没有权限ID: {},无需移除", roleId, permissionId);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean removeAllPermissionsFromRole(Long roleId) {
|
||||
// 获取角色信息以获取租户ID
|
||||
Role role = roleMapper.selectById(roleId);
|
||||
|
||||
int result = rolePermissionMapper.deleteByRoleId(roleId);
|
||||
log.info("成功移除角色ID: {} 的所有权限,共 {} 条", roleId, result);
|
||||
|
||||
// 清除相关用户的权限缓存
|
||||
if (role != null) {
|
||||
permissionCacheService.evictRoleRelatedPermissions(roleId, role.getTenantId());
|
||||
log.debug("已清除角色 {} 关联用户的权限缓存", roleId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean removeAllRolesFromPermission(Long permissionId) {
|
||||
int result = rolePermissionMapper.deleteByPermissionId(permissionId);
|
||||
log.info("成功移除权限ID: {} 的所有角色关联,共 {} 条", permissionId, result);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getPermissionIdsByRoleId(Long roleId) {
|
||||
return rolePermissionMapper.selectPermissionIdsByRoleId(roleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Permission> getPermissionsByRoleId(Long roleId) {
|
||||
List<Long> permissionIds = getPermissionIdsByRoleId(roleId);
|
||||
if (permissionIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return permissionMapper.selectBatchIds(permissionIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getRoleIdsByPermissionId(Long permissionId) {
|
||||
return rolePermissionMapper.selectRoleIdsByPermissionId(permissionId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(Long roleId, Long permissionId) {
|
||||
int count = rolePermissionMapper.existsByRoleIdAndPermissionId(roleId, permissionId);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateRolePermissions(Long roleId, List<Long> permissionIds) {
|
||||
// 验证角色是否存在
|
||||
if (roleMapper.selectById(roleId) == null) {
|
||||
throw new BusinessException("角色不存在");
|
||||
}
|
||||
|
||||
// 先删除角色的所有权限
|
||||
removeAllPermissionsFromRole(roleId);
|
||||
|
||||
// 如果新权限列表为空,则只删除不添加
|
||||
if (permissionIds == null || permissionIds.isEmpty()) {
|
||||
log.info("角色ID: {} 的权限已清空", roleId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 添加新的权限
|
||||
boolean result = assignPermissionsToRole(roleId, permissionIds);
|
||||
if (result) {
|
||||
log.info("成功更新角色ID: {} 的权限,共 {} 个", roleId, permissionIds.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
package com.kurihada.auth.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.kurihada.auth.entity.AuditLog;
|
||||
import com.kurihada.auth.context.TenantContext;
|
||||
import com.kurihada.auth.dto.CreateRoleRequest;
|
||||
import com.kurihada.auth.dto.PageRequest;
|
||||
import com.kurihada.auth.dto.PageResult;
|
||||
import com.kurihada.auth.dto.RoleDTO;
|
||||
import com.kurihada.auth.dto.UpdateRoleRequest;
|
||||
import com.kurihada.auth.entity.Role;
|
||||
import com.kurihada.auth.entity.RolePermission;
|
||||
import com.kurihada.auth.entity.User;
|
||||
import com.kurihada.auth.exception.BusinessException;
|
||||
import com.kurihada.auth.mapper.RoleMapper;
|
||||
import com.kurihada.auth.mapper.RolePermissionMapper;
|
||||
import com.kurihada.auth.mapper.UserMapper;
|
||||
import com.kurihada.auth.mapper.UserRoleMapper;
|
||||
import com.kurihada.auth.service.AuditLogService;
|
||||
import com.kurihada.auth.service.PermissionCacheService;
|
||||
import com.kurihada.auth.service.RoleService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 角色服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class RoleServiceImpl implements RoleService {
|
||||
|
||||
private final RoleMapper roleMapper;
|
||||
private final RolePermissionMapper rolePermissionMapper;
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
private final UserMapper userMapper;
|
||||
private final PermissionCacheService permissionCacheService;
|
||||
private final AuditLogService auditLogService;
|
||||
|
||||
// 系统内置角色,不允许删除
|
||||
private static final Set<String> SYSTEM_ROLES = new HashSet<>(Arrays.asList("ADMIN", "SUPER_ADMIN"));
|
||||
|
||||
@Override
|
||||
public PageResult<RoleDTO> getRolesPage(PageRequest pageRequest) {
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapper<Role> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(tenantId != null, Role::getTenantId, tenantId);
|
||||
|
||||
// 搜索条件
|
||||
if (StringUtils.hasText(pageRequest.getKeyword())) {
|
||||
queryWrapper.and(wrapper -> wrapper
|
||||
.like(Role::getName, pageRequest.getKeyword())
|
||||
.or()
|
||||
.like(Role::getCode, pageRequest.getKeyword())
|
||||
.or()
|
||||
.like(Role::getDescription, pageRequest.getKeyword())
|
||||
);
|
||||
}
|
||||
|
||||
// 排序
|
||||
if (StringUtils.hasText(pageRequest.getSortBy())) {
|
||||
boolean isAsc = "asc".equalsIgnoreCase(pageRequest.getSortDirection());
|
||||
switch (pageRequest.getSortBy()) {
|
||||
case "name" -> queryWrapper.orderBy(true, isAsc, Role::getName);
|
||||
case "code" -> queryWrapper.orderBy(true, isAsc, Role::getCode);
|
||||
case "status" -> queryWrapper.orderBy(true, isAsc, Role::getStatus);
|
||||
case "createTime" -> queryWrapper.orderBy(true, isAsc, Role::getCreateTime);
|
||||
default -> queryWrapper.orderByDesc(Role::getCreateTime);
|
||||
}
|
||||
} else {
|
||||
queryWrapper.orderByDesc(Role::getCreateTime);
|
||||
}
|
||||
|
||||
// 查询总数
|
||||
Long total = roleMapper.selectCount(queryWrapper);
|
||||
|
||||
// 分页查询 - 使用 MyBatis Plus 的安全分页方法
|
||||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<Role> page =
|
||||
new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(
|
||||
pageRequest.getPage(),
|
||||
pageRequest.getSize()
|
||||
);
|
||||
page.setSearchCount(false); // 已经查询过总数,避免重复查询
|
||||
|
||||
com.baomidou.mybatisplus.extension.plugins.pagination.Page<Role> result =
|
||||
roleMapper.selectPage(page, queryWrapper);
|
||||
List<Role> roles = result.getRecords();
|
||||
|
||||
List<RoleDTO> roleDTOs = roles.stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return PageResult.of(roleDTOs, total, pageRequest.getPage(), pageRequest.getSize());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RoleDTO getRoleById(Long id) {
|
||||
Role role = roleMapper.selectById(id);
|
||||
if (role == null) {
|
||||
throw new BusinessException("角色不存在");
|
||||
}
|
||||
|
||||
// 验证租户权限
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
if (tenantId != null && !tenantId.equals(role.getTenantId())) {
|
||||
throw new BusinessException("无权访问该角色");
|
||||
}
|
||||
|
||||
return convertToDTO(role);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public RoleDTO createRole(CreateRoleRequest request) {
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
User currentUser = getCurrentUser();
|
||||
|
||||
// 验证角色代码格式(大写字母和下划线)
|
||||
if (!request.getCode().matches("^[A-Z_]+$")) {
|
||||
auditLogRole("CREATE_ROLE", null, "FAILURE", "角色代码格式不正确", request, currentUser, tenantId);
|
||||
throw new BusinessException("角色代码只能包含大写字母和下划线");
|
||||
}
|
||||
|
||||
// 检查角色代码是否已存在
|
||||
Long count = roleMapper.selectCount(new LambdaQueryWrapper<Role>()
|
||||
.eq(Role::getCode, request.getCode())
|
||||
.eq(tenantId != null, Role::getTenantId, tenantId));
|
||||
|
||||
if (count > 0) {
|
||||
auditLogRole("CREATE_ROLE", null, "FAILURE", "角色代码已存在", request, currentUser, tenantId);
|
||||
throw new BusinessException("角色代码已存在");
|
||||
}
|
||||
|
||||
// 创建角色
|
||||
Role role = new Role();
|
||||
role.setName(request.getName());
|
||||
role.setCode(request.getCode());
|
||||
role.setDescription(request.getDescription());
|
||||
role.setTenantId(tenantId);
|
||||
role.setStatus(1);
|
||||
|
||||
roleMapper.insert(role);
|
||||
log.info("角色创建成功: {}, ID: {}", role.getName(), role.getId());
|
||||
|
||||
// 记录审计日志
|
||||
auditLogRole("CREATE_ROLE", role.getId(), "SUCCESS", null, role, currentUser, tenantId);
|
||||
|
||||
return convertToDTO(role);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public RoleDTO updateRole(Long id, UpdateRoleRequest request) {
|
||||
Role role = roleMapper.selectById(id);
|
||||
if (role == null) {
|
||||
throw new BusinessException("角色不存在");
|
||||
}
|
||||
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
User currentUser = getCurrentUser();
|
||||
|
||||
// 验证租户权限
|
||||
if (tenantId != null && !tenantId.equals(role.getTenantId())) {
|
||||
auditLogRole("UPDATE_ROLE", id, "FAILURE", "无权修改该角色", request, currentUser, tenantId);
|
||||
throw new BusinessException("无权修改该角色");
|
||||
}
|
||||
|
||||
String oldName = role.getName();
|
||||
String oldDescription = role.getDescription();
|
||||
|
||||
// 更新角色信息
|
||||
role.setName(request.getName());
|
||||
role.setDescription(request.getDescription());
|
||||
|
||||
roleMapper.updateById(role);
|
||||
log.info("角色更新成功: {}, ID: {}", role.getName(), role.getId());
|
||||
|
||||
// 清理相关用户的权限缓存
|
||||
permissionCacheService.evictRoleRelatedPermissions(id, tenantId);
|
||||
|
||||
// 记录审计日志
|
||||
String details = String.format("更新前: name=%s, description=%s; 更新后: name=%s, description=%s",
|
||||
oldName, oldDescription, role.getName(), role.getDescription());
|
||||
auditLogRole("UPDATE_ROLE", id, "SUCCESS", details, role, currentUser, tenantId);
|
||||
|
||||
return convertToDTO(role);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteRole(Long id) {
|
||||
Role role = roleMapper.selectById(id);
|
||||
if (role == null) {
|
||||
throw new BusinessException("角色不存在");
|
||||
}
|
||||
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
User currentUser = getCurrentUser();
|
||||
|
||||
// 验证租户权限
|
||||
if (tenantId != null && !tenantId.equals(role.getTenantId())) {
|
||||
auditLogRole("DELETE_ROLE", id, "FAILURE", "无权删除该角色", role, currentUser, tenantId);
|
||||
throw new BusinessException("无权删除该角色");
|
||||
}
|
||||
|
||||
// 防止删除系统内置角色
|
||||
if (SYSTEM_ROLES.contains(role.getCode())) {
|
||||
auditLogRole("DELETE_ROLE", id, "FAILURE", "不能删除系统内置角色", role, currentUser, tenantId);
|
||||
throw new BusinessException("不能删除系统内置角色: " + role.getCode());
|
||||
}
|
||||
|
||||
// 检查是否有用户正在使用该角色
|
||||
int userCount = userRoleMapper.countUsersByRoleId(id);
|
||||
if (userCount > 0) {
|
||||
String errorMsg = String.format("该角色正被 %d 个用户使用,无法删除", userCount);
|
||||
auditLogRole("DELETE_ROLE", id, "FAILURE", errorMsg, role, currentUser, tenantId);
|
||||
throw new BusinessException(errorMsg);
|
||||
}
|
||||
|
||||
// 删除角色和权限的关联
|
||||
rolePermissionMapper.delete(new LambdaQueryWrapper<RolePermission>()
|
||||
.eq(RolePermission::getRoleId, id));
|
||||
|
||||
// 删除角色
|
||||
roleMapper.deleteById(id);
|
||||
log.info("角色删除成功: {}, ID: {}", role.getName(), role.getId());
|
||||
|
||||
// 清理相关用户的权限缓存
|
||||
permissionCacheService.evictRoleRelatedPermissions(id, tenantId);
|
||||
|
||||
// 记录审计日志
|
||||
auditLogRole("DELETE_ROLE", id, "SUCCESS", null, role, currentUser, tenantId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void toggleRoleStatus(Long id) {
|
||||
Role role = roleMapper.selectById(id);
|
||||
if (role == null) {
|
||||
throw new BusinessException("角色不存在");
|
||||
}
|
||||
|
||||
Long tenantId = TenantContext.getTenantId();
|
||||
User currentUser = getCurrentUser();
|
||||
|
||||
// 验证租户权限
|
||||
if (tenantId != null && !tenantId.equals(role.getTenantId())) {
|
||||
auditLogRole("TOGGLE_ROLE_STATUS", id, "FAILURE", "无权修改该角色", role, currentUser, tenantId);
|
||||
throw new BusinessException("无权修改该角色");
|
||||
}
|
||||
|
||||
// 防止禁用系统内置角色
|
||||
if (SYSTEM_ROLES.contains(role.getCode()) && role.getStatus() == 1) {
|
||||
auditLogRole("TOGGLE_ROLE_STATUS", id, "FAILURE", "不能禁用系统内置角色", role, currentUser, tenantId);
|
||||
throw new BusinessException("不能禁用系统内置角色: " + role.getCode());
|
||||
}
|
||||
|
||||
int oldStatus = role.getStatus();
|
||||
int newStatus = oldStatus == 1 ? 0 : 1;
|
||||
role.setStatus(newStatus);
|
||||
roleMapper.updateById(role);
|
||||
log.info("角色状态已更新: {}, 新状态: {}", role.getName(), role.getStatus());
|
||||
|
||||
// 清理相关用户的权限缓存
|
||||
permissionCacheService.evictRoleRelatedPermissions(id, tenantId);
|
||||
|
||||
// 记录审计日志
|
||||
String details = String.format("状态从 %s 变更为 %s",
|
||||
oldStatus == 1 ? "启用" : "禁用",
|
||||
newStatus == 1 ? "启用" : "禁用");
|
||||
auditLogRole("TOGGLE_ROLE_STATUS", id, "SUCCESS", details, role, currentUser, tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为DTO
|
||||
*/
|
||||
private RoleDTO convertToDTO(Role role) {
|
||||
// 获取角色的权限
|
||||
List<RolePermission> rolePermissions = rolePermissionMapper.selectList(
|
||||
new LambdaQueryWrapper<RolePermission>()
|
||||
.eq(RolePermission::getRoleId, role.getId())
|
||||
);
|
||||
|
||||
Set<String> permissionCodes = rolePermissions.stream()
|
||||
.map(rp -> String.valueOf(rp.getPermissionId()))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return RoleDTO.builder()
|
||||
.id(role.getId())
|
||||
.name(role.getName())
|
||||
.code(role.getCode())
|
||||
.tenantId(role.getTenantId())
|
||||
.description(role.getDescription())
|
||||
.status(role.getStatus())
|
||||
.createTime(role.getCreateTime())
|
||||
.updateTime(role.getUpdateTime())
|
||||
.permissions(permissionCodes)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户
|
||||
*/
|
||||
private User getCurrentUser() {
|
||||
try {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
String username = authentication.getName();
|
||||
if (!"anonymousUser".equals(username)) {
|
||||
return userMapper.selectUserWithRolesAndPermissions(username);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取当前用户信息失败: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP地址
|
||||
*/
|
||||
private String getClientIp() {
|
||||
try {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取客户端IP失败: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取User Agent
|
||||
*/
|
||||
private String getUserAgent() {
|
||||
try {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
return attributes.getRequest().getHeader("User-Agent");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("获取User-Agent失败: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录角色操作审计日志
|
||||
*/
|
||||
private void auditLogRole(String actionType, Long roleId, String result,
|
||||
String errorMessage, Object details, User currentUser, Long tenantId) {
|
||||
try {
|
||||
AuditLog auditLog = AuditLog.builder()
|
||||
.actionType(actionType)
|
||||
.userId(currentUser != null ? currentUser.getId() : null)
|
||||
.username(currentUser != null ? currentUser.getUsername() : "system")
|
||||
.tenantId(tenantId != null ? tenantId : (currentUser != null ? currentUser.getTenantId() : null))
|
||||
.resourceType("ROLE")
|
||||
.resourceId(roleId != null ? String.valueOf(roleId) : null)
|
||||
.details(details != null ? details.toString() : null)
|
||||
.result(result)
|
||||
.errorMessage(errorMessage)
|
||||
.ipAddress(getClientIp())
|
||||
.userAgent(getUserAgent())
|
||||
.build();
|
||||
|
||||
auditLogService.log(auditLog);
|
||||
} catch (Exception e) {
|
||||
log.error("记录审计日志失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
package com.kurihada.auth.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.kurihada.auth.entity.Role;
|
||||
import com.kurihada.auth.entity.User;
|
||||
import com.kurihada.auth.entity.UserRole;
|
||||
import com.kurihada.auth.exception.BusinessException;
|
||||
import com.kurihada.auth.mapper.RoleMapper;
|
||||
import com.kurihada.auth.mapper.UserMapper;
|
||||
import com.kurihada.auth.mapper.UserRoleMapper;
|
||||
import com.kurihada.auth.service.UserRoleService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用户角色关联服务实现类
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> implements UserRoleService {
|
||||
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
private final UserMapper userMapper;
|
||||
private final RoleMapper roleMapper;
|
||||
private final com.kurihada.auth.service.PermissionCacheService permissionCacheService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean assignRoleToUser(Long userId, Long roleId) {
|
||||
// 验证用户是否存在
|
||||
User user = userMapper.selectById(userId);
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
// 验证角色是否存在
|
||||
Role role = roleMapper.selectById(roleId);
|
||||
if (role == null) {
|
||||
throw new BusinessException("角色不存在");
|
||||
}
|
||||
|
||||
// 验证租户一致性
|
||||
if (!user.getTenantId().equals(role.getTenantId())) {
|
||||
throw new BusinessException("用户和角色不属于同一租户,无法分配");
|
||||
}
|
||||
|
||||
// 检查是否已经存在该关联
|
||||
if (hasRole(userId, roleId)) {
|
||||
log.warn("用户ID: {} 已经拥有角色ID: {}", userId, roleId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 创建用户角色关联,包含租户ID
|
||||
UserRole userRole = new UserRole();
|
||||
userRole.setUserId(userId);
|
||||
userRole.setRoleId(roleId);
|
||||
userRole.setTenantId(user.getTenantId());
|
||||
|
||||
int result = userRoleMapper.insert(userRole);
|
||||
if (result > 0) {
|
||||
log.info("成功为用户ID: {} 分配角色ID: {},租户ID: {}", userId, roleId, user.getTenantId());
|
||||
|
||||
// 清除用户权限缓存
|
||||
permissionCacheService.evictUserPermissions(userId, user.getTenantId());
|
||||
log.debug("已清除用户 {} 的权限缓存", userId);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean assignRolesToUser(Long userId, List<Long> roleIds) {
|
||||
// 验证用户是否存在
|
||||
User user = userMapper.selectById(userId);
|
||||
if (user == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
if (roleIds == null || roleIds.isEmpty()) {
|
||||
throw new BusinessException("角色ID列表不能为空");
|
||||
}
|
||||
|
||||
// 验证所有角色是否存在,并检查租户一致性
|
||||
List<Role> roles = roleMapper.selectBatchIds(roleIds);
|
||||
if (roles.size() != roleIds.size()) {
|
||||
throw new BusinessException("部分角色不存在");
|
||||
}
|
||||
|
||||
// 验证所有角色都属于同一租户
|
||||
boolean allSameTenant = roles.stream()
|
||||
.allMatch(role -> role.getTenantId().equals(user.getTenantId()));
|
||||
if (!allSameTenant) {
|
||||
throw new BusinessException("部分角色与用户不属于同一租户,无法分配");
|
||||
}
|
||||
|
||||
// 过滤掉已存在的关联
|
||||
List<Long> existingRoleIds = getRoleIdsByUserId(userId);
|
||||
List<Long> newRoleIds = roleIds.stream()
|
||||
.filter(rid -> !existingRoleIds.contains(rid))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (newRoleIds.isEmpty()) {
|
||||
log.warn("所有角色已经分配给用户ID: {}", userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 批量创建用户角色关联
|
||||
List<UserRole> userRoles = newRoleIds.stream()
|
||||
.map(roleId -> {
|
||||
UserRole userRole = new UserRole();
|
||||
userRole.setUserId(userId);
|
||||
userRole.setRoleId(roleId);
|
||||
userRole.setTenantId(user.getTenantId());
|
||||
return userRole;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
boolean result = saveBatch(userRoles);
|
||||
if (result) {
|
||||
log.info("成功为用户ID: {} 批量分配 {} 个角色,租户ID: {}", userId, userRoles.size(), user.getTenantId());
|
||||
|
||||
// 清除用户权限缓存
|
||||
permissionCacheService.evictUserPermissions(userId, user.getTenantId());
|
||||
log.debug("已清除用户 {} 的权限缓存", userId);
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean assignUsersToRole(Long roleId, List<Long> userIds) {
|
||||
// 验证角色是否存在
|
||||
if (roleMapper.selectById(roleId) == null) {
|
||||
throw new BusinessException("角色不存在");
|
||||
}
|
||||
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
throw new BusinessException("用户ID列表不能为空");
|
||||
}
|
||||
|
||||
// 验证所有用户是否存在
|
||||
long existingCount = userMapper.selectCount(
|
||||
new LambdaQueryWrapper<User>()
|
||||
.in(User::getId, userIds)
|
||||
);
|
||||
if (existingCount != userIds.size()) {
|
||||
throw new BusinessException("部分用户不存在");
|
||||
}
|
||||
|
||||
// 过滤掉已存在的关联
|
||||
List<Long> existingUserIds = getUserIdsByRoleId(roleId);
|
||||
List<Long> newUserIds = userIds.stream()
|
||||
.filter(uid -> !existingUserIds.contains(uid))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (newUserIds.isEmpty()) {
|
||||
log.warn("所有用户已经拥有角色ID: {}", roleId);
|
||||
return true;
|
||||
}
|
||||
|
||||
int result = userRoleMapper.batchInsertUsers(roleId, newUserIds);
|
||||
if (result > 0) {
|
||||
log.info("成功为角色ID: {} 批量分配 {} 个用户", roleId, result);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean removeRoleFromUser(Long userId, Long roleId) {
|
||||
// 获取用户信息以获取租户ID
|
||||
User user = userMapper.selectById(userId);
|
||||
|
||||
int result = userRoleMapper.deleteByUserIdAndRoleId(userId, roleId);
|
||||
if (result > 0) {
|
||||
log.info("成功移除用户ID: {} 的角色ID: {}", userId, roleId);
|
||||
|
||||
// 清除用户权限缓存
|
||||
if (user != null) {
|
||||
permissionCacheService.evictUserPermissions(userId, user.getTenantId());
|
||||
log.debug("已清除用户 {} 的权限缓存", userId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
log.warn("用户ID: {} 没有角色ID: {},无需移除", userId, roleId);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean removeAllRolesFromUser(Long userId) {
|
||||
// 获取用户信息以获取租户ID
|
||||
User user = userMapper.selectById(userId);
|
||||
|
||||
int result = userRoleMapper.deleteByUserId(userId);
|
||||
log.info("成功移除用户ID: {} 的所有角色,共 {} 条", userId, result);
|
||||
|
||||
// 清除用户权限缓存
|
||||
if (user != null) {
|
||||
permissionCacheService.evictUserPermissions(userId, user.getTenantId());
|
||||
log.debug("已清除用户 {} 的权限缓存", userId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean removeAllUsersFromRole(Long roleId) {
|
||||
// 获取角色信息以获取租户ID
|
||||
Role role = roleMapper.selectById(roleId);
|
||||
|
||||
int result = userRoleMapper.deleteByRoleId(roleId);
|
||||
log.info("成功移除角色ID: {} 的所有用户关联,共 {} 条", roleId, result);
|
||||
|
||||
// 清除该角色关联的所有用户的权限缓存
|
||||
if (role != null) {
|
||||
permissionCacheService.evictRoleRelatedPermissions(roleId, role.getTenantId());
|
||||
log.debug("已清除角色 {} 关联用户的权限缓存", roleId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getRoleIdsByUserId(Long userId) {
|
||||
return userRoleMapper.selectRoleIdsByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Role> getRolesByUserId(Long userId) {
|
||||
List<Long> roleIds = getRoleIdsByUserId(userId);
|
||||
if (roleIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return roleMapper.selectBatchIds(roleIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> getUserIdsByRoleId(Long roleId) {
|
||||
return userRoleMapper.selectUserIdsByRoleId(roleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> getUsersByRoleId(Long roleId) {
|
||||
List<Long> userIds = getUserIdsByRoleId(roleId);
|
||||
if (userIds.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
return userMapper.selectBatchIds(userIds);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRole(Long userId, Long roleId) {
|
||||
int count = userRoleMapper.existsByUserIdAndRoleId(userId, roleId);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRoleByCode(Long userId, String roleCode) {
|
||||
// 获取用户的所有角色
|
||||
List<Role> roles = getRolesByUserId(userId);
|
||||
|
||||
// 检查是否有匹配的角色代码
|
||||
return roles.stream()
|
||||
.anyMatch(role -> role.getCode().equals(roleCode));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateUserRoles(Long userId, List<Long> roleIds) {
|
||||
// 验证用户是否存在
|
||||
if (userMapper.selectById(userId) == null) {
|
||||
throw new BusinessException("用户不存在");
|
||||
}
|
||||
|
||||
// 先删除用户的所有角色
|
||||
removeAllRolesFromUser(userId);
|
||||
|
||||
// 如果新角色列表为空,则只删除不添加
|
||||
if (roleIds == null || roleIds.isEmpty()) {
|
||||
log.info("用户ID: {} 的角色已清空", userId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 添加新的角色
|
||||
boolean result = assignRolesToUser(userId, roleIds);
|
||||
if (result) {
|
||||
log.info("成功更新用户ID: {} 的角色,共 {} 个", userId, roleIds.size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countUsersByRoleId(Long roleId) {
|
||||
return userRoleMapper.countUsersByRoleId(roleId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int countRolesByUserId(Long userId) {
|
||||
return userRoleMapper.countRolesByUserId(userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.kurihada.auth.util;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* IP 工具类
|
||||
*/
|
||||
public class IpUtil {
|
||||
|
||||
private static final String UNKNOWN = "unknown";
|
||||
private static final String LOCALHOST_IPV4 = "127.0.0.1";
|
||||
private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
|
||||
|
||||
/**
|
||||
* 获取客户端真实 IP 地址
|
||||
* 支持通过代理、负载均衡等场景
|
||||
*/
|
||||
public static String getIpAddress(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
String ip = null;
|
||||
|
||||
// 1. X-Forwarded-For: 通过代理时的原始IP
|
||||
ip = request.getHeader("X-Forwarded-For");
|
||||
if (isValidIp(ip)) {
|
||||
// 多次代理时,会有多个IP,取第一个非unknown的有效IP
|
||||
int index = ip.indexOf(',');
|
||||
if (index != -1) {
|
||||
ip = ip.substring(0, index);
|
||||
}
|
||||
return ip.trim();
|
||||
}
|
||||
|
||||
// 2. X-Real-IP: Nginx 代理设置的真实IP
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
if (isValidIp(ip)) {
|
||||
return ip.trim();
|
||||
}
|
||||
|
||||
// 3. Proxy-Client-IP: Apache 代理设置
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
if (isValidIp(ip)) {
|
||||
return ip.trim();
|
||||
}
|
||||
|
||||
// 4. WL-Proxy-Client-IP: WebLogic 代理设置
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
if (isValidIp(ip)) {
|
||||
return ip.trim();
|
||||
}
|
||||
|
||||
// 5. HTTP_CLIENT_IP
|
||||
ip = request.getHeader("HTTP_CLIENT_IP");
|
||||
if (isValidIp(ip)) {
|
||||
return ip.trim();
|
||||
}
|
||||
|
||||
// 6. HTTP_X_FORWARDED_FOR
|
||||
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
if (isValidIp(ip)) {
|
||||
return ip.trim();
|
||||
}
|
||||
|
||||
// 7. 最后使用 request.getRemoteAddr()
|
||||
ip = request.getRemoteAddr();
|
||||
|
||||
// 将 IPv6 的 localhost 转换为 IPv4
|
||||
if (LOCALHOST_IPV6.equals(ip)) {
|
||||
ip = LOCALHOST_IPV4;
|
||||
}
|
||||
|
||||
return StringUtils.hasText(ip) ? ip.trim() : UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 IP 是否有效
|
||||
*/
|
||||
private static boolean isValidIp(String ip) {
|
||||
return StringUtils.hasText(ip) &&
|
||||
!UNKNOWN.equalsIgnoreCase(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为内网IP
|
||||
*/
|
||||
public static boolean isInternalIp(String ip) {
|
||||
if (!StringUtils.hasText(ip)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (LOCALHOST_IPV4.equals(ip) || LOCALHOST_IPV6.equals(ip)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 私有地址范围:
|
||||
// 10.0.0.0 - 10.255.255.255
|
||||
// 172.16.0.0 - 172.31.255.255
|
||||
// 192.168.0.0 - 192.168.255.255
|
||||
byte[] addr = textToNumericFormatV4(ip);
|
||||
if (addr == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final byte b0 = addr[0];
|
||||
final byte b1 = addr[1];
|
||||
|
||||
// 10.x.x.x
|
||||
final byte SECTION_1 = 0x0A;
|
||||
// 172.16.x.x - 172.31.x.x
|
||||
final byte SECTION_2 = (byte) 0xAC;
|
||||
final byte SECTION_3 = (byte) 0x10;
|
||||
final byte SECTION_4 = (byte) 0x1F;
|
||||
// 192.168.x.x
|
||||
final byte SECTION_5 = (byte) 0xC0;
|
||||
final byte SECTION_6 = (byte) 0xA8;
|
||||
|
||||
switch (b0) {
|
||||
case SECTION_1:
|
||||
return true;
|
||||
case SECTION_2:
|
||||
return b1 >= SECTION_3 && b1 <= SECTION_4;
|
||||
case SECTION_5:
|
||||
return b1 == SECTION_6;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 IPv4 地址字符串转换为字节数组
|
||||
*/
|
||||
private static byte[] textToNumericFormatV4(String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] parts = text.split("\\.");
|
||||
if (parts.length != 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[4];
|
||||
try {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int value = Integer.parseInt(parts[i]);
|
||||
if (value < 0 || value > 255) {
|
||||
return null;
|
||||
}
|
||||
bytes[i] = (byte) value;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.kurihada.auth.util;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* JWT工具类
|
||||
*/
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.expiration}")
|
||||
private Long expiration;
|
||||
|
||||
/**
|
||||
* 获取签名密钥
|
||||
*/
|
||||
private SecretKey getSigningKey() {
|
||||
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取用户名
|
||||
*/
|
||||
public String getUsernameFromToken(String token) {
|
||||
return getClaimFromToken(token, Claims::getSubject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取租户ID
|
||||
*/
|
||||
public Long getTenantIdFromToken(String token) {
|
||||
Claims claims = getAllClaimsFromToken(token);
|
||||
Object tenantId = claims.get("tenantId");
|
||||
if (tenantId instanceof Integer) {
|
||||
return ((Integer) tenantId).longValue();
|
||||
} else if (tenantId instanceof Long) {
|
||||
return (Long) tenantId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取用户ID
|
||||
*/
|
||||
public Long getUserIdFromToken(String token) {
|
||||
Claims claims = getAllClaimsFromToken(token);
|
||||
Object userId = claims.get("userId");
|
||||
if (userId instanceof Integer) {
|
||||
return ((Integer) userId).longValue();
|
||||
} else if (userId instanceof Long) {
|
||||
return (Long) userId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取过期时间
|
||||
*/
|
||||
public Date getExpirationDateFromToken(String token) {
|
||||
return getClaimFromToken(token, Claims::getExpiration);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取Claims
|
||||
*/
|
||||
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
|
||||
final Claims claims = getAllClaimsFromToken(token);
|
||||
return claimsResolver.apply(claims);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从token中获取所有Claims
|
||||
*/
|
||||
private Claims getAllClaimsFromToken(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(getSigningKey())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查token是否过期
|
||||
*/
|
||||
private Boolean isTokenExpired(String token) {
|
||||
final Date expiration = getExpirationDateFromToken(token);
|
||||
return expiration.before(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成token
|
||||
*/
|
||||
public String generateToken(UserDetails userDetails) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
return doGenerateToken(claims, userDetails.getUsername());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成token(带额外信息)
|
||||
*/
|
||||
public String generateToken(String username, Map<String, Object> claims) {
|
||||
return doGenerateToken(claims, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行生成token
|
||||
*/
|
||||
private String doGenerateToken(Map<String, Object> claims, String subject) {
|
||||
final Date createdDate = new Date();
|
||||
final Date expirationDate = new Date(createdDate.getTime() + expiration);
|
||||
|
||||
return Jwts.builder()
|
||||
.claims(claims)
|
||||
.subject(subject)
|
||||
.issuedAt(createdDate)
|
||||
.expiration(expirationDate)
|
||||
.signWith(getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证token
|
||||
*/
|
||||
public Boolean validateToken(String token, UserDetails userDetails) {
|
||||
final String username = getUsernameFromToken(token);
|
||||
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证token是否有效
|
||||
*/
|
||||
public Boolean validateToken(String token) {
|
||||
try {
|
||||
return !isTokenExpired(token);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package com.kurihada.auth.util;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 密码强度评估工具
|
||||
*/
|
||||
public class PasswordStrengthUtil {
|
||||
|
||||
/**
|
||||
* 密码强度等级
|
||||
*/
|
||||
public enum PasswordStrength {
|
||||
VERY_WEAK(0, "非常弱"),
|
||||
WEAK(1, "弱"),
|
||||
MEDIUM(2, "中等"),
|
||||
STRONG(3, "强"),
|
||||
VERY_STRONG(4, "非常强");
|
||||
|
||||
private final int level;
|
||||
private final String description;
|
||||
|
||||
PasswordStrength(int level, String description) {
|
||||
this.level = level;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估密码强度
|
||||
*
|
||||
* @param password 密码
|
||||
* @return 密码强度等级
|
||||
*/
|
||||
public static PasswordStrength assessPasswordStrength(String password) {
|
||||
if (password == null || password.isEmpty()) {
|
||||
return PasswordStrength.VERY_WEAK;
|
||||
}
|
||||
|
||||
int score = 0;
|
||||
|
||||
// 1. 长度评分
|
||||
if (password.length() >= 8) score += 1;
|
||||
if (password.length() >= 12) score += 1;
|
||||
if (password.length() >= 16) score += 1;
|
||||
|
||||
// 2. 字符类型评分
|
||||
if (Pattern.compile("[a-z]").matcher(password).find()) score += 1; // 小写字母
|
||||
if (Pattern.compile("[A-Z]").matcher(password).find()) score += 1; // 大写字母
|
||||
if (Pattern.compile("[0-9]").matcher(password).find()) score += 1; // 数字
|
||||
if (Pattern.compile("[!@#$%^&*()_+\\-=\\[\\]{}|;:,.<>?]").matcher(password).find()) score += 1; // 特殊字符
|
||||
|
||||
// 3. 字符多样性评分
|
||||
long uniqueChars = password.chars().distinct().count();
|
||||
if (uniqueChars >= password.length() * 0.6) score += 1; // 60%以上字符不重复
|
||||
|
||||
// 4. 扣分项
|
||||
if (hasSequentialChars(password, 3)) score -= 1; // 连续字符
|
||||
if (hasRepeatingChars(password, 3)) score -= 1; // 重复字符
|
||||
|
||||
// 根据分数返回强度等级
|
||||
if (score >= 8) return PasswordStrength.VERY_STRONG;
|
||||
if (score >= 6) return PasswordStrength.STRONG;
|
||||
if (score >= 4) return PasswordStrength.MEDIUM;
|
||||
if (score >= 2) return PasswordStrength.WEAK;
|
||||
return PasswordStrength.VERY_WEAK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含连续字符
|
||||
*/
|
||||
private static boolean hasSequentialChars(String password, int length) {
|
||||
if (password.length() < length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i <= password.length() - length; i++) {
|
||||
boolean isSequential = true;
|
||||
char firstChar = password.charAt(i);
|
||||
|
||||
for (int j = 1; j < length; j++) {
|
||||
if (password.charAt(i + j) != firstChar + j) {
|
||||
isSequential = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isSequential) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含重复字符
|
||||
*/
|
||||
private static boolean hasRepeatingChars(String password, int length) {
|
||||
if (password.length() < length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i <= password.length() - length; i++) {
|
||||
char firstChar = password.charAt(i);
|
||||
boolean isRepeating = true;
|
||||
|
||||
for (int j = 1; j < length; j++) {
|
||||
if (password.charAt(i + j) != firstChar) {
|
||||
isRepeating = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isRepeating) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成密码强度建议
|
||||
*/
|
||||
public static String getPasswordSuggestion(String password) {
|
||||
if (password == null || password.isEmpty()) {
|
||||
return "请输入密码";
|
||||
}
|
||||
|
||||
StringBuilder suggestions = new StringBuilder();
|
||||
|
||||
if (password.length() < 8) {
|
||||
suggestions.append("• 密码长度至少8位\n");
|
||||
}
|
||||
|
||||
if (!Pattern.compile("[a-z]").matcher(password).find()) {
|
||||
suggestions.append("• 添加小写字母\n");
|
||||
}
|
||||
|
||||
if (!Pattern.compile("[A-Z]").matcher(password).find()) {
|
||||
suggestions.append("• 添加大写字母\n");
|
||||
}
|
||||
|
||||
if (!Pattern.compile("[0-9]").matcher(password).find()) {
|
||||
suggestions.append("• 添加数字\n");
|
||||
}
|
||||
|
||||
if (!Pattern.compile("[!@#$%^&*()_+\\-=\\[\\]{}|;:,.<>?]").matcher(password).find()) {
|
||||
suggestions.append("• 添加特殊字符\n");
|
||||
}
|
||||
|
||||
if (hasSequentialChars(password, 3)) {
|
||||
suggestions.append("• 避免使用连续字符(如123、abc)\n");
|
||||
}
|
||||
|
||||
if (hasRepeatingChars(password, 3)) {
|
||||
suggestions.append("• 避免使用重复字符(如aaa、111)\n");
|
||||
}
|
||||
|
||||
if (suggestions.length() == 0) {
|
||||
return "密码强度良好";
|
||||
}
|
||||
|
||||
return "建议改进:\n" + suggestions.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算密码强度百分比(0-100)
|
||||
*/
|
||||
public static int getPasswordStrengthPercentage(String password) {
|
||||
PasswordStrength strength = assessPasswordStrength(password);
|
||||
return strength.getLevel() * 25; // 0, 25, 50, 75, 100
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user