org.apache.shiro.session.UnknownSessionException: There is no session with id [xxxx]的解決方案
背景描述
SpringBoot項目,使用Shiro進行權限管理。測試過程中發現執行文件導入時最開始一切正常,但是導入幾次之后再次執行導入就會報錯,此時執行其他功能一切正常
排查過程
- [x] 1. 網上搜索,大部分都是說法如下:
Shiro的Cookie名稱默認是JSESSIONID,與servlet容器沖突。修改Shiro的SessionID即可
- [x] 2. 假如是上述原因,應該從登錄開始就出問題,而不應該是極個別操作出現問題。而我這里只有多次導入之后出問題,並且此時其他功能還是正常的。因此排除這個原因,自己進行排查。
- [x] 3. 因為只有導入出現這個問題,其他功能都一切正常;因此懷疑是導入的代碼存在問題。
- [x] 4. 排查導入代碼發現,只有在執行保存語句的時候獲取當前用戶使用了Shiro相關代碼,因此懷疑此處出現問題。
this.operator = (String) SecurityUtils.getSubject().getPrincipal();
- [x] 5. 在此處斷點發現,手動新增數據和批量導入數據都執行該語句,但兩次獲取的Subject不一致。因此閱讀源碼進行排查。
// 從SecurityUtils中獲取Subject源碼如下
// package: org.apache.shiro.SecurityUtils
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject(); // ①
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
// 繼續跟進上面①中的方法
// package: org.apache.shiro.util.ThreadContext
public static Subject getSubject() {
return (Subject) get(SUBJECT_KEY); // ②
}
// 繼續跟進上面②中的方法
// package: org.apache.shiro.util.ThreadContext
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;
}
// 繼續跟進上面③中的方法
// package: org.apache.shiro.util.ThreadContext
private static Object getValue(Object key) {
Map<Object, Object> perThreadResources = resources.get(); // ④
return perThreadResources != null ? perThreadResources.get(key) : null;
}
// 上面④中的resources在ThreadContext中定義如下
// package: org.apache.shiro.util.ThreadContext
private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();
- [x] 6. 根據上面對源碼的跟蹤,發現Subject是與ThreadLocal也就是線程綁定的。獲取Subject時先獲取當前線程綁定的Subject,若沒有則重新創建並綁定到當前線程。而我導入的時候為了提高導入效率,使用了多線程。到此就發現問題的原因了
問題原因
- 假設項目中線程池設置核心線程數量為10,而核心線程默認是不會被超時回收的
ps: 可通過threadPoolExecutor.allowCoreThreadTimeOut(true);設置核心線程超時回收
- 當用戶A登錄后,執行導入操作,從線程池中拿出5個線程,此時這5個線程將綁定用戶A的Subject
- 當用戶A多次執行導入操作后,線程池全部核心線程與用戶A的Subject綁定。用戶A退出登錄后,線程池並不會將核心線程進行銷毀。
- 后續用戶B登錄,再次執行導入操作,此時線程池分配線程進行操作,但此時所有的線程都已與用戶A綁定,因此獲取到的Subject都是用戶A的Subject,從Subject中獲取session時此session已被銷毀,因此報錯
// 根據sessionId獲取session,獲取為空則報錯
// package: org.apache.shiro.session.mgt.eis.AbstractSessionDAO
public Session readSession(Serializable sessionId) throws UnknownSessionException {
Session s = doReadSession(sessionId);
if (s == null) {
throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
}
return s;
}
解決方案
多線程時不要使用Shiro相關代碼。將用戶名作為參數傳入,不再單獨獲取。
PS: 該解決方案不適用於所有情況,請根據實際情況按照上述排查步驟進行排查。