Spring MVC + Shiro + Redis 實現集羣會話管理

  以前用的單機Shiro實現用戶單點登錄,基本問題不大,可是集羣間的session共享單靠Shiro就很差實現了。因此就藉助Redis數據庫來實現。css

這裏Redis的搭建我以前說過,感興趣的能夠去看看:http://www.cnblogs.com/hz-cww/p/6074062.htmlhtml

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

相關文章
相關標籤/搜索