0


多线程异步方法Spring Security框架的SecurityContext无法获取认证信息的原因及解决方案

  Spring Security是Spring生态提供的用户应用安全保护的一个安全框架,其提供了一种高度可定制的实现身份认证(Authentication),授权(Authorization)以及对常见的web攻击手段做防护的方法。

  之前我的博客Oauth2与Spring Security框架的认证授权管理讲到过,使用Spring Security结合Oauth2进行身份认证,以及授权集成到项目的步骤。

在集成成功后,每次接口的请求,都会在请求头中携带Authrization的请求头,携带access-token信息,然后在项目中使用SecutityContext对象就可以获取到用户身份信息。

一般在项目中会创建一个工具类,用来获取用户信息,其中就是使用的SecutityContext进行封装的工具类。

 比如我在项目中创建了一个UserUtil工具类:
package com.dcboot.module.visit.system.util;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.dcboot.base.config.security.support.MyUserDetails;
import com.dcboot.module.common.enums.system.UserLevelEnum;
import com.dcboot.module.common.model.system.UserLevelVo;
import com.dcboot.module.system.dept.entity.Dept;
import com.dcboot.module.system.dept.service.DeptService;
import com.dcboot.module.system.deptusermid.entity.DeptUserMid;
import com.dcboot.module.system.deptusermid.service.DeptUserMidService;
import com.dcboot.module.system.user.entity.User;
import com.dcboot.module.system.user.service.UserService;
import com.dcboot.module.visit.system.service.BusUserService;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 2022/3/5 15:07
 * @Description
 */
@Component
@RequiredArgsConstructor
public class UserUtil {
    private final UserService userService;
    private final DeptUserMidService deptUserMidService;
    private final DeptService deptService;

    List<Long> deptIdList=new ArrayList<>();

    /**
     * @description: 获取当前用户账号
     * @author: xiaomifeng1010
     * @date: 2022/3/5
     * @param
     * @return: String
     **/
    public String getUserAccount(){
        MyUserDetails userDetails = (MyUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String userAccount = userDetails.getUsername();
        return userAccount;
    }

    /**
     * @description: 获取userId
     * @author:xiaomifeng1010
     * @date: 2022/4/11
     * @param
     * @return: Long
     **/
    public Long getUserId(){
        String userAccount = getUserAccount();
        return userService.getObj(Wrappers.<User> lambdaQuery()
                .select(User ::getId).eq(User ::getUserAccount,userAccount ),a -> Long.valueOf(String.valueOf(a)));
    }

    public User getUserInfo(){
        String userAccount = getUserAccount();
        return userService.getOne(Wrappers.<User> lambdaQuery()
                .eq(User ::getUserAccount,userAccount ));
    }

    /**
     * @description: 获取当前用户的主部门id
     * @author:xiaomifeng1010
     * @date: 2022/4/11
     * @param
     * @return: Long
     **/
    public Long getMasterDeptId(){
        Long userId = getUserId();
        return deptUserMidService.getObj(Wrappers.<DeptUserMid> lambdaQuery()
        .select(DeptUserMid::getDeptid).eq(DeptUserMid::getUserid,userId).eq(DeptUserMid::getIsmaster,1),
                a ->Long.valueOf(String.valueOf(a)));

    }

    public String getDeptCode(){
        Long masterDeptId = getMasterDeptId();
        Dept dept = deptService.getById(masterDeptId);
        return Objects.nonNull(dept) ? dept.getDeptcode() : StringUtils.EMPTY;
    }

    /**
     * @description: 获取所在主部门以及子级部门
     * @author: xiaomifeng1010
     * @date: 2022/4/11
     * @param deptId 主部门id
     * @return: List<Long>
     **/
    public List<Long> getDeptIdList(Long deptId){
        deptIdList.add(deptId);
        Long childDeptId = deptService.getObj(Wrappers.<Dept>lambdaQuery()
                .select(Dept::getId)
                .eq(Dept::getParentid, deptId), a -> Long.valueOf(a.toString()));
        if (Objects.nonNull(childDeptId)){
            getDeptIdList(childDeptId);

        }

        return deptIdList;

    }
 
}

Spring Security的安全上下文是由SecurityContext接口描述的。

/*
 * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.core.context;

import org.springframework.security.core.Authentication;

import java.io.Serializable;

/**
 * Interface defining the minimum security information associated with the current thread
 * of execution.
 *
 * <p>
 * The security context is stored in a {@link SecurityContextHolder}.
 * </p>
 *
 * @author Ben Alex
 */
public interface SecurityContext extends Serializable {
    // ~ Methods
    // ========================================================================================================

