中间件 —— 黑白名单(Block Allow List)
简介
- 将黑白名单封装为一个中间件
- 使用时可通过Apollo配置黑名单,白名单
- 集成飞书机器人通知
- 集成功能开关
实现
Apollo 配置
#黑名单(多个IP以逗号间隔,与白名单二选一配置,默认为空)
block.list =
#白名单(多个IP以逗号间隔,与白名单二选一配置,默认为空)
allow.list =
#是否开启Controller黑白名单验证(默认不开启)
block.allow.list.controller.check = false
#是否开启Dubbo Provider黑白名单验证(默认不开启)
block.allow.list.dubbo.provider.check = false
#是否开启飞书通知(默认不开启)
block.allow.list.fei.shu.notice = false
#飞书机器人ID(参照官方文档获取:https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN)
block.allow.list.fei.shu.robot.id =
核心工具类
Controller和DUBBO Provider都会用到,主要功能是读取Apollo配置进行黑白名单的验证、处理请求IP、飞书告警相关工作
@Configuration
@Slf4j
public class BlockAllowListUtil {
/**
* Controller黑白名单开关(默认关闭)
*/
private static Boolean BLOCK_ALLOW_LIST_CONTROLLER_CHECK = false;
@Value("${block.allow.list.controller.check:}")
private void setBlockAllowListControllerCheck(Boolean controllerCheck){
BLOCK_ALLOW_LIST_CONTROLLER_CHECK = controllerCheck;
}
/**
* Dubbo Provider黑白名单开关(默认关闭)
*/
private static Boolean BLOCK_ALLOW_LIST_DUBBO_PROVIDER_CHECK = false;
@Value("${block.allow.list.dubbo.provider.check:}")
private void setBlockAllowListDubboProviderCheck(Boolean dubboProviderCheck){
BLOCK_ALLOW_LIST_DUBBO_PROVIDER_CHECK = dubboProviderCheck;
}
/**
* 飞书通知开关(默认关闭)
*/
private static Boolean FEI_SHU_NOTICE = false;
@Value("${block.allow.list.fei.shu.notice:}")
private void setFeiShuNotice(Boolean feiShuNotice){
FEI_SHU_NOTICE = feiShuNotice;
}
/**
* 飞书机器人ID
*/
private static String FEI_SHU_ROBOT_ID;
@Value("${block.allow.list.fei.shu.robot.id:}")
private void setFeiShuRobotId(String feiShuRobotId){
FEI_SHU_ROBOT_ID = feiShuRobotId;
}
/**
* 黑名单
*/
private static String BLOCK_LIST;
@Value("${block.list:}")
private void setBlockList(String blockList){
log.info("初始化 block_list :{}", blockList);
BLOCK_LIST = blockList;
}
/**
* 白名单
*/
private static String ALLOW_LIST;
@Value("${allow.list:}")
private void setAllowList(String allowList){
log.info("初始化 allow_list :{}", allowList);
ALLOW_LIST = allowList;
}
/**
* 获取Controller黑白名单开关
* @return
*/
public static Boolean getBlockAllowListControllerCheck() {
return BLOCK_ALLOW_LIST_CONTROLLER_CHECK;
}
/**
* 获取Dubbo Provider黑白名单开关
* @return
*/
public static Boolean getBlockAllowListDubboProviderCheck() {
return BLOCK_ALLOW_LIST_DUBBO_PROVIDER_CHECK;
}
/**
* 校验黑名单
* @param requestIpAddress
* @return true:不在黑名单内;false:在黑名单内拒绝;
*/
public static Boolean checkBlockList(String interceptType, String requestIpAddress, String uri) {
// 当没有配置黑名单时,返回false
if(StringUtils.isBlank(BLOCK_LIST)) {
// 没有配置黑名单,直接放行
log.info("没有配置黑名单,直接放行");
return true;
}
// 当黑名单内没有请求的IP,则放行
if(!BLOCK_LIST.contains(requestIpAddress)) {
return true;
}
// 当配置了飞书通知并且飞书机器人ID配置没问题
if(FEI_SHU_NOTICE && StringUtils.isNotBlank(FEI_SHU_ROBOT_ID)) {
// 当开启了通知
String noticeMessage = initNoticeMessage(interceptType, "黑名单", requestIpAddress, uri);
// FeishuUtil 飞书告警是另一个组件,参照官方文档封装一个即可
Result noticeResult = FeishuUtil.sendSimpleMessage(FEI_SHU_ROBOT_ID, noticeMessage);
if(!"00000".equals(noticeResult.getCode())) {
log.error("黑名单拦截,发送飞书通知失败:{}", noticeResult.getMessage());
}
}
return false;
}
/**
* 校验白名单
* @param requestIpAddress
* @return true:不在白名单内;false:在白名单内拒绝;
*/
public static Boolean checkAllowList(String interceptType, String requestIpAddress, String uri) {
// 当没有配置白名单时,返回false
if(StringUtils.isBlank(ALLOW_LIST)) {
log.info("没有配置白名单,直接放行");
// 没有配置白名单,直接放行
return true;
}
// 当白名单内有请求的IP,则放行
if(ALLOW_LIST.contains(requestIpAddress)) {
return true;
}
// 当配置了飞书通知并且飞书机器人ID配置没问题
if(FEI_SHU_NOTICE && StringUtils.isNotBlank(FEI_SHU_ROBOT_ID)) {
// 当开启了通知
String noticeMessage = initNoticeMessage(interceptType, "白名单", requestIpAddress, uri);
// FeishuUtil 飞书告警是另一个组件,参照官方文档封装一个即可
Result noticeResult = FeishuUtil.sendSimpleMessage(FEI_SHU_ROBOT_ID, noticeMessage);
if(!"00000".equals(noticeResult.getCode())) {
log.error("白名单拦截,发送飞书通知失败:{}", noticeResult.getMessage());
}
}
return false;
}
/**
* 组装飞书通知信息
* @param listType
* @param requestIpAddress
* @param uri
* @return
*/
private static String initNoticeMessage(String interceptType, String listType, String requestIpAddress, String uri) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(interceptType);
stringBuffer.append("拦截 - IP【");
stringBuffer.append(requestIpAddress);
stringBuffer.append("】触发");
stringBuffer.append(listType);
stringBuffer.append("控制,请求URI【");
stringBuffer.append(uri);
stringBuffer.append("】");
stringBuffer.append("被拒绝!!");
return stringBuffer.toString();
}
}
Controller 验证
- RequestFilter
@Slf4j
public class RequestFilter extends OncePerRequestFilter {
@Override
public void destroy() {
super.destroy();
log.info("RequestFilter destroy");
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
try {
RequestWrapper requestWrapper = new RequestWrapper(httpServletRequest);
filterChain.doFilter(requestWrapper, httpServletResponse);
} catch (Exception exception) {
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.write(exception.toString());
}
}
}
- RequestWrapper
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
public RequestWrapper(HttpServletRequest request) {
super(request);
Boolean blockAllowCheck = BlockAllowListUtil.getBlockAllowListControllerCheck();
if(!blockAllowCheck) {
// 当黑白名校验关闭状态时,直接return,不进行白黑白名单判断
return;
}
// 请求当地址
String uri = request.getRequestURI();
// 请求人IP
String requestIpAddress = getIpAddr(request);
Boolean checkBLockListResult = BlockAllowListUtil.checkBlockList("Controller", requestIpAddress, uri);
if(!checkBLockListResult) {
log.error("请求IP:{}在黑名单内,拦截本次请求!uri:{}", requestIpAddress, uri);
throw new RuntimeException("黑名单拦截请求");
}
Boolean checkAllowListResult = BlockAllowListUtil.checkAllowList("Controller", requestIpAddress, uri);
if(!checkAllowListResult) {
log.error("请求IP:{}不在白名单内,拦截本次请求!uri:{}", requestIpAddress, uri);
throw new RuntimeException("白名单拦截请求");
}
}
@Override
public String getParameter(String name) {
// 可以对请求参数进行过滤
return super.getParameter(name);
}
@Override
public String[] getParameterValues(String name) {
// 对请求参数值进行过滤
String[] values =super.getRequest().getParameterValues(name);
return super.getParameterValues(name);
}
/**
* 获取用户真实ip方法
* @param request
* @return
*/
public String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x - forwarded - for");
if (ip == null || ip.length() == 0 ||"unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("Proxy - Client - IP");
}
if (ip == null || ip.length() == 0 ||"unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("WL - Proxy - Client - IP");
}
if (ip == null || ip.length() == 0 ||"unknown".equalsIgnoreCase(ip)){
ip = request.getRemoteAddr();
}
return ip;
}
}
- FilterConfig
@Configuration
public class FilterConfig {
@Bean
public RequestFilter requestFilter(){
return new RequestFilter();
}
@Bean
public FilterRegistrationBean<RequestFilter> registrationBean() {
FilterRegistrationBean<RequestFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(requestFilter());
// 拦截所有目录,这里可以通过Apollo配置,获取指定拦截路径
registrationBean.addUrlPatterns("/*");
// 设置请求过滤
registrationBean.setName("RequestFilter");
//过滤器的级别,值越小级别越高越先执行
registrationBean.setOrder(1);
return registrationBean;
}
}
DUBBO Provider 验证
增加一个DUBBO Provider过滤器即可
@Slf4j
@Activate(group = Constants.PROVIDER)
public class DubboProviderFilter implements Filter, Runnable {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
Boolean blockAllowListCheck = BlockAllowListUtil.getBlockAllowListDubboProviderCheck();
if(!blockAllowListCheck) {
// 当黑白名校验关闭状态时,直接return,不进行白黑白名单判断
return invoker.invoke(invocation);
}
// 获取执行方法名
Result result = null;
Long takeTime = 0L;
try {
// IP
String requestIpAddress = RpcContext.getContext().getRemoteHost();
// 应用
String applicationName = RpcContext.getContext().getRemoteApplicationName();
// 方法
String method = invoker.getInterface() + "(" + invocation.getMethodName() + ")";
// 参数
// String parameters = Arrays.toString(invocation.getArguments());
//执行方法
result = invoker.invoke(invocation);
if (result.getException() instanceof Exception) {
throw new RpcException(result.getException());
}
String uri = applicationName+"/"+method;
Boolean checkBLockListResult = BlockAllowListUtil.checkBlockList("DUBBO Provider", requestIpAddress, uri);
if(!checkBLockListResult) {
log.error("请求IP:{}在黑名单内,拦截本次请求!uri:{}", requestIpAddress, uri);
throw new RpcException("黑名单拦截请求");
}
Boolean checkAllowListResult = BlockAllowListUtil.checkAllowList("DUBBO Provider", requestIpAddress, uri);
if(!checkAllowListResult) {
log.error("请求IP:{}不在白名单内,拦截本次请求!uri:{}", requestIpAddress, uri);
throw new RpcException("白名单拦截请求");
}
} catch (Exception e) {
log.error("filter拦截器发生错误", e);
result.setException(new RpcException(e.getMessage()));
return result;
}
return result;
}
@Override
public void run() {
}
}
- 配置到引用的DUBBO工程中(PROJECT/src/main/resources/META-INF/dubbo/
com.alibaba.dubbo.rpc.Filter
)
# 配置DUBBO Provider拦截器,为指定组件的地址
# 前面是key,后面是DUBBO Provider过滤器的完整类路径
dubboProviderFilter=com.block.allow.list.config.DubboProviderFilter