背景:
最近遇到了一个很诡异的问题,就是用SecurityUtils.getSubject().getPrincipal();获取当前的登录用户时A用户会获取到B用户,导致数据插入失败!
而且用Spring Data JPA的 @CreatedBy 方式自动获取当前用户也时常会报异常, 大概意思就是 当前的路径不在shiro的管辖范围之内的错误!
分析:
顺着shiro源码去找,获取当前用户方法SecurityUtils.getSubject();
/*ThreadContext线程上下文环境,主要靠InheritableThreadLocal保存线程变量;这里使用InheritableThreadLocal而不是普通的ThreadLocal,保证在servlet分配的每个线程,获取 到父线程的ThreadLocal变量,因为servlet分配线程也是用了线程池的*/ public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Subject.Builder()).buildSubject(); //获取不到,直接bind,调用ThreadContext put方法 ThreadContext.bind(subject); } return subject; }
首先聊一下ThreadLocal的作用,ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,
每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,
各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。
如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。
接下来继续看代码,进getSubject(),再进get();
public static Subject getSubject() { return (Subject) get(SUBJECT_KEY); } public static Object get(Object key) { if (log.isTraceEnabled()) { String msg = "get() - in thread [" + Thread.currentThread().getName() + "]"; log.trace(msg); } Object value = getValue(key); if ((value != null) && log.isTraceEnabled()) { String msg = "Retrieved value of type [" + value.getClass().getName() + "] for key [" + key + "] " + "bound to thread [" + Thread.currentThread().getName() + "]"; log.trace(msg); } return value; } //resources是private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>(); private static Object getValue(Object key) { return resources.get().get(key); } //getValue和put都是操作resources而已 public static void put(Object key, Object value) { if (key == null) { throw new IllegalArgumentException("key cannot be null"); } if (value == null) { remove(key); return; } resources.get().put(key, value); if (log.isTraceEnabled()) { String msg = "Bound value of type [" + value.getClass().getName() + "] for key [" + key + "] to thread " + "[" + Thread.currentThread().getName() + "]"; log.trace(msg); } }
造成这个问题的原因是什么呢?如何让任务之间使用缓存的线程不受影响呢?实际原因是,我们的线程在执行完毕的时候并没有清除ThreadLocal中的值,导致后面的任务重用现在的localMap。
解决办法:
虽然google到了很多的解决办法,但是在阅读官方的文章和api 文档时发现,shiro 已经提供相应方法来解决这个问题:
如果你有一个Callable
或Runnable
应该执行的情况下Subject
,你将执行Callable
或Runnable
自己(或它交给一个线程池或Executor
或ExecutorService
为例),
你应该使用Subject.associa
teWith
*的方法。这些方法确保在最终执行的线程上保留和访问Subject。
<V> Callable<V> | associateWith(Callable<V> callable) Returns a Callable instance matching the given argument while additionally ensuring that it will retain and execute under this Subject's identity. |
Runnable | associateWith(Runnable runnable) Returns a Runnable instance matching the given argument while additionally ensuring that it will retain and execute under this Subject's identity. |
在associateWith
*方法自动执行必要的线程清理,以确保线程池环境中保持清洁。
//官方给的例子 Subject subject = new Subject.Builder()... Callable work = //build/acquire a Callable instance. //associate the work with the built subject so SecurityUtils.getSubject() calls works properly: work = subject.associateWith(work); ExecutorService executorService = new java.util.concurrent.Executors.newCachedThreadPool(); //execute the work on a different thread as the built Subject: executor.execute(work); Subject subject = new Subject.Builder()... Runnable work = //build/acquire a Runnable instance. //associate the work with the built subject so SecurityUtils.getSubject() calls works properly: work = subject.associateWith(work); Executor executor = new java.util.concurrent.Executors.newCachedThreadPool(); //execute the work on a different thread as the built Subject: executor.execute(work);
实际上就是在你用线程池 运行 Runnable / Callable 之前,先调用下 Subject.associa
teWith
* 方法.
官方文档参考 : https://shiro.apache.org/subject.html#a-different-thread
http://shiro.apache.org/static/1.4.0/apidocs/
线程池的中的解决办法:
虽然每次调用下Subject.associa
teWith
* 方法可以保证在线程池中获取到正确的用户信息,但是 但是没有都手动写 Subject.associa
teWith
* 感觉是很傻的!!!
那么我们就重写下ThreadPoolTaskExecutor 下面的一些execute/submit 等一些方法
package com.screenshot.autotestplatform.config; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; import org.springframework.util.concurrent.ListenableFuture; import java.util.concurrent.Callable; import java.util.concurrent.Future; /** * @author 高亚轩 * @version 1.0 * @date 2019/3/21 0021 下午 2:03 */ @Component public class SubjectAwareTaskExecutor extends ThreadPoolTaskExecutor { @Override public boolean prefersShortLivedTasks() { return false; } @Override public void execute(Runnable aTask) { final Subject currentSubject = ThreadContext.getSubject(); if (currentSubject != null) { super.execute(currentSubject.associateWith(aTask)); } else { super.execute(aTask); } } @Override public void execute(Runnable task, long startTimeout) { Subject currentSubject = ThreadContext.getSubject(); if (currentSubject != null) { super.execute(currentSubject.associateWith(task), startTimeout); } else { super.execute(task, startTimeout); } } @Override public Future<?> submit(Runnable task) { Subject currentSubject = ThreadContext.getSubject(); if (currentSubject != null) { return super.submit(currentSubject.associateWith(task)); } else { return super.submit(task); } } @Override public <T> Future<T> submit(Callable<T> task) { Subject currentSubject = ThreadContext.getSubject(); if (currentSubject != null) { return super.submit(currentSubject.associateWith(task)); } else { return super.submit(task); } } @Override public ListenableFuture<?> submitListenable(Runnable task) { Subject currentSubject = ThreadContext.getSubject(); if (currentSubject != null) { return super.submitListenable(currentSubject.associateWith(task)); } else { return super.submitListenable(task); } } @Override public <T> ListenableFuture<T> submitListenable(Callable<T> task) { Subject currentSubject = ThreadContext.getSubject(); if (currentSubject != null) { return super.submitListenable(currentSubject.associateWith(task)); } else { return super.submitListenable(task); } } }
然后把重写的这个类 注册到spring 中
/** * 线程池 * * */ @Bean public ThreadPoolTaskExecutor taskExecutor() { SubjectAwareTaskExecutor executor = new SubjectAwareTaskExecutor(); //线程池维护线程的最少数量 executor.setCorePoolSize(50); //允许的空闲时间 executor.setKeepAliveSeconds(200); //线程池维护线程的最大数量 executor.setMaxPoolSize(100); //缓存队列 executor.setQueueCapacity(40); //对拒绝task的处理策略 ThreadPoolExecutor.CallerRunsPolicy callerRunsPolicy = new ThreadPoolExecutor.CallerRunsPolicy(); executor.setRejectedExecutionHandler(callerRunsPolicy); executor.setThreadNamePrefix("Custom Thread-"); executor.initialize(); return executor; }
还没有评论,来说两句吧...