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
* @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;
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
* @since 1.37.0
*/
@FunctionalInterface
public interface SaCheckRequestPathFunction {
public interface SaFirewallCheckFailHandleFunction {
/**
* 执行函数
* @param path 请求 path
* @param extArg1 扩展参数1
* @param extArg2 扩展参数2
* @param e 防火墙校验异常
* @param req 请求对象
* @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;
import cn.dev33.satoken.exception.RequestPathInvalidException;
import cn.dev33.satoken.context.model.SaRequest;
import cn.dev33.satoken.context.model.SaResponse;
/**
* 函数式接口当请求 path 校验不通过时处理方案的算法
* 函数式接口防火墙校验函数
*
* @author click33
* @since 1.37.0
*/
@FunctionalInterface
public interface SaRequestPathInvalidHandleFunction {
public interface SaFirewallCheckFunction {
/**
* 执行函数
* @param e 请求 path 无效的异常对象
* @param extArg1 扩展参数1
* @param extArg2 扩展参数2
*
* @param req 请求对象
* @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;
import cn.dev33.satoken.exception.RequestPathInvalidException;
import cn.dev33.satoken.fun.strategy.SaCheckRequestPathFunction;
import cn.dev33.satoken.fun.strategy.SaRequestPathInvalidHandleFunction;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.fun.strategy.SaFirewallCheckFailHandleFunction;
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 防火墙策略
@@ -27,9 +34,6 @@ import cn.dev33.satoken.fun.strategy.SaRequestPathInvalidHandleFunction;
*/
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 = {};
/**
* 请求 path 白名单
*/
public String[] whitePaths = {};
/**
* 请求 path 不允许出现的字符
*/
public String[] invalidCharacter = {
"//", // //
"\\", // \
"%2e", "%2E", // .
"%2f", "%2F", // /
"%5c", "%5C", // \
";", "%3b", "%3B", // ; // 参考资料https://mp.weixin.qq.com/s/77CIDZbgBwRunJeluofPTA
"%25" // 空格
public SaFirewallCheckFunction check = (req, res, extArg) -> {
for (SaFirewallCheckHook checkHook : checkHooks) {
checkHook.execute(req, res, extArg);
}
};
/**
* 校验请求 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 校验不通过时处理方案的算法,自定义示例:
* 请求 path 校验不通过时地处理方案,自定义示例:
* <pre>
* SaFirewallStrategy.instance.requestPathInvalidHandle = (e, extArg1, extArg2) -> {
* SaFirewallStrategy.instance.checkFailHandle = (e, req, res, extArg) -> {
* // 自定义处理逻辑 ...
* };
* </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>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-fastjson2</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>

View File

@@ -38,13 +38,13 @@ public class SaPathCheckFilterForReactor implements WebFilter {
// 校验本次请求 path 是否合法
try {
SaFirewallStrategy.instance.checkRequestPath.run(exchange.getRequest().getPath().toString(), exchange, null);
SaFirewallStrategy.instance.check.run(exchange.getRequest().getPath().toString(), exchange, null);
} 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);
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(e.getMessage().getBytes())));
} else {
SaFirewallStrategy.instance.requestPathInvalidHandle.run(e, exchange, null);
SaFirewallStrategy.instance.checkFailHandle.run(e, exchange, null);
}
return Mono.empty();
}

View File

@@ -38,13 +38,13 @@ public class SaPathCheckFilterForReactor implements WebFilter {
// 校验本次请求 path 是否合法
try {
SaFirewallStrategy.instance.checkRequestPath.run(exchange.getRequest().getPath().toString(), exchange, null);
SaFirewallStrategy.instance.check.run(exchange.getRequest().getPath().toString(), exchange, null);
} 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);
return exchange.getResponse().writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(e.getMessage().getBytes())));
} else {
SaFirewallStrategy.instance.requestPathInvalidHandle.run(e, exchange, null);
SaFirewallStrategy.instance.checkFailHandle.run(e, exchange, null);
}
return Mono.empty();
}

View File

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

View File

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

View File

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