    /**
     * Obtains the currently authenticated principal, or an authentication request token.
     *
     * @return the <code>Authentication</code> or <code>null</code> if no authentication
     * information is available
     */
    Authentication getAuthentication();

    /**
     * Changes the currently authenticated principal, or removes the authentication
     * information.
     *
     * @param authentication the new <code>Authentication</code> token, or
     * <code>null</code> if no further authentication information should be stored
     */
    void setAuthentication(Authentication authentication);
}

从源码中可以看出这个接口的主要职责就是存储身份认证对象和获取身份认证对象的,那么SecurityContext本身是如何被管理的呢?Spring Security框架提供了3种策略来管理SecurityContext.管理该类的对象是SecurityContextHolder.

这3种策略也定义在了SecurityContextHolder类中:

/*
 * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.security.core.context;

import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Constructor;

/**
 * Associates a given {@link SecurityContext} with the current execution thread.
 * <p>
 * This class provides a series of static methods that delegate to an instance of
 * {@link org.springframework.security.core.context.SecurityContextHolderStrategy}. The
 * purpose of the class is to provide a convenient way to specify the strategy that should
 * be used for a given JVM. This is a JVM-wide setting, since everything in this class is
 * <code>static</code> to facilitate ease of use in calling code.
 * <p>
 * To specify which strategy should be used, you must provide a mode setting. A mode
 * setting is one of the three valid <code>MODE_</code> settings defined as
 * <code>static final</code> fields, or a fully qualified classname to a concrete
 * implementation of
 * {@link org.springframework.security.core.context.SecurityContextHolderStrategy} that
 * provides a public no-argument constructor.
 * <p>
 * There are two ways to specify the desired strategy mode <code>String</code>. The first
 * is to specify it via the system property keyed on {@link #SYSTEM_PROPERTY}. The second
 * is to call {@link #setStrategyName(String)} before using the class. If neither approach
 * is used, the class will default to using {@link #MODE_THREADLOCAL}, which is backwards
 * compatible, has fewer JVM incompatibilities and is appropriate on servers (whereas
 * {@link #MODE_GLOBAL} is definitely inappropriate for server use).
 *
 * @author Ben Alex
 *
 */
public class SecurityContextHolder {
    // ~ Static fields/initializers
    // =====================================================================================

    public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
    public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
    public static final String MODE_GLOBAL = "MODE_GLOBAL";
    public static final String SYSTEM_PROPERTY = "spring.security.strategy";
    private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
    private static SecurityContextHolderStrategy strategy;
    private static int initializeCount = 0;

    static {
        initialize();
    }

    // ~ Methods
    // ========================================================================================================

    /**
     * Explicitly clears the context value from the current thread.
     */
    public static void clearContext() {
        strategy.clearContext();
    }

    /**
     * Obtain the current <code>SecurityContext</code>.
     *
     * @return the security context (never <code>null</code>)
     */
    public static SecurityContext getContext() {
        return strategy.getContext();
    }

    /**
     * Primarily for troubleshooting purposes, this method shows how many times the class
     * has re-initialized its <code>SecurityContextHolderStrategy</code>.
     *
     * @return the count (should be one unless you've called
     * {@link #setStrategyName(String)} to switch to an alternate strategy.
     */
    public static int getInitializeCount() {
        return initializeCount;
    }

    private static void initialize() {
        if (!StringUtils.hasText(strategyName)) {
            // Set default
            strategyName = MODE_THREADLOCAL;
        }

        if (strategyName.equals(MODE_THREADLOCAL)) {
            strategy = new ThreadLocalSecurityContextHolderStrategy();
        }
        else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
            strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
        }
        else if (strategyName.equals(MODE_GLOBAL)) {
            strategy = new GlobalSecurityContextHolderStrategy();
        }
        else {
            // Try to load a custom strategy
            try {
                Class<?> clazz = Class.forName(strategyName);
                Constructor<?> customStrategy = clazz.getConstructor();
                strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
            }
            catch (Exception ex) {
                ReflectionUtils.handleReflectionException(ex);
            }
        }

        initializeCount++;
    }

    /**
     * Associates a new <code>SecurityContext</code> with the current thread of execution.
     *
     * @param context the new <code>SecurityContext</code> (may not be <code>null</code>)
     */
    public static void setContext(SecurityContext context) {
        strategy.setContext(context);
    }

    /**
     * Changes the preferred strategy. Do <em>NOT</em> call this method more than once for
     * a given JVM, as it will re-initialize the strategy and adversely affect any
     * existing threads using the old strategy.
     *
     * @param strategyName the fully qualified class name of the strategy that should be
     * used.
     */
    public static void setStrategyName(String strategyName) {
        SecurityContextHolder.strategyName = strategyName;
        initialize();
    }

