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();
}
}
}
}