LoginIpService.java 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. package com.dms.service;
  2. import com.dms.common.Errors;
  3. import com.dms.common.IpAddresses;
  4. import com.dms.mapper.IpEntryMapper;
  5. import com.dms.model.UserEntity;
  6. import jakarta.servlet.http.HttpServletRequest;
  7. import java.net.InetAddress;
  8. import java.net.UnknownHostException;
  9. import java.util.List;
  10. import org.springframework.stereotype.Service;
  11. /**
  12. * 登录 IP 限制校验,对齐 backend/dms/services/ip_entry_service.py verify_login_ip。
  13. */
  14. @Service
  15. public class LoginIpService {
  16. private final IpEntryMapper ipEntryMapper;
  17. public LoginIpService(IpEntryMapper ipEntryMapper) {
  18. this.ipEntryMapper = ipEntryMapper;
  19. }
  20. /**
  21. * 仅内置 admin 账号豁免(管理员类别不再整体豁免,避免普通机构管理员
  22. * 绕过本部门IP限制登录);机构未分配任何IP段仍放行。
  23. */
  24. public void verifyLoginIp(UserEntity user, HttpServletRequest request) {
  25. if (AdminPermission.isSuperAdmin(user) || user.getOrganizationId() == null) {
  26. return;
  27. }
  28. List<String> ipValues =
  29. ipEntryMapper.selectActiveIpValuesByOrganization(user.getOrganizationId());
  30. if (ipValues.isEmpty()) {
  31. return;
  32. }
  33. String rawIp = AuditService.clientIp(request);
  34. if (rawIp == null) {
  35. throw Errors.loginIpForbidden();
  36. }
  37. InetAddress address;
  38. try {
  39. address = InetAddress.getByName(rawIp.strip());
  40. } catch (UnknownHostException ex) {
  41. throw Errors.loginIpForbidden();
  42. }
  43. for (String value : ipValues) {
  44. if (matchesValue(address, value)) {
  45. return;
  46. }
  47. }
  48. throw Errors.loginIpForbidden();
  49. }
  50. /**
  51. * 对齐 _ip_matches:单条 ip_value(精确IP/CIDR网段/IP范围)是否命中来源地址;
  52. * 存量数据格式异常按不匹配处理(安全优先),不中断登录流程判断。
  53. * 包级私有便于单测。
  54. */
  55. static boolean matchesValue(InetAddress address, String value) {
  56. try {
  57. if (value.contains("/")) {
  58. String[] parts = value.split("/", 2);
  59. InetAddress network = InetAddress.getByName(parts[0]);
  60. int prefix = Integer.parseInt(parts[1]);
  61. return networkInetVersion(address) == networkInetVersion(network)
  62. && sameNetwork(address, network, prefix);
  63. }
  64. if (value.contains("-")) {
  65. String[] parts = value.split("-", -1);
  66. if (parts.length != 2 || parts[0].isEmpty() || parts[1].isEmpty()) {
  67. return false;
  68. }
  69. InetAddress start = InetAddress.getByName(parts[0]);
  70. InetAddress end = InetAddress.getByName(parts[1]);
  71. return networkInetVersion(start) == networkInetVersion(end)
  72. && networkInetVersion(address) == networkInetVersion(start)
  73. && IpAddresses.unsignedCompare(
  74. address.getAddress(), start.getAddress()) >= 0
  75. && IpAddresses.unsignedCompare(
  76. address.getAddress(), end.getAddress()) <= 0;
  77. }
  78. return address.equals(InetAddress.getByName(value));
  79. } catch (UnknownHostException | NumberFormatException ex) {
  80. return false;
  81. }
  82. }
  83. private static int networkInetVersion(InetAddress address) {
  84. return address.getAddress().length == 4 ? 4 : 6;
  85. }
  86. /** 前缀位匹配(地址族已由调用方保证一致)。 */
  87. private static boolean sameNetwork(InetAddress address, InetAddress network, int prefixLength) {
  88. byte[] addressBytes = address.getAddress();
  89. byte[] networkBytes = network.getAddress();
  90. int totalBits = addressBytes.length * 8;
  91. if (prefixLength < 0 || prefixLength > totalBits) {
  92. return false;
  93. }
  94. int fullBytes = prefixLength / 8;
  95. for (int i = 0; i < fullBytes; i++) {
  96. if (addressBytes[i] != networkBytes[i]) {
  97. return false;
  98. }
  99. }
  100. int remainderBits = prefixLength % 8;
  101. if (remainderBits == 0) {
  102. return true;
  103. }
  104. int mask = 0xFF << (8 - remainderBits);
  105. return (addressBytes[fullBytes] & mask) == (networkBytes[fullBytes] & mask);
  106. }
  107. }