拦截过滤器模式:Web权限校验的经典解法
说实话,拦截过滤器模式(Intercepting Filter)是我在Web开发中最常用的模式之一。它解决了一个很实际的问题:在请求到达真正的业务处理之前,我们需要做一堆预处理工作——权限校验、日志记录、参数校验、编码转换等等。你想想看,如果把这些逻辑都塞到业务代码里,那代码得多臃肿?
我在做电商平台的时候,就遇到过这种场景。用户下单前要检查是否登录、是否有权限、请求是否合法……一开始我把这些判断都写在Controller里,结果一个方法几百行,维护起来简直噩梦。后来重构时用了拦截过滤器模式,整个世界清净了。
模式核心:三个关键角色
拦截过滤器模式其实就三个核心角色,理解它们就掌握了精髓:
- 过滤器接口(Filter):定义过滤器的统一契约
- 具体过滤器(Concrete Filter):实现具体的过滤逻辑
- 过滤链(Filter Chain):管理多个过滤器的执行顺序
说白了,这就是一个"责任链"的变体。但区别在于,拦截过滤器模式更强调"拦截"这个动作——每个过滤器都可以决定是否让请求继续往下走。
先看代码:Java实现
我个人习惯先定义接口,再实现具体逻辑。这样扩展起来特别方便。
// 1. 过滤器接口
public interface Filter {
void execute(String request);
}
// 2. 具体过滤器:权限校验
public class AuthenticationFilter implements Filter {
@Override
public void execute(String request) {
if (request == null || !request.contains("token")) {
throw new SecurityException("未授权访问!");
}
System.out.println("权限校验通过:" + request);
}
}
// 3. 具体过滤器:日志记录
public class LoggingFilter implements Filter {
@Override
public void execute(String request) {
System.out.println("[日志] 收到请求:" + request);
}
}
// 4. 过滤链
public class FilterChain {
private List<Filter> filters = new ArrayList<>();
private int index = 0;
public void addFilter(Filter filter) {
filters.add(filter);
}
public void execute(String request) {
if (index < filters.size()) {
Filter filter = filters.get(index++);
filter.execute(request);
// 继续执行下一个过滤器
execute(request);
}
}
}
// 5. 客户端使用
public class Client {
public static void main(String[] args) {
FilterChain chain = new FilterChain();
chain.addFilter(new AuthenticationFilter());
chain.addFilter(new LoggingFilter());
// 模拟请求
chain.execute("GET /api/order?token=abc123");
}
}
关键点:过滤链中的每个过滤器执行完后,需要主动调用下一个过滤器。这种递归调用的方式,保证了过滤器的执行顺序可控。
C++实现:模板与多态
C++版本我用了虚函数和多态,效果是一样的。嗯,这里要注意内存管理,别漏了析构。
#include <iostream>
#include <vector>
#include <string>
// 1. 过滤器接口
class Filter {
public:
virtual ~Filter() {}
virtual void execute(const std::string& request) = 0;
};
// 2. 具体过滤器:权限校验
class AuthenticationFilter : public Filter {
public:
void execute(const std::string& request) override {
if (request.find("token") == std::string::npos) {
throw std::runtime_error("未授权访问!");
}
std::cout << "权限校验通过:" << request << std::endl;
}
};
// 3. 具体过滤器:日志记录
class LoggingFilter : public Filter {
public:
void execute(const std::string& request) override {
std::cout << "[日志] 收到请求:" << request << std::endl;
}
};
// 4. 过滤链
class FilterChain {
private:
std::vector<Filter*> filters;
size_t index = 0;
public:
void addFilter(Filter* filter) {
filters.push_back(filter);
}
void execute(const std::string& request) {
if (index < filters.size()) {
Filter* filter = filters[index++];
filter->execute(request);
execute(request); // 递归调用下一个
}
}
~FilterChain() {
for (auto f : filters) delete f;
}
};
// 5. 使用示例
int main() {
FilterChain chain;
chain.addFilter(new AuthenticationFilter());
chain.addFilter(new LoggingFilter());
chain.execute("GET /api/order?token=abc123");
return 0;
}
避坑指南:我曾经在C++版本中忘记处理过滤器的生命周期,导致内存泄漏。建议使用智能指针(如std::shared_ptr)来管理过滤器对象,或者像上面这样在析构函数中统一释放。
Web应用中的权限校验实战
在实际的Web框架中(比如Java的Servlet Filter、Spring的Interceptor),拦截过滤器模式被广泛使用。我拿一个典型的权限校验场景来说:
- 请求到达:用户发起HTTP请求
- 过滤器链开始执行:
- 第一个过滤器:检查请求是否包含有效的Session或Token
- 第二个过滤器:检查用户角色是否有权限访问该资源
- 第三个过滤器:记录访问日志
- 通过所有过滤器:请求到达真正的业务处理器
- 任一过滤器拒绝:直接返回错误响应,不再继续执行
你想想看,这种设计最大的好处是什么?解耦。权限校验的逻辑和业务逻辑完全分离,改权限规则时不用动业务代码,改业务逻辑时也不用担心影响权限校验。
核心流程图
下面这张图展示了拦截过滤器模式的完整流程。我特意把"请求被拦截"和"请求通过"两种情况都画出来了:
实际项目中的最佳实践
我在多个项目中总结了几条经验,分享给你:
| 场景 | 推荐做法 | 不推荐做法 |
|---|---|---|
| 过滤器顺序 | 先做轻量级校验(如Token),再做重量级校验(如数据库查询) | 把耗时操作放在前面,导致无效请求也消耗资源 |
| 过滤器复用 | 将通用逻辑(如日志、编码转换)抽取为独立过滤器 | 每个业务模块都写重复的校验代码 |
| 异常处理 | 在过滤链末尾添加一个全局异常过滤器 | 每个过滤器各自处理异常,导致错误响应格式不统一 |
| 性能考虑 | 对频繁调用的过滤器做缓存(如角色权限缓存) | 每次请求都查数据库,不考虑缓存 |
特别注意:过滤器的执行顺序非常重要!我曾经在一个项目中,把日志过滤器放在了权限过滤器前面,结果每次未授权的请求都先记录日志,导致日志里全是"未授权"的错误记录,排查问题时根本找不到有效信息。正确的做法是:权限校验在前,日志记录在后。
模式优缺点总结
用这个模式这么多年,我觉得它的优缺点都很明显:
优点:
- 关注点分离:每个过滤器只做一件事,符合单一职责原则
- 可插拔:新增或移除过滤器不影响现有代码
- 可复用:同一个过滤器可以应用到不同的请求路径上
缺点:
- 调试困难:过滤器链较长时,定位问题需要逐个排查
- 性能开销:每个请求都要经过多个过滤器,会有一定的性能损耗
- 顺序依赖:过滤器之间的顺序如果搞错,可能导致逻辑错误
嗯,总的来说,拦截过滤器模式是Web开发中不可或缺的工具。我个人建议,只要你的应用需要做前置处理(权限、日志、校验等),优先考虑这个模式。它可能不是最炫酷的设计模式,但绝对是最实用的之一。
公众号:蓝海资料掘金营,微信deep3321