spring 异步请求的安全性 oauth2 用法

spring security oauth2 usage with async requests

据我所知,SpringSecurityFilter 链针对@Async 请求的每个请求运行两次,因为它在入站请求线程上运行,被传递到在不同线程上运行的异步代码,然后在它尝试时写入响应线程 SpringSecurityFilter 链再次运行。

这会导致 access_token 到期时出现问题,因为我正在使用 RemoteTokenServices,发生的情况是原始请求已通过身份验证,服务 activity 需要大约一秒钟,然后 RemoteTokenServices 被调用再次,此时 access_token 已过期,因此请求 returns 401.

此处推荐的解决方案是什么?我无法在响应线程上第二次阻止 SecurityFilterChain 运行。我做错了什么,还是这是预期的行为?我看到 SecurityContext 正确传递给了 @Async 线程,但它在响应线程中为 null。

有没有办法确保 SecurityFilterChain 每个请求只运行一次?或者是接受每个请求的多个过滤器调用并以某种方式缓存处理它的解决方案?

我正在使用 spring-boot 1.3.3.RELEASE 和 spring-security-oauth2 2.0.9.RELEASE。

日志:

INFO [..nio-exec-1] [Caching...] loadAuthentication: 0bc97f92-9ebb-411f-9e8e-e7dc137aeffe
DEBUG [..nio-exec-1] [Caching...] Entering CachingRemoteTokenService auth: null
DEBUG [..nio-exec-1] [Audit...] AuditEvent [timestamp=Wed Mar 30 12:27:45 PDT 2016, principal=testClient, type=AUTHENTICATION_SUCCESS, data={details=remoteAddress=127.0.0.1, tokenType=BearertokenValue=<TOKEN>}]
INFO [..nio-exec-1] [Controller] Callable testing request received
DEBUG [MvcAsync1] [TaskService] taskBegin
DEBUG [MvcAsync1] [TaskService] Entering TaskService auth: org.springframework.security.oauth2.provider.OAuth2Authentication@47c78d1a: Principal: testClient; Credentials: [PROTECTED]; Authenticated: true; Details: remoteAddress=127.0.0.1, tokenType=BearertokenValue=<TOKEN>; Granted Authorities: ROLE_CLIENT
DEBUG [MvcAsync1] [TaskService] end of task
INFO [..nio-exec-2] [Caching...] loadAuthentication: 0bc97f92-9ebb-411f-9e8e-e7dc137aeffe
DEBUG [..nio-exec-2] [Caching...] Entering CachingRemoteTokenService auth: null
DEBUG [..nio-exec-2] [RemoteTokenServices] check_token returned error: invalid_token
DEBUG [..nio-exec-2] [Audit...] AuditEvent [timestamp=Wed Mar 30 12:27:47 PDT 2016, principal=access-token, type=AUTHENTICATION_FAILURE, data={type=org.springframework.security.authentication.BadCredentialsException, message=0bc97f92-9ebb-411f-9e8e-e7dc137aeffe}]

相关代码:

控制器:

@RequestMapping(value = "/callable",
method = RequestMethod.GET, 
produces = { MediaType.APPLICATION_JSON_VALUE })
public @ApiResponseObject Callable<ApiResponse> runCallable(HttpServletRequest httpServletRequest)
        throws InterruptedException {
    log.info(String.format("Callable testing request received"));
    Callable<ApiResponse> rv = taskService::execute;
    return rv;
}

异步服务:

    @Override
public ApiResponse execute() {
    log.debug("taskBegin");
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    log.debug("Entering TaskService auth: " + auth);
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    ApiResponse rv = new ApiResponse();
    rv.setStatus(HttpStatus.OK.value());
    log.debug("end of task");
    return rv;
}

RemoteTokenServices 实现(注意缓存被注释掉):

    public class CachingRemoteTokenService extends RemoteTokenServices {

    private static Log log = LogFactory.getLog(CachingRemoteTokenService.class);

    @Override
    //@Cacheable(cacheNames="tokens", key="#root.methodName + #accessToken")
    public OAuth2Authentication loadAuthentication(String accessToken)
            throws org.springframework.security.core.AuthenticationException,
                   InvalidTokenException {
        log.info("loadAuthentication: " + accessToken);
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        log.debug("Entering CachingRemoteTokenService auth: " + auth);
        return super.loadAuthentication(accessToken);
    }

    @Override
    //@Cacheable(cacheNames="tokens", key="#root.methodName + #accessToken")
    public OAuth2AccessToken readAccessToken(String accessToken) {
        log.info("readAccessToken: " + accessToken);
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        log.debug("Entering CachingRemoteTokenService auth: " + auth);
        return super.readAccessToken(accessToken);
    }
}

最后是我的安全配置:

 @Configuration
public class Oauth2ResourceConfig {

    private static Log log = LogFactory.getLog(Oauth2ResourceConfig.class);

    @Value("${client.secret}") 
    private String clientSecret;

    @Value("${check.token.endpoint}") 
    private String checkTokenEndpoint;

    @Bean
    @Lazy
    public ResourceServerTokenServices tokenService() {
        CachingRemoteTokenService tokenServices = new CachingRemoteTokenService();
        tokenServices.setClientId("test-service");
        tokenServices.setClientSecret(clientSecret);
        tokenServices.setCheckTokenEndpointUrl(checkTokenEndpoint);

        return tokenServices;
    }

    @Configuration
    @EnableResourceServer
    protected static class ResourceServerConfig extends ResourceServerConfigurerAdapter {

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                .antMatchers("/health-check").permitAll()
                .antMatchers("/**").access("#oauth2.isClient() and #oauth2.hasScope('trust')");

        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId("test-service");
        }
    }
}

在这里得到了答案:https://github.com/spring-projects/spring-security-oauth/issues/736

显然解决方法是配置 security.filter-dispatcher-types=REQUEST, ERROR