diff --git a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteClientVo.java b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteClientVo.java index e141f2b67..662885582 100644 --- a/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteClientVo.java +++ b/ruoyi-api/ruoyi-api-system/src/main/java/org/dromara/system/api/domain/vo/RemoteClientVo.java @@ -52,6 +52,16 @@ public class RemoteClientVo implements Serializable { */ private String deviceType; + /** + * 允许访问路径 + */ + private String accessPath; + + /** + * IP白名单 + */ + private String ipWhitelist; + /** * token活跃超时时间 */ diff --git a/ruoyi-auth/src/main/java/org/dromara/auth/service/IAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/IAuthStrategy.java index 28307c386..c07b29596 100644 --- a/ruoyi-auth/src/main/java/org/dromara/auth/service/IAuthStrategy.java +++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/IAuthStrategy.java @@ -1,10 +1,15 @@ package org.dromara.auth.service; +import cn.dev33.satoken.stp.parameter.SaLoginParameter; +import cn.hutool.core.util.ObjectUtil; import org.dromara.auth.domain.vo.LoginVo; import org.dromara.common.core.exception.ServiceException; import org.dromara.common.core.utils.SpringUtils; +import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.system.api.domain.vo.RemoteClientVo; +import java.util.function.Consumer; + /** * 授权策略 * @@ -32,6 +37,30 @@ public interface IAuthStrategy { return instance.login(body, client); } + /** + * 按客户端配置构建统一登录参数。 + */ + static SaLoginParameter buildLoginParameter(RemoteClientVo client) { + return buildLoginParameter(client, null); + } + + /** + * 按客户端配置构建统一登录参数,并预留自定义扩展入口。 + */ + static SaLoginParameter buildLoginParameter(RemoteClientVo 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-auth/src/main/java/org/dromara/auth/service/impl/EmailAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/EmailAuthStrategy.java index acc2d020c..120bbe9a6 100644 --- a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/EmailAuthStrategy.java +++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/EmailAuthStrategy.java @@ -49,13 +49,7 @@ public class EmailAuthStrategy implements IAuthStrategy { loginService.checkLogin(LoginType.EMAIL, loginUser.getUsername(), () -> !validateEmailCode(email, emailCode)); 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-auth/src/main/java/org/dromara/auth/service/impl/PasswordAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/PasswordAuthStrategy.java index 22e5c0566..a0b132c11 100644 --- a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/PasswordAuthStrategy.java +++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/PasswordAuthStrategy.java @@ -61,13 +61,7 @@ public class PasswordAuthStrategy implements IAuthStrategy { loginService.checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, loginUser.getPassword())); 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-auth/src/main/java/org/dromara/auth/service/impl/SmsAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SmsAuthStrategy.java index 758a2a15f..873d20285 100644 --- a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SmsAuthStrategy.java +++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SmsAuthStrategy.java @@ -49,13 +49,7 @@ public class SmsAuthStrategy implements IAuthStrategy { loginService.checkLogin(LoginType.SMS, loginUser.getUsername(), () -> !validateSmsCode(phoneNumber, smsCode)); 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-auth/src/main/java/org/dromara/auth/service/impl/SocialAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SocialAuthStrategy.java index 16df491f1..c3380ad57 100644 --- a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SocialAuthStrategy.java +++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/SocialAuthStrategy.java @@ -69,13 +69,7 @@ public class SocialAuthStrategy implements IAuthStrategy { LoginUser loginUser = remoteUserService.getUserInfo(socialVo.getUserId()); 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-auth/src/main/java/org/dromara/auth/service/impl/XcxAuthStrategy.java b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/XcxAuthStrategy.java index f3248f911..5ba7829d6 100644 --- a/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/XcxAuthStrategy.java +++ b/ruoyi-auth/src/main/java/org/dromara/auth/service/impl/XcxAuthStrategy.java @@ -70,13 +70,7 @@ public class XcxAuthStrategy implements IAuthStrategy { 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-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..abcf56998 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 1344a4eda..bc28bdb4c 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 @@ -39,6 +39,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-gateway/src/main/java/org/dromara/gateway/filter/AuthFilter.java b/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/AuthFilter.java index c28f60de8..7b5182e2c 100644 --- a/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/AuthFilter.java +++ b/ruoyi-gateway/src/main/java/org/dromara/gateway/filter/AuthFilter.java @@ -1,6 +1,7 @@ package org.dromara.gateway.filter; 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; @@ -8,21 +9,21 @@ import cn.dev33.satoken.router.SaRouter; import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.util.SaResult; import cn.dev33.satoken.util.SaTokenConsts; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; 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; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.gateway.config.properties.IgnoreWhiteProperties; -import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.Ordered; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import java.util.List; /** * [Sa-Token 权限认证] 拦截器配置 @@ -32,6 +33,8 @@ import jakarta.servlet.http.HttpServletResponse; @Configuration public class AuthFilter implements WebMvcConfigurer { + private static final String CLIENT_RULE_SEPARATOR_REGEX = "[,;\\r\\n]+"; + private final IgnoreWhiteProperties ignoreWhite; public AuthFilter(IgnoreWhiteProperties ignoreWhite) { @@ -60,6 +63,7 @@ public class AuthFilter implements WebMvcConfigurer { "-100", "客户端ID与Token不匹配", StpUtil.getTokenValue()); } + validateClientAccessRules(request); }))) .addPathPatterns("/**") .excludePathPatterns("/favicon.ico", "/actuator", "/actuator/**", "/resource/sse" , "/error"); @@ -86,4 +90,36 @@ public class AuthFilter implements WebMvcConfigurer { }); } + /** + * 按客户端配置校验接口访问路径与来源IP。 + */ + 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扩展信息,兼容空值场景。 + */ + 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 f6cfcaeb3..28fceb89f 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 @@ -65,6 +65,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 0c36720bb..b5011c5d2 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 @@ -66,6 +66,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 39fb59173..8fd3e8296 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业务层处理 @@ -36,6 +37,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; /** @@ -44,7 +47,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; } @@ -54,7 +57,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; } /** @@ -64,7 +69,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()); } @@ -74,7 +79,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; } private LambdaQueryWrapper buildQueryWrapper(SysClientBo bo) { @@ -94,6 +101,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(); @@ -113,6 +122,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; } @@ -151,4 +162,59 @@ public class SysClientServiceImpl implements ISysClientService { return !exist; } + /** + * 回填客户端扩展规则字段,便于前端直接展示和编辑。 + */ + 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())); + } + + /** + * 统一处理白名单与路径规则的入库格式。 + */ + 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); + } + + /** + * 将规则串转换为列表。 + */ + 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/** 时无法命中。 + */ + 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_cloud.sql b/script/sql/oracle/oracle_ry_cloud.sql index 0ee8c27e1..d83fbb025 100644 --- a/script/sql/oracle/oracle_ry_cloud.sql +++ b/script/sql/oracle/oracle_ry_cloud.sql @@ -1286,6 +1286,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(1024) default null, + ip_whitelist varchar2(1024) default null, active_timeout number(11) default 1800, timeout number(11) default 604800, status char(1) default '0', @@ -1306,6 +1308,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停用)'; @@ -1316,8 +1320,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', null, 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_cloud.sql b/script/sql/postgres/postgres_ry_cloud.sql index b54211e92..fc97f0868 100644 --- a/script/sql/postgres/postgres_ry_cloud.sql +++ b/script/sql/postgres/postgres_ry_cloud.sql @@ -1285,6 +1285,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(1024) default ''::varchar, + ip_whitelist varchar(1024) default ''::varchar, active_timeout int4 default 1800, timeout int4 default 604800, status char(1) default '0'::bpchar, @@ -1304,6 +1306,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停用)'; @@ -1314,8 +1318,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', null, null, 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, now(), 1761100000000000001, now()); +insert into sys_client values (1762000000000000002, '428a8310cd442757ae699df5d894f051', 'app', 'app123', 'password,sms,social', 'android', null, null, 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, now(), 1761100000000000001, now()); create table if not exists test_demo ( diff --git a/script/sql/ry-cloud.sql b/script/sql/ry-cloud.sql index d752d0d10..b1fcde52f 100644 --- a/script/sql/ry-cloud.sql +++ b/script/sql/ry-cloud.sql @@ -929,6 +929,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(1024) default null comment '允许访问路径', + ip_whitelist varchar(1024) 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停用)', @@ -941,8 +943,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', null, null, 1800, 604800, 0, 0, 1761000000000000103, 1761100000000000001, sysdate(), 1761100000000000001, sysdate()); CREATE TABLE test_demo