我们小创业公司使用的宽带是个人家庭宽带(申请企业宽带的成本很高,每月几千块),公网IP过一段时间就会变动,平时使用都没有什么影响,只有一点,阿里云上的服务都配置了安全组,只允许阿里云内网或者白名单IP访问,从而保障公司服务安全性。公司公网IP会动态变更,每一次变更之后都要修改安全组的ip配置,十分麻烦。
来自攻城师的思考:每一次IP变动都打断了大家专心工作的心流(Flow)状态,而且登录阿里云控制台修改也很繁琐,修改安全组配置的IP名单,能不能自动化?
大概瞄了一下阿里云相关的API,这事情可以搞。
看看需要实现哪些功能:
如何获取公司宽带的公网IP地址?
这个简单,市场上有很多ip查询的网站,比如 ip.cn,ipip.net ,你访问他们的网站,默认就会显示你的公网ip。
调用阿里云API没什么难度,只是工作量。
核心逻辑就是,获取当前公司ip,存储下来,每隔N分钟,再次获取公司当前ip,与之前记录对比,如果不同,则调用阿里云api,更新安全组白名单IP,最后存储新IP。否则什么也不做。
于是我创建了一个SpringBoot应用,为什么要创建一个java web应用?首先因为最熟悉java,其次是工程创建快速,使用简单,之前我实现了一个模版工程,可以快速创建SpringBoot应用,而且配置了常用的类库,非常方便。
一些核心功能实现:
@Component public class ScheduleComp { Logger logger = LoggerFactory.getLogger(ScheduleComp.class); @Autowired RefreshSecurityRulService securityRulService; /** * 每隔3分钟检查一下 */ @Scheduled(cron = "0 */3 * * * ?") public void cornJob() { logger.warn(" >>cron执行...."); securityRulService.refreshAll(false); } }
securityRulService 的refreshAll()方法
public void refreshAll(boolean force) { String nowIp = getLocalIp(); if (StringUtils.isEmpty(nowIp)) { logger.error("get current ip is empty! try next time..."); return; } String oldIp = getCurrentIp(); if (StringUtils.isEmpty(oldIp)) { throw new RuntimeException("current ip data is empty!"); } if (StringUtils.equals(nowIp, oldIp) && !force) { logger.warn("same ip, nothing todo"); return; } ecsRefreshSecurityRuleService.refreshAll(nowIp, oldIp); updateCurrentIp(nowIp); }
getLocalIp()
的实现:
public static String getLocalIp() { String ip = getIpCN(); ... return ip; } public static String getIpCN() { //<p>您现在的 IP:<code>114.0.1.2</code> return getLocalIp("http://ip.cn", "<p>您现在的 IP:<code>"); } public static String getLocalIp(String url, String prefix) { Request request = new Request.Builder() .get() .url(url) .build(); try { Response response = okHttpClient.newCall(request).execute(); if (response.isSuccessful()) { String regexPatten = prefix + "((?:(?:25[0-5]|2[0-4]//d|((1//d{2})|([1-9]?//d))).){3}(?:25[0-5]|2[0-4]//d|((1//d{2})|([1-9]?//d))))"; Pattern pattern = Pattern.compile(regexPatten); String resp = response.body().string(); Matcher matcher = pattern.matcher(resp); if (matcher.find()) { String matchStr = matcher.group(0); return matchStr.replace(prefix, ""); } } response.close(); } catch (IOException e) { e.printStackTrace(); } return ""; }
这里通过 ip.cn 获取本机的公网地址。
还可以配置其他网站,防止ip.cn挂掉而导致服务失败。
getCurrentIp
的实现很简单,从本地文件读取上次记录的ip地址。
public String getCurrentIp() { File file = new File("ip.txt"); if (!file.exists()) { throw new RuntimeException("cannot find ip data!"); } try { BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream(file))); String strLine = bufferedReader.readLine(); bufferedReader.close(); return strLine.trim(); } catch (Exception e) { e.printStackTrace(); } return ""; }
采用文件存储是最简单直接的方式。更新文件内容的代码:
private void updateCurrentIp(String ip) { if (StringUtils.isEmpty(ip)) return; File file = new File("ip.txt"); file.delete(); try { file.createNewFile(); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file))); bufferedWriter.write(ip.trim()); bufferedWriter.close(); } catch (Exception e) { e.printStackTrace(); } }
刷新阿里云安全组配置的逻辑:
public void refreshAll(String nowIp, String oldIp) { List<SecurityRuleDO> newSecurityRuleDOList = new ArrayList<>(testServer(nowIp)); for (SecurityRuleDO securityRuleDO : newSecurityRuleDOList) { refresh(securityRuleDO); } if (!StringUtils.equals(nowIp, oldIp)) { List<SecurityRuleDO> oldSecurityRuleDOList = new ArrayList<>(); oldSecurityRuleDOList.addAll(testServer(oldIp)); for (SecurityRuleDO securityRuleDO : oldSecurityRuleDOList) { deleteOld(securityRuleDO); } } } private List<SecurityRuleDO> testServer(String ip) { List<SecurityRuleDO> securityRuleDOList = new ArrayList<>(); SecurityRuleDO securityRuleDO = new SecurityRuleDO(); securityRuleDO.sourceIp = ip; securityRuleDO.groupId = "YourECSGroupId"; securityRuleDO.ipProtocol = "tcp"; securityRuleDO.portRange = "yourPort/yourPort"; securityRuleDOList.add(securityRuleDO); ... return securityRuleDOList; } public boolean refresh(SecurityRuleDO securityRuleDO) { AuthorizeSecurityGroupRequest request = new AuthorizeSecurityGroupRequest(); request.setSecurityGroupId(securityRuleDO.groupId); request.setIpProtocol(securityRuleDO.ipProtocol); request.setPortRange(securityRuleDO.portRange); request.setNicType(securityRuleDO.nicType); request.setPolicy("accept"); request.setSourceCidrIp(securityRuleDO.sourceIp); try { AuthorizeSecurityGroupResponse response = client.getAcsResponse(request); String requestId = response.getRequestId(); System.out.println(requestId); return true; } catch (ClientException e) { e.printStackTrace(); return false; } } public boolean deleteOld(SecurityRuleDO securityRuleDO) { RevokeSecurityGroupRequest request = new RevokeSecurityGroupRequest(); request.setSecurityGroupId(securityRuleDO.groupId); request.setIpProtocol(securityRuleDO.ipProtocol); request.setPortRange(securityRuleDO.portRange); request.setNicType(securityRuleDO.nicType); request.setPolicy("accept"); request.setSourceCidrIp(securityRuleDO.sourceIp); try { RevokeSecurityGroupResponse response = client.getAcsResponse(request); String requestId = response.getRequestId(); System.out.println(requestId); return true; } catch (Exception e) { e.printStackTrace(); return false; } }
其中client的初始化:
DefaultProfile profile = DefaultProfile.getProfile(reginId, appkey, secret); client = new DefaultAcsClient(profile);
定时3分钟刷新有个小问题,就是有极小的概率,发生IP变更但是还没有及时刷新安全组的配置。怎么办,再加一个主动刷新的接口呗:
@RestController public class RefreshSecurityRuleController { @Autowired RefreshSecurityRulService securityRulService; @GetMapping("/ip") public String getMyIp() { return securityRulService.getLocalIp(); } @GetMapping("/fresh") public String forceFresh() { securityRulService.refreshAll(true); return "done!"; } }
其中的 /fresh
强制刷新IP。还附赠了一个接口,获取公司当前的IP。
这个小应用部署在公司局域网内,一直工作的很好,再也不用烦心IP变动这些琐事。
直到有一次,ip.cn 网站挂了。挂了很久,我们就增加了从 ip.tool.chinaz.com
网站获取IP。
收工。