feat: 初始化项目并完成基础功能开发
- 完成项目初始化
- 实现用户注册、登录功能
- 完成用户管理与权限管理模块
- 开发后端 Tracker 服务器项目管理接口
- 实现日志管理接口
Change-Id: Ia4bde1c9ff600352a7ff0caca0cc50b02cad1af7
diff --git a/ruoyi-framework/pom.xml b/ruoyi-framework/pom.xml
new file mode 100644
index 0000000..6ed5f58
--- /dev/null
+++ b/ruoyi-framework/pom.xml
@@ -0,0 +1,64 @@
+<?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">
+ <parent>
+ <artifactId>ruoyi</artifactId>
+ <groupId>com.ruoyi</groupId>
+ <version>3.8.8</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>ruoyi-framework</artifactId>
+
+ <description>
+ framework框架核心
+ </description>
+
+ <dependencies>
+
+ <!-- SpringBoot Web容器 -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-web</artifactId>
+ </dependency>
+
+ <!-- SpringBoot 拦截器 -->
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-aop</artifactId>
+ </dependency>
+
+ <!-- 阿里数据库连接池 -->
+ <dependency>
+ <groupId>com.alibaba</groupId>
+ <artifactId>druid-spring-boot-3-starter</artifactId>
+ </dependency>
+
+ <!-- 验证码 -->
+ <dependency>
+ <groupId>pro.fessional</groupId>
+ <artifactId>kaptcha</artifactId>
+ <exclusions>
+ <exclusion>
+ <artifactId>servlet-api</artifactId>
+ <groupId>javax.servlet</groupId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+ <!-- 获取系统信息 -->
+ <dependency>
+ <groupId>com.github.oshi</groupId>
+ <artifactId>oshi-core</artifactId>
+ </dependency>
+
+ <!-- 系统模块-->
+ <dependency>
+ <groupId>com.ruoyi</groupId>
+ <artifactId>ruoyi-system</artifactId>
+ </dependency>
+
+ </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java
new file mode 100644
index 0000000..b2337c9
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java
@@ -0,0 +1,184 @@
+package com.ruoyi.framework.aspectj;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.annotation.DataScope;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.BaseEntity;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.core.text.Convert;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.security.context.PermissionContextHolder;
+
+/**
+ * 数据过滤处理
+ *
+ * @author ruoyi
+ */
+@Aspect
+@Component
+public class DataScopeAspect
+{
+ /**
+ * 全部数据权限
+ */
+ public static final String DATA_SCOPE_ALL = "1";
+
+ /**
+ * 自定数据权限
+ */
+ public static final String DATA_SCOPE_CUSTOM = "2";
+
+ /**
+ * 部门数据权限
+ */
+ public static final String DATA_SCOPE_DEPT = "3";
+
+ /**
+ * 部门及以下数据权限
+ */
+ public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
+
+ /**
+ * 仅本人数据权限
+ */
+ public static final String DATA_SCOPE_SELF = "5";
+
+ /**
+ * 数据权限过滤关键字
+ */
+ public static final String DATA_SCOPE = "dataScope";
+
+ @Before("@annotation(controllerDataScope)")
+ public void doBefore(JoinPoint point, DataScope controllerDataScope) throws Throwable
+ {
+ clearDataScope(point);
+ handleDataScope(point, controllerDataScope);
+ }
+
+ protected void handleDataScope(final JoinPoint joinPoint, DataScope controllerDataScope)
+ {
+ // 获取当前的用户
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (StringUtils.isNotNull(loginUser))
+ {
+ SysUser currentUser = loginUser.getUser();
+ // 如果是超级管理员,则不过滤数据
+ if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
+ {
+ String permission = StringUtils.defaultIfEmpty(controllerDataScope.permission(), PermissionContextHolder.getContext());
+ dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(), controllerDataScope.userAlias(), permission);
+ }
+ }
+ }
+
+ /**
+ * 数据范围过滤
+ *
+ * @param joinPoint 切点
+ * @param user 用户
+ * @param deptAlias 部门别名
+ * @param userAlias 用户别名
+ * @param permission 权限字符
+ */
+ public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias, String permission)
+ {
+ StringBuilder sqlString = new StringBuilder();
+ List<String> conditions = new ArrayList<String>();
+ List<String> scopeCustomIds = new ArrayList<String>();
+ user.getRoles().forEach(role -> {
+ if (DATA_SCOPE_CUSTOM.equals(role.getDataScope()) && StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL) && StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))
+ {
+ scopeCustomIds.add(Convert.toStr(role.getRoleId()));
+ }
+ });
+
+ for (SysRole role : user.getRoles())
+ {
+ String dataScope = role.getDataScope();
+ if (conditions.contains(dataScope) || StringUtils.equals(role.getStatus(), UserConstants.ROLE_DISABLE))
+ {
+ continue;
+ }
+ if (!StringUtils.containsAny(role.getPermissions(), Convert.toStrArray(permission)))
+ {
+ continue;
+ }
+ if (DATA_SCOPE_ALL.equals(dataScope))
+ {
+ sqlString = new StringBuilder();
+ conditions.add(dataScope);
+ break;
+ }
+ else if (DATA_SCOPE_CUSTOM.equals(dataScope))
+ {
+ if (scopeCustomIds.size() > 1)
+ {
+ // 多个自定数据权限使用in查询,避免多次拼接。
+ sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id in ({}) ) ", deptAlias, String.join(",", scopeCustomIds)));
+ }
+ else
+ {
+ sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias, role.getRoleId()));
+ }
+ }
+ else if (DATA_SCOPE_DEPT.equals(dataScope))
+ {
+ sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+ }
+ else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
+ {
+ sqlString.append(StringUtils.format(" OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )", deptAlias, user.getDeptId(), user.getDeptId()));
+ }
+ else if (DATA_SCOPE_SELF.equals(dataScope))
+ {
+ if (StringUtils.isNotBlank(userAlias))
+ {
+ sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
+ }
+ else
+ {
+ // 数据权限为仅本人且没有userAlias别名不查询任何数据
+ sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
+ }
+ }
+ conditions.add(dataScope);
+ }
+
+ // 角色都不包含传递过来的权限字符,这个时候sqlString也会为空,所以要限制一下,不查询任何数据
+ if (StringUtils.isEmpty(conditions))
+ {
+ sqlString.append(StringUtils.format(" OR {}.dept_id = 0 ", deptAlias));
+ }
+
+ if (StringUtils.isNotBlank(sqlString.toString()))
+ {
+ Object params = joinPoint.getArgs()[0];
+ if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+ {
+ BaseEntity baseEntity = (BaseEntity) params;
+ baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
+ }
+ }
+ }
+
+ /**
+ * 拼接权限sql前先清空params.dataScope参数防止注入
+ */
+ private void clearDataScope(final JoinPoint joinPoint)
+ {
+ Object params = joinPoint.getArgs()[0];
+ if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+ {
+ BaseEntity baseEntity = (BaseEntity) params;
+ baseEntity.getParams().put(DATA_SCOPE, "");
+ }
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java
new file mode 100644
index 0000000..8c2c9f4
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataSourceAspect.java
@@ -0,0 +1,72 @@
+package com.ruoyi.framework.aspectj;
+
+import java.util.Objects;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.annotation.DataSource;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.datasource.DynamicDataSourceContextHolder;
+
+/**
+ * 多数据源处理
+ *
+ * @author ruoyi
+ */
+@Aspect
+@Order(1)
+@Component
+public class DataSourceAspect
+{
+ protected Logger logger = LoggerFactory.getLogger(getClass());
+
+ @Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)"
+ + "|| @within(com.ruoyi.common.annotation.DataSource)")
+ public void dsPointCut()
+ {
+
+ }
+
+ @Around("dsPointCut()")
+ public Object around(ProceedingJoinPoint point) throws Throwable
+ {
+ DataSource dataSource = getDataSource(point);
+
+ if (StringUtils.isNotNull(dataSource))
+ {
+ DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
+ }
+
+ try
+ {
+ return point.proceed();
+ }
+ finally
+ {
+ // 销毁数据源 在执行方法之后
+ DynamicDataSourceContextHolder.clearDataSourceType();
+ }
+ }
+
+ /**
+ * 获取需要切换的数据源
+ */
+ public DataSource getDataSource(ProceedingJoinPoint point)
+ {
+ MethodSignature signature = (MethodSignature) point.getSignature();
+ DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
+ if (Objects.nonNull(dataSource))
+ {
+ return dataSource;
+ }
+
+ return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
new file mode 100644
index 0000000..7f59d50
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
@@ -0,0 +1,255 @@
+package com.ruoyi.framework.aspectj;
+
+import java.util.Collection;
+import java.util.Map;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.commons.lang3.ArrayUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.NamedThreadLocal;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.multipart.MultipartFile;
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.annotation.Log;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.enums.BusinessStatus;
+import com.ruoyi.common.enums.HttpMethod;
+import com.ruoyi.common.filter.PropertyPreExcludeFilter;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.ip.IpUtils;
+import com.ruoyi.framework.manager.AsyncManager;
+import com.ruoyi.framework.manager.factory.AsyncFactory;
+import com.ruoyi.system.domain.SysOperLog;
+
+/**
+ * 操作日志记录处理
+ *
+ * @author ruoyi
+ */
+@Aspect
+@Component
+public class LogAspect
+{
+ private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
+
+ /** 排除敏感属性字段 */
+ public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
+
+ /** 计算操作消耗时间 */
+ private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");
+
+ /**
+ * 处理请求前执行
+ */
+ @Before(value = "@annotation(controllerLog)")
+ public void boBefore(JoinPoint joinPoint, Log controllerLog)
+ {
+ TIME_THREADLOCAL.set(System.currentTimeMillis());
+ }
+
+ /**
+ * 处理完请求后执行
+ *
+ * @param joinPoint 切点
+ */
+ @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
+ public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult)
+ {
+ handleLog(joinPoint, controllerLog, null, jsonResult);
+ }
+
+ /**
+ * 拦截异常操作
+ *
+ * @param joinPoint 切点
+ * @param e 异常
+ */
+ @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
+ public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e)
+ {
+ handleLog(joinPoint, controllerLog, e, null);
+ }
+
+ protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
+ {
+ try
+ {
+ // 获取当前的用户
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+
+ // *========数据库日志=========*//
+ SysOperLog operLog = new SysOperLog();
+ operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
+ // 请求的地址
+ String ip = IpUtils.getIpAddr();
+ operLog.setOperIp(ip);
+ operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
+ if (loginUser != null)
+ {
+ operLog.setOperName(loginUser.getUsername());
+ SysUser currentUser = loginUser.getUser();
+ if (StringUtils.isNotNull(currentUser) && StringUtils.isNotNull(currentUser.getDept()))
+ {
+ operLog.setDeptName(currentUser.getDept().getDeptName());
+ }
+ }
+
+ if (e != null)
+ {
+ operLog.setStatus(BusinessStatus.FAIL.ordinal());
+ operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
+ }
+ // 设置方法名称
+ String className = joinPoint.getTarget().getClass().getName();
+ String methodName = joinPoint.getSignature().getName();
+ operLog.setMethod(className + "." + methodName + "()");
+ // 设置请求方式
+ operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
+ // 处理设置注解上的参数
+ getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
+ // 设置消耗时间
+ operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
+ // 保存数据库
+ AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
+ }
+ catch (Exception exp)
+ {
+ // 记录本地异常日志
+ log.error("异常信息:{}", exp.getMessage());
+ exp.printStackTrace();
+ }
+ finally
+ {
+ TIME_THREADLOCAL.remove();
+ }
+ }
+
+ /**
+ * 获取注解中对方法的描述信息 用于Controller层注解
+ *
+ * @param log 日志
+ * @param operLog 操作日志
+ * @throws Exception
+ */
+ public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception
+ {
+ // 设置action动作
+ operLog.setBusinessType(log.businessType().ordinal());
+ // 设置标题
+ operLog.setTitle(log.title());
+ // 设置操作人类别
+ operLog.setOperatorType(log.operatorType().ordinal());
+ // 是否需要保存request,参数和值
+ if (log.isSaveRequestData())
+ {
+ // 获取参数的信息,传入到数据库中。
+ setRequestValue(joinPoint, operLog, log.excludeParamNames());
+ }
+ // 是否需要保存response,参数和值
+ if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult))
+ {
+ operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
+ }
+ }
+
+ /**
+ * 获取请求的参数,放到log中
+ *
+ * @param operLog 操作日志
+ * @throws Exception 异常
+ */
+ private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception
+ {
+ Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
+ String requestMethod = operLog.getRequestMethod();
+ if (StringUtils.isEmpty(paramsMap)
+ && (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)))
+ {
+ String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
+ operLog.setOperParam(StringUtils.substring(params, 0, 2000));
+ }
+ else
+ {
+ operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000));
+ }
+ }
+
+ /**
+ * 参数拼装
+ */
+ private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames)
+ {
+ String params = "";
+ if (paramsArray != null && paramsArray.length > 0)
+ {
+ for (Object o : paramsArray)
+ {
+ if (StringUtils.isNotNull(o) && !isFilterObject(o))
+ {
+ try
+ {
+ String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames));
+ params += jsonObj.toString() + " ";
+ }
+ catch (Exception e)
+ {
+ }
+ }
+ }
+ }
+ return params.trim();
+ }
+
+ /**
+ * 忽略敏感属性
+ */
+ public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames)
+ {
+ return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
+ }
+
+ /**
+ * 判断是否需要过滤的对象。
+ *
+ * @param o 对象信息。
+ * @return 如果是需要过滤的对象,则返回true;否则返回false。
+ */
+ @SuppressWarnings("rawtypes")
+ public boolean isFilterObject(final Object o)
+ {
+ Class<?> clazz = o.getClass();
+ if (clazz.isArray())
+ {
+ return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+ }
+ else if (Collection.class.isAssignableFrom(clazz))
+ {
+ Collection collection = (Collection) o;
+ for (Object value : collection)
+ {
+ return value instanceof MultipartFile;
+ }
+ }
+ else if (Map.class.isAssignableFrom(clazz))
+ {
+ Map map = (Map) o;
+ for (Object value : map.entrySet())
+ {
+ Map.Entry entry = (Map.Entry) value;
+ return entry.getValue() instanceof MultipartFile;
+ }
+ }
+ return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+ || o instanceof BindingResult;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java
new file mode 100644
index 0000000..b720bc1
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RateLimiterAspect.java
@@ -0,0 +1,89 @@
+package com.ruoyi.framework.aspectj;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.RedisScript;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.annotation.RateLimiter;
+import com.ruoyi.common.enums.LimitType;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.ip.IpUtils;
+
+/**
+ * 限流处理
+ *
+ * @author ruoyi
+ */
+@Aspect
+@Component
+public class RateLimiterAspect
+{
+ private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);
+
+ private RedisTemplate<Object, Object> redisTemplate;
+
+ private RedisScript<Long> limitScript;
+
+ @Autowired
+ public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate)
+ {
+ this.redisTemplate = redisTemplate;
+ }
+
+ @Autowired
+ public void setLimitScript(RedisScript<Long> limitScript)
+ {
+ this.limitScript = limitScript;
+ }
+
+ @Before("@annotation(rateLimiter)")
+ public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable
+ {
+ int time = rateLimiter.time();
+ int count = rateLimiter.count();
+
+ String combineKey = getCombineKey(rateLimiter, point);
+ List<Object> keys = Collections.singletonList(combineKey);
+ try
+ {
+ Long number = redisTemplate.execute(limitScript, keys, count, time);
+ if (StringUtils.isNull(number) || number.intValue() > count)
+ {
+ throw new ServiceException("访问过于频繁,请稍候再试");
+ }
+ log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);
+ }
+ catch (ServiceException e)
+ {
+ throw e;
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException("服务器限流异常,请稍候再试");
+ }
+ }
+
+ public String getCombineKey(RateLimiter rateLimiter, JoinPoint point)
+ {
+ StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
+ if (rateLimiter.limitType() == LimitType.IP)
+ {
+ stringBuffer.append(IpUtils.getIpAddr()).append("-");
+ }
+ MethodSignature signature = (MethodSignature) point.getSignature();
+ Method method = signature.getMethod();
+ Class<?> targetClass = method.getDeclaringClass();
+ stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
+ return stringBuffer.toString();
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java
new file mode 100644
index 0000000..1d4dc1f
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ApplicationConfig.java
@@ -0,0 +1,30 @@
+package com.ruoyi.framework.config;
+
+import java.util.TimeZone;
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+/**
+ * 程序注解配置
+ *
+ * @author ruoyi
+ */
+@Configuration
+// 表示通过aop框架暴露该代理对象,AopContext能够访问
+@EnableAspectJAutoProxy(exposeProxy = true)
+// 指定要扫描的Mapper类的包的路径
+@MapperScan("com.ruoyi.**.mapper")
+public class ApplicationConfig
+{
+ /**
+ * 时区配置
+ */
+ @Bean
+ public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization()
+ {
+ return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java
new file mode 100644
index 0000000..43e78ae
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/CaptchaConfig.java
@@ -0,0 +1,83 @@
+package com.ruoyi.framework.config;
+
+import java.util.Properties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import com.google.code.kaptcha.impl.DefaultKaptcha;
+import com.google.code.kaptcha.util.Config;
+import static com.google.code.kaptcha.Constants.*;
+
+/**
+ * 验证码配置
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class CaptchaConfig
+{
+ @Bean(name = "captchaProducer")
+ public DefaultKaptcha getKaptchaBean()
+ {
+ DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+ Properties properties = new Properties();
+ // 是否有边框 默认为true 我们可以自己设置yes,no
+ properties.setProperty(KAPTCHA_BORDER, "yes");
+ // 验证码文本字符颜色 默认为Color.BLACK
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
+ // 验证码图片宽度 默认为200
+ properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+ // 验证码图片高度 默认为50
+ properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+ // 验证码文本字符大小 默认为40
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
+ // KAPTCHA_SESSION_KEY
+ properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
+ // 验证码文本字符长度 默认为5
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
+ // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+ // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+ properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+ Config config = new Config(properties);
+ defaultKaptcha.setConfig(config);
+ return defaultKaptcha;
+ }
+
+ @Bean(name = "captchaProducerMath")
+ public DefaultKaptcha getKaptchaBeanMath()
+ {
+ DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
+ Properties properties = new Properties();
+ // 是否有边框 默认为true 我们可以自己设置yes,no
+ properties.setProperty(KAPTCHA_BORDER, "yes");
+ // 边框颜色 默认为Color.BLACK
+ properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
+ // 验证码文本字符颜色 默认为Color.BLACK
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
+ // 验证码图片宽度 默认为200
+ properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
+ // 验证码图片高度 默认为50
+ properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
+ // 验证码文本字符大小 默认为40
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
+ // KAPTCHA_SESSION_KEY
+ properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
+ // 验证码文本生成器
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.ruoyi.framework.config.KaptchaTextCreator");
+ // 验证码文本字符间距 默认为2
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
+ // 验证码文本字符长度 默认为5
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
+ // 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
+ properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
+ // 验证码噪点颜色 默认为Color.BLACK
+ properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
+ // 干扰实现类
+ properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
+ // 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
+ properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
+ Config config = new Config(properties);
+ defaultKaptcha.setConfig(config);
+ return defaultKaptcha;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java
new file mode 100644
index 0000000..d9cb8c5
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/DruidConfig.java
@@ -0,0 +1,126 @@
+package com.ruoyi.framework.config;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import javax.sql.DataSource;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceBuilder;
+import com.alibaba.druid.spring.boot3.autoconfigure.properties.DruidStatProperties;
+import com.alibaba.druid.util.Utils;
+import com.ruoyi.common.enums.DataSourceType;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.framework.config.properties.DruidProperties;
+import com.ruoyi.framework.datasource.DynamicDataSource;
+import jakarta.servlet.Filter;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+
+/**
+ * druid 配置多数据源
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class DruidConfig
+{
+ @Bean
+ @ConfigurationProperties("spring.datasource.druid.master")
+ public DataSource masterDataSource(DruidProperties druidProperties)
+ {
+ DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+ return druidProperties.dataSource(dataSource);
+ }
+
+ @Bean
+ @ConfigurationProperties("spring.datasource.druid.slave")
+ @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
+ public DataSource slaveDataSource(DruidProperties druidProperties)
+ {
+ DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
+ return druidProperties.dataSource(dataSource);
+ }
+
+ @Bean(name = "dynamicDataSource")
+ @Primary
+ public DynamicDataSource dataSource(DataSource masterDataSource)
+ {
+ Map<Object, Object> targetDataSources = new HashMap<>();
+ targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
+ setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
+ return new DynamicDataSource(masterDataSource, targetDataSources);
+ }
+
+ /**
+ * 设置数据源
+ *
+ * @param targetDataSources 备选数据源集合
+ * @param sourceName 数据源名称
+ * @param beanName bean名称
+ */
+ public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName)
+ {
+ try
+ {
+ DataSource dataSource = SpringUtils.getBean(beanName);
+ targetDataSources.put(sourceName, dataSource);
+ }
+ catch (Exception e)
+ {
+ }
+ }
+
+ /**
+ * 去除监控页面底部的广告
+ */
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Bean
+ @ConditionalOnProperty(name = "spring.datasource.druid.statViewServlet.enabled", havingValue = "true")
+ public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
+ {
+ // 获取web监控页面的参数
+ DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+ // 提取common.js的配置路径
+ String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+ String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+ final String filePath = "support/http/resources/js/common.js";
+ // 创建filter进行过滤
+ Filter filter = new Filter()
+ {
+ @Override
+ public void init(jakarta.servlet.FilterConfig filterConfig) throws ServletException
+ {
+ }
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException
+ {
+ chain.doFilter(request, response);
+ // 重置缓冲区,响应头不会被重置
+ response.resetBuffer();
+ // 获取common.js
+ String text = Utils.readFromResource(filePath);
+ // 正则替换banner, 除去底部的广告信息
+ text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+ text = text.replaceAll("powered.*?shrek.wang</a>", "");
+ response.getWriter().write(text);
+ }
+ @Override
+ public void destroy()
+ {
+ }
+ };
+ FilterRegistrationBean registrationBean = new FilterRegistrationBean();
+ registrationBean.setFilter(filter);
+ registrationBean.addUrlPatterns(commonJsPattern);
+ return registrationBean;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java
new file mode 100644
index 0000000..4adbb7f
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FastJson2JsonRedisSerializer.java
@@ -0,0 +1,52 @@
+package com.ruoyi.framework.config;
+
+import java.nio.charset.Charset;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
+import com.alibaba.fastjson2.filter.Filter;
+import com.ruoyi.common.constant.Constants;
+
+/**
+ * Redis使用FastJson序列化
+ *
+ * @author ruoyi
+ */
+public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
+{
+ public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+
+ static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(Constants.JSON_WHITELIST_STR);
+
+ private Class<T> clazz;
+
+ public FastJson2JsonRedisSerializer(Class<T> clazz)
+ {
+ super();
+ this.clazz = clazz;
+ }
+
+ @Override
+ public byte[] serialize(T t) throws SerializationException
+ {
+ if (t == null)
+ {
+ return new byte[0];
+ }
+ return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
+ }
+
+ @Override
+ public T deserialize(byte[] bytes) throws SerializationException
+ {
+ if (bytes == null || bytes.length <= 0)
+ {
+ return null;
+ }
+ String str = new String(bytes, DEFAULT_CHARSET);
+
+ return JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java
new file mode 100644
index 0000000..fad6bb8
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/FilterConfig.java
@@ -0,0 +1,58 @@
+package com.ruoyi.framework.config;
+
+import java.util.HashMap;
+import java.util.Map;
+import jakarta.servlet.DispatcherType;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import com.ruoyi.common.filter.RepeatableFilter;
+import com.ruoyi.common.filter.XssFilter;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * Filter配置
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class FilterConfig
+{
+ @Value("${xss.excludes}")
+ private String excludes;
+
+ @Value("${xss.urlPatterns}")
+ private String urlPatterns;
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Bean
+ @ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
+ public FilterRegistrationBean xssFilterRegistration()
+ {
+ FilterRegistrationBean registration = new FilterRegistrationBean();
+ registration.setDispatcherTypes(DispatcherType.REQUEST);
+ registration.setFilter(new XssFilter());
+ registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
+ registration.setName("xssFilter");
+ registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
+ Map<String, String> initParameters = new HashMap<String, String>();
+ initParameters.put("excludes", excludes);
+ registration.setInitParameters(initParameters);
+ return registration;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ @Bean
+ public FilterRegistrationBean someFilterRegistration()
+ {
+ FilterRegistrationBean registration = new FilterRegistrationBean();
+ registration.setFilter(new RepeatableFilter());
+ registration.addUrlPatterns("/*");
+ registration.setName("repeatableFilter");
+ registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
+ return registration;
+ }
+
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java
new file mode 100644
index 0000000..163fd01
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/I18nConfig.java
@@ -0,0 +1,43 @@
+package com.ruoyi.framework.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.LocaleResolver;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
+import org.springframework.web.servlet.i18n.SessionLocaleResolver;
+import com.ruoyi.common.constant.Constants;
+
+/**
+ * 资源文件配置加载
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class I18nConfig implements WebMvcConfigurer
+{
+ @Bean
+ public LocaleResolver localeResolver()
+ {
+ SessionLocaleResolver slr = new SessionLocaleResolver();
+ // 默认语言
+ slr.setDefaultLocale(Constants.DEFAULT_LOCALE);
+ return slr;
+ }
+
+ @Bean
+ public LocaleChangeInterceptor localeChangeInterceptor()
+ {
+ LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
+ // 参数名
+ lci.setParamName("lang");
+ return lci;
+ }
+
+ @Override
+ public void addInterceptors(InterceptorRegistry registry)
+ {
+ registry.addInterceptor(localeChangeInterceptor());
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java
new file mode 100644
index 0000000..7f8e1d5
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/KaptchaTextCreator.java
@@ -0,0 +1,68 @@
+package com.ruoyi.framework.config;
+
+import java.util.Random;
+import com.google.code.kaptcha.text.impl.DefaultTextCreator;
+
+/**
+ * 验证码文本生成器
+ *
+ * @author ruoyi
+ */
+public class KaptchaTextCreator extends DefaultTextCreator
+{
+ private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
+
+ @Override
+ public String getText()
+ {
+ Integer result = 0;
+ Random random = new Random();
+ int x = random.nextInt(10);
+ int y = random.nextInt(10);
+ StringBuilder suChinese = new StringBuilder();
+ int randomoperands = random.nextInt(3);
+ if (randomoperands == 0)
+ {
+ result = x * y;
+ suChinese.append(CNUMBERS[x]);
+ suChinese.append("*");
+ suChinese.append(CNUMBERS[y]);
+ }
+ else if (randomoperands == 1)
+ {
+ if ((x != 0) && y % x == 0)
+ {
+ result = y / x;
+ suChinese.append(CNUMBERS[y]);
+ suChinese.append("/");
+ suChinese.append(CNUMBERS[x]);
+ }
+ else
+ {
+ result = x + y;
+ suChinese.append(CNUMBERS[x]);
+ suChinese.append("+");
+ suChinese.append(CNUMBERS[y]);
+ }
+ }
+ else
+ {
+ if (x >= y)
+ {
+ result = x - y;
+ suChinese.append(CNUMBERS[x]);
+ suChinese.append("-");
+ suChinese.append(CNUMBERS[y]);
+ }
+ else
+ {
+ result = y - x;
+ suChinese.append(CNUMBERS[y]);
+ suChinese.append("-");
+ suChinese.append(CNUMBERS[x]);
+ }
+ }
+ suChinese.append("=?@" + result);
+ return suChinese.toString();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java
new file mode 100644
index 0000000..057c941
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java
@@ -0,0 +1,132 @@
+package com.ruoyi.framework.config;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import javax.sql.DataSource;
+import org.apache.ibatis.io.VFS;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.SqlSessionFactoryBean;
+import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.util.ClassUtils;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * Mybatis支持*匹配扫描包
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class MyBatisConfig
+{
+ @Autowired
+ private Environment env;
+
+ static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
+
+ public static String setTypeAliasesPackage(String typeAliasesPackage)
+ {
+ ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
+ MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
+ List<String> allResult = new ArrayList<String>();
+ try
+ {
+ for (String aliasesPackage : typeAliasesPackage.split(","))
+ {
+ List<String> result = new ArrayList<String>();
+ aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+ + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
+ Resource[] resources = resolver.getResources(aliasesPackage);
+ if (resources != null && resources.length > 0)
+ {
+ MetadataReader metadataReader = null;
+ for (Resource resource : resources)
+ {
+ if (resource.isReadable())
+ {
+ metadataReader = metadataReaderFactory.getMetadataReader(resource);
+ try
+ {
+ result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
+ }
+ catch (ClassNotFoundException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ if (result.size() > 0)
+ {
+ HashSet<String> hashResult = new HashSet<String>(result);
+ allResult.addAll(hashResult);
+ }
+ }
+ if (allResult.size() > 0)
+ {
+ typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
+ }
+ else
+ {
+ throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
+ }
+ }
+ catch (IOException e)
+ {
+ e.printStackTrace();
+ }
+ return typeAliasesPackage;
+ }
+
+ public Resource[] resolveMapperLocations(String[] mapperLocations)
+ {
+ ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
+ List<Resource> resources = new ArrayList<Resource>();
+ if (mapperLocations != null)
+ {
+ for (String mapperLocation : mapperLocations)
+ {
+ try
+ {
+ Resource[] mappers = resourceResolver.getResources(mapperLocation);
+ resources.addAll(Arrays.asList(mappers));
+ }
+ catch (IOException e)
+ {
+ // ignore
+ }
+ }
+ }
+ return resources.toArray(new Resource[resources.size()]);
+ }
+
+ @Bean
+ public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
+ {
+ String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
+ String mapperLocations = env.getProperty("mybatis.mapperLocations");
+ String configLocation = env.getProperty("mybatis.configLocation");
+ typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+ VFS.addImplClass(SpringBootVFS.class);
+
+ final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
+ sessionFactory.setDataSource(dataSource);
+ sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+ sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
+ sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+ return sessionFactory.getObject();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java
new file mode 100644
index 0000000..3453237
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/RedisConfig.java
@@ -0,0 +1,70 @@
+package com.ruoyi.framework.config;
+
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+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.core.script.DefaultRedisScript;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+/**
+ * redis配置
+ *
+ * @author ruoyi
+ */
+@SuppressWarnings("deprecation")
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport
+{
+ @Bean
+ @SuppressWarnings(value = { "unchecked", "rawtypes" })
+ public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
+ {
+ RedisTemplate<Object, Object> template = new RedisTemplate<>();
+ template.setConnectionFactory(connectionFactory);
+
+ FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+ // 使用StringRedisSerializer来序列化和反序列化redis的key值
+ template.setKeySerializer(new StringRedisSerializer());
+ template.setValueSerializer(serializer);
+
+ // Hash的key也采用StringRedisSerializer的序列化方式
+ template.setHashKeySerializer(new StringRedisSerializer());
+ template.setHashValueSerializer(serializer);
+
+ template.afterPropertiesSet();
+ return template;
+ }
+
+ @Bean
+ public DefaultRedisScript<Long> limitScript()
+ {
+ DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+ redisScript.setScriptText(limitScriptText());
+ redisScript.setResultType(Long.class);
+ return redisScript;
+ }
+
+ /**
+ * 限流脚本
+ */
+ private String limitScriptText()
+ {
+ return "local key = KEYS[1]\n" +
+ "local count = tonumber(ARGV[1])\n" +
+ "local time = tonumber(ARGV[2])\n" +
+ "local current = redis.call('get', key);\n" +
+ "if current and tonumber(current) > count then\n" +
+ " return tonumber(current);\n" +
+ "end\n" +
+ "current = redis.call('incr', key)\n" +
+ "if tonumber(current) == 1 then\n" +
+ " redis.call('expire', key, time)\n" +
+ "end\n" +
+ "return tonumber(current);";
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java
new file mode 100644
index 0000000..74fb93e
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java
@@ -0,0 +1,73 @@
+package com.ruoyi.framework.config;
+
+import java.util.concurrent.TimeUnit;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.CacheControl;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.filter.CorsFilter;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import com.ruoyi.common.config.RuoYiConfig;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
+
+/**
+ * 通用配置
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class ResourcesConfig implements WebMvcConfigurer
+{
+ @Autowired
+ private RepeatSubmitInterceptor repeatSubmitInterceptor;
+
+ @Override
+ public void addResourceHandlers(ResourceHandlerRegistry registry)
+ {
+ /** 本地文件上传路径 */
+ registry.addResourceHandler(Constants.RESOURCE_PREFIX + "/**")
+ .addResourceLocations("file:" + RuoYiConfig.getProfile() + "/");
+
+ /** swagger配置 */
+ registry.addResourceHandler("/swagger-ui/**")
+ .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/")
+ .setCacheControl(CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic());;
+ }
+
+ /**
+ * 自定义拦截规则
+ */
+ @Override
+ public void addInterceptors(InterceptorRegistry registry)
+ {
+ registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
+ }
+
+ /**
+ * 跨域配置
+ */
+ @Bean
+ public CorsFilter corsFilter()
+ {
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowCredentials(true);
+ // 设置访问源地址
+ config.addAllowedOriginPattern("*");
+ // 设置访问源请求头
+ config.addAllowedHeader("*");
+ // 设置访问源请求方法
+ config.addAllowedMethod("*");
+ // 有效期 1800秒
+ config.setMaxAge(1800L);
+ // 添加映射路径,拦截一切请求
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/**", config);
+ // 返回新的CorsFilter
+ return new CorsFilter(source);
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
new file mode 100644
index 0000000..330039f
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
@@ -0,0 +1,139 @@
+package com.ruoyi.framework.config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.ProviderManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+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.web.SecurityFilterChain;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import org.springframework.security.web.authentication.logout.LogoutFilter;
+import org.springframework.web.filter.CorsFilter;
+import com.ruoyi.framework.config.properties.PermitAllUrlProperties;
+import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
+import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
+import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
+
+/**
+ * spring security配置
+ *
+ * @author ruoyi
+ */
+@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
+@Configuration
+public class SecurityConfig
+{
+ /**
+ * 自定义用户认证逻辑
+ */
+ @Autowired
+ private UserDetailsService userDetailsService;
+
+ /**
+ * 认证失败处理类
+ */
+ @Autowired
+ private AuthenticationEntryPointImpl unauthorizedHandler;
+
+ /**
+ * 退出处理类
+ */
+ @Autowired
+ private LogoutSuccessHandlerImpl logoutSuccessHandler;
+
+ /**
+ * token认证过滤器
+ */
+ @Autowired
+ private JwtAuthenticationTokenFilter authenticationTokenFilter;
+
+ /**
+ * 跨域过滤器
+ */
+ @Autowired
+ private CorsFilter corsFilter;
+
+ /**
+ * 允许匿名访问的地址
+ */
+ @Autowired
+ private PermitAllUrlProperties permitAllUrl;
+
+ /**
+ * 身份验证实现
+ */
+ @Bean
+ public AuthenticationManager authenticationManager()
+ {
+ DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
+ daoAuthenticationProvider.setUserDetailsService(userDetailsService);
+ daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
+ return new ProviderManager(daoAuthenticationProvider);
+ }
+
+ /**
+ * anyRequest | 匹配所有请求路径
+ * access | SpringEl表达式结果为true时可以访问
+ * anonymous | 匿名可以访问
+ * denyAll | 用户不能访问
+ * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
+ * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
+ * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
+ * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
+ * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+ * hasRole | 如果有参数,参数表示角色,则其角色可以访问
+ * permitAll | 用户可以任意访问
+ * rememberMe | 允许通过remember-me登录的用户访问
+ * authenticated | 用户登录后可访问
+ */
+ @Bean
+ protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception
+ {
+ return httpSecurity
+ // CSRF禁用,因为不使用session
+ .csrf(csrf -> csrf.disable())
+ // 禁用HTTP响应标头
+ .headers((headersCustomizer) -> {
+ headersCustomizer.cacheControl(cache -> cache.disable()).frameOptions(options -> options.sameOrigin());
+ })
+ // 认证失败处理类
+ .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
+ // 基于token,所以不需要session
+ .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
+ // 注解标记允许匿名访问的url
+ .authorizeHttpRequests((requests) -> {
+ permitAllUrl.getUrls().forEach(url -> requests.requestMatchers(url).permitAll());
+ // 对于登录login 注册register 验证码captchaImage 允许匿名访问
+ requests.requestMatchers("/login", "/register", "/captchaImage").permitAll()
+ // 静态资源,可匿名访问
+ .requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll()
+ .requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll()
+ // 除上面外的所有请求全部需要鉴权认证
+ .anyRequest().authenticated();
+ })
+ // 添加Logout filter
+ .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
+ // 添加JWT filter
+ .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
+ // 添加CORS filter
+ .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)
+ .addFilterBefore(corsFilter, LogoutFilter.class)
+ .build();
+ }
+
+ /**
+ * 强散列哈希加密实现
+ */
+ @Bean
+ public BCryptPasswordEncoder bCryptPasswordEncoder()
+ {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java
new file mode 100644
index 0000000..cd8fb09
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ServerConfig.java
@@ -0,0 +1,32 @@
+package com.ruoyi.framework.config;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.utils.ServletUtils;
+
+/**
+ * 服务相关配置
+ *
+ * @author ruoyi
+ */
+@Component
+public class ServerConfig
+{
+ /**
+ * 获取完整的请求路径,包括:域名,端口,上下文访问路径
+ *
+ * @return 服务地址
+ */
+ public String getUrl()
+ {
+ HttpServletRequest request = ServletUtils.getRequest();
+ return getDomain(request);
+ }
+
+ public static String getDomain(HttpServletRequest request)
+ {
+ StringBuffer url = request.getRequestURL();
+ String contextPath = request.getServletContext().getContextPath();
+ return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java
new file mode 100644
index 0000000..7840141
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/ThreadPoolConfig.java
@@ -0,0 +1,63 @@
+package com.ruoyi.framework.config;
+
+import com.ruoyi.common.utils.Threads;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+ * @author ruoyi
+ **/
+@Configuration
+public class ThreadPoolConfig
+{
+ // 核心线程池大小
+ private int corePoolSize = 50;
+
+ // 最大可创建的线程数
+ private int maxPoolSize = 200;
+
+ // 队列最大长度
+ private int queueCapacity = 1000;
+
+ // 线程池维护线程所允许的空闲时间
+ private int keepAliveSeconds = 300;
+
+ @Bean(name = "threadPoolTaskExecutor")
+ public ThreadPoolTaskExecutor threadPoolTaskExecutor()
+ {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setMaxPoolSize(maxPoolSize);
+ executor.setCorePoolSize(corePoolSize);
+ executor.setQueueCapacity(queueCapacity);
+ executor.setKeepAliveSeconds(keepAliveSeconds);
+ // 线程池对拒绝任务(无线程可用)的处理策略
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+ return executor;
+ }
+
+ /**
+ * 执行周期性或定时任务
+ */
+ @Bean(name = "scheduledExecutorService")
+ protected ScheduledExecutorService scheduledExecutorService()
+ {
+ return new ScheduledThreadPoolExecutor(corePoolSize,
+ new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
+ new ThreadPoolExecutor.CallerRunsPolicy())
+ {
+ @Override
+ protected void afterExecute(Runnable r, Throwable t)
+ {
+ super.afterExecute(r, t);
+ Threads.printException(r, t);
+ }
+ };
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java
new file mode 100644
index 0000000..c8a5c8a
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/DruidProperties.java
@@ -0,0 +1,89 @@
+package com.ruoyi.framework.config.properties;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import com.alibaba.druid.pool.DruidDataSource;
+
+/**
+ * druid 配置属性
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class DruidProperties
+{
+ @Value("${spring.datasource.druid.initialSize}")
+ private int initialSize;
+
+ @Value("${spring.datasource.druid.minIdle}")
+ private int minIdle;
+
+ @Value("${spring.datasource.druid.maxActive}")
+ private int maxActive;
+
+ @Value("${spring.datasource.druid.maxWait}")
+ private int maxWait;
+
+ @Value("${spring.datasource.druid.connectTimeout}")
+ private int connectTimeout;
+
+ @Value("${spring.datasource.druid.socketTimeout}")
+ private int socketTimeout;
+
+ @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
+ private int timeBetweenEvictionRunsMillis;
+
+ @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
+ private int minEvictableIdleTimeMillis;
+
+ @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
+ private int maxEvictableIdleTimeMillis;
+
+ @Value("${spring.datasource.druid.validationQuery}")
+ private String validationQuery;
+
+ @Value("${spring.datasource.druid.testWhileIdle}")
+ private boolean testWhileIdle;
+
+ @Value("${spring.datasource.druid.testOnBorrow}")
+ private boolean testOnBorrow;
+
+ @Value("${spring.datasource.druid.testOnReturn}")
+ private boolean testOnReturn;
+
+ public DruidDataSource dataSource(DruidDataSource datasource)
+ {
+ /** 配置初始化大小、最小、最大 */
+ datasource.setInitialSize(initialSize);
+ datasource.setMaxActive(maxActive);
+ datasource.setMinIdle(minIdle);
+
+ /** 配置获取连接等待超时的时间 */
+ datasource.setMaxWait(maxWait);
+
+ /** 配置驱动连接超时时间,检测数据库建立连接的超时时间,单位是毫秒 */
+ datasource.setConnectTimeout(connectTimeout);
+
+ /** 配置网络超时时间,等待数据库操作完成的网络超时时间,单位是毫秒 */
+ datasource.setSocketTimeout(socketTimeout);
+
+ /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
+ datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
+
+ /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
+ datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
+ datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
+
+ /**
+ * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
+ */
+ datasource.setValidationQuery(validationQuery);
+ /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
+ datasource.setTestWhileIdle(testWhileIdle);
+ /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+ datasource.setTestOnBorrow(testOnBorrow);
+ /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+ datasource.setTestOnReturn(testOnReturn);
+ return datasource;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java
new file mode 100644
index 0000000..277ba3b
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/properties/PermitAllUrlProperties.java
@@ -0,0 +1,73 @@
+package com.ruoyi.framework.config.properties;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import org.apache.commons.lang3.RegExUtils;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
+import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
+import com.ruoyi.common.annotation.Anonymous;
+
+/**
+ * 设置Anonymous注解允许匿名访问的url
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware
+{
+ private static final Pattern PATTERN = Pattern.compile("\\{(.*?)\\}");
+
+ private ApplicationContext applicationContext;
+
+ private List<String> urls = new ArrayList<>();
+
+ public String ASTERISK = "*";
+
+ @Override
+ public void afterPropertiesSet()
+ {
+ RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
+ Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
+
+ map.keySet().forEach(info -> {
+ HandlerMethod handlerMethod = map.get(info);
+
+ // 获取方法上边的注解 替代path variable 为 *
+ Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class);
+ Optional.ofNullable(method).ifPresent(anonymous -> Objects.requireNonNull(info.getPathPatternsCondition().getPatternValues()) //
+ .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));
+
+ // 获取类上边的注解, 替代path variable 为 *
+ Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class);
+ Optional.ofNullable(controller).ifPresent(anonymous -> Objects.requireNonNull(info.getPathPatternsCondition().getPatternValues())
+ .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));
+ });
+ }
+
+ @Override
+ public void setApplicationContext(ApplicationContext context) throws BeansException
+ {
+ this.applicationContext = context;
+ }
+
+ public List<String> getUrls()
+ {
+ return urls;
+ }
+
+ public void setUrls(List<String> urls)
+ {
+ this.urls = urls;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java
new file mode 100644
index 0000000..e70b8cf
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSource.java
@@ -0,0 +1,26 @@
+package com.ruoyi.framework.datasource;
+
+import java.util.Map;
+import javax.sql.DataSource;
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+/**
+ * 动态数据源
+ *
+ * @author ruoyi
+ */
+public class DynamicDataSource extends AbstractRoutingDataSource
+{
+ public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
+ {
+ super.setDefaultTargetDataSource(defaultTargetDataSource);
+ super.setTargetDataSources(targetDataSources);
+ super.afterPropertiesSet();
+ }
+
+ @Override
+ protected Object determineCurrentLookupKey()
+ {
+ return DynamicDataSourceContextHolder.getDataSourceType();
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java
new file mode 100644
index 0000000..9770af6
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/datasource/DynamicDataSourceContextHolder.java
@@ -0,0 +1,45 @@
+package com.ruoyi.framework.datasource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 数据源切换处理
+ *
+ * @author ruoyi
+ */
+public class DynamicDataSourceContextHolder
+{
+ public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
+
+ /**
+ * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
+ * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
+ */
+ private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
+
+ /**
+ * 设置数据源的变量
+ */
+ public static void setDataSourceType(String dsType)
+ {
+ log.info("切换到{}数据源", dsType);
+ CONTEXT_HOLDER.set(dsType);
+ }
+
+ /**
+ * 获得数据源的变量
+ */
+ public static String getDataSourceType()
+ {
+ return CONTEXT_HOLDER.get();
+ }
+
+ /**
+ * 清空数据源变量
+ */
+ public static void clearDataSourceType()
+ {
+ CONTEXT_HOLDER.remove();
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java
new file mode 100644
index 0000000..4e8d20f
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/RepeatSubmitInterceptor.java
@@ -0,0 +1,56 @@
+package com.ruoyi.framework.interceptor;
+
+import java.lang.reflect.Method;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.annotation.RepeatSubmit;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.ServletUtils;
+
+/**
+ * 防止重复提交拦截器
+ *
+ * @author ruoyi
+ */
+@Component
+public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
+{
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
+ {
+ if (handler instanceof HandlerMethod)
+ {
+ HandlerMethod handlerMethod = (HandlerMethod) handler;
+ Method method = handlerMethod.getMethod();
+ RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
+ if (annotation != null)
+ {
+ if (this.isRepeatSubmit(request, annotation))
+ {
+ AjaxResult ajaxResult = AjaxResult.error(annotation.message());
+ ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
+ return false;
+ }
+ }
+ return true;
+ }
+ else
+ {
+ return true;
+ }
+ }
+
+ /**
+ * 验证是否重复提交由子类实现具体的防重复提交的规则
+ *
+ * @param request 请求信息
+ * @param annotation 防重复注解参数
+ * @return 结果
+ * @throws Exception
+ */
+ public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java
new file mode 100644
index 0000000..c93df50
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/impl/SameUrlDataInterceptor.java
@@ -0,0 +1,110 @@
+package com.ruoyi.framework.interceptor.impl;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.annotation.RepeatSubmit;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.filter.RepeatedlyRequestWrapper;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.http.HttpHelper;
+import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
+
+/**
+ * 判断请求url和数据是否和上一次相同,
+ * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
+ *
+ * @author ruoyi
+ */
+@Component
+public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
+{
+ public final String REPEAT_PARAMS = "repeatParams";
+
+ public final String REPEAT_TIME = "repeatTime";
+
+ // 令牌自定义标识
+ @Value("${token.header}")
+ private String header;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation)
+ {
+ String nowParams = "";
+ if (request instanceof RepeatedlyRequestWrapper)
+ {
+ RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
+ nowParams = HttpHelper.getBodyString(repeatedlyRequest);
+ }
+
+ // body参数为空,获取Parameter的数据
+ if (StringUtils.isEmpty(nowParams))
+ {
+ nowParams = JSON.toJSONString(request.getParameterMap());
+ }
+ Map<String, Object> nowDataMap = new HashMap<String, Object>();
+ nowDataMap.put(REPEAT_PARAMS, nowParams);
+ nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
+
+ // 请求地址(作为存放cache的key值)
+ String url = request.getRequestURI();
+
+ // 唯一值(没有消息头则使用请求地址)
+ String submitKey = StringUtils.trimToEmpty(request.getHeader(header));
+
+ // 唯一标识(指定key + url + 消息头)
+ String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
+
+ Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
+ if (sessionObj != null)
+ {
+ Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
+ if (sessionMap.containsKey(url))
+ {
+ Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
+ if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval()))
+ {
+ return true;
+ }
+ }
+ }
+ Map<String, Object> cacheMap = new HashMap<String, Object>();
+ cacheMap.put(url, nowDataMap);
+ redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
+ return false;
+ }
+
+ /**
+ * 判断参数是否相同
+ */
+ private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
+ {
+ String nowParams = (String) nowMap.get(REPEAT_PARAMS);
+ String preParams = (String) preMap.get(REPEAT_PARAMS);
+ return nowParams.equals(preParams);
+ }
+
+ /**
+ * 判断两次间隔时间
+ */
+ private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval)
+ {
+ long time1 = (Long) nowMap.get(REPEAT_TIME);
+ long time2 = (Long) preMap.get(REPEAT_TIME);
+ if ((time1 - time2) < interval)
+ {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java
new file mode 100644
index 0000000..7387a02
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/AsyncManager.java
@@ -0,0 +1,55 @@
+package com.ruoyi.framework.manager;
+
+import java.util.TimerTask;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import com.ruoyi.common.utils.Threads;
+import com.ruoyi.common.utils.spring.SpringUtils;
+
+/**
+ * 异步任务管理器
+ *
+ * @author ruoyi
+ */
+public class AsyncManager
+{
+ /**
+ * 操作延迟10毫秒
+ */
+ private final int OPERATE_DELAY_TIME = 10;
+
+ /**
+ * 异步操作任务调度线程池
+ */
+ private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
+
+ /**
+ * 单例模式
+ */
+ private AsyncManager(){}
+
+ private static AsyncManager me = new AsyncManager();
+
+ public static AsyncManager me()
+ {
+ return me;
+ }
+
+ /**
+ * 执行任务
+ *
+ * @param task 任务
+ */
+ public void execute(TimerTask task)
+ {
+ executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * 停止任务线程池
+ */
+ public void shutdown()
+ {
+ Threads.shutdownAndAwaitTermination(executor);
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java
new file mode 100644
index 0000000..095b865
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/ShutdownManager.java
@@ -0,0 +1,39 @@
+package com.ruoyi.framework.manager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import jakarta.annotation.PreDestroy;
+
+/**
+ * 确保应用退出时能关闭后台线程
+ *
+ * @author ruoyi
+ */
+@Component
+public class ShutdownManager
+{
+ private static final Logger logger = LoggerFactory.getLogger("sys-user");
+
+ @PreDestroy
+ public void destroy()
+ {
+ shutdownAsyncManager();
+ }
+
+ /**
+ * 停止异步执行任务
+ */
+ private void shutdownAsyncManager()
+ {
+ try
+ {
+ logger.info("====关闭后台任务任务线程池====");
+ AsyncManager.me().shutdown();
+ }
+ catch (Exception e)
+ {
+ logger.error(e.getMessage(), e);
+ }
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java
new file mode 100644
index 0000000..267e305
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/factory/AsyncFactory.java
@@ -0,0 +1,102 @@
+package com.ruoyi.framework.manager.factory;
+
+import java.util.TimerTask;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.utils.LogUtils;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.ip.AddressUtils;
+import com.ruoyi.common.utils.ip.IpUtils;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.system.domain.SysLogininfor;
+import com.ruoyi.system.domain.SysOperLog;
+import com.ruoyi.system.service.ISysLogininforService;
+import com.ruoyi.system.service.ISysOperLogService;
+import eu.bitwalker.useragentutils.UserAgent;
+
+/**
+ * 异步工厂(产生任务用)
+ *
+ * @author ruoyi
+ */
+public class AsyncFactory
+{
+ private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
+
+ /**
+ * 记录登录信息
+ *
+ * @param username 用户名
+ * @param status 状态
+ * @param message 消息
+ * @param args 列表
+ * @return 任务task
+ */
+ public static TimerTask recordLogininfor(final String username, final String status, final String message,
+ final Object... args)
+ {
+ final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
+ final String ip = IpUtils.getIpAddr();
+ return new TimerTask()
+ {
+ @Override
+ public void run()
+ {
+ String address = AddressUtils.getRealAddressByIP(ip);
+ StringBuilder s = new StringBuilder();
+ s.append(LogUtils.getBlock(ip));
+ s.append(address);
+ s.append(LogUtils.getBlock(username));
+ s.append(LogUtils.getBlock(status));
+ s.append(LogUtils.getBlock(message));
+ // 打印信息到日志
+ sys_user_logger.info(s.toString(), args);
+ // 获取客户端操作系统
+ String os = userAgent.getOperatingSystem().getName();
+ // 获取客户端浏览器
+ String browser = userAgent.getBrowser().getName();
+ // 封装对象
+ SysLogininfor logininfor = new SysLogininfor();
+ logininfor.setUserName(username);
+ logininfor.setIpaddr(ip);
+ logininfor.setLoginLocation(address);
+ logininfor.setBrowser(browser);
+ logininfor.setOs(os);
+ logininfor.setMsg(message);
+ // 日志状态
+ if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
+ {
+ logininfor.setStatus(Constants.SUCCESS);
+ }
+ else if (Constants.LOGIN_FAIL.equals(status))
+ {
+ logininfor.setStatus(Constants.FAIL);
+ }
+ // 插入数据
+ SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
+ }
+ };
+ }
+
+ /**
+ * 操作日志记录
+ *
+ * @param operLog 操作日志信息
+ * @return 任务task
+ */
+ public static TimerTask recordOper(final SysOperLog operLog)
+ {
+ return new TimerTask()
+ {
+ @Override
+ public void run()
+ {
+ // 远程查询操作地点
+ operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
+ SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
+ }
+ };
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java
new file mode 100644
index 0000000..6c776ce
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/AuthenticationContextHolder.java
@@ -0,0 +1,28 @@
+package com.ruoyi.framework.security.context;
+
+import org.springframework.security.core.Authentication;
+
+/**
+ * 身份验证信息
+ *
+ * @author ruoyi
+ */
+public class AuthenticationContextHolder
+{
+ private static final ThreadLocal<Authentication> contextHolder = new ThreadLocal<>();
+
+ public static Authentication getContext()
+ {
+ return contextHolder.get();
+ }
+
+ public static void setContext(Authentication context)
+ {
+ contextHolder.set(context);
+ }
+
+ public static void clearContext()
+ {
+ contextHolder.remove();
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java
new file mode 100644
index 0000000..5472f3d
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/context/PermissionContextHolder.java
@@ -0,0 +1,27 @@
+package com.ruoyi.framework.security.context;
+
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import com.ruoyi.common.core.text.Convert;
+
+/**
+ * 权限信息
+ *
+ * @author ruoyi
+ */
+public class PermissionContextHolder
+{
+ private static final String PERMISSION_CONTEXT_ATTRIBUTES = "PERMISSION_CONTEXT";
+
+ public static void setContext(String permission)
+ {
+ RequestContextHolder.currentRequestAttributes().setAttribute(PERMISSION_CONTEXT_ATTRIBUTES, permission,
+ RequestAttributes.SCOPE_REQUEST);
+ }
+
+ public static String getContext()
+ {
+ return Convert.toStr(RequestContextHolder.currentRequestAttributes().getAttribute(PERMISSION_CONTEXT_ATTRIBUTES,
+ RequestAttributes.SCOPE_REQUEST));
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java
new file mode 100644
index 0000000..9d2f494
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java
@@ -0,0 +1,44 @@
+package com.ruoyi.framework.security.filter;
+
+import java.io.IOException;
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.web.service.TokenService;
+
+/**
+ * token过滤器 验证token有效性
+ *
+ * @author ruoyi
+ */
+@Component
+public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
+{
+ @Autowired
+ private TokenService tokenService;
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+ throws ServletException, IOException
+ {
+ LoginUser loginUser = tokenService.getLoginUser(request);
+ if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
+ {
+ tokenService.verifyToken(loginUser);
+ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
+ authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
+ SecurityContextHolder.getContext().setAuthentication(authenticationToken);
+ }
+ chain.doFilter(request, response);
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java
new file mode 100644
index 0000000..e1789f8
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/AuthenticationEntryPointImpl.java
@@ -0,0 +1,34 @@
+package com.ruoyi.framework.security.handle;
+
+import java.io.IOException;
+import java.io.Serializable;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.constant.HttpStatus;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
+
+/**
+ * 认证失败处理类 返回未授权
+ *
+ * @author ruoyi
+ */
+@Component
+public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
+{
+ private static final long serialVersionUID = -8970718410437077606L;
+
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
+ throws IOException
+ {
+ int code = HttpStatus.UNAUTHORIZED;
+ String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
+ ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java
new file mode 100644
index 0000000..595bd3f
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/security/handle/LogoutSuccessHandlerImpl.java
@@ -0,0 +1,53 @@
+package com.ruoyi.framework.security.handle;
+
+import java.io.IOException;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
+import com.alibaba.fastjson2.JSON;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.manager.AsyncManager;
+import com.ruoyi.framework.manager.factory.AsyncFactory;
+import com.ruoyi.framework.web.service.TokenService;
+
+/**
+ * 自定义退出处理类 返回成功
+ *
+ * @author ruoyi
+ */
+@Configuration
+public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler
+{
+ @Autowired
+ private TokenService tokenService;
+
+ /**
+ * 退出处理
+ *
+ * @return
+ */
+ @Override
+ public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
+ throws IOException, ServletException
+ {
+ LoginUser loginUser = tokenService.getLoginUser(request);
+ if (StringUtils.isNotNull(loginUser))
+ {
+ String userName = loginUser.getUsername();
+ // 删除用户缓存记录
+ tokenService.delLoginUser(loginUser.getToken());
+ // 记录用户退出日志
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGOUT, MessageUtils.message("user.logout.success")));
+ }
+ ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.success(MessageUtils.message("user.logout.success"))));
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java
new file mode 100644
index 0000000..63b03da
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/Server.java
@@ -0,0 +1,240 @@
+package com.ruoyi.framework.web.domain;
+
+import java.net.UnknownHostException;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Properties;
+import com.ruoyi.common.utils.Arith;
+import com.ruoyi.common.utils.ip.IpUtils;
+import com.ruoyi.framework.web.domain.server.Cpu;
+import com.ruoyi.framework.web.domain.server.Jvm;
+import com.ruoyi.framework.web.domain.server.Mem;
+import com.ruoyi.framework.web.domain.server.Sys;
+import com.ruoyi.framework.web.domain.server.SysFile;
+import oshi.SystemInfo;
+import oshi.hardware.CentralProcessor;
+import oshi.hardware.CentralProcessor.TickType;
+import oshi.hardware.GlobalMemory;
+import oshi.hardware.HardwareAbstractionLayer;
+import oshi.software.os.FileSystem;
+import oshi.software.os.OSFileStore;
+import oshi.software.os.OperatingSystem;
+import oshi.util.Util;
+
+/**
+ * 服务器相关信息
+ *
+ * @author ruoyi
+ */
+public class Server
+{
+ private static final int OSHI_WAIT_SECOND = 1000;
+
+ /**
+ * CPU相关信息
+ */
+ private Cpu cpu = new Cpu();
+
+ /**
+ * 內存相关信息
+ */
+ private Mem mem = new Mem();
+
+ /**
+ * JVM相关信息
+ */
+ private Jvm jvm = new Jvm();
+
+ /**
+ * 服务器相关信息
+ */
+ private Sys sys = new Sys();
+
+ /**
+ * 磁盘相关信息
+ */
+ private List<SysFile> sysFiles = new LinkedList<SysFile>();
+
+ public Cpu getCpu()
+ {
+ return cpu;
+ }
+
+ public void setCpu(Cpu cpu)
+ {
+ this.cpu = cpu;
+ }
+
+ public Mem getMem()
+ {
+ return mem;
+ }
+
+ public void setMem(Mem mem)
+ {
+ this.mem = mem;
+ }
+
+ public Jvm getJvm()
+ {
+ return jvm;
+ }
+
+ public void setJvm(Jvm jvm)
+ {
+ this.jvm = jvm;
+ }
+
+ public Sys getSys()
+ {
+ return sys;
+ }
+
+ public void setSys(Sys sys)
+ {
+ this.sys = sys;
+ }
+
+ public List<SysFile> getSysFiles()
+ {
+ return sysFiles;
+ }
+
+ public void setSysFiles(List<SysFile> sysFiles)
+ {
+ this.sysFiles = sysFiles;
+ }
+
+ public void copyTo() throws Exception
+ {
+ SystemInfo si = new SystemInfo();
+ HardwareAbstractionLayer hal = si.getHardware();
+
+ setCpuInfo(hal.getProcessor());
+
+ setMemInfo(hal.getMemory());
+
+ setSysInfo();
+
+ setJvmInfo();
+
+ setSysFiles(si.getOperatingSystem());
+ }
+
+ /**
+ * 设置CPU信息
+ */
+ private void setCpuInfo(CentralProcessor processor)
+ {
+ // CPU信息
+ long[] prevTicks = processor.getSystemCpuLoadTicks();
+ Util.sleep(OSHI_WAIT_SECOND);
+ long[] ticks = processor.getSystemCpuLoadTicks();
+ long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()];
+ long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()];
+ long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()];
+ long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()];
+ long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()];
+ long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()];
+ long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()];
+ long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()];
+ long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal;
+ cpu.setCpuNum(processor.getLogicalProcessorCount());
+ cpu.setTotal(totalCpu);
+ cpu.setSys(cSys);
+ cpu.setUsed(user);
+ cpu.setWait(iowait);
+ cpu.setFree(idle);
+ }
+
+ /**
+ * 设置内存信息
+ */
+ private void setMemInfo(GlobalMemory memory)
+ {
+ mem.setTotal(memory.getTotal());
+ mem.setUsed(memory.getTotal() - memory.getAvailable());
+ mem.setFree(memory.getAvailable());
+ }
+
+ /**
+ * 设置服务器信息
+ */
+ private void setSysInfo()
+ {
+ Properties props = System.getProperties();
+ sys.setComputerName(IpUtils.getHostName());
+ sys.setComputerIp(IpUtils.getHostIp());
+ sys.setOsName(props.getProperty("os.name"));
+ sys.setOsArch(props.getProperty("os.arch"));
+ sys.setUserDir(props.getProperty("user.dir"));
+ }
+
+ /**
+ * 设置Java虚拟机
+ */
+ private void setJvmInfo() throws UnknownHostException
+ {
+ Properties props = System.getProperties();
+ jvm.setTotal(Runtime.getRuntime().totalMemory());
+ jvm.setMax(Runtime.getRuntime().maxMemory());
+ jvm.setFree(Runtime.getRuntime().freeMemory());
+ jvm.setVersion(props.getProperty("java.version"));
+ jvm.setHome(props.getProperty("java.home"));
+ }
+
+ /**
+ * 设置磁盘信息
+ */
+ private void setSysFiles(OperatingSystem os)
+ {
+ FileSystem fileSystem = os.getFileSystem();
+ List<OSFileStore> fsArray = fileSystem.getFileStores();
+ for (OSFileStore fs : fsArray)
+ {
+ long free = fs.getUsableSpace();
+ long total = fs.getTotalSpace();
+ long used = total - free;
+ SysFile sysFile = new SysFile();
+ sysFile.setDirName(fs.getMount());
+ sysFile.setSysTypeName(fs.getType());
+ sysFile.setTypeName(fs.getName());
+ sysFile.setTotal(convertFileSize(total));
+ sysFile.setFree(convertFileSize(free));
+ sysFile.setUsed(convertFileSize(used));
+ sysFile.setUsage(Arith.mul(Arith.div(used, total, 4), 100));
+ sysFiles.add(sysFile);
+ }
+ }
+
+ /**
+ * 字节转换
+ *
+ * @param size 字节大小
+ * @return 转换后值
+ */
+ public String convertFileSize(long size)
+ {
+ long kb = 1024;
+ long mb = kb * 1024;
+ long gb = mb * 1024;
+ if (size >= gb)
+ {
+ return String.format("%.1f GB", (float) size / gb);
+ }
+ else if (size >= mb)
+ {
+ float f = (float) size / mb;
+ return String.format(f > 100 ? "%.0f MB" : "%.1f MB", f);
+ }
+ else if (size >= kb)
+ {
+ float f = (float) size / kb;
+ return String.format(f > 100 ? "%.0f KB" : "%.1f KB", f);
+ }
+ else
+ {
+ return String.format("%d B", size);
+ }
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java
new file mode 100644
index 0000000..a13a66c
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Cpu.java
@@ -0,0 +1,101 @@
+package com.ruoyi.framework.web.domain.server;
+
+import com.ruoyi.common.utils.Arith;
+
+/**
+ * CPU相关信息
+ *
+ * @author ruoyi
+ */
+public class Cpu
+{
+ /**
+ * 核心数
+ */
+ private int cpuNum;
+
+ /**
+ * CPU总的使用率
+ */
+ private double total;
+
+ /**
+ * CPU系统使用率
+ */
+ private double sys;
+
+ /**
+ * CPU用户使用率
+ */
+ private double used;
+
+ /**
+ * CPU当前等待率
+ */
+ private double wait;
+
+ /**
+ * CPU当前空闲率
+ */
+ private double free;
+
+ public int getCpuNum()
+ {
+ return cpuNum;
+ }
+
+ public void setCpuNum(int cpuNum)
+ {
+ this.cpuNum = cpuNum;
+ }
+
+ public double getTotal()
+ {
+ return Arith.round(Arith.mul(total, 100), 2);
+ }
+
+ public void setTotal(double total)
+ {
+ this.total = total;
+ }
+
+ public double getSys()
+ {
+ return Arith.round(Arith.mul(sys / total, 100), 2);
+ }
+
+ public void setSys(double sys)
+ {
+ this.sys = sys;
+ }
+
+ public double getUsed()
+ {
+ return Arith.round(Arith.mul(used / total, 100), 2);
+ }
+
+ public void setUsed(double used)
+ {
+ this.used = used;
+ }
+
+ public double getWait()
+ {
+ return Arith.round(Arith.mul(wait / total, 100), 2);
+ }
+
+ public void setWait(double wait)
+ {
+ this.wait = wait;
+ }
+
+ public double getFree()
+ {
+ return Arith.round(Arith.mul(free / total, 100), 2);
+ }
+
+ public void setFree(double free)
+ {
+ this.free = free;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java
new file mode 100644
index 0000000..1fdc6ac
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Jvm.java
@@ -0,0 +1,130 @@
+package com.ruoyi.framework.web.domain.server;
+
+import java.lang.management.ManagementFactory;
+import com.ruoyi.common.utils.Arith;
+import com.ruoyi.common.utils.DateUtils;
+
+/**
+ * JVM相关信息
+ *
+ * @author ruoyi
+ */
+public class Jvm
+{
+ /**
+ * 当前JVM占用的内存总数(M)
+ */
+ private double total;
+
+ /**
+ * JVM最大可用内存总数(M)
+ */
+ private double max;
+
+ /**
+ * JVM空闲内存(M)
+ */
+ private double free;
+
+ /**
+ * JDK版本
+ */
+ private String version;
+
+ /**
+ * JDK路径
+ */
+ private String home;
+
+ public double getTotal()
+ {
+ return Arith.div(total, (1024 * 1024), 2);
+ }
+
+ public void setTotal(double total)
+ {
+ this.total = total;
+ }
+
+ public double getMax()
+ {
+ return Arith.div(max, (1024 * 1024), 2);
+ }
+
+ public void setMax(double max)
+ {
+ this.max = max;
+ }
+
+ public double getFree()
+ {
+ return Arith.div(free, (1024 * 1024), 2);
+ }
+
+ public void setFree(double free)
+ {
+ this.free = free;
+ }
+
+ public double getUsed()
+ {
+ return Arith.div(total - free, (1024 * 1024), 2);
+ }
+
+ public double getUsage()
+ {
+ return Arith.mul(Arith.div(total - free, total, 4), 100);
+ }
+
+ /**
+ * 获取JDK名称
+ */
+ public String getName()
+ {
+ return ManagementFactory.getRuntimeMXBean().getVmName();
+ }
+
+ public String getVersion()
+ {
+ return version;
+ }
+
+ public void setVersion(String version)
+ {
+ this.version = version;
+ }
+
+ public String getHome()
+ {
+ return home;
+ }
+
+ public void setHome(String home)
+ {
+ this.home = home;
+ }
+
+ /**
+ * JDK启动时间
+ */
+ public String getStartTime()
+ {
+ return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, DateUtils.getServerStartDate());
+ }
+
+ /**
+ * JDK运行时间
+ */
+ public String getRunTime()
+ {
+ return DateUtils.timeDistance(DateUtils.getNowDate(), DateUtils.getServerStartDate());
+ }
+
+ /**
+ * 运行参数
+ */
+ public String getInputArgs()
+ {
+ return ManagementFactory.getRuntimeMXBean().getInputArguments().toString();
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java
new file mode 100644
index 0000000..13eec52
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Mem.java
@@ -0,0 +1,61 @@
+package com.ruoyi.framework.web.domain.server;
+
+import com.ruoyi.common.utils.Arith;
+
+/**
+ * 內存相关信息
+ *
+ * @author ruoyi
+ */
+public class Mem
+{
+ /**
+ * 内存总量
+ */
+ private double total;
+
+ /**
+ * 已用内存
+ */
+ private double used;
+
+ /**
+ * 剩余内存
+ */
+ private double free;
+
+ public double getTotal()
+ {
+ return Arith.div(total, (1024 * 1024 * 1024), 2);
+ }
+
+ public void setTotal(long total)
+ {
+ this.total = total;
+ }
+
+ public double getUsed()
+ {
+ return Arith.div(used, (1024 * 1024 * 1024), 2);
+ }
+
+ public void setUsed(long used)
+ {
+ this.used = used;
+ }
+
+ public double getFree()
+ {
+ return Arith.div(free, (1024 * 1024 * 1024), 2);
+ }
+
+ public void setFree(long free)
+ {
+ this.free = free;
+ }
+
+ public double getUsage()
+ {
+ return Arith.mul(Arith.div(used, total, 4), 100);
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java
new file mode 100644
index 0000000..45d64d9
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/Sys.java
@@ -0,0 +1,84 @@
+package com.ruoyi.framework.web.domain.server;
+
+/**
+ * 系统相关信息
+ *
+ * @author ruoyi
+ */
+public class Sys
+{
+ /**
+ * 服务器名称
+ */
+ private String computerName;
+
+ /**
+ * 服务器Ip
+ */
+ private String computerIp;
+
+ /**
+ * 项目路径
+ */
+ private String userDir;
+
+ /**
+ * 操作系统
+ */
+ private String osName;
+
+ /**
+ * 系统架构
+ */
+ private String osArch;
+
+ public String getComputerName()
+ {
+ return computerName;
+ }
+
+ public void setComputerName(String computerName)
+ {
+ this.computerName = computerName;
+ }
+
+ public String getComputerIp()
+ {
+ return computerIp;
+ }
+
+ public void setComputerIp(String computerIp)
+ {
+ this.computerIp = computerIp;
+ }
+
+ public String getUserDir()
+ {
+ return userDir;
+ }
+
+ public void setUserDir(String userDir)
+ {
+ this.userDir = userDir;
+ }
+
+ public String getOsName()
+ {
+ return osName;
+ }
+
+ public void setOsName(String osName)
+ {
+ this.osName = osName;
+ }
+
+ public String getOsArch()
+ {
+ return osArch;
+ }
+
+ public void setOsArch(String osArch)
+ {
+ this.osArch = osArch;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java
new file mode 100644
index 0000000..1320cde
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/domain/server/SysFile.java
@@ -0,0 +1,114 @@
+package com.ruoyi.framework.web.domain.server;
+
+/**
+ * 系统文件相关信息
+ *
+ * @author ruoyi
+ */
+public class SysFile
+{
+ /**
+ * 盘符路径
+ */
+ private String dirName;
+
+ /**
+ * 盘符类型
+ */
+ private String sysTypeName;
+
+ /**
+ * 文件类型
+ */
+ private String typeName;
+
+ /**
+ * 总大小
+ */
+ private String total;
+
+ /**
+ * 剩余大小
+ */
+ private String free;
+
+ /**
+ * 已经使用量
+ */
+ private String used;
+
+ /**
+ * 资源的使用率
+ */
+ private double usage;
+
+ public String getDirName()
+ {
+ return dirName;
+ }
+
+ public void setDirName(String dirName)
+ {
+ this.dirName = dirName;
+ }
+
+ public String getSysTypeName()
+ {
+ return sysTypeName;
+ }
+
+ public void setSysTypeName(String sysTypeName)
+ {
+ this.sysTypeName = sysTypeName;
+ }
+
+ public String getTypeName()
+ {
+ return typeName;
+ }
+
+ public void setTypeName(String typeName)
+ {
+ this.typeName = typeName;
+ }
+
+ public String getTotal()
+ {
+ return total;
+ }
+
+ public void setTotal(String total)
+ {
+ this.total = total;
+ }
+
+ public String getFree()
+ {
+ return free;
+ }
+
+ public void setFree(String free)
+ {
+ this.free = free;
+ }
+
+ public String getUsed()
+ {
+ return used;
+ }
+
+ public void setUsed(String used)
+ {
+ this.used = used;
+ }
+
+ public double getUsage()
+ {
+ return usage;
+ }
+
+ public void setUsage(double usage)
+ {
+ this.usage = usage;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..1ca0283
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
@@ -0,0 +1,145 @@
+package com.ruoyi.framework.web.exception;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.validation.BindException;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingPathVariableException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+import com.ruoyi.common.constant.HttpStatus;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.core.text.Convert;
+import com.ruoyi.common.exception.DemoModeException;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.html.EscapeUtil;
+
+/**
+ * 全局异常处理器
+ *
+ * @author ruoyi
+ */
+@RestControllerAdvice
+public class GlobalExceptionHandler
+{
+ private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
+
+ /**
+ * 权限校验异常
+ */
+ @ExceptionHandler(AccessDeniedException.class)
+ public AjaxResult handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request)
+ {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
+ return AjaxResult.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权");
+ }
+
+ /**
+ * 请求方式不支持
+ */
+ @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
+ public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
+ HttpServletRequest request)
+ {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
+ return AjaxResult.error(e.getMessage());
+ }
+
+ /**
+ * 业务异常
+ */
+ @ExceptionHandler(ServiceException.class)
+ public AjaxResult handleServiceException(ServiceException e, HttpServletRequest request)
+ {
+ log.error(e.getMessage(), e);
+ Integer code = e.getCode();
+ return StringUtils.isNotNull(code) ? AjaxResult.error(code, e.getMessage()) : AjaxResult.error(e.getMessage());
+ }
+
+ /**
+ * 请求路径中缺少必需的路径变量
+ */
+ @ExceptionHandler(MissingPathVariableException.class)
+ public AjaxResult handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request)
+ {
+ String requestURI = request.getRequestURI();
+ log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
+ return AjaxResult.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
+ }
+
+ /**
+ * 请求参数类型不匹配
+ */
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request)
+ {
+ String requestURI = request.getRequestURI();
+ String value = Convert.toStr(e.getValue());
+ if (StringUtils.isNotEmpty(value))
+ {
+ value = EscapeUtil.clean(value);
+ }
+ log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
+ return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), value));
+ }
+
+ /**
+ * 拦截未知的运行时异常
+ */
+ @ExceptionHandler(RuntimeException.class)
+ public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request)
+ {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',发生未知异常.", requestURI, e);
+ return AjaxResult.error(e.getMessage());
+ }
+
+ /**
+ * 系统异常
+ */
+ @ExceptionHandler(Exception.class)
+ public AjaxResult handleException(Exception e, HttpServletRequest request)
+ {
+ String requestURI = request.getRequestURI();
+ log.error("请求地址'{}',发生系统异常.", requestURI, e);
+ return AjaxResult.error(e.getMessage());
+ }
+
+ /**
+ * 自定义验证异常
+ */
+ @ExceptionHandler(BindException.class)
+ public AjaxResult handleBindException(BindException e)
+ {
+ log.error(e.getMessage(), e);
+ String message = e.getAllErrors().get(0).getDefaultMessage();
+ return AjaxResult.error(message);
+ }
+
+ /**
+ * 自定义验证异常
+ */
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e)
+ {
+ log.error(e.getMessage(), e);
+ String message = e.getBindingResult().getFieldError().getDefaultMessage();
+ return AjaxResult.error(message);
+ }
+
+ /**
+ * 演示模式异常
+ */
+ @ExceptionHandler(DemoModeException.class)
+ public AjaxResult handleDemoModeException(DemoModeException e)
+ {
+ return AjaxResult.error("演示模式,不允许操作");
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java
new file mode 100644
index 0000000..07d259a
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/PermissionService.java
@@ -0,0 +1,159 @@
+package com.ruoyi.framework.web.service;
+
+import java.util.Set;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.security.context.PermissionContextHolder;
+
+/**
+ * RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母
+ *
+ * @author ruoyi
+ */
+@Service("ss")
+public class PermissionService
+{
+ /**
+ * 验证用户是否具备某权限
+ *
+ * @param permission 权限字符串
+ * @return 用户是否具备某权限
+ */
+ public boolean hasPermi(String permission)
+ {
+ if (StringUtils.isEmpty(permission))
+ {
+ return false;
+ }
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
+ {
+ return false;
+ }
+ PermissionContextHolder.setContext(permission);
+ return hasPermissions(loginUser.getPermissions(), permission);
+ }
+
+ /**
+ * 验证用户是否不具备某权限,与 hasPermi逻辑相反
+ *
+ * @param permission 权限字符串
+ * @return 用户是否不具备某权限
+ */
+ public boolean lacksPermi(String permission)
+ {
+ return hasPermi(permission) != true;
+ }
+
+ /**
+ * 验证用户是否具有以下任意一个权限
+ *
+ * @param permissions 以 PERMISSION_DELIMETER 为分隔符的权限列表
+ * @return 用户是否具有以下任意一个权限
+ */
+ public boolean hasAnyPermi(String permissions)
+ {
+ if (StringUtils.isEmpty(permissions))
+ {
+ return false;
+ }
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
+ {
+ return false;
+ }
+ PermissionContextHolder.setContext(permissions);
+ Set<String> authorities = loginUser.getPermissions();
+ for (String permission : permissions.split(Constants.PERMISSION_DELIMETER))
+ {
+ if (permission != null && hasPermissions(authorities, permission))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 判断用户是否拥有某个角色
+ *
+ * @param role 角色字符串
+ * @return 用户是否具备某角色
+ */
+ public boolean hasRole(String role)
+ {
+ if (StringUtils.isEmpty(role))
+ {
+ return false;
+ }
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
+ {
+ return false;
+ }
+ for (SysRole sysRole : loginUser.getUser().getRoles())
+ {
+ String roleKey = sysRole.getRoleKey();
+ if (Constants.SUPER_ADMIN.equals(roleKey) || roleKey.equals(StringUtils.trim(role)))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 验证用户是否不具备某角色,与 isRole逻辑相反。
+ *
+ * @param role 角色名称
+ * @return 用户是否不具备某角色
+ */
+ public boolean lacksRole(String role)
+ {
+ return hasRole(role) != true;
+ }
+
+ /**
+ * 验证用户是否具有以下任意一个角色
+ *
+ * @param roles 以 ROLE_NAMES_DELIMETER 为分隔符的角色列表
+ * @return 用户是否具有以下任意一个角色
+ */
+ public boolean hasAnyRoles(String roles)
+ {
+ if (StringUtils.isEmpty(roles))
+ {
+ return false;
+ }
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getUser().getRoles()))
+ {
+ return false;
+ }
+ for (String role : roles.split(Constants.ROLE_DELIMETER))
+ {
+ if (hasRole(role))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 判断是否包含权限
+ *
+ * @param permissions 权限列表
+ * @param permission 权限字符串
+ * @return 用户是否具备某权限
+ */
+ private boolean hasPermissions(Set<String> permissions, String permission)
+ {
+ return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java
new file mode 100644
index 0000000..2279f3c
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysLoginService.java
@@ -0,0 +1,181 @@
+package com.ruoyi.framework.web.service;
+
+import jakarta.annotation.Resource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.exception.user.BlackListException;
+import com.ruoyi.common.exception.user.CaptchaException;
+import com.ruoyi.common.exception.user.CaptchaExpireException;
+import com.ruoyi.common.exception.user.UserNotExistsException;
+import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
+import com.ruoyi.common.utils.DateUtils;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.ip.IpUtils;
+import com.ruoyi.framework.manager.AsyncManager;
+import com.ruoyi.framework.manager.factory.AsyncFactory;
+import com.ruoyi.framework.security.context.AuthenticationContextHolder;
+import com.ruoyi.system.service.ISysConfigService;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 登录校验方法
+ *
+ * @author ruoyi
+ */
+@Component
+public class SysLoginService
+{
+ @Autowired
+ private TokenService tokenService;
+
+ @Resource
+ private AuthenticationManager authenticationManager;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ @Autowired
+ private ISysUserService userService;
+
+ @Autowired
+ private ISysConfigService configService;
+
+ /**
+ * 登录验证
+ *
+ * @param username 用户名
+ * @param password 密码
+ * @param code 验证码
+ * @param uuid 唯一标识
+ * @return 结果
+ */
+ public String login(String username, String password, String code, String uuid)
+ {
+ // 验证码校验
+ validateCaptcha(username, code, uuid);
+ // 登录前置校验
+ loginPreCheck(username, password);
+ // 用户验证
+ Authentication authentication = null;
+ try
+ {
+ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
+ AuthenticationContextHolder.setContext(authenticationToken);
+ // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
+ authentication = authenticationManager.authenticate(authenticationToken);
+ }
+ catch (Exception e)
+ {
+ if (e instanceof BadCredentialsException)
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+ throw new UserPasswordNotMatchException();
+ }
+ else
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
+ throw new ServiceException(e.getMessage());
+ }
+ }
+ finally
+ {
+ AuthenticationContextHolder.clearContext();
+ }
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
+ LoginUser loginUser = (LoginUser) authentication.getPrincipal();
+ recordLoginInfo(loginUser.getUserId());
+ // 生成token
+ return tokenService.createToken(loginUser);
+ }
+
+ /**
+ * 校验验证码
+ *
+ * @param username 用户名
+ * @param code 验证码
+ * @param uuid 唯一标识
+ * @return 结果
+ */
+ public void validateCaptcha(String username, String code, String uuid)
+ {
+ boolean captchaEnabled = configService.selectCaptchaEnabled();
+ if (captchaEnabled)
+ {
+ String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
+ String captcha = redisCache.getCacheObject(verifyKey);
+ if (captcha == null)
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire")));
+ throw new CaptchaExpireException();
+ }
+ redisCache.deleteObject(verifyKey);
+ if (!code.equalsIgnoreCase(captcha))
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
+ throw new CaptchaException();
+ }
+ }
+ }
+
+ /**
+ * 登录前置校验
+ * @param username 用户名
+ * @param password 用户密码
+ */
+ public void loginPreCheck(String username, String password)
+ {
+ // 用户名或密码为空 错误
+ if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
+ throw new UserNotExistsException();
+ }
+ // 密码如果不在指定范围内 错误
+ if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
+ || password.length() > UserConstants.PASSWORD_MAX_LENGTH)
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+ throw new UserPasswordNotMatchException();
+ }
+ // 用户名不在指定范围内 错误
+ if (username.length() < UserConstants.USERNAME_MIN_LENGTH
+ || username.length() > UserConstants.USERNAME_MAX_LENGTH)
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
+ throw new UserPasswordNotMatchException();
+ }
+ // IP黑名单校验
+ String blackStr = configService.selectConfigByKey("sys.login.blackIPList");
+ if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr()))
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked")));
+ throw new BlackListException();
+ }
+ }
+
+ /**
+ * 记录登录信息
+ *
+ * @param userId 用户ID
+ */
+ public void recordLoginInfo(Long userId)
+ {
+ SysUser sysUser = new SysUser();
+ sysUser.setUserId(userId);
+ sysUser.setLoginIp(IpUtils.getIpAddr());
+ sysUser.setLoginDate(DateUtils.getNowDate());
+ userService.updateUserProfile(sysUser);
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java
new file mode 100644
index 0000000..6728c7b
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPasswordService.java
@@ -0,0 +1,86 @@
+package com.ruoyi.framework.web.service;
+
+import java.util.concurrent.TimeUnit;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
+import com.ruoyi.common.exception.user.UserPasswordRetryLimitExceedException;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.framework.security.context.AuthenticationContextHolder;
+
+/**
+ * 登录密码方法
+ *
+ * @author ruoyi
+ */
+@Component
+public class SysPasswordService
+{
+ @Autowired
+ private RedisCache redisCache;
+
+ @Value(value = "${user.password.maxRetryCount}")
+ private int maxRetryCount;
+
+ @Value(value = "${user.password.lockTime}")
+ private int lockTime;
+
+ /**
+ * 登录账户密码错误次数缓存键名
+ *
+ * @param username 用户名
+ * @return 缓存键key
+ */
+ private String getCacheKey(String username)
+ {
+ return CacheConstants.PWD_ERR_CNT_KEY + username;
+ }
+
+ public void validate(SysUser user)
+ {
+ Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
+ String username = usernamePasswordAuthenticationToken.getName();
+ String password = usernamePasswordAuthenticationToken.getCredentials().toString();
+
+ Integer retryCount = redisCache.getCacheObject(getCacheKey(username));
+
+ if (retryCount == null)
+ {
+ retryCount = 0;
+ }
+
+ if (retryCount >= Integer.valueOf(maxRetryCount).intValue())
+ {
+ throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime);
+ }
+
+ if (!matches(user, password))
+ {
+ retryCount = retryCount + 1;
+ redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
+ throw new UserPasswordNotMatchException();
+ }
+ else
+ {
+ clearLoginRecordCache(username);
+ }
+ }
+
+ public boolean matches(SysUser user, String rawPassword)
+ {
+ return SecurityUtils.matchesPassword(rawPassword, user.getPassword());
+ }
+
+ public void clearLoginRecordCache(String loginName)
+ {
+ if (redisCache.hasKey(getCacheKey(loginName)))
+ {
+ redisCache.deleteObject(getCacheKey(loginName));
+ }
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java
new file mode 100644
index 0000000..51c4dd9
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysPermissionService.java
@@ -0,0 +1,88 @@
+package com.ruoyi.framework.web.service;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.entity.SysRole;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.service.ISysMenuService;
+import com.ruoyi.system.service.ISysRoleService;
+
+/**
+ * 用户权限处理
+ *
+ * @author ruoyi
+ */
+@Component
+public class SysPermissionService
+{
+ @Autowired
+ private ISysRoleService roleService;
+
+ @Autowired
+ private ISysMenuService menuService;
+
+ /**
+ * 获取角色数据权限
+ *
+ * @param user 用户信息
+ * @return 角色权限信息
+ */
+ public Set<String> getRolePermission(SysUser user)
+ {
+ Set<String> roles = new HashSet<String>();
+ // 管理员拥有所有权限
+ if (user.isAdmin())
+ {
+ roles.add("admin");
+ }
+ else
+ {
+ roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));
+ }
+ return roles;
+ }
+
+ /**
+ * 获取菜单数据权限
+ *
+ * @param user 用户信息
+ * @return 菜单权限信息
+ */
+ public Set<String> getMenuPermission(SysUser user)
+ {
+ Set<String> perms = new HashSet<String>();
+ // 管理员拥有所有权限
+ if (user.isAdmin())
+ {
+ perms.add("*:*:*");
+ }
+ else
+ {
+ List<SysRole> roles = user.getRoles();
+ if (!CollectionUtils.isEmpty(roles))
+ {
+ // 多角色设置permissions属性,以便数据权限匹配权限
+ for (SysRole role : roles)
+ {
+ if (StringUtils.equals(role.getStatus(), UserConstants.ROLE_NORMAL))
+ {
+ Set<String> rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId());
+ role.setPermissions(rolePerms);
+ perms.addAll(rolePerms);
+ }
+ }
+ }
+ else
+ {
+ perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
+ }
+ }
+ return perms;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java
new file mode 100644
index 0000000..f2afe31
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/SysRegisterService.java
@@ -0,0 +1,115 @@
+package com.ruoyi.framework.web.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.constant.UserConstants;
+import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.RegisterBody;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.exception.user.CaptchaException;
+import com.ruoyi.common.exception.user.CaptchaExpireException;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.manager.AsyncManager;
+import com.ruoyi.framework.manager.factory.AsyncFactory;
+import com.ruoyi.system.service.ISysConfigService;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 注册校验方法
+ *
+ * @author ruoyi
+ */
+@Component
+public class SysRegisterService
+{
+ @Autowired
+ private ISysUserService userService;
+
+ @Autowired
+ private ISysConfigService configService;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ /**
+ * 注册
+ */
+ public String register(RegisterBody registerBody)
+ {
+ String msg = "", username = registerBody.getUsername(), password = registerBody.getPassword();
+ SysUser sysUser = new SysUser();
+ sysUser.setUserName(username);
+
+ // 验证码开关
+ boolean captchaEnabled = configService.selectCaptchaEnabled();
+ if (captchaEnabled)
+ {
+ validateCaptcha(username, registerBody.getCode(), registerBody.getUuid());
+ }
+
+ if (StringUtils.isEmpty(username))
+ {
+ msg = "用户名不能为空";
+ }
+ else if (StringUtils.isEmpty(password))
+ {
+ msg = "用户密码不能为空";
+ }
+ else if (username.length() < UserConstants.USERNAME_MIN_LENGTH
+ || username.length() > UserConstants.USERNAME_MAX_LENGTH)
+ {
+ msg = "账户长度必须在2到20个字符之间";
+ }
+ else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
+ || password.length() > UserConstants.PASSWORD_MAX_LENGTH)
+ {
+ msg = "密码长度必须在5到20个字符之间";
+ }
+ else if (!userService.checkUserNameUnique(sysUser))
+ {
+ msg = "保存用户'" + username + "'失败,注册账号已存在";
+ }
+ else
+ {
+ sysUser.setNickName(username);
+ sysUser.setPassword(SecurityUtils.encryptPassword(password));
+ boolean regFlag = userService.registerUser(sysUser);
+ if (!regFlag)
+ {
+ msg = "注册失败,请联系系统管理人员";
+ }
+ else
+ {
+ AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.REGISTER, MessageUtils.message("user.register.success")));
+ }
+ }
+ return msg;
+ }
+
+ /**
+ * 校验验证码
+ *
+ * @param username 用户名
+ * @param code 验证码
+ * @param uuid 唯一标识
+ * @return 结果
+ */
+ public void validateCaptcha(String username, String code, String uuid)
+ {
+ String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + StringUtils.nvl(uuid, "");
+ String captcha = redisCache.getCacheObject(verifyKey);
+ redisCache.deleteObject(verifyKey);
+ if (captcha == null)
+ {
+ throw new CaptchaExpireException();
+ }
+ if (!code.equalsIgnoreCase(captcha))
+ {
+ throw new CaptchaException();
+ }
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java
new file mode 100644
index 0000000..a03ebad
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/TokenService.java
@@ -0,0 +1,231 @@
+package com.ruoyi.framework.web.service;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import jakarta.servlet.http.HttpServletRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import com.ruoyi.common.constant.CacheConstants;
+import com.ruoyi.common.constant.Constants;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.core.redis.RedisCache;
+import com.ruoyi.common.utils.ServletUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.common.utils.ip.AddressUtils;
+import com.ruoyi.common.utils.ip.IpUtils;
+import com.ruoyi.common.utils.uuid.IdUtils;
+import eu.bitwalker.useragentutils.UserAgent;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+
+/**
+ * token验证处理
+ *
+ * @author ruoyi
+ */
+@Component
+public class TokenService
+{
+ private static final Logger log = LoggerFactory.getLogger(TokenService.class);
+
+ // 令牌自定义标识
+ @Value("${token.header}")
+ private String header;
+
+ // 令牌秘钥
+ @Value("${token.secret}")
+ private String secret;
+
+ // 令牌有效期(默认30分钟)
+ @Value("${token.expireTime}")
+ private int expireTime;
+
+ protected static final long MILLIS_SECOND = 1000;
+
+ protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
+
+ private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ /**
+ * 获取用户身份信息
+ *
+ * @return 用户信息
+ */
+ public LoginUser getLoginUser(HttpServletRequest request)
+ {
+ // 获取请求携带的令牌
+ String token = getToken(request);
+ if (StringUtils.isNotEmpty(token))
+ {
+ try
+ {
+ Claims claims = parseToken(token);
+ // 解析对应的权限以及用户信息
+ String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
+ String userKey = getTokenKey(uuid);
+ LoginUser user = redisCache.getCacheObject(userKey);
+ return user;
+ }
+ catch (Exception e)
+ {
+ log.error("获取用户信息异常'{}'", e.getMessage());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 设置用户身份信息
+ */
+ public void setLoginUser(LoginUser loginUser)
+ {
+ if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken()))
+ {
+ refreshToken(loginUser);
+ }
+ }
+
+ /**
+ * 删除用户身份信息
+ */
+ public void delLoginUser(String token)
+ {
+ if (StringUtils.isNotEmpty(token))
+ {
+ String userKey = getTokenKey(token);
+ redisCache.deleteObject(userKey);
+ }
+ }
+
+ /**
+ * 创建令牌
+ *
+ * @param loginUser 用户信息
+ * @return 令牌
+ */
+ public String createToken(LoginUser loginUser)
+ {
+ String token = IdUtils.fastUUID();
+ loginUser.setToken(token);
+ setUserAgent(loginUser);
+ refreshToken(loginUser);
+
+ Map<String, Object> claims = new HashMap<>();
+ claims.put(Constants.LOGIN_USER_KEY, token);
+ return createToken(claims);
+ }
+
+ /**
+ * 验证令牌有效期,相差不足20分钟,自动刷新缓存
+ *
+ * @param loginUser
+ * @return 令牌
+ */
+ public void verifyToken(LoginUser loginUser)
+ {
+ long expireTime = loginUser.getExpireTime();
+ long currentTime = System.currentTimeMillis();
+ if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
+ {
+ refreshToken(loginUser);
+ }
+ }
+
+ /**
+ * 刷新令牌有效期
+ *
+ * @param loginUser 登录信息
+ */
+ public void refreshToken(LoginUser loginUser)
+ {
+ loginUser.setLoginTime(System.currentTimeMillis());
+ loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
+ // 根据uuid将loginUser缓存
+ String userKey = getTokenKey(loginUser.getToken());
+ redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
+ }
+
+ /**
+ * 设置用户代理信息
+ *
+ * @param loginUser 登录信息
+ */
+ public void setUserAgent(LoginUser loginUser)
+ {
+ UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
+ String ip = IpUtils.getIpAddr();
+ loginUser.setIpaddr(ip);
+ loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
+ loginUser.setBrowser(userAgent.getBrowser().getName());
+ loginUser.setOs(userAgent.getOperatingSystem().getName());
+ }
+
+ /**
+ * 从数据声明生成令牌
+ *
+ * @param claims 数据声明
+ * @return 令牌
+ */
+ private String createToken(Map<String, Object> claims)
+ {
+ String token = Jwts.builder()
+ .setClaims(claims)
+ .signWith(SignatureAlgorithm.HS512, secret).compact();
+ return token;
+ }
+
+ /**
+ * 从令牌中获取数据声明
+ *
+ * @param token 令牌
+ * @return 数据声明
+ */
+ private Claims parseToken(String token)
+ {
+ return Jwts.parser()
+ .setSigningKey(secret)
+ .parseClaimsJws(token)
+ .getBody();
+ }
+
+ /**
+ * 从令牌中获取用户名
+ *
+ * @param token 令牌
+ * @return 用户名
+ */
+ public String getUsernameFromToken(String token)
+ {
+ Claims claims = parseToken(token);
+ return claims.getSubject();
+ }
+
+ /**
+ * 获取请求token
+ *
+ * @param request
+ * @return token
+ */
+ private String getToken(HttpServletRequest request)
+ {
+ String token = request.getHeader(header);
+ if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
+ {
+ token = token.replace(Constants.TOKEN_PREFIX, "");
+ }
+ return token;
+ }
+
+ private String getTokenKey(String uuid)
+ {
+ return CacheConstants.LOGIN_TOKEN_KEY + uuid;
+ }
+}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java
new file mode 100644
index 0000000..5dcdf90
--- /dev/null
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/service/UserDetailsServiceImpl.java
@@ -0,0 +1,66 @@
+package com.ruoyi.framework.web.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+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 com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.model.LoginUser;
+import com.ruoyi.common.enums.UserStatus;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.system.service.ISysUserService;
+
+/**
+ * 用户验证处理
+ *
+ * @author ruoyi
+ */
+@Service
+public class UserDetailsServiceImpl implements UserDetailsService
+{
+ private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
+
+ @Autowired
+ private ISysUserService userService;
+
+ @Autowired
+ private SysPasswordService passwordService;
+
+ @Autowired
+ private SysPermissionService permissionService;
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
+ {
+ SysUser user = userService.selectUserByUserName(username);
+ if (StringUtils.isNull(user))
+ {
+ log.info("登录用户:{} 不存在.", username);
+ throw new ServiceException(MessageUtils.message("user.not.exists"));
+ }
+ else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
+ {
+ log.info("登录用户:{} 已被删除.", username);
+ throw new ServiceException(MessageUtils.message("user.password.delete"));
+ }
+ else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
+ {
+ log.info("登录用户:{} 已被停用.", username);
+ throw new ServiceException(MessageUtils.message("user.blocked"));
+ }
+
+ passwordService.validate(user);
+
+ return createLoginUser(user);
+ }
+
+ public UserDetails createLoginUser(SysUser user)
+ {
+ return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
+ }
+}