Compare commits

..

24 Commits
v6.0.0 ... 6.X

Author SHA1 Message Date
疯狂的狮子Li
4d1448b4b4 update 优化 流程分类代码 2026-09-10 11:22:43 +08:00
疯狂的狮子Li
ef7832eff6 update 优化 去除重复的 del_flag条件 2026-09-10 11:18:48 +08:00
疯狂的狮子Li
9fd1d56831 fix 修复 流程节点自定义变量配置重复 key 时解析报 Duplicate key 的问题 2026-09-07 15:51:12 +08:00
疯狂的狮子Li
91a71f5c25 update 优化 发送验证码接口 异常提醒内容修改 2026-09-07 15:49:38 +08:00
疯狂的狮子Li
bffc39a89f update 优化 EnumPattern 支持任意类型校验 2026-09-04 16:13:57 +08:00
疯狂的狮子Li
d577ad28c6 fix 修复 云oss cdn域名使用问题 2026-09-04 14:34:42 +08:00
疯狂的狮子Li
79bd1db16f fix 修复 多节点提交任务 变量覆盖问题 2026-08-28 14:45:12 +08:00
疯狂的狮子Li
e3e4c0abc0 update 优化 校验当前登录用户是否可以读取任务信息 2026-08-28 13:31:54 +08:00
疯狂的狮子Li
13e8ac5f85 update 优化 对mybatis游标查询 进行加解密支持 2026-08-26 09:38:00 +08:00
疯狂的狮子Li
b37db3fd01 update 优化 增加 groupby 函数(感谢 喃风 贡献) 2026-08-24 15:07:11 +08:00
疯狂的狮子Li
629e344af5 fix 修复 数据权限对union语句不兼容问题 2026-08-24 11:48:56 +08:00
疯狂的狮子Li
2933badb91 update 优化 sql输出性能 去掉锁 2026-08-21 12:57:49 +08:00
YueYe
387c4f0a20 !880 [fix]修复 启用本地缓存
* [fix]修复 启用本地缓存
2026-08-20 06:37:54 +00:00
疯狂的狮子Li
a16f249024 fix 修复 mapper.lambda 链式调用无法走 @DS 直接切换数据源问题 2026-08-17 13:16:17 +08:00
疯狂的狮子Li
b9538b0d1c fix 修复 liteflow 关闭之后 有一些配置类没有关闭导致报错问题 临时修复等官方处理 2026-08-17 09:38:09 +08:00
疯狂的狮子Li
1df89481db update liteflow 2.16.1.2 2026-08-13 09:23:18 +08:00
疯狂的狮子Li
04ef4b1147 update 优化 更新依赖名称 2026-08-10 15:24:29 +08:00
疯狂的狮子Li
430478c040 update README.md 2026-08-07 13:08:31 +08:00
疯狂的狮子Li
70deffdf05 fix 修复 树表模板 没有导入parseTime问题 2026-08-07 09:21:24 +08:00
疯狂的狮子Li
fba7b40075 update 优化 使用我们自行fork的justauth版本 彻底去除fastjson 2026-08-04 14:30:02 +08:00
Kayleigh Wang
6d8c0d6d47 !877 fix 修复代码生成模板导出逻辑与多余空行问题
* fix 修复代码生成模板导出逻辑与多余空行问题
2026-08-03 01:09:44 +00:00
疯狂的狮子Li
d31082f4a7 fix 修复 更换依赖将之前修复的代码覆盖了 2026-07-31 14:15:00 +08:00
疯狂的狮子Li
c76212003c fix 修复 主线程先虚拟线程关闭导致无法获取request问题 2026-07-31 12:03:25 +08:00
疯狂的狮子Li
41ea764a8c update 优化 独立snail-ai esclient版本号 2026-07-30 13:09:00 +08:00
49 changed files with 332 additions and 985 deletions

View File

