1. Spring AOP 做登录校验的可行性分析
第一次在项目中看到用Spring AOP实现登录校验的代码时,我的第一反应是"这操作真骚"。AOP(面向切面编程)本意是用来处理横切关注点的,比如日志、事务这些与业务逻辑解耦的操作。但登录校验这种强业务相关的功能,真的适合放在切面里吗?
先看一个典型的AOP登录校验实现:
java复制@Aspect
@Component
public class LoginCheckAspect {
@Around("@annotation(com.xxx.annotation.RequireLogin)")
public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
String token = request.getHeader("Authorization");
if(StringUtils.isEmpty(token)) {
throw new UnauthorizedException("请先登录");
}
// 验证token有效性
User user = authService.verifyToken(token);
if(user == null) {
throw new UnauthorizedException("登录已过期");
}
// 将用户信息存入上下文
UserContext.setCurrentUser(user);
return joinPoint.proceed();
}
}
这种实现看似优雅,用个注解就能控制接口的登录校验。但实际项目中,我踩过以下几个坑:
-
请求上下文依赖问题:AOP中获取HttpServletRequest是通过RequestContextHolder,这要求请求必须处于Spring Web的线程上下文环境中。如果你在异步任务、消息队列等非Web线程中调用被切的方法,会直接报NPE
-
执行顺序不可控:AOP的优先级可能低于其他切面(如事务切面),导致校验逻辑在事务开启后才执行。如果校验失败抛出异常,已经开启的事务不会回滚,造成连接泄漏
-
细粒度控制困难:比如某些接口需要管理员权限,某些只需要普通用户权限。用AOP实现这种层级校验,要么写多个切面,要么在切面里写一堆if-else,代码会变得很臃肿
提示:我曾在一个定时任务调用的Service方法上误加了@RequireLogin注解,因为任务线程没有RequestContext导致生产环境事故。AOP对运行环境的隐形依赖是个大坑。
2. 对比传统拦截器方案
相比AOP,用拦截器实现登录校验是更主流的选择。我们看下两种方案的对比:
| 维度 | AOP方案 | 拦截器方案 |
|---|---|---|
| 执行时机 | 方法调用前后 | 请求进入Controller前 |
| 请求上下文依赖 | 强依赖 | 原生支持 |
| 性能影响 | 每个方法调用都会代理 | 仅过滤配置的路径 |
| 细粒度控制 | 依赖注解,较灵活 | 基于路径匹配,灵活性一般 |
| 异常处理 | 可能绕过Spring MVC的异常处理 | 完美集成MVC异常处理流程 |
| 多切面协作 | 需处理切面顺序 | 天然支持过滤器链 |
拦截器的典型实现:
java复制public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getHeader("Authorization");
if(StringUtils.isEmpty(token)) {
throw new UnauthorizedException("请先登录");
}
User user = authService.verifyToken(token);
if(user == null) {
throw new UnauthorizedException("登录已过期");
}
request.setAttribute("CURRENT_USER", user);
return true;
}
}
从实践经验看,拦截器方案有几点优势:
- 生命周期明确:只在Web请求处理链中执行,不会误用于非Web场景
- 性能更好:仅对配置的URL路径生效,不像AOP需要为每个方法创建代理
- 集成度高:可以直接操作HttpServletRequest/Response,方便设置CORS等头部
但拦截器也有局限——它只能基于URL路径做匹配,无法像AOP那样精确到具体方法。对于需要方法级细粒度控制的场景,可以结合两者使用。
3. 那些年我们踩过的坑
3.1 循环依赖问题
当AOP切面中需要注入其他Bean,而这些Bean又依赖被切面的Bean时,会导致Spring启动失败。比如:
java复制@Aspect
@Component
public class LoginAspect {
@Autowired
private UserService userService; // 依赖UserService
}
@Service
public class UserService {
@RequireLogin // 又被LoginAspect切面处理
public void updateProfile(User user) {
//...
}
}
这种循环依赖在拦截器方案中不会出现,因为拦截器本身不参与业务Bean的依赖注入。
解决方案:
- 使用@Lazy延迟加载
- 通过ApplicationContext.getBean()手动获取依赖
- 重构代码,打破循环依赖链
3.2 内部调用失效
Spring AOP基于动态代理实现,当类内部方法互相调用时,切面逻辑不会生效:
java复制@Service
public class OrderService {
public void createOrder(Order order) {
checkInventory(); // 内部调用,AOP失效
//...
}
@RequireLogin
public void checkInventory() {
//...
}
}
解决方案:
- 将方法拆分到不同类
- 通过AopContext.currentProxy()获取代理对象调用
- 改用AspectJ编译时织入
3.3 异常处理陷阱
AOP抛出的异常可能绕过Spring MVC的统一异常处理。比如在@RestControllerAdvice中定义的异常处理器可能捕获不到AOP抛出的异常。
解决方案:
- 在切面中直接处理响应(破坏REST风格)
- 抛出特定异常类型并在全局处理器中捕获
- 优先使用拦截器方案
4. 最佳实践建议
经过多个项目的实践验证,我总结出以下经验:
-
分层校验原则:
- 基础校验(如登录态)放在拦截器层
- 业务校验(如权限检查)放在AOP或Service层
- 数据校验(如参数格式)放在Controller层
-
性能敏感场景:
- 对于高频调用的方法,避免使用AOP增加代理开销
- 在拦截器中实现短路返回(如已缓存的校验结果)
-
分布式环境:
- 登录态校验建议放在API网关层统一处理
- 如果必须在服务内校验,使用Redis共享会话状态
-
测试友好性:
- AOP切面会干扰单元测试,需要额外配置
- 拦截器可以通过MockHttpServletRequest测试
-
监控与日志:
- 在切面/拦截器中添加埋点,监控校验耗时
- 记录详细的失败原因(如token过期vs无效token)
一个折中的实现方案是:用拦截器处理基础登录校验,用AOP处理业务权限校验。例如:
java复制// 拦截器处理基础登录态
public class AuthInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, ...) {
// 基础token校验
User user = checkToken(request);
request.setAttribute("CURRENT_USER", user);
return true;
}
}
// AOP处理精细权限
@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(RequireRole)")
public Object checkRole(ProceedingJoinPoint jp) {
User user = UserContext.getCurrentUser();
if(!user.hasRole(requiredRole)) {
throw new ForbiddenException("权限不足");
}
return jp.proceed();
}
}
这种分层设计既保证了基础校验的性能,又提供了足够的灵活性。在最近的一个电商项目中,我们采用这种方案成功支撑了双11大促期间每秒上万次的权限校验请求。
