以前用的單機Shiro實現用戶單點登錄,基本問題不大,可是集羣間的session共享單靠Shiro就很差實現了。因此就藉助Redis數據庫來實現。css
這裏Redis的搭建我以前說過,感興趣的能夠去看看:http://www.cnblogs.com/hz-cww/p/6074062.html。html
1、pom.xmljava
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.8.0</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>1.8.1.RELEASE</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency>
2、spring-shiro.xmlweb
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 繼承自AuthorizingRealm的自定義Realm,即指定Shiro驗證用戶登陸的類爲自定義的ShiroDbRealm.java --> <bean id="passwordRealm" class="com.rongke.web.shiro.PasswordRealm"/> <!-- 踢出用戶 --> <bean id="sessionDAO" class="com.rongke.web.shiro.RedisSessionDao" /> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="sessionDAO" ref="sessionDAO" /> <property name="globalSessionTimeout" value="-1000"/> </bean> <!-- Shiro默認會使用Servlet容器的Session,可經過sessionMode屬性來指定使用Shiro原生Session --> <!-- 即<property name="sessionMode" value="native"/>,詳細說明見官方文檔 --> <!-- 這裏主要是設置自定義的單Realm應用,如有多個Realm,可以使用'realms'屬性代替 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realms"> <list> <ref bean="passwordRealm" /> </list> </property> <property name="sessionManager" ref="sessionManager" /> </bean> <!-- Shiro主過濾器自己功能十分強大,其強大之處就在於它支持任何基於URL路徑表達式的、自定義的過濾器的執行 --> <!-- Web應用中,Shiro可控制的Web請求必須通過Shiro主過濾器的攔截,Shiro對基於Spring的Web應用提供了完美的支持 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!-- Shiro的核心安全接口,這個屬性是必須的 --> <property name="securityManager" ref="securityManager"/> <!-- 要求登陸時的連接(可根據項目的URL進行替換),非必須的屬性,默認會自動尋找Web工程根目錄下的"/login.jsp"頁面 --> <property name="loginUrl" value="/login.map"/> <!-- 登陸成功後要跳轉的鏈接(本例中此屬性用不到,由於登陸成功後的處理邏輯在LoginController裏硬編碼爲main.jsp了) --> <!-- <property name="successUrl" value="/index.html"/> --> <!-- 用戶訪問未對其受權的資源時,所顯示的鏈接 --> <!-- 若想更明顯的測試此屬性能夠修改它的值,如unauthor.jsp,而後用[玄玉]登陸後訪問/admin/listUser.jsp就看見瀏覽器會顯示unauthor.jsp --> <property name="unauthorizedUrl" value="/unauthorized.html"/> <!-- Shiro鏈接約束配置,即過濾鏈的定義 --> <!-- 此處可配合個人這篇文章來理解各個過濾連的做用http://blog.csdn.net/jadyer/article/details/12172839 --> <!-- 下面value值的第一個'/'表明的路徑是相對於HttpServletRequest.getContextPath()的值來的 --> <!-- anon:它對應的過濾器裏面是空的,什麼都沒作,這裏.do和.jsp後面的*表示參數,比方說login.jsp?main這種 --> <!-- authc:該過濾器下的頁面必須驗證後才能訪問,它是Shiro內置的一個攔截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter --> <property name="filterChainDefinitions"> <value> # 注意此處配置的路徑不須要輸入工程名, / 就包括 http://地址:端口/項目名/ <!--/api/user/login/** = anon--> <!--/api/user/pcLogin/** = anon--> <!--/api/user/register/** = anon--> <!----> <!--/api/user/logout = logout--> <!----> <!--/assets/** = anon--> <!--/css/** = anon--> <!--/img/** = anon--> <!--/js/** = anon--> <!--/tpl/** = anon--> <!--/login.html = anon--> # authc 必須放在最後 /** = anon </value> </property> </bean> <!-- 保證明現了Shiro內部lifecycle函數的bean執行 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <!-- 開啓Shiro的註解(如@RequiresRoles,@RequiresPermissions),需藉助SpringAOP掃描使用Shiro註解的類,並在必要時進行安全邏輯驗證 --> <!-- 配置如下兩個bean便可實現此功能 --> <!-- Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor has run --> <!-- 因爲本例中並未使用Shiro註解,故註釋掉這兩個bean(我的以爲將權限經過註解的方式硬編碼在程序中,查看起來不是很方便,不必使用) --> <!-- <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>
3、繼承org.apache.shiro.session.mgt.eis.AbstractSessionDAO,AbstractSessionDAO實現了org.apache.shiro.session.mgt.eis.SessionDAO接口redis
package com.rongke.web.shiro; import com.rongke.redis.RedisClusterCache; 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 javax.annotation.Resource; import java.io.Serializable; import java.util.Collection; import java.util.Collections; /** * Created by cww on 2017/2/26. */ public class RedisSessionDao extends AbstractSessionDAO { @Resource private RedisClusterCache redisClusterCache; Logger log= LoggerFactory.getLogger(getClass()); @Override public void update(Session session) throws UnknownSessionException { log.info("更新seesion,id=[{}]",session.getId().toString()); try { redisClusterCache.putCache(session.getId().toString(),session); } catch (Exception e) { e.printStackTrace(); } } @Override public void delete(Session session) { log.info("刪除seesion,id=[{}]",session.getId().toString()); try { redisClusterCache.delCache(session.getId().toString()); } catch (Exception e) { e.printStackTrace(); } } @Override public Collection<Session> getActiveSessions() { log.info("獲取存活的session"); return Collections.emptySet(); } @Override protected Serializable doCreate(Session session) { Serializable sessionId = generateSessionId(session); assignSessionId(session, sessionId); log.info("建立seesion,id=[{}]",session.getId().toString()); try { redisClusterCache.putCache(sessionId.toString(),session); } catch (Exception e) { log.error(e.getMessage()); } return sessionId; } @Override protected Session doReadSession(Serializable sessionId) { log.info("獲取seesion,id=[{}]",sessionId.toString()); Session session = null; try { session = redisClusterCache.getCache(sessionId.toString()); } catch (Exception e) { log.error(e.getMessage()); } return session; } }
OK,到這裏就能實現集羣間的session共享問題,可是這裏有個注意的地方就是這裏不能限制單點登錄問題,因此我在我登錄的作了判斷,若是這次登錄的sessionIdspring
和我以前保存的不同,就將以前緩存裏面session移除。數據庫
public JsonResp login(String userName, String password){ Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(userName, Md5.md5Encode(password)); try { //踢除用戶 this.kickOutUser(token); }catch (Exception e) { String message = e.getMessage(); if (!message.contains("There is no session with id [")) { e.printStackTrace(); } }finally { this.kickOutUser(token); } try{ subject.login(token); Session session = subject.getSession(); Serializable sessionId = session.getId(); System.out.println("sessionId:"+sessionId); System.out.println("sessionHost:"+session.getHost()); System.out.println("sessionTimeout:"+session.getTimeout()); System.out.println(userName+"登陸成功"); User user = userDao.selectByUserName(userName); //刪除其餘登錄信息 if(user.getToken() != null &&!sessionId.toString().equals(user.getToken())){ redisClusterCache.delCache(user.getToken()); } user.setToken(sessionId.toString()); userDao.updateById(user); return JsonResp.ok(user); }catch(Exception e){ throw new FailException("用戶名或密碼錯誤!"); } }
就是我上面代碼標紅的地方,這樣也能解決單點登錄問題。apache