@ -22,7 +22,8 @@
> 系统演示: [传送门](https://plus-doc.dromara.org/#/common/demo_system)
> 官方前端项目地址: [gitee](https://gitee.com/JavaLionLi/plus-ui) - [github](https://github.com/CrazyLionCat/plus-ui) - [gitcode](https://gitcode.com/dromara/plus-ui)<br>
> 官方前端项目地址: 基于 Vue + ElementPlus 版本前端项目 [gitee](https://gitee.com/JavaLionLi/plus-ui) - [github](https://github.com/CrazyLionCat/plus-ui) - [gitcode](https://gitcode.com/dromara/plus-ui)<br>
> 官方前端项目地址: 基于 React + Ant Design 版本前端项目 [plus-ui-react](https://gitee.com/JavaLionLi/plus-ui/tree/6.X-React/)
> 成员前端项目地址: 基于vben5 [ruoyi-plus-vben5](https://github.com/imdap/ruoyi-plus-vben5)<br>
> 成员前端项目地址: 基于soybean [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)<br>
> 成员项目地址: 删除多租户与工作流 [RuoYi-Vue-Plus-Single](https://gitee.com/ColorDreams/RuoYi-Vue-Plus-Single)<br>

View File

@ -36,7 +36,7 @@
<!-- 安全认证与加密相关依赖版本 -->
<satoken.version>1.45.0</satoken.version>
<justauth.version>2.0.0</justauth.version>
<justauth.version>3.0.1</justauth.version>
<bouncycastle.version>1.85</bouncycastle.version>
<!-- 缓存、锁与任务调度相关依赖版本 -->
@ -63,7 +63,7 @@
<sms4j.version>3.3.5</sms4j.version>
<!-- 工作流配置 -->
<warm-flow.version>1.8.9</warm-flow.version>
<liteflow.version>2.16.0</liteflow.version>
<liteflow.version>2.16.1.2</liteflow.version>
<!-- mqtt客户端 -->
<mica-mqtt.version>2.6.8</mica-mqtt.version>
@ -349,7 +349,7 @@
<!-- Warm-Flow国产工作流引擎, 在线文档http://warm-flow.cn/ -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
<artifactId>warm-flow-mybatis-plus-sb4-starter</artifactId>
<version>${warm-flow.version}</version>
</dependency>
<!-- Warm-Flow UI 插件 -->
@ -396,7 +396,7 @@
<!-- JustAuth 的依赖配置-->
<dependency>
<groupId>cn.herodotus.opensteward</groupId>
<groupId>io.github.windtool</groupId>
<artifactId>JustAuth</artifactId>
<version>${justauth.version}</version>
</dependency>

View File

@ -116,7 +116,7 @@ public class CaptchaController {
.send();
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
} catch (Exception e) {
log.error("验证码短信发送异常 => {}", e.getMessage());
log.error("验证码邮件发送异常 => {}", e.getMessage());
throw new ServiceException(e.getMessage());
}
}

View File

@ -7,6 +7,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.lock.annotation.Lock4j;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.model.AuthUser;
@ -136,7 +137,12 @@ public class SysLoginService {
loginInfoEvent.setUsername(username);
loginInfoEvent.setStatus(status);
loginInfoEvent.setMessage(message);
loginInfoEvent.setRequest(ServletUtils.getRequest());
HttpServletRequest request = ServletUtils.getRequest();
if (request != null) {
loginInfoEvent.setIp(ServletUtils.getClientIP(request));
loginInfoEvent.setUserAgent(request.getHeader("User-Agent"));
loginInfoEvent.setClientId(request.getHeader(LoginHelper.CLIENT_KEY));
}
SpringUtils.context().publishEvent(loginInfoEvent);
}

View File

@ -1,6 +1,7 @@
package org.dromara.web.service;
import cn.hutool.crypto.digest.BCrypt;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.constant.GlobalConstants;
@ -14,6 +15,7 @@ import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.log.event.LoginInfoEvent;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.config.properties.CaptchaProperties;
import org.dromara.system.api.model.RegisterBody;
import org.dromara.system.domain.SysUser;
@ -103,7 +105,12 @@ public class SysRegisterService {
loginInfoEvent.setUsername(username);
loginInfoEvent.setStatus(status);
loginInfoEvent.setMessage(message);
loginInfoEvent.setRequest(ServletUtils.getRequest());
HttpServletRequest request = ServletUtils.getRequest();
if (request != null) {
loginInfoEvent.setIp(ServletUtils.getClientIP(request));
loginInfoEvent.setUserAgent(request.getHeader("User-Agent"));
loginInfoEvent.setClientId(request.getHeader(LoginHelper.CLIENT_KEY));
}
SpringUtils.context().publishEvent(loginInfoEvent);
}

View File

@ -249,6 +249,9 @@ warm-flow:
liteflow:
# 跟随工作流模块开关,避免禁用工作流时仍加载任务办理链
enable: ${warm-flow.enabled:true}
metrics:
# 指标自动配置使用独立开关,需同步关闭以避免初始化未启用的 LiteFlow
enabled: ${liteflow.enable:true}
# 工作流模块用于编排任务办理链路
rule-source: classpath:liteflow/*.el.xml
# 开启虚拟线程

View File

@ -1,5 +1,7 @@
package org.dromara.common.core.validate.enums;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.BooleanUtil;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.dromara.common.core.utils.StringUtils;
@ -14,12 +16,17 @@ import java.util.Set;
* @author 秋辞未寒
* @date 2024-12-09
*/
public class EnumPatternValidator implements ConstraintValidator<EnumPattern, String> {
public class EnumPatternValidator implements ConstraintValidator<EnumPattern, Object> {
/**
* 枚举允许值集合
*/
private final Set<String> values = new HashSet<>();
private final Set<Object> values = new HashSet<>();
/**
* 枚举字段值类型
*/
private Class<?> valueType;
/**
* 初始化枚举允许值集合
@ -36,24 +43,43 @@ public class EnumPatternValidator implements ConstraintValidator<EnumPattern, St
for (Object e : annotation.type().getEnumConstants()) {
Object fieldValue = ReflectUtils.invokeGetter(e, fieldName);
if (fieldValue != null) {
values.add(String.valueOf(fieldValue));
if (valueType == null) {
valueType = fieldValue.getClass();
}
values.add(fieldValue);
}
}
}
/**
* 校验字符串是否在枚举允许值集合内
* 校验值是否在枚举允许值集合内
*
* <p>以枚举字段的实际类型转换输入值后进行比较可兼容字符串数字布尔值等字段类型
* 同时避免将无法转换的值视为合法值字符串类型的空白值按未填写处理其他类型仅 null
* 按未填写处理</p>
*
* @param value 待校验值
* @param constraintValidatorContext 校验上下文
* @return true 校验通过 false 校验失败
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (StringUtils.isBlank(value)) {
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
if (value == null) {
return true;
}
return values.contains(value);
if (valueType == null) {
return false;
}
if (value instanceof CharSequence && StringUtils.isBlank(value.toString())
&& CharSequence.class.isAssignableFrom(valueType)) {
return true;
}
if ((valueType == Boolean.class || valueType == boolean.class) && value instanceof CharSequence
&& BooleanUtil.toBooleanObject(value.toString()) == null) {
return false;
}
Object convertedValue = Convert.convertWithCheck(valueType, value, null, true);
return convertedValue != null && values.contains(convertedValue);
}
}

View File

@ -1,11 +1,14 @@
package org.dromara.common.encrypt.interceptor;
import lombok.AllArgsConstructor;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import org.dromara.common.encrypt.core.EncryptedFieldProcessor;
import java.io.IOException;
import java.sql.Statement;
import java.util.Iterator;
import java.util.Properties;
/**
@ -13,10 +16,10 @@ import java.util.Properties;
*
* @author Lion Li
*/
@Intercepts({@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class})
@Intercepts({
@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}),
// MP 3.5.17 selectOne/selectVoOne 走游标需一并拦截解密
@Signature(type = ResultSetHandler.class, method = "handleCursorResultSets", args = {Statement.class})
})
@AllArgsConstructor
public class MybatisDecryptInterceptor implements Interceptor {
@ -32,11 +35,13 @@ public class MybatisDecryptInterceptor implements Interceptor {
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取执行mysql执行结果
Object result = invocation.proceed();
if (result == null) {
return null;
}
if (result instanceof Cursor<?> cursor) {
return new DecryptCursor<>(cursor, encryptedFieldProcessor);
}
encryptedFieldProcessor.decrypt(result);
return result;
}
@ -61,4 +66,52 @@ public class MybatisDecryptInterceptor implements Interceptor {
public void setProperties(Properties properties) {
}
/**
* 查询结果解密游标保持 MyBatis 游标的惰性读取语义
*
* @param <T> 查询结果类型
*/
private record DecryptCursor<T>(Cursor<T> delegate, EncryptedFieldProcessor encryptedFieldProcessor)
implements Cursor<T> {
@Override
public boolean isOpen() {
return delegate.isOpen();
}
@Override
public boolean isConsumed() {
return delegate.isConsumed();
}
@Override
public int getCurrentIndex() {
return delegate.getCurrentIndex();
}
@Override
public Iterator<T> iterator() {
Iterator<T> iterator = delegate.iterator();
return new Iterator<>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public T next() {
T result = iterator.next();
encryptedFieldProcessor.decrypt(result);
return result;
}
};
}
@Override
public void close() throws IOException {
delegate.close();
}
}
}

View File

@ -1,6 +1,5 @@
package org.dromara.common.log.event;
import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import java.io.Serial;
@ -34,9 +33,19 @@ public class LoginInfoEvent implements Serializable {
private String message;
/**
* 请求体
* 客户端IP
*/
private HttpServletRequest request;
private String ip;
/**
* 用户代理
*/
private String userAgent;
/**
* 客户端标识
*/
private String clientId;
/**
* 其他参数

View File

@ -26,14 +26,19 @@ public class DataPermissionAdvice implements MethodInterceptor {
public Object invoke(MethodInvocation invocation) throws Throwable {
Object target = invocation.getThis();
Method method = invocation.getMethod();
DataPermission previousPermission = DataPermissionHelper.getPermission();
// 设置权限注解
DataPermissionHelper.setPermission(getDataPermissionAnnotation(target, method));
try {
// 执行代理方法
return invocation.proceed();
} finally {
// 清除权限注解
DataPermissionHelper.removePermission();
// 恢复上一层权限上下文兼容嵌套 Mapper 调用
if (previousPermission == null) {
DataPermissionHelper.removePermission();
} else {
DataPermissionHelper.setPermission(previousPermission);
}
}
}

View File

@ -6,7 +6,9 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.MapperProxyMetadata;
import com.baomidou.mybatisplus.core.toolkit.reflect.GenericTypeUtils;
import com.baomidou.mybatisplus.core.toolkit.MybatisUtils;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers;
@ -14,6 +16,7 @@ import com.baomidou.mybatisplus.extension.toolkit.Db;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import java.io.Serializable;
@ -100,7 +103,26 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return Lambda CRUD 链式包装器
*/
default LambdaCrudChainWrapper<T, V> lambda() {
return new LambdaCrudChainWrapper<>(this);
return new LambdaCrudChainWrapper<>(mapperProxy());
}
/**
* 获取当前 Mapper 对应的 Spring 代理对象确保 Mapper 上的切面注解能够继续生效
*
* @return Mapper 代理对象
*/
private BaseMapperPlus<T, V> mapperProxy() {
BaseMapperPlus<T, V> mapper = this;
try {
MapperProxyMetadata metadata = MybatisUtils.getMapperProxy(this);
Object proxy = SpringUtils.getBean(metadata.getMapperInterface());
if (proxy instanceof BaseMapperPlus<?, ?>) {
mapper = (BaseMapperPlus<T, V>) proxy;
}
} catch (RuntimeException ignored) {
// Mapper 可能未托管于 Spring 容器保留当前对象作为回退
}
return mapper;
}
/**
@ -109,7 +131,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
* @return Lambda 链式更新包装器
*/
default LambdaUpdateChainWrapper<T> lambdaUpdate() {
return ChainWrappers.lambdaUpdateChain(this);
return ChainWrappers.lambdaUpdateChain(mapperProxy());
}
/**

View File

@ -49,6 +49,44 @@ public final class LambdaJoinQueryBuilder<T> {
return this;
}
/**
* 添加分组字段
*
* @param columns 分组字段
* @param <E> 字段所属实体类型
* @return 当前联表查询构造辅助对象
*/
@SafeVarargs
public final <E> LambdaJoinQueryBuilder<T> groupBy(SFunction<E, ?>... columns) {
if (columns == null) {
return this;
}
for (SFunction<E, ?> column : columns) {
wrapper.groupBy(column);
}
return this;
}
/**
* 添加带表别名的分组字段
*
* @param tableAlias 表别名
* @param columns 分组字段
* @param <E> 字段所属实体类型
* @return 当前联表查询构造辅助对象
*/
@SafeVarargs
public final <E> LambdaJoinQueryBuilder<T> groupBy(String tableAlias, SFunction<E, ?>... columns) {
if (columns == null || columns.length == 0) {
return this;
}
String checkedAlias = AggregateSelectUtils.checkAlias(tableAlias);
for (SFunction<E, ?> column : columns) {
wrapper.groupBy(checkedAlias, column);
}
return this;
}
/**
* 指定主表查询字段
*

View File

@ -71,13 +71,17 @@ public class PlusDataPermissionHandler {
*/
public Expression getSqlSegment(Expression where, boolean isSelect) {
try {
DataPermission dataPermission = getDataPermission();
if (dataPermission == null) {
throw new ServiceException("数据权限上下文缺失,请检查 @DataPermission 注解处理生命周期");
}
LoginUser currentUser = currentUser();
// 如果是超级管理员或租户管理员则不过滤数据
if (LoginHelper.isSuperAdmin()) {
return where;
}
// 构造数据过滤条件的 SQL 片段
String dataFilterSql = buildDataFilter(getDataPermission(), currentUser, isSelect);
String dataFilterSql = buildDataFilter(dataPermission, currentUser, isSelect);
if (StringUtils.isBlank(dataFilterSql)) {
return where;
}
@ -91,8 +95,6 @@ public class PlusDataPermissionHandler {
}
} catch (JSQLParserException e) {
throw new ServiceException("数据权限解析异常 => " + e.getMessage());
} finally {
DataPermissionHelper.removePermission();
}
}

View File

@ -24,13 +24,12 @@ import java.sql.Statement;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.TemporalAccessor;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;
/**
* 完整 SQL 日志拦截器
@ -71,11 +70,6 @@ public class SqlLogInterceptor implements Interceptor {
*/
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
/**
* 控制台输出锁避免多线程 SQL 日志互相穿插
*/
private static final ReentrantLock CONSOLE_LOCK = new ReentrantLock();
/**
* SQL 日志配置
*/
@ -340,13 +334,7 @@ public class SqlLogInterceptor implements Interceptor {
* @param message 日志内容
*/
private void printConsole(String message) {
CONSOLE_LOCK.lock();
try {
System.err.println(message);
System.err.println();
} finally {
CONSOLE_LOCK.unlock();
}
System.err.print(message + System.lineSeparator() + System.lineSeparator());
}
/**

View File

@ -220,11 +220,37 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
* @return 桶URL地址
*/
public String getBucketUrl(String bucket) {
String url = getAccessBaseUrl();
// 云厂商的自定义域名通常是桶绑定的 CDN/映射域名域名本身已经包含桶信息不能再拼接桶名
// 自定义域名仅对默认桶生效其它桶仍使用访问站点生成标准 S3 地址
if (!usePathStyleAccess && isDefaultBucket(bucket) && hasCustomDomain()) {
return getDomainUrl();
}
String url = (!usePathStyleAccess && hasCustomDomain() && !isDefaultBucket(bucket))
? getEndpoint()
: getAccessBaseUrl();
// 根据是否使用路径风格配置项决定存储桶的URL风格
return usePathStyleAccess ? BucketUrlUtil.getPathStyleBucketUrl(useHttps, url, bucket) : BucketUrlUtil.getSiteStyleBucketUrl(useHttps, url, bucket);
}
/**
* 判断是否配置了自定义域名
*
* @return 是否配置了非空自定义域名
*/
private boolean hasCustomDomain() {
return domain().filter(StringUtils::isNotBlank).isPresent();
}
/**
* 判断指定桶是否为当前客户端配置的默认桶
*
* @param bucket 存储桶名称
* @return 是否为默认桶
*/
private boolean isDefaultBucket(String bucket) {
return bucket().filter(defaultBucket -> defaultBucket.equals(bucket)).isPresent();
}
/**
* 解析 S3 Region
*

View File

@ -38,8 +38,8 @@ public class CacheConfig {
* 自定义缓存管理器 整合spring-cache
*/
@Bean
public CacheManager cacheManager() {
return new PlusSpringCacheManager();
public CacheManager cacheManager(Cache<Object, Object> caffeine) {
return new PlusSpringCacheManager(caffeine);
}
}

View File

@ -18,23 +18,10 @@
<dependencies>
<!-- 授权认证 -->
<dependency>
<groupId>cn.herodotus.opensteward</groupId>
<groupId>io.github.windtool</groupId>
<artifactId>JustAuth</artifactId>
</dependency>
<!-- 仅供 justauth 内部解析参数使用 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.60</version>
</dependency>
<!-- 序列化模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<!-- 缓存服务 -->
<dependency>
<groupId>org.dromara</groupId>

View File

@ -1,185 +0,0 @@
package me.zhyd.oauth.request;
import com.alibaba.fastjson2.JSONObject;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.constant.Keys;
import me.zhyd.oauth.enums.AuthResponseStatus;
import me.zhyd.oauth.enums.AuthUserGender;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.utils.HttpUtils;
import me.zhyd.oauth.utils.StringUtils;
import me.zhyd.oauth.utils.UrlBuilder;
/**
* <p>
* 企业微信登录父类
* </p>
*
* @author liguanhua (347826496(a)qq.com)
* @since 1.15.9
*/
public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultRequest {
/**
* 创建企业微信认证请求
*
* @param config 授权配置
* @param source 授权来源
*/
public AbstractAuthWeChatEnterpriseRequest(AuthConfig config, AuthSource source) {
super(config, source);
}
/**
* 创建企业微信认证请求
*
* @param config 授权配置
* @param source 授权来源
* @param authStateCache 授权状态缓存
*/
public AbstractAuthWeChatEnterpriseRequest(AuthConfig config, AuthSource source, AuthStateCache authStateCache) {
super(config, source, authStateCache);
}
/**
* 获取企业微信访问令牌
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String response = doGetAuthorizationCode(accessTokenUrl(null));
JSONObject object = this.checkResponse(response);
return AuthToken.builder()
.accessToken(object.getString(Keys.OAUTH2_ACCESS_TOKEN))
.expireIn(object.getIntValue(Keys.OAUTH2_EXPIRES_IN))
.code(authCallback.getCode())
.build();
}
/**
* 获取企业微信用户信息
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String response = doGetUserInfo(authToken);
JSONObject object = this.checkResponse(response);
// 返回 OpenId 或其他均代表非当前企业用户不支持
// https://github.com/justauth/JustAuth/issues/227 修复bug
String userId = null;
if (object.containsKey("userid")) {
userId = object.getString("userid");
} else if (object.containsKey("UserId")) {
userId = object.getString("UserId");
}
if (userId == null) {
throw new AuthException(AuthResponseStatus.UNIDENTIFIED_PLATFORM, source);
}
String userTicket = object.getString("user_ticket");
JSONObject userDetail = getUserDetail(authToken.getAccessToken(), userId, userTicket);
return AuthUser.builder()
.rawUserInfo(userDetail)
.username(userDetail.getString(Keys.NAME))
.nickname(userDetail.getString("alias"))
.avatar(userDetail.getString(Keys.AVATAR))
.location(userDetail.getString(Keys.OAUTH2_SCOPE__ADDRESS))
.email(userDetail.getString(Keys.OAUTH2_SCOPE__EMAIL))
.uuid(userId)
.gender(AuthUserGender.getWechatRealGender(userDetail.getString(Keys.GENDER)))
.token(authToken)
.source(source.toString())
.build();
}
/**
* 校验请求结果
*
* @param response 请求结果
* @return 如果请求结果正常则返回JSONObject
*/
private JSONObject checkResponse(String response) {
JSONObject object = JSONObject.parseObject(response);
if (object.containsKey(Keys.VARIANT__ERRCODE) && object.getIntValue(Keys.VARIANT__ERRCODE) != 0) {
throw new AuthException(object.getString(Keys.VARIANT__ERRMSG), source);
}
return object;
}
/**
* 返回获取accessToken的url
*
* @param code 授权码
* @return 返回获取accessToken的url
*/
@Override
protected String accessTokenUrl(String code) {
return UrlBuilder.fromBaseUrl(source.accessToken())
.queryParam("corpid", config.getClientId())
.queryParam("corpsecret", config.getClientSecret())
.build();
}
/**
* 返回获取userInfo的url
*
* @param authToken 用户授权后的token
* @return 返回获取userInfo的url
*/
@Override
protected String userInfoUrl(AuthToken authToken) {
return UrlBuilder.fromBaseUrl(source.userInfo())
.queryParam(Keys.OAUTH2_ACCESS_TOKEN, authToken.getAccessToken())
.queryParam(Keys.OAUTH2_CODE, authToken.getCode())
.build();
}
/**
* 用户详情
*
* @param accessToken accessToken
* @param userId 企业内用户id
* @param userTicket 成员票据用于获取用户信息或敏感信息
* @return 用户详情
*/
private JSONObject getUserDetail(String accessToken, String userId, String userTicket) {
// 用户基础信息
String userInfoUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/user/get")
.queryParam(Keys.OAUTH2_ACCESS_TOKEN, accessToken)
.queryParam(Keys.USERID, userId)
.build();
String userInfoResponse = new HttpUtils(config.getHttpConfig()).get(userInfoUrl).getBody();
JSONObject userInfo = checkResponse(userInfoResponse);
// 用户敏感信息
if (StringUtils.isNotEmpty(userTicket)) {
String userDetailUrl = UrlBuilder.fromBaseUrl("https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail")
.queryParam(Keys.OAUTH2_ACCESS_TOKEN, accessToken)
.build();
JSONObject param = new JSONObject();
param.put("user_ticket", userTicket);
String userDetailResponse = new HttpUtils(config.getHttpConfig()).post(userDetailUrl, param.toJSONString()).getBody();
JSONObject userDetail = checkResponse(userDetailResponse);
userInfo.putAll(userDetail);
}
return userInfo;
}
}

View File

@ -1,138 +0,0 @@
package me.zhyd.oauth.request;
import com.alibaba.fastjson2.JSONObject;
import com.xkcoding.http.support.HttpHeader;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.config.AuthDefaultSource;
import me.zhyd.oauth.constant.Keys;
import me.zhyd.oauth.enums.scope.AuthDingTalkScope;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.utils.AuthScopeUtils;
import me.zhyd.oauth.utils.HttpUtils;
import me.zhyd.oauth.utils.UrlBuilder;
import java.util.HashMap;
import java.util.Map;
/**
* 新版钉钉二维码登录
*
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
* @since 1.16.7
*/
public class AuthDingTalkV2Request extends AuthDefaultRequest {
/**
* 创建钉钉 V2 认证请求
*
* @param config 授权配置
*/
public AuthDingTalkV2Request(AuthConfig config) {
super(config, AuthDefaultSource.DINGTALK_V2);
}
/**
* 创建钉钉 V2 认证请求
*
* @param config 授权配置
* @param authStateCache 授权状态缓存
*/
public AuthDingTalkV2Request(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthDefaultSource.DINGTALK_V2, authStateCache);
}
/**
* 生成钉钉 V2 授权地址
*
* @param state 授权状态
* @return 授权地址
*/
@Override
public String authorize(String state) {
return UrlBuilder.fromBaseUrl(source.authorize())
.queryParam(Keys.OAUTH2_RESPONSE_TYPE, Keys.OAUTH2_CODE)
.queryParam(Keys.OAUTH2_CLIENT_ID, config.getClientId())
.queryParam(Keys.OAUTH2_SCOPE, this.getScopes(",", true, AuthScopeUtils.getDefaultScopes(AuthDingTalkScope.values())))
.queryParam(Keys.OAUTH2_REDIRECT_URI, config.getRedirectUri())
.queryParam("prompt", "consent")
.queryParam("org_type", config.getDingTalkOrgType())
.queryParam("corpId", config.getDingTalkCorpId())
.queryParam("exclusiveLogin", config.isDingTalkExclusiveLogin())
.queryParam("exclusiveCorpId", config.getDingTalkExclusiveCorpId())
.queryParam(Keys.OAUTH2_STATE, getRealState(state))
.build();
}
/**
* 获取钉钉 V2 访问令牌
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
Map<String, String> params = new HashMap<>();
params.put("grantType", Keys.OAUTH2_GRANT_TYPE__AUTHORIZATION_CODE);
params.put("clientId", config.getClientId());
params.put("clientSecret", config.getClientSecret());
params.put(Keys.OAUTH2_CODE, authCallback.getCode());
String response = new HttpUtils(config.getHttpConfig()).post(this.source.accessToken(), JSONObject.toJSONString(params)).getBody();
JSONObject accessTokenObject = JSONObject.parseObject(response);
if (!accessTokenObject.containsKey("accessToken")) {
throw new AuthException(JSONObject.toJSONString(response), source);
}
return AuthToken.builder()
.accessToken(accessTokenObject.getString("accessToken"))
.refreshToken(accessTokenObject.getString("refreshToken"))
.expireIn(accessTokenObject.getIntValue("expireIn"))
.corpId(accessTokenObject.getString("corpId"))
.build();
}
/**
* 获取钉钉 V2 用户信息
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
HttpHeader header = new HttpHeader();
header.add("x-acs-dingtalk-access-token", authToken.getAccessToken());
String response = new HttpUtils(config.getHttpConfig()).get(this.source.userInfo(), null, header, false).getBody();
JSONObject object = JSONObject.parseObject(response);
authToken.setOpenId(object.getString("openId"));
authToken.setUnionId(object.getString("unionId"));
return AuthUser.builder()
.rawUserInfo(object)
.uuid(object.getString("unionId"))
.username(object.getString("nick"))
.nickname(object.getString("nick"))
.avatar(object.getString("avatarUrl"))
.snapshotUser(object.getBooleanValue("visitor"))
.token(authToken)
.source(source.toString())
.build();
}
/**
* 返回获取accessToken的url
*
* @param code 授权码
* @return 返回获取accessToken的url
*/
protected String accessTokenUrl(String code) {
return UrlBuilder.fromBaseUrl(source.accessToken())
.queryParam(Keys.OAUTH2_CODE, code)
.queryParam("clientId", config.getClientId())
.queryParam("clientSecret", config.getClientSecret())
.queryParam("grantType", Keys.OAUTH2_GRANT_TYPE__AUTHORIZATION_CODE)
.build();
}
}

View File

@ -68,7 +68,7 @@ public class SocialLoginConfigProperties {
private String clientOsType;
/**
* maxkey 服务器地址
* 自托管授权服务器地址
*/
private String serverUrl;

View File

@ -1,119 +0,0 @@
package org.dromara.common.social.gitea;
import cn.hutool.core.lang.Dict;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthDefaultRequest;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.json.utils.JsonUtils;
/**
* @author lcry
*/
@Slf4j
public class AuthGiteaRequest extends AuthDefaultRequest {
/**
* 服务器地址
*/
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.gitea.server-url");
/**
* 设定归属域
*/
public AuthGiteaRequest(AuthConfig config) {
super(config, AuthGiteaSource.GITEA);
}
/**
* 创建 Gitea 认证请求
*
* @param config 授权配置
* @param authStateCache 授权状态缓存
*/
public AuthGiteaRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthGiteaSource.GITEA, authStateCache);
}
/**
* 获取 Gitea 访问令牌
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode());
Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthToken.builder()
.accessToken(object.getStr("access_token"))
.refreshToken(object.getStr("refresh_token"))
.idToken(object.getStr("id_token"))
.tokenType(object.getStr("token_type"))
.scope(object.getStr("scope"))
.build();
}
/**
* 使用授权码换取 Gitea 访问令牌响应
*
* @param code 授权码
* @return 令牌接口响应体
*/
@Override
protected String doPostAuthorizationCode(String code) {
HttpRequest request = HttpRequest.post(source.accessToken())
.form("client_id", config.getClientId())
.form("client_secret", config.getClientSecret())
.form("grant_type", "authorization_code")
.form("code", code)
.form("redirect_uri", config.getRedirectUri());
HttpResponse response = request.execute();
return response.body();
}
/**
* 获取 Gitea 用户信息
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken);
Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthUser.builder()
.uuid(object.getStr("sub"))
.username(object.getStr("name"))
.nickname(object.getStr("preferred_username"))
.avatar(object.getStr("picture"))
.email(object.getStr("email"))
.token(authToken)
.source(source.toString())
.build();
}
}

View File

@ -1,50 +0,0 @@
package org.dromara.common.social.gitea;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.request.AuthDefaultRequest;
/**
* gitea Oauth2 默认接口说明
*
* @author lcry
*/
public enum AuthGiteaSource implements AuthSource {
/**
* 自己搭建的 gitea 私服
*/
GITEA {
/**
* 授权的api
*/
@Override
public String authorize() {
return AuthGiteaRequest.SERVER_URL + "/login/oauth/authorize";
}
/**
* 获取accessToken的api
*/
@Override
public String accessToken() {
return AuthGiteaRequest.SERVER_URL + "/login/oauth/access_token";
}
/**
* 获取用户信息的api
*/
@Override
public String userInfo() {
return AuthGiteaRequest.SERVER_URL + "/login/oauth/userinfo";
}
/**
* 平台对应的 AuthRequest 实现类必须继承自 {@link AuthDefaultRequest}
*/
@Override
public Class<? extends AuthDefaultRequest> getTargetClass() {
return AuthGiteaRequest.class;
}
}
}

View File

@ -1,101 +0,0 @@
package org.dromara.common.social.maxkey;
import cn.hutool.core.lang.Dict;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthDefaultRequest;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.json.utils.JsonUtils;
/**
* @author 长春叭哥 2023年03月26日
*/
public class AuthMaxKeyRequest extends AuthDefaultRequest {
/**
* 服务器地址
*/
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.maxkey.server-url");
/**
* 设定归属域
*/
public AuthMaxKeyRequest(AuthConfig config) {
super(config, AuthMaxKeySource.MAXKEY);
}
/**
* 创建 MaxKey 认证请求
*
* @param config 授权配置
* @param authStateCache 授权状态缓存
*/
public AuthMaxKeyRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthMaxKeySource.MAXKEY, authStateCache);
}
/**
* 获取 MaxKey 访问令牌
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode());
Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthToken.builder()
.accessToken(object.getStr("access_token"))
.refreshToken(object.getStr("refresh_token"))
.idToken(object.getStr("id_token"))
.tokenType(object.getStr("token_type"))
.scope(object.getStr("scope"))
.build();
}
/**
* 获取 MaxKey 用户信息
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken);
Dict object = JsonUtils.parseMap(body);
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
return AuthUser.builder()
.uuid(object.getStr("userId"))
.username(object.getStr("username"))
.nickname(object.getStr("displayName"))
.avatar(object.getStr("avatar_url"))
.blog(object.getStr("web_url"))
.company(object.getStr("organization"))
.location(object.getStr("location"))
.email(object.getStr("email"))
.remark(object.getStr("bio"))
.token(authToken)
.source(source.toString())
.build();
}
}

View File

@ -1,52 +0,0 @@
package org.dromara.common.social.maxkey;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.request.AuthDefaultRequest;
/**
* Oauth2 默认接口说明
*
* @author 长春叭哥 2023年03月26日
*
*/
public enum AuthMaxKeySource implements AuthSource {
/**
* 自己搭建的 maxkey 私服
*/
MAXKEY {
/**
* 授权的api
*/
@Override
public String authorize() {
return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/authorize";
}
/**
* 获取accessToken的api
*/
@Override
public String accessToken() {
return AuthMaxKeyRequest.SERVER_URL + "/sign/authz/oauth/v20/token";
}
/**
* 获取用户信息的api
*/
@Override
public String userInfo() {
return AuthMaxKeyRequest.SERVER_URL + "/sign/api/oauth/v20/me";
}
/**
* 平台对应的 AuthRequest 实现类必须继承自 {@link AuthDefaultRequest}
*/
@Override
public Class<? extends AuthDefaultRequest> getTargetClass() {
return AuthMaxKeyRequest.class;
}
}
}

View File

@ -1,156 +0,0 @@
package org.dromara.common.social.topiam;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.xkcoding.http.support.HttpHeader;
import lombok.extern.slf4j.Slf4j;
import me.zhyd.oauth.cache.AuthStateCache;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.exception.AuthException;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthToken;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.AuthDefaultRequest;
import me.zhyd.oauth.utils.HttpUtils;
import me.zhyd.oauth.utils.UrlBuilder;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.json.utils.JsonUtils;
import static org.dromara.common.social.topiam.AuthTopIamSource.TOPIAM;
/**
* TopIAM 认证请求
*
* @author xlsea
* @since 2024-01-06
*/
@Slf4j
public class AuthTopIamRequest extends AuthDefaultRequest {
/**
* 服务器地址
*/
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.topiam.server-url");
/**
* 设定归属域
*/
public AuthTopIamRequest(AuthConfig config) {
super(config, TOPIAM);
}
/**
* 创建 TopIAM 认证请求
*
* @param config 授权配置
* @param authStateCache 授权状态缓存
*/
public AuthTopIamRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, TOPIAM, authStateCache);
}
/**
* 获取 TopIAM 访问令牌
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode());
Dict object = JsonUtils.parseMap(body);
checkResponse(object);
return AuthToken.builder()
.accessToken(object.getStr("access_token"))
.refreshToken(object.getStr("refresh_token"))
.idToken(object.getStr("id_token"))
.tokenType(object.getStr("token_type"))
.scope(object.getStr("scope"))
.build();
}
/**
* 获取 TopIAM 用户信息
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken);
Dict object = JsonUtils.parseMap(body);
checkResponse(object);
return AuthUser.builder()
.uuid(object.getStr("sub"))
.username(object.getStr("preferred_username"))
.nickname(object.getStr("nickname"))
.avatar(object.getStr("picture"))
.email(object.getStr("email"))
.token(authToken)
.source(source.toString())
.build();
}
/**
* 使用授权码换取 TopIAM 访问令牌响应
*
* @param code 授权码
* @return 令牌接口响应体
*/
@Override
protected String doPostAuthorizationCode(String code) {
HttpRequest request = HttpRequest.post(source.accessToken())
.header("Authorization", "Basic " + Base64.encode("%s:%s".formatted(config.getClientId(), config.getClientSecret())))
.form("grant_type", "authorization_code")
.form("code", code)
.form("redirect_uri", config.getRedirectUri());
HttpResponse response = request.execute();
return response.body();
}
/**
* 获取 TopIAM 用户信息接口响应
*
* @param authToken 访问令牌
* @return 用户信息响应体
*/
@Override
protected String doGetUserInfo(AuthToken authToken) {
return new HttpUtils(config.getHttpConfig()).get(source.userInfo(), null, new HttpHeader()
.add("Content-Type", "application/json")
.add("Authorization", "Bearer " + authToken.getAccessToken()), false).getBody();
}
/**
* 生成 TopIAM 授权地址
*
* @param state 授权状态
* @return 授权地址
*/
@Override
public String authorize(String state) {
return UrlBuilder.fromBaseUrl(super.authorize(state))
.queryParam("scope", StrUtil.join("%20", config.getScopes()))
.build();
}
/**
* 校验 TopIAM 接口响应
*
* @param object 响应内容
*/
private static void checkResponse(Dict object) {
// oauth/token 验证异常
if (object.containsKey("error")) {
throw new AuthException(object.getStr("error_description"));
}
// user 验证异常
if (object.containsKey("message")) {
throw new AuthException(object.getStr("message"));
}
}
}

View File

@ -1,51 +0,0 @@
package org.dromara.common.social.topiam;
import me.zhyd.oauth.config.AuthSource;
import me.zhyd.oauth.request.AuthDefaultRequest;
/**
* Oauth2 默认接口说明
*
* @author xlsea
* @since 2024-01-06
*/
public enum AuthTopIamSource implements AuthSource {
/**
* 测试
*/
TOPIAM {
/**
* 授权的api
*/
@Override
public String authorize() {
return AuthTopIamRequest.SERVER_URL + "/oauth2/auth";
}
/**
* 获取accessToken的api
*/
@Override
public String accessToken() {
return AuthTopIamRequest.SERVER_URL + "/oauth2/token";
}
/**
* 获取用户信息的api
*/
@Override
public String userInfo() {
return AuthTopIamRequest.SERVER_URL + "/oauth2/userinfo";
}
/**
* 平台对应的 AuthRequest 实现类必须继承自 {@link AuthDefaultRequest}
*/
@Override
public Class<? extends AuthDefaultRequest> getTargetClass() {
return AuthTopIamRequest.class;
}
}
}

View File

@ -10,9 +10,6 @@ import me.zhyd.oauth.request.*;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.social.config.properties.SocialLoginConfigProperties;
import org.dromara.common.social.config.properties.SocialProperties;
import org.dromara.common.social.gitea.AuthGiteaRequest;
import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
import org.dromara.common.social.topiam.AuthTopIamRequest;
/**
* 认证授权工具类
@ -58,6 +55,7 @@ public class SocialUtils {
.clientId(obj.getClientId())
.clientSecret(obj.getClientSecret())
.redirectUri(obj.getRedirectUri())
.serverUrl(obj.getServerUrl())
.scopes(obj.getScopes());
return switch (source.toLowerCase()) {
case "dingtalk" -> new AuthDingTalkV2Request(builder.build(), STATE_CACHE);

View File

@ -19,6 +19,17 @@
<version>${snailai.version}</version>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-rest5-client</artifactId>
<version>9.4.2</version>
</dependency>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>9.4.2</version>
</dependency>
<!-- Spring Boot Admin 客户端 -->
<dependency>
<groupId>de.codecentric</groupId>

View File

@ -59,10 +59,10 @@ public class ${ClassName}Controller extends BaseController {
}
</#if>
<#if enableExport>
/**
* 导出${functionName}列表
*/
<#if enableExport>
@SaCheckPermission("${permissionPrefix}:export")
@Log(title = "${functionName}", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@ -153,5 +153,3 @@ public class ${ClassName}Controller extends BaseController {
return toAjax(${className}Service.deleteWithValidByIds(List.of(${pkColumn.javaField}s), true));
}
}

View File

@ -46,5 +46,3 @@ public class ${ClassName} extends BaseEntity {
</#list>
}

View File

@ -13,6 +13,3 @@ import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
public interface ${ClassName}Mapper extends BaseMapperPlus<${ClassName}, ${ClassName}Vo> {
}

View File

@ -101,7 +101,5 @@ public interface I${ClassName}Service {
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<${pkColumn.javaType}> ids, Boolean isValid);
}

View File

@ -43,7 +43,7 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service {
private final ${ClassName}Mapper ${className}Mapper;
/**
/**
* 查询${functionName}
*
* @param ${pkColumn.javaField} 主键
@ -342,6 +342,5 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service {
}
return ${className}Mapper.deleteByIds(ids) > 0;
}
}

View File

@ -17,8 +17,6 @@ import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* ${functionName}视图对象 ${tableName}
*
@ -61,5 +59,3 @@ public class ${ClassName}Vo implements Serializable {
</#list>
}

View File

@ -14,9 +14,7 @@ values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
<#if enableExport>
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '');
</#if>

View File

@ -14,9 +14,7 @@ values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
<#if enableExport>
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, sysdate, null, null, '');
</#if>

View File

@ -14,10 +14,7 @@ values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
<#if enableExport>
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, now(), null, null, '');
</#if>

View File

@ -14,9 +14,7 @@ values(${table.menuIds[3]}, '${functionName}修改', ${table.menuIds[0]}, 3, '#
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[4]}, '${functionName}删除', ${table.menuIds[0]}, 4, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:remove', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
<#if enableExport>
insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark)
values(${table.menuIds[5]}, '${functionName}导出', ${table.menuIds[0]}, 5, '#', '', 'N', 'Y', 'F', '0', '0', '${permissionPrefix}:export', '#', 1761000000000000103, 1761100000000000001, getdate(), null, null, '');
</#if>

View File

@ -10,7 +10,6 @@ import request from '@/utils/request';
* @param query
* @returns {*}
*/
export const list${BusinessName} = (query?: ${BusinessName}Query): AxiosPromise<<#if table.tree>${BusinessName}VO[]<#else>PageResult<${BusinessName}VO></#if>> => {
return request({
url: '/${moduleName}/${businessName}/list',
@ -100,5 +99,3 @@ export const del${BusinessName} = (${pkColumn.javaField}: string | number | Arra
method: 'delete'
});
};

View File

@ -425,7 +425,7 @@ import { useTreeTableExpand } from '@/hooks/tree/useTreeTableExpand';
import { useDict } from '@/utils/dict';
</#if>
import modal from '@/plugins/modal';
import { handleTree } from '@/utils/ruoyi';
import { handleTree<#if needParseTime>, parseTime</#if> } from '@/utils/ruoyi';
<#if enableExport>
import { download as requestDownload } from '@/utils/request';
</#if>

View File

@ -4,6 +4,3 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${packageName}.mapper.${ClassName}Mapper">
</mapper>

View File

@ -97,7 +97,6 @@ public class SysDeptServiceImpl implements ISysDeptService, DeptService {
private LambdaQueryWrapper<SysDept> buildQueryWrapper(SysDeptBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryBuilder<SysDept> builder = QueryBuilder.lambda(SysDept.class)
.eq(SysDept::getDelFlag, SystemConstants.NORMAL)
.eqIfPresent(SysDept::getDeptId, bo.getDeptId())
.eqIfPresent(SysDept::getParentId, bo.getParentId())
.likeIfText(SysDept::getDeptName, bo.getDeptName())

View File

@ -4,19 +4,16 @@ import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.Constants;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.ip.AddressUtils;
import org.dromara.common.log.event.LoginInfoEvent;
import org.dromara.common.mybatis.core.mapper.LambdaCrudChainWrapper;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.domain.SysLoginInfo;
import org.dromara.system.domain.bo.SysLoginInfoBo;
import org.dromara.system.domain.vo.SysClientVo;
@ -55,11 +52,10 @@ public class SysLoginInfoServiceImpl implements ISysLoginInfoService {
@Async
@EventListener
public void recordLoginInfo(LoginInfoEvent loginInfoEvent) {
HttpServletRequest request = loginInfoEvent.getRequest();
UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
String ip = ServletUtils.getClientIP(request);
UserAgent userAgent = UserAgentUtil.parse(loginInfoEvent.getUserAgent());
String ip = loginInfoEvent.getIp();
// 客户端信息
String clientId = request.getHeader(LoginHelper.CLIENT_KEY);
String clientId = loginInfoEvent.getClientId();
SysClientVo client = null;
if (StringUtils.isNotBlank(clientId)) {
client = clientService.queryByClientId(clientId);

View File

@ -88,7 +88,6 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
private Wrapper<SysUser> buildQueryWrapper(SysUserBo user) {
Map<String, Object> params = user.getParams();
LambdaQueryWrapper<SysUser> wrapper = QueryBuilder.lambda(SysUser.class)
.eq(SysUser::getDelFlag, SystemConstants.NORMAL)
.eqIfPresent(SysUser::getUserId, user.getUserId())
.in(StringUtils.isNotBlank(user.getUserIds()), SysUser::getUserId, StringUtils.splitTo(user.getUserIds(), Convert::toLong))
.likeIfText(SysUser::getUserName, user.getUserName())

View File

@ -87,7 +87,7 @@
<!-- Warm-Flow 工作流引擎 -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
<artifactId>warm-flow-mybatis-plus-sb4-starter</artifactId>
</dependency>
<!-- Warm-Flow UI 插件 -->

View File

@ -92,7 +92,8 @@ public class WorkflowGlobalListener implements GlobalListener {
bo.setNickName(nickNameMap.getOrDefault(id, StringUtils.EMPTY));
return bo;
});
variable.put(FlowConstant.FLOW_COPY_LIST, list);
// 节点配置仅作为默认值保留本次办理显式提交的抄送人或空名单
variable.putIfAbsent(FlowConstant.FLOW_COPY_LIST, list);
}
if (CollUtil.isNotEmpty(nodeExt.getVariables())) {
variable.putAll(nodeExt.getVariables());

View File

@ -5,11 +5,10 @@ import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.*;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.warm.flow.core.service.DefService;
import org.dromara.warm.flow.orm.entity.FlowDefinition;
import org.dromara.warm.flow.ui.service.CategoryService;
@ -186,16 +185,12 @@ public class FlwCategoryServiceImpl implements IFlwCategoryService, CategoryServ
* @return 查询条件包装器
*/
private LambdaQueryWrapper<FlowCategory> buildQueryWrapper(FlowCategoryBo bo) {
LambdaQueryWrapper<FlowCategory> lqw = Wrappers.lambdaQuery();
lqw.eq(FlowCategory::getDelFlag, SystemConstants.NORMAL);
lqw.eq(ObjectUtil.isNotNull(bo.getCategoryId()), FlowCategory::getCategoryId, bo.getCategoryId());
lqw.eq(ObjectUtil.isNotNull(bo.getParentId()), FlowCategory::getParentId, bo.getParentId());
lqw.like(StringUtils.isNotBlank(bo.getCategoryName()), FlowCategory::getCategoryName, bo.getCategoryName());
lqw.orderByAsc(FlowCategory::getAncestors);
lqw.orderByAsc(FlowCategory::getParentId);
lqw.orderByAsc(FlowCategory::getOrderNum);
lqw.orderByAsc(FlowCategory::getCategoryId);
return lqw;
return QueryBuilder.lambda(FlowCategory.class)
.eqIfPresent(FlowCategory::getCategoryId, bo.getCategoryId())
.eqIfPresent(FlowCategory::getParentId, bo.getParentId())
.likeIfText(FlowCategory::getCategoryName, bo.getCategoryName())
.orderByAsc(FlowCategory::getAncestors, FlowCategory::getParentId, FlowCategory::getOrderNum, FlowCategory::getCategoryId)
.build();
}
/**

View File

@ -262,8 +262,7 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
Map<String, String> variables = StringUtils.str2List(value, StringUtils.SEPARATOR, true, true).stream()
.map(s -> StringUtils.split(s, "="))
.filter(arr -> arr.length == 2)
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1], (l, r) -> l));
nodeExtVo.setVariables(variables);
} else {
// 未知扩展类型记录日志

View File

@ -1,5 +1,7 @@
package org.dromara.workflow.service.impl;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
@ -79,6 +81,12 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
private static final String START_PROCESS_CHAIN = "startProcessChain";
private static final String COMPLETE_TASK_CHAIN = "completeTaskChain";
private static final String TASK_OPERATION_CHAIN = "taskOperationChain";
private static final Set<String> TASK_READ_ASSIGNEE_TYPES = Set.of(
TaskAssigneeType.APPROVER.getCode(),
TaskAssigneeType.TRANSFER.getCode(),
TaskAssigneeType.DELEGATE.getCode(),
TaskAssigneeType.COPY.getCode()
);
private final TaskService taskService;
private final InsService insService;
@ -504,6 +512,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
if (ObjectUtil.isNull(task)) {
return null;
}
checkTaskReadAccess(task);
FlowTaskVo flowTaskVo = BeanUtil.toBean(task, FlowTaskVo.class);
Instance instance = insService.getById(task.getInstanceId());
if (ObjectUtil.isNull(instance)) {
@ -698,14 +707,62 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
*/
@Override
public List<UserDTO> currentTaskAllUser(List<Long> taskIds) {
if (CollUtil.isEmpty(taskIds)) {
return Collections.emptyList();
}
List<Long> validTaskIds = new ArrayList<>();
for (Long taskId : taskIds) {
if (ObjectUtil.isNull(taskId)) {
continue;
}
Task task = taskService.getById(taskId);
if (ObjectUtil.isNotNull(task)) {
checkTaskReadAccess(task);
validTaskIds.add(taskId);
}
}
if (CollUtil.isEmpty(validTaskIds)) {
return Collections.emptyList();
}
// 获取与当前任务关联的用户列表
List<User> userList = FlowEngine.userService().getByAssociateds(taskIds);
List<User> userList = FlowEngine.userService().getByAssociateds(validTaskIds);
if (CollUtil.isEmpty(userList)) {
return Collections.emptyList();
}
return userService.selectListByIds(StreamUtils.toSet(userList, e -> Convert.toLong(e.getProcessedBy())));
}
/**
* 校验当前登录用户是否可以读取任务信息
*
* <p>任务读取接口同时服务于普通审批和流程监控不能只依赖管理菜单权限
* 普通用户必须是任务办理关系人流程发起人或抄送接收人监控/管理角色
* 通过任务列表或编辑权限进入管理读取范围</p>
*
* @param task 当前任务
*/
private void checkTaskReadAccess(Task task) {
if (LoginHelper.isSuperAdmin()
|| StpUtil.hasPermission("workflow:task:list")
|| StpUtil.hasPermission("workflow:task:edit")) {
return;
}
String userId = LoginHelper.getUserIdStr();
List<User> associatedUsers = FlowEngine.userService().getByAssociateds(List.of(task.getId()));
boolean taskAssignee = CollUtil.isNotEmpty(associatedUsers)
&& associatedUsers.stream()
.anyMatch(user -> Objects.equals(userId, user.getProcessedBy())
&& TASK_READ_ASSIGNEE_TYPES.contains(Convert.toStr(user.getType())));
if (taskAssignee) {
return;
}
Instance instance = insService.getById(task.getInstanceId());
if (ObjectUtil.isNotNull(instance) && Objects.equals(instance.getCreateBy(), userId)) {
return;
}
throw new NotPermissionException("无权访问该流程任务");
}
/**
* 按照节点编码查询节点
*