直接進入主題,項目是使用springboot,框架用的shiro作權限,mybatis作orm框架,項目須要作先後分離,這樣就會致使一個問題,shiro是根據sessionID來識別是否是同一個request,但若是先後分離的話,就會出現跨域的問題,session極可能就會發生變化,這樣就須要用一個標記來代表是同一個請求。初步的方案就是用token來代替session,但本質上說,如今的這種方式,仍是用的session的那一套,不過是對中間進行了處理,下面上代碼:前端
咱們要先解決的是跨域的問題:java
springboot解決跨域很好解決,以下便可。nginx
package com.common.config.cors; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; /** * @author :LX * 建立時間: 2019/5/30. 14:03 * 地點:廣州 * 目的: 跨域訪問控制 * 作先後分離的話,這個也是必配的 * 備註說明: */ @Configuration public class CorsConfig { private CorsConfiguration buildConfig() { CorsConfiguration corsConfiguration = new CorsConfiguration(); // 容許任何域名使用 corsConfiguration.addAllowedOrigin("*"); // 容許任何頭 corsConfiguration.addAllowedHeader("*"); // 容許任何方法(post、get等) corsConfiguration.addAllowedMethod("*"); return corsConfiguration; } @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // 對接口配置跨域設置 source.registerCorsConfiguration("/**", buildConfig()); return new CorsFilter(source); } }
而後自定義 realm ,簡單點說,就是你實現查詢用戶角色和權限的類。這一步就省略了,不外乎查詢數據庫,查詢當前用戶的角色和權限。web
接着自定義token,簡單的說,這裏其實就是讓前端請求的時候在請求頭中帶一個特定的標識,而後根據這個標識找到vlues,匹配上咱們的sessionId。redis
package com.common.config.shiro; import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.util.WebUtils; import org.springframework.util.StringUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.Serializable; /** * @author :LX * 建立時間: 2019/5/30. 18:08 * 地點:廣州 * 目的: shiro 的 session 管理 * 自定義session規則,實現先後分離,在跨域等狀況下使用token 方式進行登陸驗證才須要,不然沒必須使用本類。 * shiro默認使用 ServletContainerSessionManager 來作 session 管理,它是依賴於瀏覽器的 cookie 來維護 session 的,調用 storeSessionId 方法保存sesionId 到 cookie中 * 爲了支持無狀態會話,咱們就須要繼承 DefaultWebSessionManager * 自定義生成sessionId 則要實現 SessionIdGenerator * 備註說明: */ public class ShiroSession extends DefaultWebSessionManager { /** * 定義的請求頭中使用的標記key,用來傳遞 token */ private static final String AUTH_TOKEN = "authToken"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; public ShiroSession() { super(); //設置 shiro session 失效時間,默認爲30分鐘,這裏如今設置爲15分鐘 //setGlobalSessionTimeout(MILLIS_PER_MINUTE * 15); } /** * 獲取sessionId,本來是根據sessionKey來獲取一個sessionId * 重寫的部分多了一個把獲取到的token設置到request的部分。這是由於app調用登錄接口的時候,是沒有token的,登錄成功後,產生了token,咱們把它放到request中,返回結 * 果給客戶端的時候,把它從request中取出來,而且傳遞給客戶端,客戶端每次帶着這個token過來,就至關因而瀏覽器的cookie的做用,也就能維護會話了 * @param request * @param response * @return */ @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { //獲取請求頭中的 AUTH_TOKEN 的值,若是請求頭中有 AUTH_TOKEN 則其值爲sessionId。shiro就是經過sessionId 來控制的 String sessionId = WebUtils.toHttp(request).getHeader(AUTH_TOKEN); if (StringUtils.isEmpty(sessionId)){ //若是沒有攜帶id參數則按照父類的方式在cookie進行獲取sessionId return super.getSessionId(request, response); } else { //請求頭中若是有 authToken, 則其值爲sessionId request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); //sessionId request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return sessionId; } } }
你們看上面的代碼,其實就是咱們的請求頭中獲取 authToken 的值,而後塞入到sessionId中,代替了session.spring
若是你們還須要自定義這個token,或者說自定義生成的seesionId,就須要看下面的這個方法。數據庫
根據個人研究,最終找到 JavaUuidSessionIdGenerator 這個類,而後能夠找到apache
public Serializable generateId(Session session) { return UUID.randomUUID().toString(); }
上面的代碼,其實就是生成了一串UUID,咱們能夠實現SessionIdGenerator接口來完成自定義的sessionID生成json
public class UuidSessionIdGenerator implements SessionIdGenerator{ @Override public Serializable generateId(Session session) { Serializable uuid = new JavaUuidSessionIdGenerator().generateId(session); GGLogger.info("生成的sessionid是:"+uuid); return uuid; } }
###自定義生成sessionid sessionIdGenerator=ggauth.shiro.user.common.UuidSessionIdGenerator securityManager.sessionManager.sessionDAO.sessionIdGenerator=$sessionIdGenerator
上面自定義生成的代碼是 參考的 https://blog.csdn.net/yaomingyang/article/details/78142763 的代碼,未通過驗證,但應該是沒問題的。我這裏是沒有去修改生成UUID的邏輯。後端
其實配置了這2個東西以後,就能夠弄shiro最終的配置了。
package com.common.config.shiro; import com.yunji.kwxt.common.Constant; import com.yunji.kwxt.common.filter.CORSAuthenticationFilter; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; /** * @author :LX * 建立時間: 2019/5/27. 11:39 * 地點:廣州 * 目的: shiro配置 * 備註說明: */ @Configuration public class ShiroConfig { private static Logger log = LoggerFactory.getLogger(ShiroConfig.class); /** * 對shiro的攔截器進行注入 * <p> * securityManager: * 全部Subject 實例都必須綁定到一個SecurityManager上,SecurityManager 是 Shiro的核心,初始化時協調各個模塊運行。然而,一旦 SecurityManager協調完畢, * SecurityManager 會被單獨留下,且咱們只須要去操做Subject便可,無需操做SecurityManager 。 可是咱們得知道,當咱們正與一個 Subject 進行交互時,實質上是 * SecurityManager在處理 Subject 安全操做 * * @param securityManager * @return */ @Bean public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); //設置遇到未登陸、未受權等狀況時候,請求這些地址,返回相應的錯誤 shiroFilter.setLoginUrl("/user/shiroError?errorId=" + Constant.NEED_LOGIN); shiroFilter.setUnauthorizedUrl("/user/shiroError?errorId=" + Constant.NO_UNAUTHORIZED); //攔截器,配置訪問權限 必須是LinkedHashMap,由於它必須保證有序。濾鏈定義,從上向下順序執行,通常將 /**放在最爲下邊 Map<String, String> filterMap = new LinkedHashMap<String, String>(); // 配置不會被攔截的連接 順序判斷 filterMap.put("/user/login", "anon"); filterMap.put("/user/shiroError", "anon"); filterMap.put("/user/reg", "anon"); //剩餘的請求shiro都攔截 filterMap.put("/**/*", "authc"); shiroFilter.setFilterChainDefinitionMap(filterMap); //自定義攔截器 Map<String, Filter> customFilterMap = new LinkedHashMap<>(); customFilterMap.put("corsAuthenticationFilter", new CORSAuthenticationFilter()); shiroFilter.setFilters(customFilterMap); return shiroFilter; } /** * securityManager 核心配置 * 安全控制層 * @return */ @Bean public org.apache.shiro.mgt.SecurityManager securityManager(){ DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); //設置自定義的realm defaultWebSecurityManager.setRealm(myRealm()); //自定義的shiro session 緩存管理器 defaultWebSecurityManager.setSessionManager(sessionManager()); //將緩存對象注入到SecurityManager中 defaultWebSecurityManager.setCacheManager(ehCacheManager()); return defaultWebSecurityManager; } /** * 自定義的realm * @return */ @Bean public MyRealm myRealm() { return new MyRealm(); } /** * 開啓shiro 的AOP註解支持 * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * shiro緩存管理器 * 1 添加相關的maven支持 * 2 註冊這個bean,將緩存的配置文件導入 * 3 在securityManager 中註冊緩存管理器,以後就不會每次都會去查詢數據庫了,相關的權限和角色會保存在緩存中,但須要注意一點,更新了權限等操做以後,須要及時的清理緩存 */ @Bean public EhCacheManager ehCacheManager() { EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:config/ehcache.xml"); return cacheManager; } /** * 自定義的 shiro session 緩存管理器,用於跨域等狀況下使用 token 進行驗證,不依賴於sessionId * @return */ @Bean public SessionManager sessionManager(){ //將咱們繼承後重寫的shiro session 註冊 ShiroSession shiroSession = new ShiroSession(); //若是後續考慮多tomcat部署應用,可使用shiro-redis開源插件來作session 的控制,或者nginx 的負載均衡 shiroSession.setSessionDAO(new EnterpriseCacheSessionDAO()); return shiroSession; } }
完成了上面的流程,基本就已經大功告成了,對了,還要加上下面的代碼。
package com.common.filter; import com.alibaba.fastjson.JSONObject; import com.yunji.kwxt.common.Constant; import com.yunji.kwxt.common.enums.ResultEnum; import com.yunji.kwxt.common.model.ResultJson; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; /** * @author :LX * 建立時間: 2019/5/31. 10:25 * 地點:廣州 * 目的: 過濾OPTIONS請求 * 繼承shiro 的form表單過濾器,對 OPTIONS 請求進行過濾。 * 先後端分離項目中,因爲跨域,會致使複雜請求,即會發送preflighted request,這樣會致使在GET/POST等請求以前會先發一個OPTIONS請求,但OPTIONS請求並不帶shiro * 的'authToken'字段(shiro的SessionId),即OPTIONS請求不能經過shiro驗證,會返回未認證的信息。 * * 備註說明: 須要在 shiroConfig 進行註冊 */ public class CORSAuthenticationFilter extends FormAuthenticationFilter { /** * 直接過濾能夠訪問的請求類型 */ private static final String REQUET_TYPE = "OPTIONS"; public CORSAuthenticationFilter() { super(); } @Override public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (((HttpServletRequest) request).getMethod().toUpperCase().equals(REQUET_TYPE)) { return true; } return super.isAccessAllowed(request, response, mappedValue); } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse res = (HttpServletResponse)response; res.setHeader("Access-Control-Allow-Origin", "*"); res.setStatus(HttpServletResponse.SC_OK); res.setCharacterEncoding("UTF-8"); PrintWriter writer = res.getWriter(); ResultJson resultJson = new ResultJson(Constant.ERROR_CODE_NO_LOGIN, ResultEnum.ERROR.getStatus(), "請先登陸系統!", null); writer.write(JSONObject.toJSONString(resultJson)); writer.close(); return false; } }
爲何要過濾,上面的註釋說的很清楚了,建議你們仍是加上,這個類最終在shiro的攔截器那裏配置了。
固然還有登陸那裏要說一下,不少的新手否則就搞不懂了。
/** * 用戶登陸 * @param username 用戶名 * @param password 用戶密碼 * @return */ @RequestMapping(value = "/login", method = RequestMethod.POST) @ResponseBody public ResultJson login(String username, String password, HttpServletRequest request){ //TODO 驗證碼驗證 UsernamePasswordToken token = new UsernamePasswordToken(username, password); User user = userService.login(username, password); SecurityUtils.getSubject().login(token); //更新登陸信息 user.setIp(HttpTool.getIpAddr(request)); user.setOs(HttpTool.getOs(request)); user.setUpdateUserId(user.getId()); user.setUpdateTime(CommonTool.getTimestamp()); //設置session時間 //SecurityUtils.getSubject().getSession().setTimeout(1000*60*30); //token信息 Subject subject = SecurityUtils.getSubject(); Serializable tokenId = subject.getSession().getId(); return new ResultJson(null, ResultEnum.SUCCESS.getStatus(), "登陸認證成功", tokenId); }
咱們最終從shiro的session中取到了sessionId,回傳給前端,前端後續的請求都要帶這個token。
這樣就實現了token方式的shiro整合springboot。
若是爲了安全,還能夠建議你們,獲取到sessionId 以後,咱們進行一次加密,而後返回給前端,前端返回給咱們的時候,咱們能夠在shirosession 類中對加密的sessionId解密,這樣就更安全了。
最後還有一個問題須要說明一下,上述的代碼中shiro使用了緩存,但個人緩存相關的配置卻沒有貼出來,由於我這裏用的是java的緩存框架,建議使用redis的緩存框架,若是使用了緩存框架,細心的小夥伴就會發現,若是登陸後,在必定時間沒有和後臺進行交互,這個sessionId就會失效。
這是由於,當咱們登陸後若是走了緩存,session的存活時間就被緩存管理起來,咱們即便設置了shiro的緩存時間,設置應用的緩存時間都沒法管理到第三方的緩存,shiro的sesssion和server的session不是同一個東西。他並非servlet來管理的,故而設置了也沒有做用,須要去設置緩存中這個對象存活時間纔有用,好比咱們弄了redis來管理sessionId,只有設置了在redis中session的存活時間才行,咱們直接設置
SecurityUtils.getSubject().getSession().setTimeout(1000*60*30);
#session過時時間,單位秒 server.servlet.session.timeout=30000
都沒有任何用。好比我上面用ehcache來管理緩存,那只有在該緩存框架中設置這個參數纔有用
我這裏設置了120S,那若是120S沒有任何交互,那這個緩存sessionId就會失效