SecurityUtils.getSubject().getPrincipal()為null的問題
SecurityUtils.getSubject().getPrincipal()為null
我在項目中獲取getUserId(),和getUserName()獲取不到值。
他們都是通過SecurityUtils.getSubject().getPrincipal()去獲取的。
反復測試發現原因是 :在shiroConfig裡面:
該方法,註意(是該接口名)被寫為anon,不通過驗證,即:
filterMap.put("/druid/**", “anon”);
這種寫法是為瞭postman測試時不被攔截。
解決辦法
從頁面訪問後端不需要anon瞭。
anon
是不做攔截,authc是走自定義攔截oauth2
是自帶攔截
修改為:
filterMap.put("/druid/**", “authc”);
shiro SecurityUtils.getSubject()深度分析
1.總的來說,SecurityUtils.getSubject()是每個請求創建一個Subject, 並保存到ThreadContext的resources(ThreadLocal<Map<Object, Object>>)變量中,也就是一個http請求一個subject,並綁定到當前過程。
問題來瞭:.subject.login()登陸認證成功後,下一次請求如何知道是那個用戶的請求呢?
友情提示:本文唯一可以讀一下的就是分析這個疑問,如果你已經明白就不用往下看瞭。
首先給出內部原理:1個請求1個Subject原理:由於ShiroFilterFactoryBean本質是個AbstractShiroFilter過濾器,所以每次請求都會執行doFilterInternal裡面的createSubject方法。
猜想:因為subject是綁定到當前線程,這肯定需要一個中介存儲狀態
public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Builder()).buildSubject(); ThreadContext.bind(subject); } return subject; }
public abstract class ThreadContext { private static final Logger log = LoggerFactory.getLogger(ThreadContext.class); public static final String SECURITY_MANAGER_KEY = ThreadContext.class.getName() + "_SECURITY_MANAGER_KEY"; public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY"; private static final ThreadLocal<Map<Object, Object>> resources = new ThreadContext.InheritableThreadLocalMap(); protected ThreadContext() { } public static Map<Object, Object> getResources() { return (Map)(resources.get() == null ? Collections.emptyMap() : new HashMap((Map)resources.get())); } public static void setResources(Map<Object, Object> newResources) { if (!CollectionUtils.isEmpty(newResources)) { ensureResourcesInitialized(); ((Map)resources.get()).clear(); ((Map)resources.get()).putAll(newResources); } } private static Object getValue(Object key) { Map<Object, Object> perThreadResources = (Map)resources.get(); return perThreadResources != null ? perThreadResources.get(key) : null; } private static void ensureResourcesInitialized() { if (resources.get() == null) { resources.set(new HashMap()); } } 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; } public static void put(Object key, Object value) { if (key == null) { throw new IllegalArgumentException("key cannot be null"); } else if (value == null) { remove(key); } else { ensureResourcesInitialized(); ((Map)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); } } } public static Object remove(Object key) { Map<Object, Object> perThreadResources = (Map)resources.get(); Object value = perThreadResources != null ? perThreadResources.remove(key) : null; if (value != null && log.isTraceEnabled()) { String msg = "Removed value of type [" + value.getClass().getName() + "] for key [" + key + "]from thread [" + Thread.currentThread().getName() + "]"; log.trace(msg); } return value; } public static void remove() { resources.remove(); } public static SecurityManager getSecurityManager() { return (SecurityManager)get(SECURITY_MANAGER_KEY); } public static void bind(SecurityManager securityManager) { if (securityManager != null) { put(SECURITY_MANAGER_KEY, securityManager); } } public static SecurityManager unbindSecurityManager() { return (SecurityManager)remove(SECURITY_MANAGER_KEY); } public static Subject getSubject() { return (Subject)get(SUBJECT_KEY); } public static void bind(Subject subject) { if (subject != null) { put(SUBJECT_KEY, subject); } } public static Subject unbindSubject() { return (Subject)remove(SUBJECT_KEY); } private static final class InheritableThreadLocalMap<T extends Map<Object, Object>> extends InheritableThreadLocal<Map<Object, Object>> { private InheritableThreadLocalMap() { } protected Map<Object, Object> childValue(Map<Object, Object> parentValue) { return parentValue != null ? (Map)((HashMap)parentValue).clone() : null; } } }
subject登陸成功後,下一次請求如何知道是那個用戶的請求呢?
經過源碼分析,核心實現如下DefaultSecurityManager類中:
public Subject createSubject(SubjectContext subjectContext) { SubjectContext context = this.copy(subjectContext); context = this.ensureSecurityManager(context); context = this.resolveSession(context); context = this.resolvePrincipals(context); Subject subject = this.doCreateSubject(context); this.save(subject); return subject; }
每次請求都會重新設置Session和Principals,看到這裡大概就能猜到:如果是web工程,直接從web容器獲取httpSession,然後再從httpSession獲取Principals,本質就是從cookie獲取用戶信息,然後每次都設置Principal,這樣就知道是哪個用戶的請求,並隻得到這個用戶有沒有人認證成功,–本質:依賴於瀏覽器的cookie來維護session的
擴展,如果不是web容器的app,如何實現實現無狀態的會話
1.一般的作法會在header中帶有一個token,或者是在參數中,後臺根據這個token來進行校驗這個用戶的身份,但是這個時候,servlet中的session就無法保存,我們在這個時候,就要實現自己的會話創建,普通的作法就是重寫session與request的接口,然後在過濾器在把它替換成自己的request,所以得到的session也是自己的session,然後根據token來創建和維護會話
2.shiro實現:
重寫shiro的sessionManage
import org.apache.shiro.session.mgt.SessionKey; import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.util.WebUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.Serializable; import java.util.UUID; /** * @author zxj<br> * 時間 2017/11/8 15:55 * 說明 ... */ public class StatelessSessionManager extends DefaultWebSessionManager { /** * 這個是服務端要返回給客戶端, */ public final static String TOKEN_NAME = "TOKEN"; /** * 這個是客戶端請求給服務端帶的header */ public final static String HEADER_TOKEN_NAME = "token"; public final static Logger LOG = LoggerFactory.getLogger(StatelessSessionManager.class); @Override public Serializable getSessionId(SessionKey key) { Serializable sessionId = key.getSessionId(); if(sessionId == null){ HttpServletRequest request = WebUtils.getHttpRequest(key); HttpServletResponse response = WebUtils.getHttpResponse(key); sessionId = this.getSessionId(request,response); } HttpServletRequest request = WebUtils.getHttpRequest(key); request.setAttribute(TOKEN_NAME,sessionId.toString()); return sessionId; } @Override protected Serializable getSessionId(ServletRequest servletRequest, ServletResponse servletResponse) { HttpServletRequest request = (HttpServletRequest) servletRequest; String token = request.getHeader(HEADER_TOKEN_NAME); if(token == null){ token = UUID.randomUUID().toString(); } //這段代碼還沒有去查看其作用,但是這是其父類中所擁有的代碼,重寫完後我復制瞭過來...開始 request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled()); //這段代碼還沒有去查看其作用,但是這是其父類中所擁有的代碼,重寫完後我復制瞭過來...結束 return token; } } @RequestMapping("/") public void login(@RequestParam("code")String code, HttpServletRequest request){ Map<String,Object> data = new HashMap<>(); if(SecurityUtils.getSubject().isAuthenticated()){ //這裡代碼著已經登陸成功,所以自然不用再次認證,直接從rquest中取出就行瞭, data.put(StatelessSessionManager.HEADER_TOKEN_NAME,getServerToken()); data.put(BIND,ShiroKit.getUser().getTel() != null); response(data); } LOG.info("授權碼為:" + code); AuthorizationService authorizationService = authorizationFactory.getAuthorizationService(Constant.clientType); UserDetail authorization = authorizationService.authorization(code); Oauth2UserDetail userDetail = (Oauth2UserDetail) authorization; loginService.login(userDetail); User user = userService.saveUser(userDetail,Constant.clientType.toString()); ShiroKit.getSession().setAttribute(ShiroKit.USER_DETAIL_KEY,userDetail); ShiroKit.getSession().setAttribute(ShiroKit.USER_KEY,user); data.put(BIND,user.getTel() != null); //這裡的代碼,必須放到login之執行,因為login後,才會創建session,才會得到最新的token咯 data.put(StatelessSessionManager.HEADER_TOKEN_NAME,getServerToken()); response(data); } }
import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; import java.util.Map; /** * @author zxj<br> * 時間 2017/11/8 15:40 * 說明 ... */ @Configuration public class ShiroConfiguration { @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){ return new LifecycleBeanPostProcessor(); } /** * 此處註入一個realm * @param realm * @return */ @Bean public SecurityManager securityManager(Realm realm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setSessionManager(new StatelessSessionManager()); securityManager.setRealm(realm); return securityManager; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(securityManager); Map<String,String> map = new LinkedHashMap<>(); map.put("/public/**","anon"); map.put("/login/**","anon"); map.put("/**","user"); bean.setFilterChainDefinitionMap(map); return bean; } }
以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。
推薦閱讀:
- Java shiro安全框架使用介紹
- SpringBoot 整合 Shiro 密碼登錄的實現代碼
- springboot整合shiro多驗證登錄功能的實現(賬號密碼登錄和使用手機驗證碼登錄)
- shiro攔截認證的全過程記錄
- shiro與spring security用自定義異常處理401錯誤