一、解決跨域、過慮options請求問題
1.創建過慮類
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component @WebFilter(filterName = "SimpleCORSFilter",urlPatterns ="/*" ) public class SimpleCORSFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(SimpleCORSFilter.class); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; logger.info("請求攔截:"+ ((HttpServletRequest) request).getRequestURI()); /* * 設置允許跨域請求 */ httpServletResponse.setHeader("Access-Control-Allow-Headers", "content-type, accept"); httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST"); httpServletResponse.setStatus(200); httpServletResponse.setContentType("text/plain;charset=utf-8"); httpServletResponse.setCharacterEncoding("utf-8"); httpServletResponse.setHeader("Access-Control-Allow-Origin", "*"); httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); httpServletResponse.setHeader("Access-Control-Max-Age", "0"); httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,userId,token,WG-Token, Authorization"); httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true"); httpServletResponse.setHeader("XDomainRequestAllowed", "1"); /* 過慮 OPTIONS 請求 */ String type = httpServletRequest.getMethod(); if (type.toUpperCase().equals("OPTIONS")) { return; } chain.doFilter(request, response); } @Override public void init(FilterConfig filterConfig) throws ServletException { // String isCrossStr = filterConfig.getInitParameter("IsCross"); // isCross = isCrossStr.equals("true") ? true : false; // System.out.println(isCrossStr); } @Override public void destroy() { // isCross = false; } }
2.在 ShiroConfig.java 中設置攔截器
Map<String, Filter> filterMap = new LinkedHashMap<String, Filter>(); filterMap.put("/sys/*",new SimpleCORSFilter()); // SimpleCORSFilter 為步驟1中創建的類 shiroFilterFactoryBean.setFilters(filterMap); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/index.html*", "anon"); //anon下不需要認證 filterChainDefinitionMap.put("/anon/**", "anon"); filterChainDefinitionMap.put("/loginSys", "anon"); filterChainDefinitionMap.put("/index", "anon"); filterChainDefinitionMap.put("/back", "anon"); filterChainDefinitionMap.put("/sys/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
二、前后端分離shiro管理session問題
SessionManager.java代碼參考:https://blog.csdn.net/palerock/article/details/73457415?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task
1.重寫DefaultWebSessionManager的getSessionId方法
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 java.io.Serializable; public class SessionManager extends DefaultWebSessionManager { private static final Logger log = LoggerFactory.getLogger(DefaultWebSessionManager.class); private String authorization = "Authorization"; /** * 重寫獲取sessionId的方法調用當前Manager的獲取方法 * * @param request * @param response * @return */ @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { return this.getReferencedSessionId(request, response); } /** * 獲取sessionId從請求中 * * @param request * @param response * @return */ private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) { String id = this.getSessionIdCookieValue(request, response); if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "cookie"); } else { id = this.getUriPathSegmentParamValue(request, "JSESSIONID"); if (id == null) { // 獲取請求頭中的session id = WebUtils.toHttp(request).getHeader(this.authorization); if (id == null) { String name = this.getSessionIdName(); id = request.getParameter(name); if (id == null) { id = request.getParameter(name.toLowerCase()); } } } if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "url"); } } if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); } return id; } // copy from super class private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) { if (!this.isSessionIdCookieEnabled()) { log.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie."); return null; } else if (!(request instanceof HttpServletRequest)) { log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie. Returning null."); return null; } else { HttpServletRequest httpRequest = (HttpServletRequest) request; return this.getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response)); } } // copy from super class private String getUriPathSegmentParamValue(ServletRequest servletRequest, String paramName) { if (!(servletRequest instanceof HttpServletRequest)) { return null; } else { HttpServletRequest request = (HttpServletRequest) servletRequest; String uri = request.getRequestURI(); if (uri == null) { return null; } else { int queryStartIndex = uri.indexOf(63); if (queryStartIndex >= 0) { uri = uri.substring(0, queryStartIndex); } int index = uri.indexOf(59); if (index < 0) { return null; } else { String TOKEN = paramName + "="; uri = uri.substring(index + 1); index = uri.lastIndexOf(TOKEN); if (index < 0) { return null; } else { uri = uri.substring(index + TOKEN.length()); index = uri.indexOf(59); if (index >= 0) { uri = uri.substring(0, index); } return uri; } } } } } // copy from super class private String getSessionIdName() { String name = this.getSessionIdCookie() != null ? this.getSessionIdCookie().getName() : null; if (name == null) { name = "JSESSIONID"; } return name; } }
2.在 ShiroConfig.java 配置SessionManager
@Bean
public SecurityManager securityManager(MyRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
securityManager.setSessionManager(sessionManager());
return securityManager;
}
@Bean("sessionManager")
public SessionManager sessionManager(){
SessionManager manager = new SessionManager(); // SessionManager 為步驟1中創建的 SessionManager
/*使用了shiro自帶緩存,
如果設置 redis為緩存需要重寫CacheManager(其中需要重寫Cache)
manager.setCacheManager(this.RedisCacheManager());*/
manager.setSessionDAO(new EnterpriseCacheSessionDAO());
return manager;
}
3.用戶登錄成功后,將sessionId返回前端
@ResponseBody
@RequestMapping(value = "/login", method = RequestMethod.POST, produces = "application/json;charset=utf-8")
public String login(
@RequestParam(required = false) String username,
@RequestParam(required = false) String password
) {
JSONObject jsonObject = new JSONObject();
Subject subject = SecurityUtils.getSubject();
password = MD5Tools.MD5(password);
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
// 登錄,即身份驗證
subject.login(token);
onlineSessionManager.addOnlineSession(subject.getSession().getId());
User user = userService.getUserByLoginName(token.getUsername());
// 在session中存放用戶信息
subject.getSession().setAttribute("userLogin", user);
jsonObject.put("error", 0);
jsonObject.put("msg", "登錄成功");
// 返回sessionId
jsonObject.put("sessionId",subject.getSession().getId());
} catch (IncorrectCredentialsException e) {
throw new JsonException("用戶名或密碼錯誤", 405);
} catch (LockedAccountException e) {
throw new JsonException("登錄失敗,該用戶已被凍結", 405);
} catch (AuthenticationException e) {
throw new JsonException("用戶名或密碼錯誤", 405);
}
return jsonObject.toString();
}
4.前端請求時請求頭帶上sessionId
模擬跨域請求的兩種方法:
方法一:啟動本地項目,瀏覽器隨便打開某個網站,F12 > Console下執行以下代碼實現跨域請求,最方便也是最容易實現
可參考:https://www.cnblogs.com/L237/p/12556488.html
方法二:啟動本地項目,在自己的某台服務器上的某個項目下創建一個test.jsp頁面,放入以下代碼,然后通過瀏覽器訪問test.jsp實現跨域請求
$.ajax({ type:'post', url:'http://192.168.1.31:8080/login', data:'{"username":"admin","password":"admin"}', contentType: "application/json", //提交數據類型 dataType:'json', success:function(data){ console.log(data); var sessionId = data.data.sessionId; //登錄成功后返回的sessionId $.ajax({ type:'post', url:'http://192.168.1.31:8080/sys/noticeNews/selectList', data:'{"pageNum":"1","pageSize":"10","verify":"2"}', contentType: "application/json", //提交數據類型 headers: { "Authorization": sessionId, // Authorization 為重定的SessionManager類中定義的屬性 "Content-Type":"application/json;charset=utf8" }, dataType:'json', crossDomain: true, success:function(data){ console.log(data); }, error:function (msg){ console.log(msg); } }); }, error:function (msg){ console.log(msg); layer.msg("用戶名或密碼錯誤"); } });
5. 在解決跨域類(一、1)SimpleCORSFilter中的httpServletResponse.setHeader() 中添加(二、4)Authorization
shiro配置類參考 ShiroConfig.java
package com.sx.ucenter.shiro; import com.sx.common.util.SimpleCORSFilter; import com.sx.ucenter.filter.SysFilter; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.mgt.SecurityManager; 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.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; @Configuration public class ShiroConfig { //將自己的驗證方式加入容器 @Bean public MyRealm myShiroRealm(EhCacheManager ehCacheManager) { MyRealm myRealm = new MyRealm(); return myRealm; } //權限管理,配置主要是Realm的管理認證 @Bean public SecurityManager securityManager(MyRealm myRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(myRealm); securityManager.setSessionManager(sessionManager()); return securityManager; } //Filter工廠,設置對應的過濾條件和跳轉條件 @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> map = new LinkedHashMap<>(); /* *常用的過濾器如下: *anon:所有用戶可訪問,通常作為指定頁面的靜態資源時使用 *authc:所有已登陸用戶可訪問 *roles:有指定角色的用戶可訪問,通過[ ]指定具體角色,這里的角色名稱與數據庫中配置一致 *perms:有指定權限的用戶可訪問,通過[ ]指定具體權限,這里的權限名稱與數據庫中配置一致 */ // //配置shiro默認登錄界面地址,前后端分離中登錄界面跳轉應由前端路由控制,后台僅返回json數據 // shiroFilterFactoryBean.setLoginUrl("/unauth"); /*跨域請求設置 start */ Map<String, Filter> filterMap = new LinkedHashMap<String, Filter>(); // filterMap.put("authc",new SimpleCORSFilter()); filterMap.put("/sys/*",new SimpleCORSFilter()); shiroFilterFactoryBean.setFilters(filterMap); Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/index.html*", "anon"); //anon下不需要認證 filterChainDefinitionMap.put("/anon/**", "anon"); filterChainDefinitionMap.put("/loginSys", "anon"); filterChainDefinitionMap.put("/index", "anon"); filterChainDefinitionMap.put("/back", "anon"); filterChainDefinitionMap.put("/sys/**", "authc"); // filterChainDefinitionMap.put("/**", "anon"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); /*跨域請求設置 end */ //登錄 shiroFilterFactoryBean.setLoginUrl("/login"); //首頁 shiroFilterFactoryBean.setSuccessUrl("/index"); //開放匿名訪問 //錯誤頁面,認證不通過跳轉 shiroFilterFactoryBean.setUnauthorizedUrl("/error"); return shiroFilterFactoryBean; } /* * 設置session */ @Bean("sessionManager") public SessionManager sessionManager(){ SessionManager manager = new SessionManager(); /*使用了shiro自帶緩存, 如果設置 redis為緩存需要重寫CacheManager(其中需要重寫Cache) manager.setCacheManager(this.RedisCacheManager());*/ manager.setSessionDAO(new EnterpriseCacheSessionDAO()); return manager; } //加入注解的使用,不加入這個注解不生效 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
6.遇到的問題
參考:https://blog.csdn.net/wangjun5159/article/details/89875746
按照以上方式可以實現shiro 的管理了,但是發現登錄成功后只要停2分鍾沒有任何操作 session就會失效需要重新登錄,
后面發現是因為設置了ehcache緩存的原因,當時的ehcache設置如下:
timeToIdleSeconds配置為2分鍾,如果2分鍾內沒有交互,session就會從ehcache中刪除
<!-- 是否永久保存 --> <!-- 最大空閑時間 單位:秒 --> <!-- 存活時間 --> <!-- 溢出到磁盤 --> <!-- 磁盤上最大存儲的對象數 --> <!-- 服務器重啟后磁盤上的數據是否有效 --> <!-- 每隔多長時間去開啟一次線程清理數據 --> <!-- 淘汰策略 最近一段時間利用率低的會被優先清理掉 --> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" maxElementsOnDisk="10000000" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU" />