利用 AOP 给用户认证加点料

1,249 阅读4分钟

场景

作为一名系统管理员,我需要知道谁在什么时候登录登出过系统,当出现问题时,能够通过日志排查责任人。

技术分析

一个 Web 应用通常自带了用户管理系统,实现登录用户的认证和授权,通过 Spring Security 可以实现基本的认证功能。对于企业级应用,通常还需要支持其他第三方认证系统,Web 应用提供接口,用户基于此实现自定义认证系统。

在使用了 Spring Security 的应用中,用户登录时会调用 AuthenticationProvider#authenticate(Authentication authentication) 接口进行认证,Web 应用通过实现该接口,完成自定义认证流程。

基于以上分析,想到如下方案:

方案一

既然要加日志,那不挺简单,在 authenticate 方法中,在所有抛异常的地方打印错误日志,在认证成功后打印成功的日志。

如果应用中只有一个自带的认证系统,以上方案没问题,无非多写几行日志。如果异常类型变多了,再多加几行日志。

如果有多个认证系统,也就是代码中有多个地方实现了 authenticate 接口,根据配置选择不同的认证方式。那么此时,每种认证系统都需要加日志,用户自定义的系统也需要加日志,很显然这种方式不合适。

方案二

使用 Spring AOP 实现日志功能,不需要改动原来的任何代码,只需要定义切面,以 AuthenticationProvider.authenticate 作为切点,所有的认证系统都能统一输出日志,代码量也非常少。

具体实现

以下代码实现不一定好,只是作为理解 AOP 的实际例子的参考。

@Aspect // 使用 AspectJ 实现切面
@Component // 将该类声明注入到 Spring 容器
public class AuthenticationLogAspect {
    // 用户登录成功
    // @AfterReturning 表示目标方法执行成功后调用
    // @AfterReturning 括号中的内容表示切点 pointcut,指某个或某些方法执行时,要应用该切面。
    // AspectJ 语法:第一个 * 表示返回值不限,"..“ 表示参数个数、类型不限制
    // args 表示方法的参数
    @AfterReturning("execution(* org.springframework.security.authentication.AuthenticationProvider.authenticate(..))" +
            "&& args(authentication)")
    public void doAfterLoginSuccess(Authentication authentication) {
        log.info("user: " + authentication.getName() + " login success.")
    }

		// 用户登录失败
    // @AfterThrowing 表示目标方法抛异常后调用
    // pointcut 声明切点
    // throwing 声明异常参数
    @AfterThrowing(pointcut = "execution(* org.springframework.security.authentication.AuthenticationProvider.authenticate(..)) " +
            "&& args(authentication)", throwing = "exception")
    public void doAfterLoginError(Authentication authentication, Exception exception) {
      	log.error("user: " + authentication.getName() + " login failed. error stack: ", exception)
    }

    // 用户登出
    @AfterReturning(value = "execution(* org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler.onLogoutSuccess(..)) " +
            "&& args(request, response, authentication)", argNames = "request, response, authentication")
    public void doAfterLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                     Authentication authentication) {
        log.info("user: " + authentication.getName() + " log out success.")
    }
}

再理解 AOP

什么是 AOP

OOP 是一种编程思想:面向对象编程,AOP 也是一种编程思想:面向切面编程,它不是指某种特定的技术,更不仅仅指 Spring AOP。

AOP 可以用于多个模块共同需要调用的逻辑,比如日志管理、事务处理等。通过以上的例子可以看出,通过 AOP 我们可以减少重复代码,简单实现了登录模块的统一日志管理。

AOP 的相关概念

  • Pointcut 切点:定义执行增强的方法,对应以上 pointcut 中的 AspectJ 表达式匹配的方法;
  • Joinpoint 连接点:所有能够定义为切点的方法,也就是能应用增强的方法,都能称为连接点;
  • Aspect 切面:Pointcut 和 Advice 的结合,理解为实现插入日志功能的实现就是定义切面;
  • Advice 增强/通知:要通过切面完成的事情,有五种类型:
    • @Before 前置通知,在目标方法被调用前执行;
    • @AfterThrowing 异常返回通知,在目标方法抛出异常时执行;
    • @AfterReturning 正常返回通知,在目标方法正常返回时执行;
    • @After 返回通知,无论目标方法正常还是异常返回,都会执行;
    • @Around 环绕通知,在方法调用前后都可以定义一些操作;

Spring AOP 和 AspectJ

这两者都是 AOP 的不同实现,运用的技术不同。具体的细节自己理解也不深就不写了。

Spring AOP 的底层实现

  • JDK 动态代理 ,面向接口的代理模式,被代理目标需要有接口,如果代理的类没有接口则会使用CGLib代理。
  • cglib 动态代理,通过继承代理类来实现。

主要以一个例子作简单理解,工作中碰到某些细节再查就好了。