基于SpringBoot實(shí)現(xiàn)IP黑白名單的詳細(xì)步驟
業(yè)務(wù)場景
IP黑白名單是網(wǎng)絡(luò)安全管理中常見的策略工具,用于控制網(wǎng)絡(luò)訪問權(quán)限,根據(jù)業(yè)務(wù)場景的不同,其應(yīng)用范圍廣泛,以下是一些典型業(yè)務(wù)場景:
服務(wù)器安全防護(hù):
- 黑名單:可以用來阻止已知的惡意IP地址或曾經(jīng)嘗試攻擊系統(tǒng)的IP地址,防止這些來源對服務(wù)器進(jìn)行未經(jīng)授權(quán)的訪問、掃描、攻擊等行為。
- 白名單:僅允許特定IP或IP段訪問關(guān)鍵服務(wù),比如數(shù)據(jù)庫服務(wù)器、內(nèi)部管理系統(tǒng)等,實(shí)現(xiàn)最小授權(quán)原則,降低被未知風(fēng)險(xiǎn)源入侵的可能性。
網(wǎng)站安全防護(hù):
- 黑名單:對于頻繁發(fā)起惡意請求、爬取數(shù)據(jù)、DDoS攻擊等活動的IP,將其加入黑名單以限制其對網(wǎng)站的訪問。
- 白名單:如果只希望特定合作伙伴、內(nèi)部員工或特定區(qū)域用戶訪問網(wǎng)站內(nèi)容,則可通過白名單來限定合法訪問者的范圍。
API接口保護(hù):
- 對于對外提供的API接口,通過設(shè)置IP黑白名單,確保只有經(jīng)過認(rèn)證或信任的系統(tǒng)和客戶端才能調(diào)用接口。
比如比較容易被盜刷的短信接口、文件接口,都需要添加IP黑白名單加以限制。
核心實(shí)現(xiàn)
獲取客戶端IP地址
@UtilityClass public class IpUtils { private final String UNKNOWN = "unknown"; private final String X_FORWARDED_FOR = "X-Forwarded-For"; private final String PROXY_CLIENT_IP = "Proxy-Client-IP"; private final String WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP"; private final Pattern COMMA_SEPARATED_VALUES_PATTERN = Pattern.compile("\s*,\s*"); /** * 默認(rèn)情況下內(nèi)網(wǎng)代理的子網(wǎng)可以是(后面有需要可以進(jìn)行配置): * 1. 10/8 * 2. 192.168/16 * 3. 169.254/16 * 4. 127/8 * 5. 172.16/12 * 6. ::1 */ private final Pattern INTERNAL_PROXIES = Pattern.compile( "10\.\d{1,3}\.\d{1,3}\.\d{1,3}|" + "192\.168\.\d{1,3}\.\d{1,3}|" + "169\.254\.\d{1,3}\.\d{1,3}|" + "127\.\d{1,3}\.\d{1,3}\.\d{1,3}|" + "172\.1[6-9]\.\d{1,3}\.\d{1,3}|" + "172\.2[0-9]\.\d{1,3}\.\d{1,3}|" + "172\.3[0-1]\.\d{1,3}\.\d{1,3}|" + "0:0:0:0:0:0:0:1|::1" ); /** * 獲取請求的IP * * @return 請求的IP */ public String getIp() { var requestAttributes = RequestContextHolder.getRequestAttributes(); if (Objects.isNull(requestAttributes)) { return null; } var request = ((ServletRequestAttributes) requestAttributes).getRequest(); var ip = getRemoteIp(request); if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader(PROXY_CLIENT_IP); } if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getHeader(WL_PROXY_CLIENT_IP); } if (!StringUtils.hasLength(ip) || UNKNOWN.equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } /** * 獲取客戶端真實(shí)IP地址,防止使用X-Forwarded-For進(jìn)行IP偽造攻擊,防御思路見類注釋 * * @return 真實(shí)IP地址 */ private String getRemoteIp(HttpServletRequest request) { var remoteIp = request.getRemoteAddr(); var isInternal = INTERNAL_PROXIES.matcher(remoteIp).matches(); if (isInternal) { var concatRemoteIpHeaderValue = new StringBuilder(); for (var e = request.getHeaders(X_FORWARDED_FOR); e.hasMoreElements(); ) { if (concatRemoteIpHeaderValue.length() > 0) { concatRemoteIpHeaderValue.append(", "); } concatRemoteIpHeaderValue.append(e.nextElement()); } var remoteIpHeaderValue = commaDelimitedListToArray(concatRemoteIpHeaderValue.toString()); for (var i = remoteIpHeaderValue.length - 1; i >= 0; i--) { var currentRemoteIp = remoteIpHeaderValue[i]; if (!INTERNAL_PROXIES.matcher(currentRemoteIp).matches()) { return currentRemoteIp; } } return null; } else { return remoteIp; } } private String[] commaDelimitedListToArray(String commaDelimitedStrings) { return (commaDelimitedStrings == null || commaDelimitedStrings.isEmpty()) ? new String[0] : COMMA_SEPARATED_VALUES_PATTERN.split(commaDelimitedStrings); } }
獲取到客戶端IP后,我們只要比對客戶端IP是否在配置的白名單/黑名單中即可。 為了簡化使用,可以采用注解的方式進(jìn)行攔截。
新增注解@IpCheck
/** * IP白名單校驗(yàn) */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented @Inherited public @interface IpCheck { /** * 白名單IP列表,支持${...} */ @AliasFor("whiteList") String value() default ""; /** * 白名單IP列表,支持${...} */ @AliasFor("value") String whiteList() default ""; /** * 黑名單IP列表,支持${...} */ String blackList() default ""; }
新增IpCheckHandlerInterceptorImpl
我們實(shí)現(xiàn)HandlerInterceptor
,在接口上進(jìn)行攔截,如果不滿足配置的黑白名單,則拋出異常。
/** * @author <a href="mailto:gcwm99@gmail.com" rel="external nofollow" rel="external nofollow" rel="external nofollow" >gcdd1993</a> * Created by gcdd1993 on 2023/9/20 */ @Component public class IpCheckHandlerInterceptorImpl implements HandlerInterceptor, EmbeddedValueResolverAware { private StringValueResolver stringValueResolver; @Override public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) { // 檢查是否有IpWhitelistCheck注解,并且是否開啟IP白名單檢查 if (!(handler instanceof HandlerMethod)) { return true; // 如果沒有注解或者注解中關(guān)閉了IP白名單檢查,則繼續(xù)處理請求 } var handlerMethod = (HandlerMethod) handler; var method = handlerMethod.getMethod(); var annotation = AnnotationUtils.getAnnotation(method, IpCheck.class); if (annotation == null) { return true; } var clientIp = IpUtils.getIp(); // 檢查客戶端IP是否在白名單中 var whiteList = Stream.of(Optional.ofNullable(stringValueResolver.resolveStringValue(annotation.whiteList())) .map(it -> it.split(",")) .orElse(new String[]{})) .filter(StringUtils::hasText) .map(String::trim) .collect(Collectors.toUnmodifiableSet()); if (!whiteList.isEmpty() && whiteList.contains(clientIp)) { return true; // IP在白名單中,繼續(xù)處理請求 } var blackList = Stream.of(Optional.ofNullable(stringValueResolver.resolveStringValue(annotation.blackList())) .map(it -> it.split(",")) .orElse(new String[]{})) .filter(StringUtils::hasText) .map(String::trim) .collect(Collectors.toUnmodifiableSet()); if (!blackList.isEmpty() && !blackList.contains(clientIp)) { return true; // IP不在黑名單中,繼續(xù)處理請求 } // IP不在白名單中,可以返回錯(cuò)誤響應(yīng)或者拋出異常 // 例如,返回一個(gè) HTTP 403 錯(cuò)誤 throw new RuntimeException("Access denied, remote ip " + clientIp + " is not allowed."); } @Override public void setEmbeddedValueResolver(StringValueResolver resolver) { this.stringValueResolver = resolver; } }
自動裝配
核心邏輯寫完了,該怎么使用呢?為了達(dá)到開箱即用的效果,我們可以接著新增自動裝配的代碼
新建IpCheckConfig
實(shí)現(xiàn)WebMvcConfigurer
接口,添加接口攔截器
/** * @author <a href="mailto:gcwm99@gmail.com" rel="external nofollow" rel="external nofollow" rel="external nofollow" >gcdd1993</a> * Created by gcdd1993 on 2024/1/24 */ public class IpCheckConfig implements WebMvcConfigurer { @Resource private IpCheckHandlerInterceptorImpl ipCheckHandlerInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(ipCheckHandlerInterceptor); } }
新建@EnableIpCheck
參考@EnableScheduling
的實(shí)現(xiàn),自己實(shí)現(xiàn)一個(gè)@EnableIpCheck
,該注解可以控制功能是否啟用
/** * @author <a href="mailto:gcwm99@gmail.com" rel="external nofollow" rel="external nofollow" rel="external nofollow" >gcdd1993</a> * Created by gcdd1993 on 2024/1/24 */ @Retention(value = RetentionPolicy.RUNTIME) @Target(value = ElementType.TYPE) @Documented @ComponentScan("xxx.ip") // 這里是IpCheckConfig的包名 @Import(IpCheckConfig.class) public @interface EnableIpCheck { }
業(yè)務(wù)測試
簡單地用代碼來試驗(yàn)下效果
新建SampleApplication
@SpringBootApplication @EnableIpCheck public class SampleApplication { public static void main(String[] args) { SpringApplication.run(SampleApplication.class, args); } }
新建測試接口
@RestController @RequestMapping("/sample/ip-checker") public class IpCheckSample { @GetMapping("/white") @IpCheck(value = "0:0:0:0:0:0:0:1") String whiteList() { return "127.0.0.1"; } @GetMapping("/black") @IpCheck(blackList = "0:0:0:0:0:0:0:1") String blackList() { return "127.0.0.1"; } /** * 同時(shí)配置白名單和黑名單,要求IP既在白名單,并且不在黑名單,否則拋出異常 */ @GetMapping("/all") @IpCheck(value = "0:0:0:0:0:0:0:1", blackList = "0:0:0:0:0:0:0:1") String all() { return "127.0.0.1"; } /** * 同時(shí)配置白名單和黑名單,要求IP既在白名單,并且不在黑名單,否則拋出異常 * 支持解析Spring 配置文件 */ @GetMapping("/config") @IpCheck(value = "${digit.ip.check.white-list}", blackList = "${digit.ip.check.black-list}") String config() { return "127.0.0.1"; } /** * 同時(shí)配置白名單和黑名單,要求IP既在白名單,并且不在黑名單,否則拋出異常 * 支持解析Spring 配置文件 */ @GetMapping("/black-config") @IpCheck(blackList = "${digit.ip.check.black-list}") String blackConfig() { return "127.0.0.1"; } }
由于本機(jī)請求IP地址是0:0:0:0:0:0:0:1
,所以這里使用0:0:0:0:0:0:0:1
而不是127.0.0.1
。
訪問/sample/ip-checker/white
接口返回127.0.0.1
訪問/sample/ip-checker/black
java.lang.RuntimeException: Access denied, remote ip 0:0:0:0:0:0:0:1 is not allowed.
訪問/sample/ip-checker/all
接口返回127.0.0.1
- 既配置白名單,也配置黑名單,需要既不在白名單,同時(shí)在黑名單里,才會攔截。
修改配置
digit: ip: check: white-list: 127.0.0.1, 192.168.1.1, 192.168.1.2 black-list: 127.0.0.1, 192.168.1.1, 192.168.1.2,0:0:0:0:0:0:0:1
訪問/sample/ip-checker/black-config
java.lang.RuntimeException: Access denied, remote ip 0:0:0:0:0:0:0:1 is not allowed.
最后,可以結(jié)合配置中心,以便配置后立即生效。
以上就是基于SpringBoot實(shí)現(xiàn)IP黑白名單的詳細(xì)步驟的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot IP黑白名單的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Springboot?定時(shí)任務(wù)分布式下冪等性解決方案
這篇文章主要介紹了Springboot定時(shí)任務(wù)分布式下冪等性如何解決,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07java學(xué)習(xí)DongTai被動型IAST工具部署過程
被動型IAST被認(rèn)為是DevSecOps測試階段實(shí)現(xiàn)自動化安全測試的最佳工具,而就在前幾天,洞態(tài)IAST正式開源了,這對于甲方構(gòu)建安全工具鏈來說,絕對是一個(gè)大利好2021-10-10基于JWT實(shí)現(xiàn)SSO單點(diǎn)登錄流程圖解
這篇文章主要介紹了基于JWT實(shí)現(xiàn)SSO單點(diǎn)登錄流程圖解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07Java使用ByteBuffer進(jìn)行多文件合并和拆分的代碼實(shí)現(xiàn)
因?yàn)轵?yàn)證證書的需要,需要把證書文件和公鑰給到客戶,考慮到多個(gè)文件交互的不便性,所以決定將2個(gè)文件合并成一個(gè)文件交互給客戶,但是由于是加密文件,采用字符串形式合并后,拆分后文件不可用,本文給大家介紹了Java使用ByteBuffer進(jìn)行多文件合并和拆分,需要的朋友可以參考下2024-09-09