package com.dms.service; import com.dms.common.Errors; import com.dms.common.IpAddresses; import com.dms.mapper.IpEntryMapper; import com.dms.model.UserEntity; import jakarta.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.List; import org.springframework.stereotype.Service; /** * 登录 IP 限制校验,对齐 backend/dms/services/ip_entry_service.py verify_login_ip。 */ @Service public class LoginIpService { private final IpEntryMapper ipEntryMapper; public LoginIpService(IpEntryMapper ipEntryMapper) { this.ipEntryMapper = ipEntryMapper; } /** * 仅内置 admin 账号豁免(管理员类别不再整体豁免,避免普通机构管理员 * 绕过本部门IP限制登录);机构未分配任何IP段仍放行。 */ public void verifyLoginIp(UserEntity user, HttpServletRequest request) { if (AdminPermission.isSuperAdmin(user) || user.getOrganizationId() == null) { return; } List ipValues = ipEntryMapper.selectActiveIpValuesByOrganization(user.getOrganizationId()); if (ipValues.isEmpty()) { return; } String rawIp = AuditService.clientIp(request); if (rawIp == null) { throw Errors.loginIpForbidden(); } InetAddress address; try { address = InetAddress.getByName(rawIp.strip()); } catch (UnknownHostException ex) { throw Errors.loginIpForbidden(); } for (String value : ipValues) { if (matchesValue(address, value)) { return; } } throw Errors.loginIpForbidden(); } /** * 对齐 _ip_matches:单条 ip_value(精确IP/CIDR网段/IP范围)是否命中来源地址; * 存量数据格式异常按不匹配处理(安全优先),不中断登录流程判断。 * 包级私有便于单测。 */ static boolean matchesValue(InetAddress address, String value) { try { if (value.contains("/")) { String[] parts = value.split("/", 2); InetAddress network = InetAddress.getByName(parts[0]); int prefix = Integer.parseInt(parts[1]); return networkInetVersion(address) == networkInetVersion(network) && sameNetwork(address, network, prefix); } if (value.contains("-")) { String[] parts = value.split("-", -1); if (parts.length != 2 || parts[0].isEmpty() || parts[1].isEmpty()) { return false; } InetAddress start = InetAddress.getByName(parts[0]); InetAddress end = InetAddress.getByName(parts[1]); return networkInetVersion(start) == networkInetVersion(end) && networkInetVersion(address) == networkInetVersion(start) && IpAddresses.unsignedCompare( address.getAddress(), start.getAddress()) >= 0 && IpAddresses.unsignedCompare( address.getAddress(), end.getAddress()) <= 0; } return address.equals(InetAddress.getByName(value)); } catch (UnknownHostException | NumberFormatException ex) { return false; } } private static int networkInetVersion(InetAddress address) { return address.getAddress().length == 4 ? 4 : 6; } /** 前缀位匹配(地址族已由调用方保证一致)。 */ private static boolean sameNetwork(InetAddress address, InetAddress network, int prefixLength) { byte[] addressBytes = address.getAddress(); byte[] networkBytes = network.getAddress(); int totalBits = addressBytes.length * 8; if (prefixLength < 0 || prefixLength > totalBits) { return false; } int fullBytes = prefixLength / 8; for (int i = 0; i < fullBytes; i++) { if (addressBytes[i] != networkBytes[i]) { return false; } } int remainderBits = prefixLength % 8; if (remainderBits == 0) { return true; } int mask = 0xFF << (8 - remainderBits); return (addressBytes[fullBytes] & mask) == (networkBytes[fullBytes] & mask); } }