    /**
     * Allows retrieval of the context strategy. See SEC-1188.
     *
     * @return the configured strategy for storing the security context.
     */
    public static SecurityContextHolderStrategy getContextHolderStrategy() {
        return strategy;
    }

    /**
     * Delegates the creation of a new, empty context to the configured strategy.
     */
    public static SecurityContext createEmptyContext() {
        return strategy.createEmptyContext();
    }

    @Override
    public String toString() {
        return "SecurityContextHolder[strategy='" + strategyName + "'; initializeCount="
                + initializeCount + "]";
    }
}

然后介绍一下这3种策略:

MODE_THREALOCAL: 允许每个线程在安全上下文存储自己的详细信息,在每个请求一个线程的web应用程序中,这是一种常见的方法,因为每个请求都是一个单独的线程。这个策略也是Spring Security默认使用的策略。

MODE_INHERITABLETHREADLOCAL: 类似于MODE_THREALOCAL,但是从名称中可以知道,这是一个可以继承的模式,所以这种模式,可以在使用异步方法时,将安全上下文复制到下一个线程,这样再运行带有@Async注解的异步方法时,调用该方法的线程也能继承到该安全上下文。

MODE_GLOBAL:使引用程序的所欲线程看到相同的安全上下文实例。

由于Spring Security默认使用的是 MODE_THREALOCAL模式,该模式只允许在请求的主线程中获取安全上下文信息,用来获取用户身份信息,所以如果请求的接口中,又调用了异步方法,或者自定义了线程池去执行方法,则会获取不到用户信息,并且出现NullPointerException异常.

默认策略使用ThreadLocal管理上下文,ThreadLocal是JDK提供的实现,该实现作为数据集合来执行,但会确保应用程序的每个线程只能看到存储在集合中的数据,这样,每个请求都可以访问各自的安全上下文。线程之间不可以访问到其他线程的ThreadLocal。每个请求只能看到自己的安全上下文。确保线程获取的用户信息的准确性。

 注意:每个请求绑定一个线程这种架构只适用于传统的Servlet应用程序,其中每个请求都被分配了自己的线程。它不适用于响应式编程程序。

如果我们在请求接口中调用了@Async注解的异步方法。那么就需要自定义配置,修改程序默认的策略。定义配置也很简单,只需要在配置类中注入一个bean即可,覆盖Spring Security默认的安全上下文策略即可:

配置代码如下:

     * @description: 使异步任务可以继承安全上下文(SecurityContext);
     * 注意,该方法只适用于spring 框架本身创建线程时使用(例如,在使用@Async方法时),这种方法才有效;
     * 如果是在代码中手动创建线程,则需要使用{@link org.springframework.security.concurrent.DelegatingSecurityContextRunnable}
     * 或者{@link org.springframework.security.concurrent.DelegatingSecurityContextCallable},
     * 当然使用{@link org.springframework.security.concurrent.DelegatingSecurityContextExecutorService}
     * 来转发安全上下文更好
     * @author: xiaomifeng1010
     * @date: 2022/8/1
     * @param
     * @return: InitializingBean
     **/
    @Bean
    public InitializingBean initializingBean(){
        return () -> SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
    }

但是这种方式仅适用于可以被Spring管理的线程。但是还有一种情况就是我们自己手动创建的线程池,Spring框架是没有进行管理的,或者说是不被Spirng框架知道的,这种线程称为自管理线程,针对这种情况,应该如何处理呢?好在Spring Security提供了一些实用的工具,将有助于将安全上下文传播到新创建的线程。

SecurityContextHolder如果要处理自己定义的线程任务传递安全上下文是没有指定策略的。在这种情况下,如果我们需要安全上下文在线程之间传播。用于此目的的一种解决方案是使用DelegatingSecurityContextRunnable装饰想要在单独线程上执行的任务。DelegatingSecurityContextRunnable拓展了Runnable接口。当不需要预期的值时,则可以在任务执行时使用它。如果需要返回值,则可以使用DelegatingSecurityContextCallable<T>。这两个类用于处理异步执行的任务。

例如要将任务提交给ExecutorService执行,

package com.dcboot;

import com.dcboot.base.config.security.support.MyUserDetails;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.springframework.security.concurrent.DelegatingSecurityContextCallable;
import org.springframework.security.concurrent.DelegatingSecurityContextExecutorService;
import org.springframework.security.core.context.SecurityContextHolder;

import java.util.concurrent.*;

/**
 * @author xiaomifeng1010
 * @version 1.0
 * @date: 2022/4/21 21:54
 * @Description
 */
public class Test {

