diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/IAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/IAuthStrategy.java index 15546e89f..b050c978f 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/service/IAuthStrategy.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/service/IAuthStrategy.java @@ -1,12 +1,16 @@ package org.dromara.web.service; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.hutool.core.util.ObjectUtil; import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.SpringUtils; -import org.dromara.system.domain.SysClient; +import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.system.domain.vo.SysClientVo; import org.dromara.web.domain.vo.LoginVo; +import java.util.function.Consumer; + /** * 授权策略 * @@ -34,6 +38,37 @@ public interface IAuthStrategy { return instance.login(body, client); } + /** + * 按客户端配置构建统一登录参数。 + * + * @param client 客户端配置 + * @return Sa-Token 登录参数 + */ + static SaLoginParameter buildLoginParameter(SysClientVo client) { + return buildLoginParameter(client, null); + } + + /** + * 按客户端配置构建统一登录参数,并预留自定义扩展入口。 + * + * @param client 客户端配置 + * @param customizer 自定义扩展逻辑 + * @return Sa-Token 登录参数 + */ + static SaLoginParameter buildLoginParameter(SysClientVo client, Consumer customizer) { + SaLoginParameter model = new SaLoginParameter(); + model.setDeviceType(client.getDeviceType()); + model.setTimeout(client.getTimeout()); + model.setActiveTimeout(client.getActiveTimeout()); + model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId()); + model.setExtra(LoginHelper.CLIENT_ACCESS_PATH_KEY, client.getAccessPath()); + model.setExtra(LoginHelper.CLIENT_IP_WHITELIST_KEY, client.getIpWhitelist()); + if (ObjectUtil.isNotNull(customizer)) { + customizer.accept(model); + } + return model; + } + /** * 登录 * diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java index 73e9ec92f..21fae98cd 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/EmailAuthStrategy.java @@ -61,13 +61,7 @@ public class EmailAuthStrategy implements IAuthStrategy { LoginUser loginUser = loginService.buildLoginUser(user); loginUser.setClientKey(client.getClientKey()); loginUser.setDeviceType(client.getDeviceType()); - SaLoginParameter model = new SaLoginParameter(); - model.setDeviceType(client.getDeviceType()); - // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 - // 例如: 后台用户30分钟过期 app用户1天过期 - model.setTimeout(client.getTimeout()); - model.setActiveTimeout(client.getActiveTimeout()); - model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId()); + SaLoginParameter model = IAuthStrategy.buildLoginParameter(client); // 生成token LoginHelper.login(loginUser, model); diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java index 4968d8491..68a5ebe26 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/PasswordAuthStrategy.java @@ -73,13 +73,7 @@ public class PasswordAuthStrategy implements IAuthStrategy { LoginUser loginUser = loginService.buildLoginUser(user); loginUser.setClientKey(client.getClientKey()); loginUser.setDeviceType(client.getDeviceType()); - SaLoginParameter model = new SaLoginParameter(); - model.setDeviceType(client.getDeviceType()); - // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 - // 例如: 后台用户30分钟过期 app用户1天过期 - model.setTimeout(client.getTimeout()); - model.setActiveTimeout(client.getActiveTimeout()); - model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId()); + SaLoginParameter model = IAuthStrategy.buildLoginParameter(client); // 生成token LoginHelper.login(loginUser, model); diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java index 686ee1626..46623a75c 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SmsAuthStrategy.java @@ -61,13 +61,7 @@ public class SmsAuthStrategy implements IAuthStrategy { LoginUser loginUser = loginService.buildLoginUser(user); loginUser.setClientKey(client.getClientKey()); loginUser.setDeviceType(client.getDeviceType()); - SaLoginParameter model = new SaLoginParameter(); - model.setDeviceType(client.getDeviceType()); - // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 - // 例如: 后台用户30分钟过期 app用户1天过期 - model.setTimeout(client.getTimeout()); - model.setActiveTimeout(client.getActiveTimeout()); - model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId()); + SaLoginParameter model = IAuthStrategy.buildLoginParameter(client); // 生成token LoginHelper.login(loginUser, model); diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java index d8692fbc0..7601eee4e 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/SocialAuthStrategy.java @@ -73,13 +73,7 @@ public class SocialAuthStrategy implements IAuthStrategy { LoginUser loginUser = loginService.buildLoginUser(user); loginUser.setClientKey(client.getClientKey()); loginUser.setDeviceType(client.getDeviceType()); - SaLoginParameter model = new SaLoginParameter(); - model.setDeviceType(client.getDeviceType()); - // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 - // 例如: 后台用户30分钟过期 app用户1天过期 - model.setTimeout(client.getTimeout()); - model.setActiveTimeout(client.getActiveTimeout()); - model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId()); + SaLoginParameter model = IAuthStrategy.buildLoginParameter(client); // 生成token LoginHelper.login(loginUser, model); diff --git a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/XcxAuthStrategy.java b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/XcxAuthStrategy.java index db8ba66d3..6827b9990 100644 --- a/ruoyi-admin/src/main/java/org/dromara/web/service/impl/XcxAuthStrategy.java +++ b/ruoyi-admin/src/main/java/org/dromara/web/service/impl/XcxAuthStrategy.java @@ -82,13 +82,7 @@ public class XcxAuthStrategy implements IAuthStrategy { loginUser.setDeviceType(client.getDeviceType()); loginUser.setOpenid(openid); - SaLoginParameter model = new SaLoginParameter(); - model.setDeviceType(client.getDeviceType()); - // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置 - // 例如: 后台用户30分钟过期 app用户1天过期 - model.setTimeout(client.getTimeout()); - model.setActiveTimeout(client.getActiveTimeout()); - model.setExtra(LoginHelper.CLIENT_KEY, client.getClientId()); + SaLoginParameter model = IAuthStrategy.buildLoginParameter(client); // 生成token LoginHelper.login(loginUser, model); diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/NetUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/NetUtils.java index 72fdf4033..f0223a515 100644 --- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/NetUtils.java +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/NetUtils.java @@ -7,6 +7,7 @@ import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.utils.regex.RegexUtils; +import java.math.BigInteger; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; @@ -52,7 +53,8 @@ public class NetUtils extends NetUtil { public static boolean isInnerIPv6(String ip) { try { // 判断是否为IPv6地址 - if (InetAddress.getByName(ip) instanceof Inet6Address inet6Address) { + InetAddress inetAddress = InetAddress.getByName(ip); + if (inetAddress instanceof Inet6Address inet6Address) { // isAnyLocalAddress 判断是否为通配符地址,通常不会将其视为内网地址,根据业务场景自行处理判断 // isLinkLocalAddress 判断是否为链路本地地址,通常不算内网地址,是否划分归属于内网需要根据业务场景自行处理判断 // isLoopbackAddress 判断是否为环回地址,与IPv4的 127.0.0.1 同理,用于表示本机 @@ -81,4 +83,69 @@ public class NetUtils extends NetUtil { return RegexUtils.isMatch(PatternPool.IPV4, ip); } + /** + * 匹配 IP 规则,支持精确值、通配符与 CIDR。 + * + * @param rule IP 规则 + * @param clientIp 客户端 IP + * @return 是否匹配 + */ + public static boolean isMatchIpRule(String rule, String clientIp) { + if (StringUtils.isBlank(rule) || StringUtils.isBlank(clientIp)) { + return false; + } + String ipRule = StringUtils.trim(rule); + if (StringUtils.equals(ipRule, clientIp)) { + return true; + } + if (ipRule.contains("/")) { + return isMatchCidr(ipRule, clientIp); + } + if (StringUtils.containsAny(ipRule, "*", "?")) { + String regex = ipRule + .replace(".", "\\.") + .replace("*", ".*") + .replace("?", "."); + return clientIp.matches(regex); + } + return false; + } + + /** + * 匹配 CIDR 网段。 + * + * @param cidr CIDR 规则 + * @param clientIp 客户端 IP + * @return 是否命中 + */ + public static boolean isMatchCidr(String cidr, String clientIp) { + try { + String[] parts = cidr.split("/"); + if (parts.length != 2) { + return false; + } + InetAddress networkAddress = InetAddress.getByName(parts[0]); + InetAddress currentAddress = InetAddress.getByName(clientIp); + byte[] networkBytes = networkAddress.getAddress(); + byte[] currentBytes = currentAddress.getAddress(); + if (networkBytes.length != currentBytes.length) { + return false; + } + int prefixLength = Integer.parseInt(parts[1]); + int maxPrefix = networkBytes.length * 8; + if (prefixLength < 0 || prefixLength > maxPrefix) { + return false; + } + BigInteger mask = prefixLength == 0 + ? BigInteger.ZERO + : BigInteger.ONE.shiftLeft(prefixLength).subtract(BigInteger.ONE).shiftLeft(maxPrefix - prefixLength); + BigInteger network = new BigInteger(1, networkBytes); + BigInteger current = new BigInteger(1, currentBytes); + return network.and(mask).equals(current.and(mask)); + } catch (UnknownHostException | NumberFormatException e) { + log.debug("IP白名单CIDR规则解析失败: {}", cidr, e); + return false; + } + } + } diff --git a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java index 857418f3d..5446888fb 100644 --- a/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java +++ b/ruoyi-common/ruoyi-common-satoken/src/main/java/org/dromara/common/satoken/utils/LoginHelper.java @@ -40,6 +40,8 @@ public class LoginHelper { public static final String DEPT_NAME_KEY = "deptName"; public static final String DEPT_CATEGORY_KEY = "deptCategory"; public static final String CLIENT_KEY = "clientid"; + public static final String CLIENT_ACCESS_PATH_KEY = "clientAccessPath"; + public static final String CLIENT_IP_WHITELIST_KEY = "clientIpWhitelist"; /** * 登录系统 基于 设备类型 diff --git a/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfig.java b/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfig.java index eee99c74c..45fee93b6 100644 --- a/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfig.java +++ b/ruoyi-common/ruoyi-common-security/src/main/java/org/dromara/common/security/config/SecurityConfig.java @@ -1,6 +1,7 @@ package org.dromara.common.security.config; import cn.dev33.satoken.exception.NotLoginException; +import cn.dev33.satoken.exception.NotPermissionException; import cn.dev33.satoken.filter.SaServletFilter; import cn.dev33.satoken.httpauth.basic.SaHttpBasicUtil; import cn.dev33.satoken.interceptor.SaInterceptor; @@ -13,6 +14,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.constant.HttpStatus; +import org.dromara.common.core.utils.NetUtils; import org.dromara.common.core.utils.ServletUtils; import org.dromara.common.core.utils.SpringUtils; import org.dromara.common.core.utils.StringUtils; @@ -26,6 +28,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.List; + /** * 权限安全配置 * @@ -38,6 +42,8 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @RequiredArgsConstructor public class SecurityConfig implements WebMvcConfigurer { + private static final String CLIENT_RULE_SEPARATOR_REGEX = "[,;\\r\\n]+"; + private final SecurityProperties securityProperties; @Value("${message.path:/resource/message}") private String messagePath; @@ -74,6 +80,7 @@ public class SecurityConfig implements WebMvcConfigurer { "-100", "客户端ID与Token不匹配", StpUtil.getTokenValue()); } + validateClientAccessRules(request); // 有效率影响 用于临时测试 // if (log.isDebugEnabled()) { @@ -109,4 +116,41 @@ public class SecurityConfig implements WebMvcConfigurer { }); } + /** + * 按客户端配置校验接口访问路径与来源 IP。 + * + * @param request 当前请求 + */ + private void validateClientAccessRules(HttpServletRequest request) { + String requestPath = StringUtils.blankToDefault(request.getServletPath(), request.getRequestURI()); + String accessPath = getTokenExtra(LoginHelper.CLIENT_ACCESS_PATH_KEY); + if (StringUtils.isNotBlank(accessPath)) { + List accessPathList = StringUtils.str2List(accessPath, CLIENT_RULE_SEPARATOR_REGEX, true, true); + if (!StringUtils.matches(requestPath, accessPathList)) { + throw new NotPermissionException("当前客户端未授权访问该接口路径"); + } + } + + String ipWhitelist = getTokenExtra(LoginHelper.CLIENT_IP_WHITELIST_KEY); + if (StringUtils.isNotBlank(ipWhitelist)) { + String clientIp = ServletUtils.getClientIP(request); + List ipWhitelistList = StringUtils.str2List(ipWhitelist, CLIENT_RULE_SEPARATOR_REGEX, true, true); + boolean matched = ipWhitelistList.stream().anyMatch(rule -> NetUtils.isMatchIpRule(rule, clientIp)); + if (!matched) { + throw new NotPermissionException("当前客户端IP不在白名单内"); + } + } + } + + /** + * 读取 token 扩展信息,兼容空值场景。 + * + * @param key 扩展字段 + * @return 扩展值 + */ + private String getTokenExtra(String key) { + Object extra = StpUtil.getExtra(key); + return extra == null ? null : extra.toString(); + } + } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysClient.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysClient.java index 426bc00a9..c3ae5220a 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysClient.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/SysClient.java @@ -54,6 +54,16 @@ public class SysClient extends BaseEntity { */ private String deviceType; + /** + * 允许访问路径 + */ + private String accessPath; + + /** + * IP白名单 + */ + private String ipWhitelist; + /** * token活跃超时时间 */ diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysClientBo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysClientBo.java index bbc8e0e85..74c13ce3e 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysClientBo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/bo/SysClientBo.java @@ -64,6 +64,26 @@ public class SysClientBo implements Serializable { */ private String deviceType; + /** + * 允许访问路径 + */ + private String accessPath; + + /** + * 允许访问路径列表 + */ + private List accessPathList; + + /** + * IP白名单 + */ + private String ipWhitelist; + + /** + * IP白名单列表 + */ + private List ipWhitelistList; + /** * token活跃超时时间 */ diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysClientVo.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysClientVo.java index feb151066..05e569ef8 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysClientVo.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/domain/vo/SysClientVo.java @@ -67,6 +67,28 @@ public class SysClientVo implements Serializable { */ private String deviceType; + /** + * 允许访问路径 + */ + @ExcelProperty(value = "允许访问路径") + private String accessPath; + + /** + * 允许访问路径列表 + */ + private List accessPathList; + + /** + * IP白名单 + */ + @ExcelProperty(value = "IP白名单") + private String ipWhitelist; + + /** + * IP白名单列表 + */ + private List ipWhitelistList; + /** * token活跃超时时间 */ diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysClientServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysClientServiceImpl.java index 6ffd8da73..d03d9bd48 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysClientServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysClientServiceImpl.java @@ -25,6 +25,7 @@ import org.springframework.stereotype.Service; import java.util.Collection; import java.util.List; +import java.util.function.UnaryOperator; /** * 客户端管理Service业务层处理 @@ -37,6 +38,8 @@ import java.util.List; @Service public class SysClientServiceImpl implements ISysClientService { + private static final String CLIENT_RULE_SEPARATOR_REGEX = "[,;\\r\\n]+"; + private final SysClientMapper baseMapper; /** @@ -48,7 +51,7 @@ public class SysClientServiceImpl implements ISysClientService { @Override public SysClientVo queryById(Long id) { SysClientVo vo = baseMapper.selectVoById(id); - vo.setGrantTypeList(StringUtils.splitList(vo.getGrantType())); + fillClientRuleFields(vo); return vo; } @@ -61,7 +64,9 @@ public class SysClientServiceImpl implements ISysClientService { @Cacheable(cacheNames = CacheNames.SYS_CLIENT, key = "#clientId") @Override public SysClientVo queryByClientId(String clientId) { - return baseMapper.selectVoOne(new LambdaQueryWrapper().eq(SysClient::getClientId, clientId)); + SysClientVo vo = baseMapper.selectVoOne(new LambdaQueryWrapper().eq(SysClient::getClientId, clientId)); + fillClientRuleFields(vo); + return vo; } /** @@ -75,7 +80,7 @@ public class SysClientServiceImpl implements ISysClientService { public PageResult queryPageList(SysClientBo bo, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); - result.getRecords().forEach(r -> r.setGrantTypeList(StringUtils.splitList(r.getGrantType()))); + result.getRecords().forEach(this::fillClientRuleFields); return PageResult.build(result.getRecords(), result.getTotal()); } @@ -88,7 +93,9 @@ public class SysClientServiceImpl implements ISysClientService { @Override public List queryList(SysClientBo bo) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); - return baseMapper.selectVoList(lqw); + List list = baseMapper.selectVoList(lqw); + list.forEach(this::fillClientRuleFields); + return list; } /** @@ -117,6 +124,8 @@ public class SysClientServiceImpl implements ISysClientService { public Boolean insertByBo(SysClientBo bo) { SysClient add = MapstructUtils.convert(bo, SysClient.class); add.setGrantType(CollUtil.join(bo.getGrantTypeList(), StringUtils.SEPARATOR)); + add.setAccessPath(resolveRuleValue(bo.getAccessPath(), bo.getAccessPathList(), this::normalizeAccessPath)); + add.setIpWhitelist(resolveRuleValue(bo.getIpWhitelist(), bo.getIpWhitelistList(), UnaryOperator.identity())); // 生成clientid String clientKey = bo.getClientKey(); String clientSecret = bo.getClientSecret(); @@ -139,6 +148,8 @@ public class SysClientServiceImpl implements ISysClientService { public Boolean updateByBo(SysClientBo bo) { SysClient update = MapstructUtils.convert(bo, SysClient.class); update.setGrantType(StringUtils.joinComma(bo.getGrantTypeList())); + update.setAccessPath(resolveRuleValue(bo.getAccessPath(), bo.getAccessPathList(), this::normalizeAccessPath)); + update.setIpWhitelist(resolveRuleValue(bo.getIpWhitelist(), bo.getIpWhitelistList(), UnaryOperator.identity())); return baseMapper.updateById(update) > 0; } @@ -185,4 +196,73 @@ public class SysClientServiceImpl implements ISysClientService { return !exist; } + /** + * 回填客户端扩展规则字段,便于前端直接展示和编辑。 + * + * @param vo 客户端视图对象 + */ + private void fillClientRuleFields(SysClientVo vo) { + if (ObjectUtil.isNull(vo)) { + return; + } + vo.setGrantTypeList(StringUtils.splitList(vo.getGrantType())); + vo.setAccessPathList(parseRuleList(vo.getAccessPath(), this::normalizeAccessPath)); + vo.setIpWhitelistList(parseRuleList(vo.getIpWhitelist(), UnaryOperator.identity())); + } + + /** + * 统一处理白名单与路径规则的入库格式。 + * + * @param rawValue 原始字符串 + * @param listValue 列表值 + * @param normalizer 单条规则归一化器 + * @return 逗号拼接后的规则串 + */ + private String resolveRuleValue(String rawValue, List listValue, UnaryOperator normalizer) { + List rules = CollUtil.isNotEmpty(listValue) + ? listValue + : StringUtils.str2List(rawValue, CLIENT_RULE_SEPARATOR_REGEX, true, true); + if (CollUtil.isEmpty(rules)) { + return listValue != null || rawValue != null ? "" : null; + } + return CollUtil.join(rules.stream() + .map(normalizer) + .filter(StringUtils::isNotBlank) + .toList(), StringUtils.SEPARATOR); + } + + /** + * 将规则串转换为列表。 + * + * @param value 规则串 + * @param normalizer 单条规则归一化器 + * @return 规则列表 + */ + private List parseRuleList(String value, UnaryOperator normalizer) { + return StringUtils.str2List(value, CLIENT_RULE_SEPARATOR_REGEX, true, true).stream() + .map(normalizer) + .filter(StringUtils::isNotBlank) + .toList(); + } + + /** + * 统一补齐路径前导斜杠,避免配置成 app/** 时无法命中。 + * + * @param path 路径规则 + * @return 规范化后的路径规则 + */ + private String normalizeAccessPath(String path) { + if (StringUtils.isBlank(path)) { + return null; + } + String accessPath = StringUtils.trim(path); + if (StringUtils.isBlank(accessPath)) { + return null; + } + if (StringUtils.equals(accessPath, "*") || StringUtils.equals(accessPath, "/**")) { + return "/**"; + } + return accessPath.startsWith(StringUtils.SLASH) ? accessPath : StringUtils.SLASH + accessPath; + } + } diff --git a/script/sql/oracle/oracle_ry_vue.sql b/script/sql/oracle/oracle_ry_vue.sql index 0da105dcd..b176e069a 100644 --- a/script/sql/oracle/oracle_ry_vue.sql +++ b/script/sql/oracle/oracle_ry_vue.sql @@ -1211,6 +1211,8 @@ create table sys_client ( client_secret varchar2(255) default null, grant_type varchar2(255) default null, device_type varchar2(32) default null, + access_path varchar2(2000) default null, + ip_whitelist varchar2(1000) default null, active_timeout number(11) default 1800, timeout number(11) default 604800, status char(1) default '0', @@ -1231,6 +1233,8 @@ comment on column sys_client.client_key is '客户端key'; comment on column sys_client.client_secret is '客户端秘钥'; comment on column sys_client.grant_type is '授权类型'; comment on column sys_client.device_type is '设备类型'; +comment on column sys_client.access_path is '允许访问路径'; +comment on column sys_client.ip_whitelist is 'IP白名单'; comment on column sys_client.active_timeout is 'token活跃超时时间'; comment on column sys_client.timeout is 'token固定超时'; comment on column sys_client.status is '状态(0正常 1停用)'; @@ -1241,8 +1245,8 @@ comment on column sys_client.create_time is '创建时间'; comment on column sys_client.update_by is '更新者'; comment on column sys_client.update_time is '更新时间'; -insert into sys_client values (1762000000000000001, 'e5cd7e4891bf95d1d19206ce24a7b32e', 'pc', 'pc123', 'password,social', 'pc', 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, sysdate, 1761100000000000001, sysdate); -insert into sys_client values (1762000000000000002, '428a8310cd442757ae699df5d894f051', 'app', 'app123', 'password,sms,social', 'android', 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, sysdate, 1761100000000000001, sysdate); +insert into sys_client values (1762000000000000001, 'e5cd7e4891bf95d1d19206ce24a7b32e', 'pc', 'pc123', 'password,social', 'pc', null, null, 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, sysdate, 1761100000000000001, sysdate); +insert into sys_client values (1762000000000000002, '428a8310cd442757ae699df5d894f051', 'app', 'app123', 'password,sms,social', 'android', '/app/**', null, 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, sysdate, 1761100000000000001, sysdate); create table test_demo ( id number(20) not null, diff --git a/script/sql/postgres/postgres_ry_vue.sql b/script/sql/postgres/postgres_ry_vue.sql index 93874aad8..4c6766345 100644 --- a/script/sql/postgres/postgres_ry_vue.sql +++ b/script/sql/postgres/postgres_ry_vue.sql @@ -1206,6 +1206,8 @@ create table sys_client ( client_secret varchar(255) default ''::varchar, grant_type varchar(255) default ''::varchar, device_type varchar(32) default ''::varchar, + access_path varchar(2000) default ''::varchar, + ip_whitelist varchar(1000) default ''::varchar, active_timeout int4 default 1800, timeout int4 default 604800, status char(1) default '0'::bpchar, @@ -1225,6 +1227,8 @@ comment on column sys_client.client_key is '客户端key'; comment on column sys_client.client_secret is '客户端秘钥'; comment on column sys_client.grant_type is '授权类型'; comment on column sys_client.device_type is '设备类型'; +comment on column sys_client.access_path is '允许访问路径'; +comment on column sys_client.ip_whitelist is 'IP白名单'; comment on column sys_client.active_timeout is 'token活跃超时时间'; comment on column sys_client.timeout is 'token固定超时'; comment on column sys_client.status is '状态(0正常 1停用)'; @@ -1235,8 +1239,8 @@ comment on column sys_client.create_time is '创建时间'; comment on column sys_client.update_by is '更新者'; comment on column sys_client.update_time is '更新时间'; -insert into sys_client values (1762000000000000001, 'e5cd7e4891bf95d1d19206ce24a7b32e', 'pc', 'pc123', 'password,social', 'pc', 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, now(), 1761100000000000001, now()); -insert into sys_client values (1762000000000000002, '428a8310cd442757ae699df5d894f051', 'app', 'app123', 'password,sms,social', 'android', 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, now(), 1761100000000000001, now()); +insert into sys_client values (1762000000000000001, 'e5cd7e4891bf95d1d19206ce24a7b32e', 'pc', 'pc123', 'password,social', 'pc', '', '', 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, now(), 1761100000000000001, now()); +insert into sys_client values (1762000000000000002, '428a8310cd442757ae699df5d894f051', 'app', 'app123', 'password,sms,social', 'android', '/app/**', '', 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, now(), 1761100000000000001, now()); create table if not exists test_demo ( diff --git a/script/sql/ry_vue.sql b/script/sql/ry_vue.sql index 0a9856722..2c926883b 100644 --- a/script/sql/ry_vue.sql +++ b/script/sql/ry_vue.sql @@ -852,6 +852,8 @@ create table sys_client ( client_secret varchar(255) default null comment '客户端秘钥', grant_type varchar(255) default null comment '授权类型', device_type varchar(32) default null comment '设备类型', + access_path varchar(2000) default null comment '允许访问路径', + ip_whitelist varchar(1000) default null comment 'IP白名单', active_timeout int(11) default 1800 comment 'token活跃超时时间', timeout int(11) default 604800 comment 'token固定超时', status char(1) default '0' comment '状态(0正常 1停用)', @@ -864,8 +866,8 @@ create table sys_client ( primary key (id) ) engine=innodb comment='系统授权表'; -insert into sys_client values (1762000000000000001, 'e5cd7e4891bf95d1d19206ce24a7b32e', 'pc', 'pc123', 'password,social', 'pc', 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, sysdate(), 1761100000000000001, sysdate()); -insert into sys_client values (1762000000000000002, '428a8310cd442757ae699df5d894f051', 'app', 'app123', 'password,sms,social', 'android', 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, sysdate(), 1761100000000000001, sysdate()); +insert into sys_client values (1762000000000000001, 'e5cd7e4891bf95d1d19206ce24a7b32e', 'pc', 'pc123', 'password,social', 'pc', null, null, 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, sysdate(), 1761100000000000001, sysdate()); +insert into sys_client values (1762000000000000002, '428a8310cd442757ae699df5d894f051', 'app', 'app123', 'password,sms,social', 'android', '/app/**', null, 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, sysdate(), 1761100000000000001, sysdate()); CREATE TABLE test_demo diff --git a/script/sql/sqlserver/sqlserver_ry_vue.sql b/script/sql/sqlserver/sqlserver_ry_vue.sql index 9ea51ca17..f8f702f4d 100644 --- a/script/sql/sqlserver/sqlserver_ry_vue.sql +++ b/script/sql/sqlserver/sqlserver_ry_vue.sql @@ -3060,6 +3060,8 @@ CREATE TABLE sys_client client_secret nvarchar(255) DEFAULT '' NULL, grant_type nvarchar(255) DEFAULT '' NULL, device_type nvarchar(32) DEFAULT '' NULL, + access_path nvarchar(2000) DEFAULT '' NULL, + ip_whitelist nvarchar(1000) DEFAULT '' NULL, active_timeout int DEFAULT ((1800)) NULL, timeout int DEFAULT ((604800)) NULL, status nchar(1) DEFAULT ('0') NULL, @@ -3112,6 +3114,18 @@ EXEC sp_addextendedproperty 'TABLE', N'sys_client', 'COLUMN', N'device_type' GO +EXEC sp_addextendedproperty + 'MS_Description', N'允许访问路径', + 'SCHEMA', N'dbo', + 'TABLE', N'sys_client', + 'COLUMN', N'access_path' +GO +EXEC sp_addextendedproperty + 'MS_Description', N'IP白名单', + 'SCHEMA', N'dbo', + 'TABLE', N'sys_client', + 'COLUMN', N'ip_whitelist' +GO EXEC sp_addextendedproperty 'MS_Description', N'token活跃超时时间', 'SCHEMA', N'dbo', @@ -3172,9 +3186,9 @@ EXEC sp_addextendedproperty 'TABLE', N'sys_client' GO -INSERT INTO sys_client VALUES (1762000000000000001, N'e5cd7e4891bf95d1d19206ce24a7b32e', N'pc', N'pc123', N'password,social', N'pc', 1800, 604800, N'0', N'0', 1761000000000000103, 1761100000000000001, getdate(), 1761100000000000001, getdate()); +INSERT INTO sys_client VALUES (1762000000000000001, N'e5cd7e4891bf95d1d19206ce24a7b32e', N'pc', N'pc123', N'password,social', N'pc', N'', N'', 1800, 604800, N'0', N'0', 1761000000000000103, 1761100000000000001, getdate(), 1761100000000000001, getdate()); GO -INSERT INTO sys_client VALUES (1762000000000000002, N'428a8310cd442757ae699df5d894f051', N'app', N'app123', N'password,sms,social', N'android', 1800, 604800, N'0', N'0', 1761000000000000103, 1761100000000000001, getdate(), 1761100000000000001, getdate()); +INSERT INTO sys_client VALUES (1762000000000000002, N'428a8310cd442757ae699df5d894f051', N'app', N'app123', N'password,sms,social', N'android', N'/app/**', N'', 1800, 604800, N'0', N'0', 1761000000000000103, 1761100000000000001, getdate(), 1761100000000000001, getdate()); GO CREATE TABLE test_demo