公司很多系統用的都是同一個架構 , 現在要將多個服務整合在一起 ,需要做單點登錄,下面是百度了很多抽離出來一些有用的自己組裝起來的,由於很多代碼都是不同的博主寫的,時間關系沒有記錄,還望各位原博主見諒
該配置實現了各種情況的單點登錄,后續使用ngix 負載均衡時session 的共享,以及同一時間同一個地方只能登錄一次
1. 各個程序的shiro配置一定要一樣 稍微的差別也可以 只要不影響整體就行(★★★★★ 一定要把shiro生成cookie的名字重命名;這是重點,這是重點,這是重點。因為默認為: JSESSIONID 問題: 與SERVLET容器名沖突, 如JETTY, TOMCAT 等默認JSESSIONID, 當跳出SHIRO SERVLET時會為JSESSIONID重新分配值導致登錄會話丟失!)
下面是子系統spring-shiro.xml的配置:(登錄服務器的配置大致相同,比下面的少了SSOFilter的相關配置,開放了autho, authe過濾器)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns="http://www.springframework.org/schema/beans" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:property-placeholder location="classpath:env.properties"/> <bean id="kickoutSessionControlFilter" class="com.yundun.system.shiro.KickoutSessionControlFilter"> <!-- 注入緩存管理器 --> <property name="cacheManager" ref="cacheManager"/> <!-- 注入session管理器 --> <property name="sessionManager" ref="sessionManager"/> <property name="kickoutAfter" value="false"/> <property name="maxSession" value="1"/> <property name="kickoutUrl" value="?kickout=1"/> </bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/"/> <property name="filters"> <map> <!--<entry key="autho" value-ref="authorizationFilter"/>--> <!-- <entry key="authe" value-ref="authenticationFilter"/> --> <entry key="kickout" value-ref="kickoutSessionControlFilter"/> <entry key="sso" value-ref="SSOFilter"/> </map> </property> <property name="filterChainDefinitions"> <value> /system/getProjectConfigByConfigName=anon /static/**=anon /system/logout = anon /system/login=anon /system/toLoginJsp=anon /oauth/**=anon /userInterface/**=anon /error/**=anon /v2/**/=anon /webjars/**=anon /authorization/**=anon /swagger-resources/**=anon /swagger-ui.html/**=anon /**=sso,<!--autho, authe, -->kickout </value> </property> </bean> <!-- securityManager安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realms"> <list> <ref bean="shiroRealm"/> </list> </property> <!-- 注入緩存管理器 --> <property name="cacheManager" ref="cacheManager"/> <!-- 注入session管理器 --> <property name="sessionManager" ref="sessionManager"/> <!-- 記住我 --> </bean> <bean id="shiroRealm" class="com.yundun.system.shiro.ShiroRealm"> <property name="credentialsMatcher" ref="credentialsMatcher"/> </bean> <bean id="redisSessionDAO" class="com.yundun.system.shiro.RedisSessionDao"/> <bean id="cacheManager" class="com.yundun.system.shiro.RedisCacheManager"> <property name="redisTemplate" ref="redisTemplate"/> </bean> <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="md5"/> <property name="hashIterations" value="2"/> </bean> <bean id="sessionManager" class="com.yundun.system.shiro.MyWebSessionManager"> <!-- 設置session的失效時間,單位為毫秒 --> <property name="globalSessionTimeout" value="${shiro.sessionTimeout}"/> <property name="sessionIdCookie" ref="simpleCookie"/> <!-- 設置session的失效掃描間隔,單位為毫秒 --> <property name="sessionValidationInterval" value="100000"/> <property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionDAO" ref="redisSessionDAO"/> </bean> <!-- sessionIdCookie的實現,用於重寫覆蓋容器默認的JSESSIONID --> <bean id="simpleCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <!-- 設置Cookie名字, 默認為: JSESSIONID 問題: 與SERVLET容器名沖突, 如JETTY, TOMCAT 等默認JSESSIONID, 當跳出SHIRO SERVLET時如ERROR-PAGE容器會為JSESSIONID重新分配值導致登錄會話丟失! --> <property name="name" value="SHIRO-COOKIE"/> <!-- JSESSIONID的path為/用於多個系統共享JSESSIONID --> <property name="path" value="/"/> <!-- 瀏覽器中通過document.cookie可以獲取cookie屬性,設置了HttpOnly=true,在腳本中就不能的到cookie,可以避免cookie被盜用 --> <property name="httpOnly" value="true"/> </bean> <!--<bean id="authorizationFilter" class="com.yundun.system.shiro.ShiroAuthorizationFilter"/>--> <!-- <bean id="authenticationFilter" class="com.yundun.system.shiro.ShiroAuthenticationFilter"/> --> <bean id="SSOFilter" class="com.yundun.system.shiro.SSOFilter"> <property name="serverLoginUrl" value="${serverLoginUrl}"/> <!--serverLoginUrl登錄服務器地址--> </bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean> </beans>
2、子系統添加SSOFilter 過濾器(用於監測每次請求是否都是登錄狀態)
package com.yundun.system.shiro; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.user.model.dto.LoginInfo; public class SSOFilter implements Filter{ private String serverLoginUrl; @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String sessionId = request.getParameter("sessionId"); HttpSession session = request.getSession(); String reqUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + request.getRequestURI(); LoginInfo loginInfo = (LoginInfo) session.getAttribute("loginInfo"); if(loginInfo!=null) { chain.doFilter(servletRequest, servletResponse); return; } //如果loginInfo為空 且 sessionId 不為空則證明已登錄,然后創建一個網頁傳入cookie就能模擬已登錄狀態會話 if(loginInfo==null && sessionId != null) { Cookie cookie = new Cookie("SHIRO-COOKIE", sessionId); cookie.setPath("/"); response.addCookie(cookie); String html = "<html><head><script type=\"text/javascript\">location.href='"+reqUrl+"'</script></head><body></body></html>"; byte[] bytes = html.getBytes(); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); response.getOutputStream().write(bytes); response.getOutputStream().flush(); response.getOutputStream().close(); return; } //去登錄服務器校驗是否已登錄如果已登錄 自然會傳回來sessionId,未登錄就會直接在登錄服務器那邊跳轉至登錄頁面 response.sendRedirect(serverLoginUrl + "/interfaceController/verifyIsLogin?redirectUrl=" + reqUrl); } @Override public void destroy() { } public String getServerLoginUrl() { return serverLoginUrl; } public void setServerLoginUrl(String serverLoginUrl) { this.serverLoginUrl = serverLoginUrl; } }
3、登錄服務器的校驗代碼(不解釋)
@RestController @RequestMapping("interfaceController") public class InterfaceController { @RequestMapping("verifyIsLogin") public void verifyIsLogin(HttpServletRequest request, HttpServletResponse response, String redirectUrl) { String sessionId = request.getSession().getId(); try { if (redirectUrl != null && redirectUrl != "") { response.sendRedirect(redirectUrl + "?sessionId=" + sessionId); } else { response.sendRedirect(request.getContextPath()); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
4.其他相關代碼
4.1 同一時間只能在一個地方登錄的代碼 package com.yundun.system.shiro; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.DefaultSessionKey; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.web.util.WebUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import com.user.dao.SysLoginStatusMapper; import com.user.model.entity.SysLoginStatus; import com.user.service.SystemService; import com.user.util.SystemConstant; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.Serializable; import java.util.Deque; import java.util.LinkedList; public class KickoutSessionControlFilter extends AccessControlFilter { private static final Logger log = LoggerFactory.getLogger(KickoutSessionControlFilter.class); @Autowired private SysLoginStatusMapper sysLoginStatusMapper; @Autowired private SystemService systemService; private String kickoutUrl; //踢出后到的地址 private boolean kickoutAfter = false; //踢出之前登錄的/之后登錄的用戶 默認踢出之前登錄的用戶 private int maxSession = 1; //同一個帳號最大會話數 默認1 private SessionManager sessionManager; private Cache<String, Deque<Serializable>> cache; public void setKickoutUrl(String kickoutUrl) { this.kickoutUrl = kickoutUrl; } public void setKickoutAfter(boolean kickoutAfter) { this.kickoutAfter = kickoutAfter; } public void setMaxSession(int maxSession) { this.maxSession = maxSession; } public void setSessionManager(SessionManager sessionManager) { this.sessionManager = sessionManager; } public void setCacheManager(CacheManager cacheManager) { this.cache = cacheManager.getCache(SystemConstant.KICKOUT_CACHE); } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { Subject subject = getSubject(request, response); if(!subject.isAuthenticated() && !subject.isRemembered()) { //如果沒有登錄,直接進行之后的流程 return true; } Session session = subject.getSession(); String username = (String) subject.getPrincipal(); Serializable sessionId = session.getId(); //TODO 同步控制 Deque<Serializable> deque = cache.get(SystemConstant.KICKOUT+username); if(deque == null) { deque = new LinkedList<Serializable>(); } //如果隊列里沒有此sessionId,且用戶沒有被踢出;放入隊列 if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) { deque.push(sessionId); cache.put(SystemConstant.KICKOUT+username, deque); } //如果隊列里的sessionId數超出最大會話數,開始踢人 while(deque.size() > maxSession) { Serializable kickoutSessionId = null; if(kickoutAfter) { //如果踢出后者 kickoutSessionId = deque.removeFirst(); } else { //否則踢出前者 kickoutSessionId = deque.removeLast(); } try { Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId)); if(kickoutSession != null) { //設置會話的kickout屬性表示踢出了 kickoutSession.setAttribute("kickout", true); } } catch (Exception e) {//ignore exception } } cache.put(SystemConstant.KICKOUT+username, deque); //如果被踢出了,直接退出,重定向到踢出后的地址 if (session.getAttribute("kickout") != null) { //會話被踢出了 try { subject.logout(); } catch (Exception e) { //ignore log.error("KickoutSessionControlFilter報錯====>"+e.toString()); } saveRequest(request); SysLoginStatus loginStatus = new SysLoginStatus(); loginStatus.setSessionId(deque.getFirst().toString()); SysLoginStatus sysLoginStatus = sysLoginStatusMapper.select(loginStatus); String url = ""; if(sysLoginStatus!=null) { url = "&ip="+sysLoginStatus.getLoginIp(); } String basePath = systemService.selectRedisCacheByConfigName("basePath"); WebUtils.issueRedirect(request, response,basePath+kickoutUrl+url); return false; } return true; } } 4.2,重寫DefaultSessionManager package com.yundun.system.shiro; import org.apache.shiro.session.ExpiredSessionException; import org.apache.shiro.session.InvalidSessionException; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.DefaultSessionManager; import org.apache.shiro.session.mgt.DelegatingSession; import org.apache.shiro.session.mgt.SessionContext; import org.apache.shiro.session.mgt.SessionKey; import org.apache.shiro.web.servlet.Cookie; import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.servlet.ShiroHttpSession; import org.apache.shiro.web.servlet.SimpleCookie; import org.apache.shiro.web.session.mgt.WebSessionKey; import org.apache.shiro.web.session.mgt.WebSessionManager; 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; /** * * 自定義WebSessionManager,用於替代DefaultWebSessionManager; * 解決: * 在shiro的一次認證過程中會調用10次左右的 doReadSession,如果使用內存緩存這個問題不大。 * 但是如果使用redis,而且子網絡情況不是特別好的情況下這就成為問題了。我簡單在我的環境下測試了一下。 * 一次redis請求需要80 ~ 100 ms, 一下來10次,我們一次認證就需要10 * 100 = 1000 ms, 這個就是我們無法接受的了。 * * 大部分代碼都是從DefaultWebSessionManager中復制過來的,擴展點在 line:200~212、225~229 * * @author * */ public class MyWebSessionManager extends DefaultSessionManager implements WebSessionManager { private static final Logger log = LoggerFactory.getLogger(MyWebSessionManager.class); private Cookie sessionIdCookie; private boolean sessionIdCookieEnabled; public MyWebSessionManager() { Cookie cookie = new SimpleCookie(ShiroHttpSession.DEFAULT_SESSION_ID_NAME); cookie.setHttpOnly(true); // more secure, protects against XSS attacks this.sessionIdCookie = cookie; this.sessionIdCookieEnabled = true; } public Cookie getSessionIdCookie() { return sessionIdCookie; } public void setSessionIdCookie(Cookie sessionIdCookie) { this.sessionIdCookie = sessionIdCookie; } public boolean isSessionIdCookieEnabled() { return sessionIdCookieEnabled; } public void setSessionIdCookieEnabled(boolean sessionIdCookieEnabled) { this.sessionIdCookieEnabled = sessionIdCookieEnabled; } private void storeSessionId(Serializable currentId, HttpServletRequest request, HttpServletResponse response) { if (currentId == null) { String msg = "sessionId cannot be null when persisting for subsequent requests."; throw new IllegalArgumentException(msg); } Cookie template = getSessionIdCookie(); Cookie cookie = new SimpleCookie(template); String idString = currentId.toString(); cookie.setValue(idString); cookie.saveTo(request, response); log.trace("Set session ID cookie for session with id {}", idString); } private void removeSessionIdCookie(HttpServletRequest request, HttpServletResponse response) { getSessionIdCookie().removeFrom(request, response); } private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) { if (!isSessionIdCookieEnabled()) { log.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie."); return null; } if (!(request instanceof HttpServletRequest)) { log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie. Returning null."); return null; } HttpServletRequest httpRequest = (HttpServletRequest) request; return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response)); } private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) { String id = getSessionIdCookieValue(request, response); if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE); } else { // not in a cookie, or cookie is disabled - try the request URI as a // fallback (i.e. due to URL rewriting): // try the URI path segment parameters first: id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME); if (id == null) { // not a URI path segment parameter, try the query parameters: String name = getSessionIdName(); id = request.getParameter(name); if (id == null) { // try lowercase: id = request.getParameter(name.toLowerCase()); } } if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.URL_SESSION_ID_SOURCE); } } if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); // automatically mark it valid here. If it is invalid, the // onUnknownSession method below will be invoked and we'll remove // the attribute at that time. request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); } return id; } // SHIRO-351 // also see // http://cdivilly.wordpress.com/2011/04/22/java-servlets-uri-parameters/ // since 1.2.2 private String getUriPathSegmentParamValue(ServletRequest servletRequest, String paramName) { if (!(servletRequest instanceof HttpServletRequest)) { return null; } HttpServletRequest request = (HttpServletRequest) servletRequest; String uri = request.getRequestURI(); if (uri == null) { return null; } int queryStartIndex = uri.indexOf('?'); if (queryStartIndex >= 0) { // get rid of the query string uri = uri.substring(0, queryStartIndex); } int index = uri.indexOf(';'); // now check for path segment parameters: if (index < 0) { // no path segment params - return: return null; } // there are path segment params, let's get the last one that may exist: final String TOKEN = paramName + "="; uri = uri.substring(index + 1); // uri now contains only the path // segment params // we only care about the last JSESSIONID param: index = uri.lastIndexOf(TOKEN); if (index < 0) { // no segment param: return null; } uri = uri.substring(index + TOKEN.length()); index = uri.indexOf(';'); // strip off any remaining segment params: if (index >= 0) { uri = uri.substring(0, index); } return uri; // what remains is the value } //------------------------------------------------------------------------------------------------------------------ /** * Modify By Goma */ protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException { Serializable sessionId = getSessionId(sessionKey); if (sessionId == null) { log.debug("Unable to resolve session ID from SessionKey [{}]. Returning null to indicate a " + "session could not be found.", sessionKey); return null; } // ***************Add By Goma**************** ServletRequest request = null; if (sessionKey instanceof WebSessionKey) { request = ((WebSessionKey) sessionKey).getServletRequest(); } if (request != null) { Object s = request.getAttribute(sessionId.toString()); if (s != null) { return (Session) s; } } // ***************Add By Goma**************** Session s = retrieveSessionFromDataSource(sessionId); if (s == null) { // session ID was provided, meaning one is expected to be found, but // we couldn't find one: String msg = "Could not find session with ID [" + sessionId + "]"; throw new UnknownSessionException(msg); } // ***************Add By Goma**************** if (request != null) { request.setAttribute(sessionId.toString(),s); } // ***************Add By Goma**************** return s; } //------------------------------------------------------------------------------------------------------------------ // since 1.2.1 private String getSessionIdName() { String name = this.sessionIdCookie != null ? this.sessionIdCookie.getName() : null; if (name == null) { name = ShiroHttpSession.DEFAULT_SESSION_ID_NAME; } return name; } protected Session createExposedSession(Session session, SessionContext context) { if (!WebUtils.isWeb(context)) { return super.createExposedSession(session, context); } ServletRequest request = WebUtils.getRequest(context); ServletResponse response = WebUtils.getResponse(context); SessionKey key = new WebSessionKey(session.getId(), request, response); return new DelegatingSession(this, key); } protected Session createExposedSession(Session session, SessionKey key) { if (!WebUtils.isWeb(key)) { return super.createExposedSession(session, key); } ServletRequest request = WebUtils.getRequest(key); ServletResponse response = WebUtils.getResponse(key); SessionKey sessionKey = new WebSessionKey(session.getId(), request, response); return new DelegatingSession(this, sessionKey); } /** * Stores the Session's ID, usually as a Cookie, to associate with future * requests. * * @param session * the session that was just {@link #createSession created}. */ @Override protected void onStart(Session session, SessionContext context) { super.onStart(session, context); if (!WebUtils.isHttp(context)) { log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response " + "pair. No session ID cookie will be set."); return; } HttpServletRequest request = WebUtils.getHttpRequest(context); HttpServletResponse response = WebUtils.getHttpResponse(context); if (isSessionIdCookieEnabled()) { Serializable sessionId = session.getId(); storeSessionId(sessionId, request, response); } else { log.debug("Session ID cookie is disabled. No cookie has been set for new session with id {}", session.getId()); } request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE); } @Override public Serializable getSessionId(SessionKey key) { Serializable id = super.getSessionId(key); if (id == null && WebUtils.isWeb(key)) { ServletRequest request = WebUtils.getRequest(key); ServletResponse response = WebUtils.getResponse(key); id = getSessionId(request, response); } return id; } protected Serializable getSessionId(ServletRequest request, ServletResponse response) { return getReferencedSessionId(request, response); } @Override protected void onExpiration(Session s, ExpiredSessionException ese, SessionKey key) { super.onExpiration(s, ese, key); onInvalidation(key); } @Override protected void onInvalidation(Session session, InvalidSessionException ise, SessionKey key) { super.onInvalidation(session, ise, key); onInvalidation(key); } private void onInvalidation(SessionKey key) { ServletRequest request = WebUtils.getRequest(key); if (request != null) { request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID); } if (WebUtils.isHttp(key)) { log.debug("Referenced session was invalid. Removing session ID cookie."); removeSessionIdCookie(WebUtils.getHttpRequest(key), WebUtils.getHttpResponse(key)); } else { log.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response " + "pair. Session ID cookie will not be removed due to invalidated session."); } } @Override protected void onStop(Session session, SessionKey key) { super.onStop(session, key); if (WebUtils.isHttp(key)) { HttpServletRequest request = WebUtils.getHttpRequest(key); HttpServletResponse response = WebUtils.getHttpResponse(key); log.debug("Session has been stopped (subject logout or explicit stop). Removing session ID cookie."); removeSessionIdCookie(request, response); } else { log.debug("SessionKey argument is not HTTP compatible or does not have an HTTP request/response " + "pair. Session ID cookie will not be removed due to stopped session."); } } /** * This is a native session manager implementation, so this method returns * {@code false} always. * * @return {@code false} always * @since 1.2 */ public boolean isServletContainerSessions() { return false; } } 4.3 重寫shiro cache package com.yundun.system.shiro; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; import com.user.util.SystemConstant; import java.io.InputStream; import java.io.Serializable; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; /** * @Author 王俊強 * @Description Cache redis實現 */ public class RedisCache<K, V> implements Cache<K, V>, Serializable { private static long timeout ; private transient static Logger log = LoggerFactory.getLogger(RedisCache.class); static{ Properties prop = new Properties(); InputStream in = RedisSessionDao.class.getResourceAsStream("/env.properties"); try { prop.load(in); timeout = Integer.parseInt(prop.getProperty("shiro.sessionTimeout"))/1000; } catch (Exception e) { log.error("RedisSessionDao獲取properties文件內容出錯"+e); } } private transient RedisTemplate<K, V> redisTemplate; public RedisCache(RedisTemplate<K, V> redisTemplate) { this.redisTemplate = redisTemplate; } public RedisCache() { } @Override public V get(K key) throws CacheException { log.debug("根據key:{}從redis獲取對象", key); log.debug("redisTemplate : {}", redisTemplate); return redisTemplate.opsForValue().get(SystemConstant.SHIRO_CACHE_PREFIX + key); } @Override public V put(K key, V value) throws CacheException { log.debug("根據key:{}從redis添加對象", key); redisTemplate.opsForValue().set((K) (SystemConstant.SHIRO_CACHE_PREFIX + key), value, timeout, TimeUnit.SECONDS); return value; } /** * 執行set操作並且設置生存時間,單位為:秒 * * @param key * @param value * @return */ public void setValueTime(final String key, final String value, final Integer timeOut) { redisTemplate.opsForValue().set((K) (SystemConstant.SHIRO_CACHE_PREFIX + key), (V) value, timeOut, TimeUnit.SECONDS); } @Override public V remove(K key) throws CacheException { log.debug("redis cache remove :{}", key.toString()); V value = redisTemplate.opsForValue().get(SystemConstant.SHIRO_CACHE_PREFIX + key); redisTemplate.delete(key); return value; } @Override public void clear() throws CacheException { log.debug("清除redis所有緩存對象"); Set<K> keys = redisTemplate.keys((K) SystemConstant.SHIRO_CACHE_PREFIX_KEYS); redisTemplate.delete(keys); } @Override public int size() { Set<K> keys = redisTemplate.keys((K) SystemConstant.SHIRO_CACHE_PREFIX_KEYS); log.debug("獲取redis緩存對象數量:{}", keys.size()); return keys.size(); } @Override public Set<K> keys() { Set<K> keys = redisTemplate.keys((K)SystemConstant.SHIRO_CACHE_PREFIX_KEYS); log.debug("獲取所有緩存對象的key"); if (keys.size() == 0) { return Collections.emptySet(); } return keys; } @Override public Collection<V> values() { Set<K> keys = redisTemplate.keys((K) SystemConstant.SHIRO_CACHE_PREFIX_KEYS); log.debug("獲取所有緩存對象的value"); if (keys.size() == 0) { return Collections.emptySet(); } List<V> vs = redisTemplate.opsForValue().multiGet(keys); return Collections.unmodifiableCollection(vs); } public RedisTemplate<K, V> getRedisTemplate() { return redisTemplate; } public void setRedisTemplate(RedisTemplate<K, V> redisTemplate) { this.redisTemplate = redisTemplate; } } 4.4 重寫CacheManager package com.yundun.system.shiro; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; import com.user.util.SystemConstant; import java.io.Serializable; /** * @Author 王俊強 * @Description 接口實現 */ public class RedisCacheManager implements CacheManager, Serializable { private transient static Logger log = LoggerFactory.getLogger(RedisCacheManager.class); private transient RedisTemplate<Object, Object> redisTemplate; public RedisCacheManager() { } public RedisCacheManager(RedisTemplate<Object, Object> redisTemplate) { this.redisTemplate = redisTemplate; } @Override public <K, V> Cache<K, V> getCache(String name) throws CacheException { if (!StringUtils.hasText(name)) { throw new IllegalArgumentException("Cache name cannot be null or empty."); } log.debug("redis cache manager get cache name is :{}", name); Cache cache = (Cache) redisTemplate.opsForValue().get(name); if (cache == null) { cache = new RedisCache<>(redisTemplate); redisTemplate.opsForValue().set(SystemConstant.SHIRO_CACHE_PREFIX + name, cache); } return cache; } public void setRedisTemplate(RedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } } 4.5 重寫AbstractSessionDAO package com.yundun.system.shiro; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.eis.AbstractSessionDAO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import com.user.model.dto.LoginInfo; import com.user.util.SystemConstant; import java.io.InputStream; import java.io.Serializable; import java.util.*; import java.util.concurrent.TimeUnit; /** * @Author 王俊強 * @Description session redis實現 cookie生成 已經redis key存活時間變更 */ public class RedisSessionDao extends AbstractSessionDAO { private static long timeout; private transient static Logger log = LoggerFactory.getLogger(RedisSessionDao.class); static{ Properties prop = new Properties(); InputStream in = RedisSessionDao.class.getResourceAsStream("/env.properties"); try { prop.load(in); timeout = Integer.parseInt(prop.getProperty("shiro.sessionTimeout"))/1000; } catch (Exception e) { // TODO Auto-generated catch block log.error("RedisSessionDao獲取properties文件內容出錯"+e); } } @Autowired private transient RedisTemplate<Serializable, Session> redisTemplate; //在用戶首次訪問時創建一個shiro session protected Serializable doCreate(Session session) { Serializable sessionId = SystemConstant.SHIRO_CACHE_PREFIX + UUID.randomUUID().toString(); //往session中指派ID this.assignSessionId(session, sessionId); redisTemplate.opsForValue().set(sessionId, session, timeout, TimeUnit.SECONDS); log.info("create shiro session ,sessionId is :{}", sessionId.toString()); return sessionId; } @Override protected Session doReadSession(Serializable sessionId) { if (sessionId == null) { return null; } return redisTemplate.opsForValue().get(sessionId); } @Override public void update(Session session) throws UnknownSessionException { LoginInfo loginInfo = (LoginInfo) session.getAttribute("loginInfo"); if(loginInfo!=null) { String kickoutName = SystemConstant.SHIRO_CACHE_PREFIX+SystemConstant.KICKOUT+loginInfo.getLoginName(); redisTemplate.expire(kickoutName,timeout, TimeUnit.SECONDS); String catchName = SystemConstant.SHIRO_CACHE_PREFIX+loginInfo.getLoginName(); redisTemplate.expire(catchName,timeout, TimeUnit.SECONDS); } redisTemplate.opsForValue().set(session.getId(), session, timeout, TimeUnit.SECONDS); } @Override public void delete(Session session) { LoginInfo loginInfo = (LoginInfo) session.getAttribute("loginInfo"); //刪除登錄唯一驗證的緩存 String kickoutName = SystemConstant.SHIRO_CACHE_PREFIX + SystemConstant.KICKOUT+loginInfo.getLoginName(); redisTemplate.opsForValue().getOperations().delete(kickoutName); //刪除用戶緩存 String catchName = SystemConstant.SHIRO_CACHE_PREFIX + loginInfo.getLoginName(); redisTemplate.opsForValue().getOperations().delete(catchName); //刪除用戶session redisTemplate.opsForValue().getOperations().delete(session.getId()); } @Override public Collection<Session> getActiveSessions() { Set<Serializable> keys = redisTemplate.keys(SystemConstant.SHIRO_CACHE_PREFIX_KEYS); if (keys.size() == 0) { return Collections.emptySet(); } List<Session> sessions = redisTemplate.opsForValue().multiGet(keys); return Collections.unmodifiableCollection(sessions); } } 4.6 重寫PassThruAuthenticationFilter package com.yundun.system.shiro; import com.google.gson.Gson; import org.apache.commons.lang3.StringUtils; import org.apache.shiro.SecurityUtils; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.authc.PassThruAuthenticationFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import com.user.model.dto.LoginInfo; import com.user.service.SystemService; import com.user.util.CookieUtils; import com.user.util.ResponseCode; import com.user.util.Result; import com.user.util.SystemConstant; import java.io.Serializable; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; /** * @Author 王俊強 * @Description 登錄過濾器 未啟用 */ public class ShiroAuthenticationFilter extends PassThruAuthenticationFilter { private static Logger log = LoggerFactory.getLogger(ShiroAuthorizationFilter.class); @Autowired private SystemService systemService; @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { if (isLoginRequest(request, response)) { return true; } else { saveRequest(request); if (((HttpServletRequest) request).getHeader("Accept").contains("application/json")) { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); Result result = new Result(ResponseCode.unauthenticated.getCode(), ResponseCode.unauthenticated.getMsg()); response.getWriter().append(new Gson().toJson(result)); response.getWriter().flush(); response.getWriter().close(); } else { response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); String basePath = systemService.selectRedisCacheByConfigName("basePath"); basePath += "?timeOut=1"; ((HttpServletResponse) response).sendRedirect(basePath); } return false; } } } 4.6 重寫AuthorizationFilter package com.yundun.system.shiro; import com.google.gson.Gson; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.authz.AuthorizationFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import com.user.service.SystemService; import com.user.util.ResponseCode; import com.user.util.Result; import com.user.util.StringUtil; import javax.servlet.RequestDispatcher; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Author 王俊強 * @Description 權限過濾器 未啟用 */ public class ShiroAuthorizationFilter extends AuthorizationFilter { private static Logger log = LoggerFactory.getLogger(ShiroAuthorizationFilter.class); @Autowired private SystemService systemService; @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException { HttpServletResponse response1 = (HttpServletResponse) response; Subject subject = getSubject(request, response); if (subject.getPrincipal() == null) { //未登錄跳轉至登陸頁面 saveRequest(request); if (((HttpServletRequest) request).getHeader("Accept").contains("application/json")) { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); Result result = new Result(ResponseCode.unauthenticated.getCode(), ResponseCode.unauthenticated.getMsg()); response.getWriter().append(new Gson().toJson(result)); response.getWriter().flush(); response.getWriter().close(); } else { response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); String basePath = systemService.selectRedisCacheByConfigName("basePath"); ((HttpServletResponse) response).sendRedirect(basePath); } } else { //已登錄未授權 if (((HttpServletRequest) request).getHeader("Accept").contains("application/json")) { log.debug("授權認證:未通過:json"+((HttpServletRequest) request).getRequestURL()); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); Result result = new Result(ResponseCode.unauthorized.getCode(), ResponseCode.unauthorized.getMsg()); response.getWriter().append(new Gson().toJson(result)); response.getWriter().flush(); response.getWriter().close(); } else { log.debug("授權認證:未通過:web"+((HttpServletRequest) request).getRequestURL()); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); ((HttpServletResponse) response).sendRedirect("/error/unAuthorization"); } } return false; } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { Subject subject = getSubject(request, response); String[] perms = (String[]) mappedValue; boolean isPermitted = true; if (perms != null && perms.length > 0) { if (perms.length == 1) { if (!subject.isPermitted(perms[0])) { log.debug("授權認證:未通過"); isPermitted = false; } } else { if (!subject.isPermittedAll(perms)) { log.debug("授權認證:未通過"); isPermitted = false; } } } return isPermitted; } } 4.7 重寫AuthorizingRealm package com.yundun.system.shiro; import com.user.dao.*; import com.user.model.entity.*; import com.user.util.SystemConstant; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import java.util.HashSet; import java.util.List; import java.util.Set; /** * @Author: 王俊強 * @Description 自定義realm實現 */ public class ShiroRealm extends AuthorizingRealm { private static Logger log = LoggerFactory.getLogger(ShiroRealm.class); @Autowired private SysUserMapper sysUserMapper; @Autowired private SysPermissionMapper sysPermissionMapper; @Autowired private SysRoleMapper sysRoleMapper; @Autowired private RedisTemplate<Object, Object> redisTemplate; /** * 鑒權信息 * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { log.debug("開始查詢授權信息"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); String loginStr = (String) principalCollection.getPrimaryPrincipal(); SysUser user = sysUserMapper.selectUserByLoginName(loginStr); //獲取用戶對應的角色 SysRole sysRole = sysRoleMapper.selectByUserId(user.getId()); Set<String> roles = new HashSet<>(); roles.add(sysRole.getName()); info.addRoles(roles); //獲取用戶對應的權限 List<SysPermission> sysPermissionList = sysPermissionMapper.selectByRoleId(sysRole.getId()); Set<String> permissions = new HashSet<>(); for (SysPermission sysPermission : sysPermissionList) { permissions.add(sysPermission.getCode()); } info.addStringPermissions(permissions); log.debug("角色信息: \n {}", roles.toString()); log.debug("權限信息: \n{}", permissions.toString()); return info; } /** * 登錄驗證 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { log.debug("登錄驗證"); String loginName = (String) authenticationToken.getPrincipal(); SysUser sysUser = sysUserMapper.selectUserByLoginName(loginName); AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(loginName, sysUser.getPassword(), ByteSource.Util.bytes(sysUser.getPasswordSalt()), getName()); return authenticationInfo; } @Override protected void doClearCache(PrincipalCollection principals) { redisTemplate.delete(SystemConstant.SHIRO_CACHE_PREFIX + principals.getPrimaryPrincipal().toString()); } @Override protected void clearCachedAuthorizationInfo(PrincipalCollection principals) { log.debug("clearCachedAuthorizationInfo"); } }