    public static void main(String[] args) {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("test-async-%d").build();
        ExecutorService executor = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024),
                namedThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
        Callable<String> task=() ->{
            MyUserDetails userDetails = (MyUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            String userAccount = userDetails.getUsername();
            return userAccount;
        };

//        注意此时不能直接用executor.submit(task).get()方法去获取用户账号,这样也是会空指针异常的,而是需要用DelegatingSecuityContextCallable装饰一下原来的Callbale任务
        DelegatingSecurityContextCallable<String> stringDelegatingSecurityContextCallable = new DelegatingSecurityContextCallable<>(task);
        try {
            String userAccount = executor.submit(stringDelegatingSecurityContextCallable).get();
            System.out.println(userAccount);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            executor.shutdown();
        }

    }

}

这里为了方便,所以我写在了main方法里边,在项目中,这个Callable任务对应的就是你的Service层的方法,在接口中用线程池调用的时候,就可以这样使用了。

但是这种处理方式是从要被执行的任务方法本身进行修饰处理的,还有一种就是可以直接从线程池入手,使用DelegatingSecurityContextExecutorService来转发安全上下文。

代码示例如下:

    public String test(){
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("test-async-%d").build();
        ExecutorService executor = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024),
                namedThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
        Callable<String> task=() ->{
            MyUserDetails userDetails = (MyUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            String userAccount = userDetails.getUsername();
            return userAccount;
        };

        try {
//            也可以通过修饰线程池的方式,来传播安全上下文
            executor=new DelegatingSecurityContextExecutorService(executor);
            String userAccount = executor.submit(task).get();
            System.out.println(userAccount);
            return userAccount;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }finally {
            executor.shutdown();
        }

    return  null;
    }

而我们在日常项目中,使用CompletableFuture做异步处理也是很多的。所以,在使用CompletableFuture时,就需要指定一下自定义的线程池,而不能使用默认的线程池;

示例代码如下:

    /**
     * @param taskObjectId
     * @description: 获取上市走访录入信息详情
     * @author: xiaomifeng1010
     * @date: 2022/4/12
     * @return: ApiResult
     **/
    @ApiOperation(value = "获取上市走访录入信息详情")
    @GetMapping("/getIPOVisitObjectDetail")
    @ResponseBody
    public ApiResult getIPOVisitObjectDetail(@NotNull(message = "任务对象id为空")  Long taskObjectId) {
        try {
            ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("visit-query-async-%d").build();
            ExecutorService executor = new ThreadPoolExecutor(5, 10, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1024),
                    namedThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
            executor=new DelegatingSecurityContextExecutorService(executor);
            IpoVisitInfoDetailRespVO ipoVisitInfoDetailRespVO1 = CompletableFuture.supplyAsync(() -> ipoVisitInfoService.getIPOVisitObjectDetail(taskObjectId),executor)
                    .thenApplyAsync(ipoVisitInfoDetailRespVO -> {
                        List<IpoVisitInfoDetailRespVO.approveSendBackInfo> approveSendBackInfoList = ipoVisitInfoService.getApproveSendBackInfoList(taskObjectId);
                        if (CollectionUtils.isNotEmpty(approveSendBackInfoList)) {
                            ipoVisitInfoDetailRespVO.setApproveSendBackInfoList(approveSendBackInfoList);
                        } else {
                            ipoVisitInfoDetailRespVO.setApproveSendBackInfoList(Collections.emptyList());
                        }
                        return ipoVisitInfoDetailRespVO;

                    },executor).get(5, TimeUnit.SECONDS);
            return ApiResult.success(ipoVisitInfoDetailRespVO1);
        } catch (InterruptedException | ExecutionException | TimeoutException ex ) {
            log.error("获取上市培育详情出错",ex);
            return  ApiResult.error("获取详情出错");
        }

    }

所以如果你要在异步执行的线程方法中获取用户身份信息,需要传递安全上下文,就需要使用下边截图中CompletableFuture源码中的第二个重载方法,需要传入线程池,并且是被DelegatingSecurityContextExecutorService修饰的线程池。如果异步方法中不需要获取用户身份信息,则可以使用第一个重载方法,直接使用默认线程池就可以。

此外对于线程池的修饰类,还有一些其他的工具类

去掉DelegatingSecurityContext前缀,就是这些类实现的对应JDK中的接口。比如 DelegatingSecurityContextExecutor就是实现了Executor接口,依次类比。


本文转载自: https://blog.csdn.net/u011174699/article/details/127933955
版权归原作者 xiaomifeng1010 所有, 如有侵权,请联系我们删除。

“多线程异步方法Spring Security框架的SecurityContext无法获取认证信息的原因及解决方案”的评论:

还没有评论