分类
Spring Security

Spring Security在@Async异步方法中获取登录用户

Spring Security Context Propagation with @Async

Spring Security在默认情况下无法在使用@Async注解的方法中获取当前登录用户的。若想在@Async方法中获取当前登录用户,则需要调用SecurityContextHolder.setStrategyName方法并设置相关的策略:

@SpringBootApplication
@EnableAsync
public class SpringSecurityAsyncPrincipalPropagationApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringSecurityAsyncPrincipalPropagationApplication.class, args);
        SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
    }
}

SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);决定了Spring Security可以在@Async注解的方法中可以成功的获取到当前登录用户。

以下将对该问题展开描述。

1. 简介

本文中我们将讨论@Async如何在Spring Security上下文中传播用户认证信息。

默认情况下Spring Security相关的认证信息是绑定到某个线程上的,也就是说在此线程以外的其它线程上我们无法获取当前登录用户的信息。比如在我们使用@Async来启用一个新的线程的情况下。

下面我们讨论如何在使用@Async启用的新的线程中获取到Spring Security的认证信息

2. Maven依赖

在pom.xml添加以下依赖:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>4.2.1.RELEASE</version>
</dependency>

更多的版本信息请参考:https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org.springframework.security%22

3. 准备工作

接下来,我们做一些准备工作。首先新建如下方法:

@RequestMapping(method = RequestMethod.GET, value = "/async")
@ResponseBody
public Object standardProcessing() throws Exception {
    log.info("在调用使用@Async注解的方法前,在主前线程中获取用户认证信息: "
      + SecurityContextHolder.getContext().getAuthentication().getPrincipal());
    
    // 调用异步方法
    asyncService.asyncCall();
    
    log.info("在调用使用@Async注解的方法后,在主前线程中获取用户认证信息: "
      + SecurityContextHolder.getContext().getAuthentication().getPrincipal());
    
    return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}

并在被调用异步方法中尝试获取当前认证信息:

/**
 * 异步方法
 * 将启用新的线程来运行该方法
 */
@Async
public void asyncCall() {
    try {
        log.info("在@Async注解的异步方法中获取当前登录用户信息: "
                + SecurityContextHolder.getContext().getAuthentication().getPrincipal());
    } catch (RuntimeException e) {
        log.info("未能成功的获取到当前登录用户信息");
    }
}

4. 验证

接下来让我们运行一些测试代码并查看控制台的打印内容:

2020-08-05 14:29:03.289  INFO 68033 --- [nio-8080-exec-1] ication$$EnhancerBySpringCGLIB$$149c1f0f : 
在调用使用@Async注解的方法前,在主前线程中获取用户认证信息: org.springframework.security.core.userdetails.
User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_
2020-08-05 14:29:03.292  INFO 68033 --- [nio-8080-exec-1] ication$$EnhancerBySpringCGLIB$$149c1f0f : 
在调用使用@Async注解的方法后,在主前线程中获取用户认证信息: org.springframework.security.core.userdetails.
User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_
2020-08-05 14:29:03.297  INFO 68033 --- [         task-1] c.c.s.AsyncService                       : 
未能成功的获取到当前登录用户信息

由日志信息可以轻易的得出:在使用@Async注解的异步方法中并不能够获取到当前登录用户的认证信息。

5. SecurityContextHolder

若想在新的线程中获取当前登录用户的认证信息,则需要启用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL策略:

SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

再次测试日志打印如下:

2020-08-05 14:30:21.007  INFO 69399 --- [nio-8080-exec-1] ication$$EnhancerBySpringCGLIB$$50a90aa6 : 
在调用使用@Async注解的方法前,在主前线程中获取用户认证信息: org.springframework.security.core.userdetails.
User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_
2020-08-05 14:30:21.009  INFO 69399 --- [nio-8080-exec-1] ication$$EnhancerBySpringCGLIB$$50a90aa6 : 
在调用使用@Async注解的方法后,在主前线程中获取用户认证信息: org.springframework.security.core.userdetails.
User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_
2020-08-05 14:30:21.013  INFO 69399 --- [         task-1] c.c.s.AsyncService                       : 
在@Async注解的异步方法中获取当前登录用户信息: org.springframework.security.core.userdetails.
User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_

此时便可以在asyncCall方法中获取到用户的登录用户信息了。

6. 应用场景

在生产环境中,我们可能需要SecurityContext像这样传播:

  • 比如需要同时发起多个外部的请求,而这些请求在短时间内无法完成,此时便需要使用异步方法。
  • 再比如接收用户的请求时,我们需要做一些相对比较耗时的计算,而此时并不想让用户在此过程中等待。
  • 或者用户在找回密码环节中,我们向其注册的邮箱中发送密码找回的邮件。
  • 再或者用户的登录、注册过程中发送短信校验码。

总之如果我们需要在异步方法中获取Spring Security中的认证信息,则需要利用其传播机制,将认证信息传入到异步的执行方法中。

7. 总结

本文中,我们介绍了Spring Secruity在异步上下文的传播机制。内容虽不难,主要解决了异步方法中无法获取认证用户的问题。