网站开发中,权限校验和日志记录是每个项目都绕不开的核心需求。传统做法是在每个接口方法里手动写权限判断和日志代码,这样做的结果就是代码重复率高、维护成本大、容易遗漏。真正高效的解决方案是通过框架拦截器(Interceptor)机制,在请求进入控制器之前统一完成权限校验和日志记录,把横切关注点从业务代码中剥离出来,让开发者只关注核心业务逻辑。下面我会从原理、实现、优化三个层面,把这套方案讲透。
一、为什么必须用拦截器而不是在每个方法里写逻辑
假设你有200个接口,其中150个需要登录验证,100个需要角色判断,80个需要操作日志。如果每个方法里都写一遍if-else判断和logger调用,代码量会膨胀到难以维护。更严重的是,一旦权限规则变了,你得改几十甚至上百个文件,漏改一个就是安全漏洞。拦截器的本质是AOP(面向切面编程)思想的落地,它在请求链路上设置一个关卡,所有请求都必须经过这个关卡,统一处理后再放行。这样做的好处非常明确:代码集中管理、规则统一执行、业务代码干净清爽。
二、主流框架拦截器的工作原理
不管是Java的Spring MVC、Python的Django中间件、还是Node.js的Express中间件,拦截器的核心流程都一样:请求进来 → 拦截器前置处理 → 控制器执行 → 拦截器后置处理 → 响应返回。以Spring MVC为例,HandlerInterceptor接口提供了三个方法:preHandle在控制器方法执行前调用,postHandle在方法执行后但视图渲染前调用,afterCompletion在整个请求完成后调用。权限校验放在preHandle里做,日志记录可以在preHandle记录请求信息、afterCompletion记录响应结果,形成完整的日志闭环。
三、统一权限校验的具体实现步骤
第一步,定义权限注解。创建一个自定义注解,标记哪些接口需要什么级别的权限。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String value() default "";
String role() default "";
}
第二步,编写拦截器核心逻辑。在preHandle方法中读取注解信息,从请求头或Session中获取用户身份,进行匹配校验。
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 获取请求路径和方法
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequirePermission annotation = handlerMethod.getMethodAnnotation(RequirePermission.class);
// 如果没有注解,直接放行
if (annotation == null) {
return true;
}
// 从请求头获取Token
String token = request.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
response.setStatus(401);
response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}");
return false;
}
// 验证Token并获取用户信息
UserInfo user = tokenService.validateToken(token);
if (user == null) {
response.setStatus(401);
response.getWriter().write("{\"code\":401,\"msg\":\"Token无效\"}");
return false;
}
// 校验角色权限
if (!StringUtils.isBlank(annotation.role())) {
if (!user.getRoles().contains(annotation.role())) {
response.setStatus(403);
response.getWriter().write("{\"code\":403,\"msg\":\"权限不足\"}");
return false;
}
}
// 将用户信息放入请求属性,供后续使用
request.setAttribute("currentUser", user);
return true;
}
}
第三步,注册拦截器并配置拦截路径。在Spring配置类中指定哪些路径需要拦截,哪些路径(如登录接口、静态资源)需要排除。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/")
.excludePathPatterns("/api/login", "/api/register",
"/static/", "/public/");
}
}
四、统一日志记录的完整方案
权限校验解决的是"谁能访问"的问题,日志记录解决的是"发生了什么"的问题。一个完善的拦截器日志系统需要记录四类信息:请求基本信息(IP、URL、方法、时间戳)、用户身份(如果已登录)、请求参数(脱敏处理)、响应状态和耗时。下面是日志拦截器的实现思路。
@Component
public class LogInterceptor implements HandlerInterceptor {
private static final Logger log = LoggerFactory.getLogger(LogInterceptor.class);
private static final ThreadLocal<Long> startTime = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
startTime.set(System.currentTimeMillis());
String ip = getClientIp(request);
String url = request.getRequestURI();
String method = request.getMethod();
log.info("请求开始 | IP:{} | {}:{} | 时间:{}", ip, method, url,
new Date());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
long cost = System.currentTimeMillis() - startTime.get();
int status = response.getStatus();
String ip = getClientIp(request);
String url = request.getRequestURI();
if (ex != null) {
log.error("请求异常 | IP:{} | URL:{} | 状态:{} | 耗时:{}ms | 异常:{}",
ip, url, status, cost, ex.getMessage());
} else {
log.info("请求完成 | IP:{} | URL:{} | 状态:{} | 耗时:{}ms",
ip, url, status, cost);
}
startTime.remove();
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
return ip;
}
}
五、拦截器链的顺序与多拦截器协作
实际项目中,权限拦截器和日志拦截器通常是两个独立的类,它们的执行顺序很关键。一般来说,日志拦截器要放在最外层,因为它需要记录整个请求的生命周期,包括权限拦截器的执行情况。在Spring中,addInterceptors方法的调用顺序决定了拦截器链的顺序,先注册的先执行preHandle,后执行afterCompletion。所以注册时要注意:日志拦截器先注册,权限拦截器后注册。
六、生产环境必须注意的几个坑
第一个坑是性能问题。拦截器对每个请求都会执行,如果preHandle里做了数据库查询或者远程调用,会直接拖慢接口响应。解决办法是把用户信息缓存到Redis里,Token验证时先查缓存,缓存没有再查数据库。第二个坑是异常处理不当。拦截器里抛出的异常如果没有被全局异常处理器捕获,用户会看到500错误页面而不是你自定义的JSON响应。需要配合@ControllerAdvice做统一异常处理。第三个坑是日志脱敏。手机号、身份证、密码这些字段绝对不能明文记录,必须在记录前做掩码处理。第四个坑是拦截路径配置过于宽泛。如果把静态资源、健康检查接口都拦截了,会导致不必要的性能开销和误判,excludePathPatterns一定要配仔细。
七、进阶优化:动态权限与注解组合使用
基础方案能解决大部分场景,但复杂业务需要更灵活的权限控制。比如同一个接口,普通用户只能查自己的数据,管理员可以查所有数据。这时候可以在注解里增加dataScope参数,拦截器根据用户角色动态拼接SQL条件或者在Service层做数据过滤。另外,可以把权限校验和日志记录合并到一个拦截器里,减少拦截器链的层数,但要注意单一职责原则,代码量大了就拆开。还有一种做法是用自定义表达式,在注解里写SpEL表达式,比如"#user.role == 'admin' or #user.id == #data.userId",拦截器里用SpEL解析器动态计算,灵活性极高但复杂度也高。
八、不同语言框架的对应实现思路
如果你用的不是Java,思路完全通用。Python Django用中间件(Middleware),在process_request里做权限判断,在process_response里记录日志。Node.js Express用app.use注册中间件函数,顺序决定执行顺序。Go语言的Gin框架用中间件函数,通过c.Next()控制是否继续执行。PHP的Laravel用中间件(Middleware),在handle方法里做校验。不管什么语言,核心逻辑都是:在请求进入业务逻辑之前拦截、校验、放行,在请求结束后记录日志。框架只是提供了不同的API,设计思想完全一致。
九、总结与最佳实践
拦截器实现统一权限校验与日志记录,本质上是把横切关注点从业务代码中抽离,这是软件工程中"关注点分离"原则的典型应用。最佳实践总结下来就几条:权限逻辑集中在拦截器和注解里,业务代码只写@RequirePermission;日志记录覆盖请求全生命周期,敏感数据必须脱敏;拦截器链顺序要合理,日志在外权限在内;生产环境要做缓存优化和异常兜底;拦截路径精确配置,避免误伤。把这套方案落地之后,你的项目代码会干净很多,安全审计也更容易通过,后期维护成本至少降低一半。
