注解+AOP实现全局登录认证效验(附代码)

1. 需求分析

在登录后,前端每次向后端发起请求都需要携带下发的token,而后端的每个方法都需要效验token是否有效。当然可以提取出公共的方法,但是终归是与我们业务逻辑无关的代码,而且修改需要改动原来的业务代码,造成耦合
而通过注解+AOP就能很好的解耦合,提高扩展性

2. 什么是AOP

说到AOP第一个想到的肯定是面向切面编程。在传统的面向对象编程(OOP)中,横切关注点如日志、事务管理、安全性等通常会散布在多个模块中,这会导致代码的重复和混乱。AOP的目的是通过将这样的关注点从业务逻辑中分离出来,使得它们可以在一个集中的地方被管理和维护。主要分为如下三部分

  1. 切入点 - 实际增强的方法
  2. 增强 - 需要实现的增强逻辑代码
  3. 切面 - 是一个操作,指把增强使用到切入点的过程

3. 实现

在项目的公共模块common-util中创建如下,也可以根据自己项目架构选择

3.1 新建注解 @DaiJiaLogin

//方法前校验是否登录
@Target(ElementType.METHOD) //在方法上生效
@Retention(RetentionPolicy.RUNTIME) //运行时
public @interface DaiJiaLogin {
}

3.2 新建切面类

@Component
@Aspect //切面类
public class DaiJiaAspect {
    
}

3.3 在切面类中定义增强方法

注意增强方法上标注的是环绕,格式固定,根据自己的项目架构来修改

“execution(* com.charles.daijia..controller..*(…)) *代表任意的权限修饰符 在com.charles.daijia包名下的controller下的任意类里面任意方法,方法能有任意参数(…)
&&
@annotation(daiJiaLogin)” 同时必需要有这个注解才增强方法

@Resource
private RedisTemplate redisTemplate;

@Around("execution(* com.charles.daijia.*.controller.*.*(..)) && @annotation(daiJiaLogin)")
public void process(ProceedingJoinPoint proceedingJoinPoint, DaiJiaLogin daiJiaLogin) throws Throwable {
    //获取请求
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
    HttpServletRequest request = servletRequestAttributes.getRequest();

    //请求中得到请求头的token
    String token = request.getHeader("token");

    if(token == null)
        throw new DaiJiaException(ResultCodeEnum.LOGIN_AUTH);

    //在redis中查找是否存在token
    String id = (String) redisTemplate.opsForValue().get(RedisConstant.USER_LOGIN_KEY_PREFIX + token);

    if(id == null)
        throw new DaiJiaException(ResultCodeEnum.LOGIN_AUTH);

    //放入localstorage中
    AuthContextHolder.setUserId(Long.parseLong(id));

    proceedingJoinPoint.proceed();
}

3.4 使用

只需要在响应的方法上标注@DaiJiaLogin 这个注解就能实现我们需求的功能,而不需改动原有的业务代码

//获取登录用户信息
@Operation(summary = "登录后拉取用户信息")
@DaiJiaLogin  // 切面增强实现效验登录是否过期
@GetMapping("/getCustomerLoginInfo")
public Result<CustomerLoginVo> getCustomerLoginInfo(){
    //直接从threadLocal中获取
    Long userId = AuthContextHolder.getUserId();

    return Result.ok(customerService.getCustomerLoginInfo(userId));
}
### Spring Boot Starter Validation 参数校验在 AOP 前执行导致 AOP 失效的原因分析 Spring Boot 中的 `spring-boot-starter-validation` 提供了基于 Bean Validation API 的参数校验功能。当使用此功能时,默认情况下会在控制器方法调用前完成参数校验操作。如果在此阶段抛出了异常,则后续的逻辑(如 AOP 切面)不会被执行。 #### 问题原因 参数校验通常通过拦截器实现,在请求到达目标方法之前先进行验证。而 AOP 是一种面向切面编程技术,其核心在于动态代理机制[^1]。由于参数校验发生在 AOP 切面前,一旦校验失败并抛出异常,整个流程会中断,因此 AOP 不会被触发。 --- ### 解决方案 以下是几种可能的解决方案: #### 方案一:调整校验逻辑至业务层 可以将参数校验从控制器移除到服务层或领域对象中。这种方式使得校验成为业务的一部分,而不是框架层面的行为。具体做法如下: ```java // 控制器仅负责接收数据 @RestController public class MyController { @Autowired private MyService myService; public ResponseEntity<?> handleRequest(@RequestBody Object request) { try { return new ResponseEntity<>(myService.process(request), HttpStatus.OK); } catch (ConstraintViolationException e) { // 自定义错误响应 return new ResponseEntity<>("Validation failed", HttpStatus.BAD_REQUEST); } } } // 在服务层应用校验逻辑 @Service @Validated public class MyService { public void process(@Valid Object data) throws ConstraintViolationException { // 执行其他业务逻辑 } } ``` 上述代码中,`@Validated` 注解用于启用服务层的方法级校验,从而让 AOP 能够正常工作。 --- #### 方案二:自定义拦截器顺序 可以通过配置拦截器顺序来延迟参数校验的时间点,使其位于 AOP 后再发生。这需要修改 WebMvcConfigurer 或者 HandlerInterceptor 的加载优先级。 示例代码如下: ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 添加自定义拦截器,并设置其优先级低于默认校验器 registry.addInterceptor(new CustomInterceptor()).order(-1); // 设置较低的 order 数字表示较高优先级 } static class CustomInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("Custom interceptor executed"); return true; } } } ``` 注意,这种方法可能会增加复杂度,因为需要手动管理多个拦截器之间的交互关系。 --- #### 方案三:利用 Seata 实现分布式事务控制 如果你正在使用 Seata 进行全局事务管理,也可以考虑将其作为切入点之一。Seata 支持通过特定依赖项集成到项目中[^2]。例如: ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <version>${spring-cloud-alibaba.version}</version> </dependency> <dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <version>1.5.1</version> </dependency> ``` 尽管这不是直接解决问题的方式,但它可以帮助你在更复杂的场景下协调不同组件的工作流。 --- ### 总结 最佳实践通常是 **将参数校验移到业务层**,这样既能保持清晰的责任划分,又能避免因校验提前而导致的副作用。同时需要注意的是,无论采用哪种方式,都应确保系统的可维护性和扩展性不受影响。 --- 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值