refactor: 重构防火墙模块,增加 hooks 机制

This commit is contained in:
click33
2025-02-27 05:55:55 +08:00
parent a5d8e071a7
commit db5e70db6a
15 changed files with 350 additions and 109 deletions

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.exception;
/**
* 一个异常:代表防火墙检验未通过
*
* @author click33
* @since 1.41.0
*/
public class FirewallCheckException extends SaTokenException {
/**
* 序列化版本号
*/
private static final long serialVersionUID = 8243974276159004739L;
public FirewallCheckException(String message) {
super(message);
}
public FirewallCheckException(Throwable e) {
super(e);
}
public FirewallCheckException(String message, Throwable e) {
super(message, e);
}
}

View File

@@ -21,7 +21,7 @@ package cn.dev33.satoken.exception;
* @author click33 * @author click33
* @since 1.37.0 * @since 1.37.0
*/ */
public class RequestPathInvalidException extends SaTokenException { public class RequestPathInvalidException extends FirewallCheckException {
/** /**
* 序列化版本号 * 序列化版本号

View File

@@ -15,25 +15,26 @@
*/ */
package cn.dev33.satoken.fun.strategy; package cn.dev33.satoken.fun.strategy;
import cn.dev33.satoken.exception.RequestPathInvalidException; import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.exception.FirewallCheckException;
/** /**
* 函数式接口校验请求 path 的算法 * 函数式接口当防火墙校验不通过时执行的函数
*
* <p> 如果属于无效请求 path则抛出异常 RequestPathInvalidException </p>
* *
* @author click33 * @author click33
* @since 1.37.0 * @since 1.37.0
*/ */
@FunctionalInterface @FunctionalInterface
public interface SaCheckRequestPathFunction { public interface SaFirewallCheckFailHandleFunction {
/** /**
* 执行函数 * 执行函数
* @param path 请求 path * @param e 防火墙校验异常
* @param extArg1 扩展参数1 * @param req 请求对象
* @param extArg2 扩展参数2 * @param res 响应对象
* @param extArg 预留扩展参数
*/ */
void run(String path, Object extArg1, Object extArg2); void run(FirewallCheckException e, SaRequest req, SaResponse res, Object extArg);
} }

View File

@@ -15,23 +15,25 @@
*/ */
package cn.dev33.satoken.fun.strategy; package cn.dev33.satoken.fun.strategy;
import cn.dev33.satoken.exception.RequestPathInvalidException; import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.context.model.SaResponse;
/** /**
* 函数式接口当请求 path 校验不通过时处理方案的算法 * 函数式接口防火墙校验函数
* *
* @author click33 * @author click33
* @since 1.37.0 * @since 1.37.0
*/ */
@FunctionalInterface @FunctionalInterface
public interface SaRequestPathInvalidHandleFunction { public interface SaFirewallCheckFunction {
/** /**
* 执行函数 * 执行函数
* @param e 请求 path 无效的异常对象 *
* @param extArg1 扩展参数1 * @param req 请求对象
* @param extArg2 扩展参数2 * @param res 响应对象
* @param extArg 预留扩展参数
*/ */
void run(RequestPathInvalidException e, Object extArg1, Object extArg2); void execute(SaRequest req, SaResponse res, Object extArg);
} }

View File

@@ -15,9 +15,16 @@
*/ */
package cn.dev33.satoken.strategy; package cn.dev33.satoken.strategy;
import cn.dev33.satoken.exception.RequestPathInvalidException; import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.fun.strategy.SaCheckRequestPathFunction; import cn.dev33.satoken.fun.strategy.SaFirewallCheckFailHandleFunction;
import cn.dev33.satoken.fun.strategy.SaRequestPathInvalidHandleFunction; import cn.dev33.satoken.fun.strategy.SaFirewallCheckFunction;
import cn.dev33.satoken.strategy.hooks.SaFirewallCheckHook;
import cn.dev33.satoken.strategy.hooks.SaFirewallCheckHookForBlackList;
import cn.dev33.satoken.strategy.hooks.SaFirewallCheckHookForDangerCharacter;
import cn.dev33.satoken.strategy.hooks.SaFirewallCheckHookForWhiteList;
import java.util.ArrayList;
import java.util.List;
/** /**
* Sa-Token 防火墙策略 * Sa-Token 防火墙策略
@@ -27,9 +34,6 @@ import cn.dev33.satoken.fun.strategy.SaRequestPathInvalidHandleFunction;
*/ */
public final class SaFirewallStrategy { public final class SaFirewallStrategy {
private SaFirewallStrategy() {
}
/** /**
* 全局单例引用 * 全局单例引用
*/ */
@@ -38,76 +42,38 @@ public final class SaFirewallStrategy {
// ----------------------- 所有策略 // ----------------------- 所有策略
public List<SaFirewallCheckHook> checkHooks = new ArrayList<>();
private SaFirewallStrategy() {
checkHooks.add(SaFirewallCheckHookForWhiteList.instance);
checkHooks.add(SaFirewallCheckHookForBlackList.instance);
checkHooks.add(SaFirewallCheckHookForDangerCharacter.instance);
}
// 注册一个防火墙校验 hook
public void registerCheckHook(SaFirewallCheckHook checkHook) {
SaManager.getLog().info("防火墙校验 hook 注册成功: " + checkHook.getClass());
checkHooks.add(checkHook);
}
/** /**
* 请求 path 黑名单 * 防火墙校验函数
*/ */
public String[] blackPaths = {}; public SaFirewallCheckFunction check = (req, res, extArg) -> {
for (SaFirewallCheckHook checkHook : checkHooks) {
/** checkHook.execute(req, res, extArg);
* 请求 path 白名单 }
*/
public String[] whitePaths = {};
/**
* 请求 path 不允许出现的字符
*/
public String[] invalidCharacter = {
"//", // //
"\\", // \
"%2e", "%2E", // .
"%2f", "%2F", // /
"%5c", "%5C", // \
";", "%3b", "%3B", // ; // 参考资料https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA
"%25" // 空格
}; };
/** /**
* 校验请求 path 的算法 * 请求 path 校验不通过时地处理方案,自定义示例:
*/
public SaCheckRequestPathFunction checkRequestPath = (requestPath, extArg1, extArg2) -> {
// 1、如果在白名单里则直接放行
for (String item : whitePaths) {
if (requestPath.equals(item)) {
return;
}
}
// 2、如果在黑名单里则抛出异常
for (String item : blackPaths) {
if (requestPath.equals(item)) {
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
}
}
// 3、检查是否包含非法字符
// 不允许为null
if(requestPath == null) {
throw new RequestPathInvalidException("非法请求null", null);
}
// 不允许包含非法字符
for (String item : invalidCharacter) {
if (requestPath.contains(item)) {
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
}
}
// 不允许出现跨目录字符
if(requestPath.contains("/.") || requestPath.contains("\\.")) {
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
}
};
/**
* 当请求 path 校验不通过时处理方案的算法,自定义示例:
* <pre> * <pre>
* SaFirewallStrategy.instance.requestPathInvalidHandle = (e, extArg1, extArg2) -> { * SaFirewallStrategy.instance.checkFailHandle = (e, req, res, extArg) -> {
* // 自定义处理逻辑 ... * // 自定义处理逻辑 ...
* }; * };
* </pre> * </pre>
*/ */
public SaRequestPathInvalidHandleFunction requestPathInvalidHandle = null; public SaFirewallCheckFailHandleFunction checkFailHandle = null;
} }

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.strategy.hooks;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.context.model.SaResponse;
/**
* 防火墙策略校验钩子函数 - 接口
*
* @author click33
* @since 1.41.0
*/
@FunctionalInterface
public interface SaFirewallCheckHook {
/**
* 执行的方法
*
* @param req 请求对象
* @param res 响应对象
* @param extArg 预留扩展参数
*/
void execute(SaRequest req, SaResponse res, Object extArg);
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.strategy.hooks;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.exception.RequestPathInvalidException;
/**
* 防火墙策略校验钩子函数:黑名单校验
*
* @author click33
* @since 1.41.0
*/
public class SaFirewallCheckHookForBlackList implements SaFirewallCheckHook {
/**
* 默认实例
*/
public static SaFirewallCheckHookForBlackList instance = new SaFirewallCheckHookForBlackList();
/**
* 请求 path 黑名单
*/
public String[] blackPaths = {};
/**
* 执行的方法
*
* @param req 请求对象
* @param res 响应对象
* @param extArg 扩展预留参数
*/
@Override
public void execute(SaRequest req, SaResponse res, Object extArg) {
String requestPath = req.getRequestPath();
for (String item : blackPaths) {
if (requestPath.equals(item)) {
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
}
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.strategy.hooks;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.exception.RequestPathInvalidException;
/**
* 防火墙策略校验钩子函数:危险字符校验
*
* @author click33
* @since 1.41.0
*/
public class SaFirewallCheckHookForDangerCharacter implements SaFirewallCheckHook {
/**
* 默认实例
*/
public static SaFirewallCheckHookForDangerCharacter instance = new SaFirewallCheckHookForDangerCharacter();
/**
* 请求 path 不允许出现的危险字符
*/
public String[] dangerCharacter = {
"//", // //
"\\", // \
"%2e", "%2E", // .
"%2f", "%2F", // /
"%5c", "%5C", // \
";", "%3b", "%3B", // ; // 参考资料https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA
"%25", // 空格
"/.", "\\.", // /. \. 目录遍历符
};
/**
* 执行的方法
*
* @param req 请求对象
* @param res 响应对象
* @param extArg 预留扩展参数
*/
@Override
public void execute(SaRequest req, SaResponse res, Object extArg) {
String requestPath = req.getRequestPath();
for (String item : dangerCharacter) {
if (requestPath.contains(item)) {
throw new RequestPathInvalidException("非法请求:" + requestPath, requestPath);
}
}
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2020-2099 sa-token.cc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.dev33.satoken.strategy.hooks;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.context.model.SaResponse;
import cn.dev33.satoken.exception.StopMatchException;
/**
* 防火墙策略校验钩子函数:白名单放行
*
* @author click33
* @since 1.41.0
*/
public class SaFirewallCheckHookForWhiteList implements SaFirewallCheckHook {
/**
* 默认实例
*/
public static SaFirewallCheckHookForWhiteList instance = new SaFirewallCheckHookForWhiteList();
/**
* 请求 path 白名单
*/
public String[] whitePaths = {};
/**
* 执行的方法
*
* @param req 请求对象
* @param res 响应对象
* @param extArg 预留扩展参数
*/
@Override
public void execute(SaRequest req, SaResponse res, Object extArg) {
String requestPath = req.getRequestPath();
for (String item : whitePaths) {
if (requestPath.equals(item)) {
throw new StopMatchException();
}
}
}
}

View File

@@ -60,11 +60,6 @@
<artifactId>sa-token-redis-template</artifactId> <artifactId>sa-token-redis-template</artifactId>
<version>${sa-token.version}</version> <version>${sa-token.version}</version>
</dependency> </dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-fastjson2</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 提供Redis连接池 --> <!-- 提供Redis连接池 -->
<dependency> <dependency>

View File

@@ -38,13 +38,13 @@ public class SaPathCheckFilterForReactor implements WebFilter {
// 校验本次请求 path 是否合法 // 校验本次请求 path 是否合法
try { try {
SaFirewallStrategy.instance.checkRequestPath.run(exchange.getRequest().getPath().toString(), exchange, null); SaFirewallStrategy.instance.check.run(exchange.getRequest().getPath().toString(), exchange, null);
} catch (RequestPathInvalidException e) { } catch (RequestPathInvalidException e) {
if(SaFirewallStrategy.instance.requestPathInvalidHandle == null) { if(SaFirewallStrategy.instance.checkFailHandle == null) {
exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN);
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(e.getMessage().getBytes()))); return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(e.getMessage().getBytes())));
} else { } else {
SaFirewallStrategy.instance.requestPathInvalidHandle.run(e, exchange, null); SaFirewallStrategy.instance.checkFailHandle.run(e, exchange, null);
} }
return Mono.empty(); return Mono.empty();
} }

View File

@@ -38,13 +38,13 @@ public class SaPathCheckFilterForReactor implements WebFilter {
// 校验本次请求 path 是否合法 // 校验本次请求 path 是否合法
try { try {
SaFirewallStrategy.instance.checkRequestPath.run(exchange.getRequest().getPath().toString(), exchange, null); SaFirewallStrategy.instance.check.run(exchange.getRequest().getPath().toString(), exchange, null);
} catch (RequestPathInvalidException e) { } catch (RequestPathInvalidException e) {
if(SaFirewallStrategy.instance.requestPathInvalidHandle == null) { if(SaFirewallStrategy.instance.checkFailHandle == null) {
exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN); exchange.getResponse().getHeaders().set(SaTokenConsts.CONTENT_TYPE_KEY, SaTokenConsts.CONTENT_TYPE_TEXT_PLAIN);
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(e.getMessage().getBytes()))); return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(e.getMessage().getBytes())));
} else { } else {
SaFirewallStrategy.instance.requestPathInvalidHandle.run(e, exchange, null); SaFirewallStrategy.instance.checkFailHandle.run(e, exchange, null);
} }
return Mono.empty(); return Mono.empty();
} }

View File

@@ -15,43 +15,56 @@
*/ */
package cn.dev33.satoken.filter; package cn.dev33.satoken.filter;
import cn.dev33.satoken.exception.RequestPathInvalidException; import cn.dev33.satoken.exception.FirewallCheckException;
import cn.dev33.satoken.exception.StopMatchException;
import cn.dev33.satoken.servlet.model.SaRequestForServlet;
import cn.dev33.satoken.servlet.model.SaResponseForServlet;
import cn.dev33.satoken.strategy.SaFirewallStrategy; import cn.dev33.satoken.strategy.SaFirewallStrategy;
import cn.dev33.satoken.util.SaTokenConsts; import cn.dev33.satoken.util.SaTokenConsts;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import javax.servlet.*; import javax.servlet.*;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
/** /**
* 校验请求 path 是否合法 * 防火墙校验过滤器
* *
* @author click33 * @author click33
* @since 1.37.0 * @since 1.37.0
*/ */
@Order(SaTokenConsts.PATH_CHECK_FILTER_ORDER) @Order(SaTokenConsts.PATH_CHECK_FILTER_ORDER)
public class SaPathCheckFilterForServlet implements Filter { public class SaFirewallCheckFilterForServlet implements Filter {
@Override @Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 校验本次请求 path 是否合法 HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
SaRequestForServlet saRequest = new SaRequestForServlet(req);
SaResponseForServlet saResponse = new SaResponseForServlet(res);
try { try {
HttpServletRequest req = (HttpServletRequest) request; SaFirewallStrategy.instance.check.execute(saRequest, saResponse, null);
SaFirewallStrategy.instance.checkRequestPath.run(req.getRequestURI(), request, response); }
} catch (RequestPathInvalidException e) { catch (StopMatchException e) {
if(SaFirewallStrategy.instance.requestPathInvalidHandle == null) { // 如果是 StopMatchException 异常代表通过了防火墙验证进入 Controller
}
catch (FirewallCheckException e) {
// FirewallCheckException 异常则交由异常处理策略处理
if(SaFirewallStrategy.instance.checkFailHandle == null) {
response.setContentType("text/plain; charset=utf-8"); response.setContentType("text/plain; charset=utf-8");
response.getWriter().print(e.getMessage()); response.getWriter().print(e.getMessage());
response.getWriter().flush(); response.getWriter().flush();
} else { } else {
SaFirewallStrategy.instance.requestPathInvalidHandle.run(e, request, response); SaFirewallStrategy.instance.checkFailHandle.run(e, saRequest, saResponse, null);
} }
return; return;
} }
// 更多异常则不处理交由 Web 框架处理
// 执行 // 执行
chain.doFilter(request, response); chain.doFilter(request, response);
} }

View File

@@ -16,7 +16,7 @@
package cn.dev33.satoken.spring; package cn.dev33.satoken.spring;
import cn.dev33.satoken.context.SaTokenContext; import cn.dev33.satoken.context.SaTokenContext;
import cn.dev33.satoken.filter.SaPathCheckFilterForServlet; import cn.dev33.satoken.filter.SaFirewallCheckFilterForServlet;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
/** /**
@@ -43,8 +43,8 @@ public class SaTokenContextRegister {
* @return / * @return /
*/ */
@Bean @Bean
public SaPathCheckFilterForServlet saPathCheckFilterForServlet() { public SaFirewallCheckFilterForServlet saPathCheckFilterForServlet() {
return new SaPathCheckFilterForServlet(); return new SaFirewallCheckFilterForServlet();
} }
} }

View File

@@ -39,14 +39,14 @@ public class SaPathCheckFilterForJakartaServlet implements Filter {
// 校验本次请求 path 是否合法 // 校验本次请求 path 是否合法
try { try {
HttpServletRequest req = (HttpServletRequest) request; HttpServletRequest req = (HttpServletRequest) request;
SaFirewallStrategy.instance.checkRequestPath.run(req.getRequestURI(), request, response); SaFirewallStrategy.instance.check.run(req.getRequestURI(), request, response);
} catch (RequestPathInvalidException e) { } catch (RequestPathInvalidException e) {
if(SaFirewallStrategy.instance.requestPathInvalidHandle == null) { if(SaFirewallStrategy.instance.checkFailHandle == null) {
response.setContentType("text/plain; charset=utf-8"); response.setContentType("text/plain; charset=utf-8");
response.getWriter().print(e.getMessage()); response.getWriter().print(e.getMessage());
response.getWriter().flush(); response.getWriter().flush();
} else { } else {
SaFirewallStrategy.instance.requestPathInvalidHandle.run(e, request, response); SaFirewallStrategy.instance.checkFailHandle.run(e, request, response);
} }
return; return;
} }