Shiro總結一會話,緩存管理,RememberMe

Shiro 會話管理

所謂會話,即用戶訪問應用時保持的鏈接關係,在屢次交互中應用可以識別出當前訪問的用戶是誰,且能夠在屢次交互中保存一些數據。如訪問一些網站時登陸成功後,網站能夠記住用戶,且在退出以前均可以識別當前用戶是誰。html

Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();

登陸成功後使用 Subject.getSession() 便可獲取會話;其等價於 Subject.getSession(true),即若是當前沒有建立 Session 對象會建立一個;另外 Subject.getSession(false),若是當前沒有建立 Session 則返回 null(不過默認狀況下若是啓用會話存儲功能的話在建立 Subject 時會主動建立一個 Session)。java

//獲取當前會話的惟一標識。
session.getId();
//獲取當前 Subject 的主機地址,該地址是經過 HostAuthenticationToken.getHost() 提供的。
session.getHost();
//獲取 ,設置當前 Session 的過時時間;若是不設置默認是會話管理器的全局過時時間。
session.getTimeout();
session.setTimeout(毫秒)
//獲取會話的啓動時間及最後訪問時間
session.touch();
session.stop()

更新會話最後訪問時間及銷燬會話;當 Subject.logout() 時會自動調用 stop 方法來銷燬會話。git

session.setAttribute("key", "123");
Assert.assertEquals("123", session.getAttribute("key"));
session.removeAttribute("key");

設置 / 獲取 / 刪除會話屬性;在整個會話範圍內均可以對這些屬性進行操做。web

Shiro 提供的會話能夠用於 JavaSE/JavaEE 環境,不依賴於任何底層容器,能夠獨立使用,是完整的會話模塊。redis

會話管理器

會話管理器管理着應用中全部 Subject 的會話的建立、維護、刪除、失效、驗證等工做。是 Shiro 的核心組件,頂層組件 SecurityManager 直接繼承了SessionManager,且提供了SessionsSecurityManager 實現直接把會話管理委託給相應的 SessionManager,DefaultSecurityManager 及 DefaultWebSecurityManager 默認 SecurityManager 都繼承了 SessionsSecurityManager。spring

Shiro 提供了三個默認實現:數據庫

DefaultSessionManager:DefaultSecurityManager 使用的默認實現,用於 JavaSE 環境;apache

ServletContainerSessionManager:DefaultWebSecurityManager 使用的默認實現,用於 Web 環境,其直接使用 Servlet 容器的會話;json

DefaultWebSessionManager:用於 Web 環境的實現,能夠替代 ServletContainerSessionManager,本身維護着會話,直接廢棄了 Servlet 容器的會話管理。跨域

在spring中注入會話管理 spring-shiro.xml,具體注入方式上一節已經講過,完整代碼地址參考 https://gitee.com/jiansin/ssm

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="redisSessionDAO" class="com.plantform.shiro.commons.RedisSessionDao"/>
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <!-- 設置session過時時間爲1小時(單位:毫秒),默認爲30分鐘 -->
        <property name="globalSessionTimeout" value="3600000"/>
        <property name="sessionValidationSchedulerEnabled" value="true"/>
        <property name="sessionDAO" ref="redisSessionDAO"/>
    </bean>


    <bean id="cacheManager" class="com.plantform.shiro.commons.RedisCacheManager">
        <property name="redisTemplate" ref="redisTemplate"/>
    </bean>
    <!-- Shiro默認會使用Servlet容器的Session,可經過sessionMode屬性來指定使用Shiro原生Session -->
    <!-- 即<property name="sessionMode" value="native"/>,詳細說明見官方文檔 -->
    <!-- 這裏主要是設置自定義的單Realm應用,如有多個Realm,可以使用'realms'屬性代替 -->
    <!-- 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="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <!-- 要求登陸時的連接(可根據項目的URL進行替換),非必須的屬性,默認會自動尋找Web工程根目錄下的"/login.html"頁面 -->
        <property name="loginUrl" value="/index.jsp"/>
        <!-- 用戶訪問未對其受權的資源時,所顯示的鏈接 -->
        <property name="unauthorizedUrl" value="/"/>
        <property name="filters">
            <map>
                <entry key="authc" value-ref="authenticationFilter"/>
            </map>
        </property>

        <!-- 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>
                /login.jsp=anon
                /system/captcha=anon
                /static/**=anon
                /system/logout = anon
                /system/login=anon
                /oauth/**=anon
                /error/**=anon
                /v2/**/=anon
                /webjars/**=anon
                /swagger-resources/**=anon
                /swagger-ui.html/**=anon
                /**=authc
            </value>
        </property>
    </bean>

    <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <property name="hashAlgorithmName" value="md5"/>
        <property name="hashIterations" value="2"/>
    </bean>
    <bean id="shiroRealm" class="com.plantform.shiro.commons.ShiroRealm">
        <property name="credentialsMatcher" ref="credentialsMatcher"/>
    </bean>

    <bean id="authenticationFilter" class="com.plantform.shiro.commons.ShiroAuthenticationFilter"/>

    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!-- AOP式方法級權限檢查  -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor">
        <property name="proxyTargetClass" value="true"/>
    </bean>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

</beans>

若是使用 ServletContainerSessionManager 進行會話管理,Session 的超時依賴於底層 Servlet 容器的超時時間,能夠在 web.xml 中配置其會話的超時時間(分鐘爲單位):

<session-config>
  <session-timeout>30</session-timeout>
</session-config>

會話監聽器

會話監聽器用於監聽會話建立、過時及中止事件:

public class MySessionListener1 implements SessionListener {
    @Override
    public void onStart(Session session) {//會話建立時觸發
        System.out.println("會話建立:" + session.getId());
    }
    @Override
    public void onExpiration(Session session) {//會話過時時觸發
        System.out.println("會話過時:" + session.getId());
    }
    @Override
    public void onStop(Session session) {//退出/會話過時時觸發
        System.out.println("會話中止:" + session.getId());
    }  
}

spring中注入shiro會話監聽器

<!-- shiroSessionListener  監聽類-->
<bean id="shiroSessionListener" class="com.listener.ShiroSessionListener"></bean>
<bean id="redisSessionDAO" class="com.plantform.shiro.commons.RedisSessionDao"/>
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    <!-- 設置session過時時間爲1小時(單位:毫秒),默認爲30分鐘 -->
    <property name="globalSessionTimeout" value="3600000"/>
    <property name="sessionValidationSchedulerEnabled" value="true"/>
    <property name="sessionDAO" ref="redisSessionDAO"/>
    <property name="sessionListeners">
        <list>
            <ref bean="shiroSessionListener"></ref>
        </list>
    </property>
</bean>

會話存儲 / 持久化

Shiro 提供 SessionDAO 用於會話的 CRUD,即 DAO(Data Access Object)模式實現:

//如DefaultSessionManager在建立完session後會調用該方法;如保存到關係數據庫/文件系統/NoSQL數據庫;便可以實現會話的持久化;返回會話ID;主要此處返回的ID.equals(session.getId());
Serializable create(Session session);
//根據會話ID獲取會話
Session readSession(Serializable sessionId) throws UnknownSessionException;
//更新會話;如更新會話最後訪問時間/中止會話/設置超時時間/設置移除屬性等會調用
void update(Session session) throws UnknownSessionException;
//刪除會話;當會話過時/會話中止(如用戶退出時)會調用
void delete(Session session);
//獲取當前全部活躍用戶,若是用戶量多此方法影響性能
Collection<Session> getActiveSessions();

redis實現會話持久化

spring-shiro.xml

<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
    <!-- 設置session過時時間爲1小時(單位:毫秒),默認爲30分鐘 -->
    <property name="globalSessionTimeout" value="3600000"/>
    <property name="sessionValidationSchedulerEnabled" value="true"/>
    <property name="sessionDAO" ref="redisSessionDAO"/>
</bean>
public class RedisSessionDao extends AbstractSessionDAO {
    private static final String sessionIdPrefix = "shiro-session-";
    private static final String sessionIdPrefix_keys = "shiro-session-*";
    //設置過時時間爲1小時(單位:毫秒),默認爲30分鐘 -->
    private static final long timeout = 3600000;
    private transient static Logger log = LoggerFactory.getLogger(RedisSessionDao.class);
    @Autowired
    private transient RedisTemplate<Serializable, Session> redisTemplate;

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = sessionIdPrefix + UUID.randomUUID().toString();
        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) {
        log.info("read shiro session ,sessionId is :{}", sessionId.toString());
        return redisTemplate.opsForValue().get(sessionId);
    }


    @Override
    public void update(Session session) throws UnknownSessionException {
        log.info("update shiro session ,sessionId is :{}", session.getId().toString());
        redisTemplate.opsForValue().set(session.getId(), session, timeout, TimeUnit.SECONDS);
    }

    @Override
    public void delete(Session session) {
        log.info("delete shiro session ,sessionId is :{}", session.getId().toString());
        redisTemplate.opsForValue().getOperations().delete(session.getId());
    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<Serializable> keys = redisTemplate.keys(sessionIdPrefix_keys);
        if (keys.size() == 0) {
            return Collections.emptySet();
        }
        List<Session> sessions = redisTemplate.opsForValue().multiGet(keys);
        return Collections.unmodifiableCollection(sessions);
    }
}

用戶在登陸的時候把session信息存入到數據庫中

/**
 * 登陸
 *
 * @param loginName 登陸名
 * @param password  密碼
 * @param platform  終端類型
 * @return
 */
@ApiOperation(value = "登陸", httpMethod = "POST", produces = "application/json", response = Result.class)
@ResponseBody
@RequestMapping(value = "login", method = RequestMethod.POST)
public Result login(@RequestParam String loginName,
                    @RequestParam String password,
                    @RequestParam int platform,
                    HttpServletRequest request) throws Exception {

    SysUser user = sysUserService.selectByLoginName(loginName);
    if (user == null) {
        return Result.instance(ResponseCode.unknown_account.getCode(), ResponseCode.unknown_account.getMsg());
    }
    if (user.getStatus() == 3) {
        return Result.instance(ResponseCode.forbidden_account.getCode(), ResponseCode.forbidden_account.getMsg());
    }
    Subject subject = SecurityUtils.getSubject();
    //這裏若是發生異常會拋出到繼承的類中去處理
    subject.login(new UsernamePasswordToken(loginName, password));
    //準備存入session信息到數據庫中
    LoginInfo loginInfo = sysUserService.login(user, subject.getSession().getId(), platform);
    subject.getSession().setAttribute("loginInfo", loginInfo);
    log.debug("登陸成功");
    return Result.success(loginInfo);
}

服務層中的實現方法

@Override
public LoginInfo login(SysUser user, Serializable id, int platform) {
    log.debug("sessionId is:{}", id.toString());
    LoginInfo loginInfo = new LoginInfo();
    BeanUtils.copyProperties(user, loginInfo);
    List<SysUserPermission> userPermissions = sysUserPermissionMapper.selectByUserId(user.getId());
    List<SysPermission> permissions = new ArrayList<>();
    for (SysUserPermission userPermission : userPermissions) {
        SysPermission sysPermission = sysPermissionMapper.selectById(userPermission.getSysPermissionId());
        permissions.add(sysPermission);
    }
    List<SysUserRoleOrganization> userRoleOrganizations = sysUserRoleOrganizationMapper.selectByUserId(user.getId());
    loginInfo.setJobs(userRoleOrganizations);

    SysLoginStatus newLoginStatus = new SysLoginStatus();
    newLoginStatus.setSysUserId(user.getId());
    newLoginStatus.setSysUserZhName(user.getZhName());
    newLoginStatus.setSysUserLoginName(user.getLoginName());
    newLoginStatus.setSessionId(id.toString());
    newLoginStatus.setSessionExpires(new DateTime().plusDays(30).toDate());
    newLoginStatus.setPlatform(platform);

    SysLoginStatus oldLoginStatus = sysLoginStatusMapper.selectByUserIdAndPlatform(user.getId(), platform);
    if (oldLoginStatus != null) {
        if (!oldLoginStatus.getSessionId().equals(id.toString())) {
            redisTemplate.opsForValue().getOperations().delete(oldLoginStatus.getSessionId());
        }
        oldLoginStatus.setStatus(2);
        sysLoginStatusMapper.update(oldLoginStatus);
        newLoginStatus.setLastLoginTime(oldLoginStatus.getCreateTime());
    }
    sysLoginStatusMapper.insert(newLoginStatus);
    return loginInfo;
}

緩存管理

<!-- 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="cacheManager" class="com.hunt.system.security.shiro.RedisCacheManager">
    <property name="redisTemplate" ref="redisTemplate"/>
</bean>
package com.hunt.system.security.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.hunt.util.SystemConstant;

import java.io.Serializable;

/**
 * @Author ouyangan
 * @Date 2016/10/9/14:13
 * @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;
    }
}


package com.hunt.system.security.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.hunt.util.SystemConstant;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @Author ouyangan
 * @Date 2016/10/9/13:55
 * @Description Cache   redis實現
 */
public class RedisCache<K, V> implements Cache<K, V>, Serializable {
    public static final String shiro_cache_prefix = "shiro-cache-";
    public static final String shiro_cache_prefix_keys = "shiro-cache-*";
    private static final long timeout = 2592000;
    private transient static Logger log = LoggerFactory.getLogger(RedisCache.class);

    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(shiro_cache_prefix + key);
    }

    @Override
    public V put(K key, V value) throws CacheException {
        log.debug("根據key:{}從redis刪除對象", key);
        redisTemplate.opsForValue().set((K) (shiro_cache_prefix + key), value, timeout, TimeUnit.SECONDS);
        return value;
    }

    @Override
    public V remove(K key) throws CacheException {
        log.debug("redis cache remove :{}", key.toString());
        V value = redisTemplate.opsForValue().get(shiro_cache_prefix + key);
        redisTemplate.delete(key);
        return value;
    }

    @Override
    public void clear() throws CacheException {
        log.debug("清除redis全部緩存對象");
        Set<K> keys = redisTemplate.keys((K) shiro_cache_prefix_keys);
        redisTemplate.delete(keys);
    }

    @Override
    public int size() {
        Set<K> keys = redisTemplate.keys((K) shiro_cache_prefix_keys);
        log.debug("獲取redis緩存對象數量:{}", keys.size());
        return keys.size();
    }

    @Override
    public Set<K> keys() {
        Set<K> keys = redisTemplate.keys((K)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) 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;
    }
}

RememberMe 實現記住密碼功能

安全性要求高的網站不建議有記住密碼功能,由於Cookie是保存在本機電腦瀏覽器中,不排除其餘用戶使用該電腦,複製走Cookie,導入其餘電腦繼續使用該帳號登陸。

spring-shiro.xml文件以下

<!-- 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"/>
    <!-- 記住我 -->
    <property name="rememberMeManager" ref="rememberMeManager"></property>
</bean>
<!-- 定義RememberMe功能的程序管理類 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
    <!-- 定義在進行RememberMe功能實現的時候所須要使用到的Cookie的處理類 -->
    <property name="cookie" ref="rememberMeCookie"/>
</bean>
<!-- 配置須要向Cookie中保存數據的配置模版(RememberMe) -->
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
    <!-- 設置Cookie在瀏覽器中保存內容的名字,由用戶本身來設置 -->
    <constructor-arg value="MLDNJAVA-RememberMe"/>
    <!-- 保證該系統不會受到跨域的腳本操做供給 -->
    <property name="httpOnly" value="true"/>
    <!-- 定義Cookie的過時時間爲一天設置securityManager安全管理器的rememberMeManager,具體配置以下:
	
	```
	```-->
    <property name="maxAge" value="86400"/>
</bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
	<property name="securityManager" ref="securityManager"/> 
	<property name="filterChainDefinitions"> 
	       <value>
	         /login.jsp = anon
                 /authenticated.jsp = authc
                 /logout = logout
	       	 /** = user
	       </value> 
	</property> 
</bean>

注意:/authenticated.jsp = authc」表示訪問該地址用戶必須身份驗證經過(Subject. isAuthenticated()==true);而「/** = user」表示訪問該地址的用戶是身份驗證經過或RememberMe登陸的均可以進行任何操做的。

LoginAuthRealm.java

// 認證信息
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
        try {
            UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
            String username = token.getUsername();
            SysUsers user = userSv.getByName(token.getUsername());
            if (!StringUtils.isBlank(username)) {
                if (user != null) {
                    return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
}

注:return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName());其中把用戶信息放入SimpleAuthenticationInfo對象,不能把整個user對象放入,否則會出現錯誤數組下標越界,在項目中user對象信息過於龐大,不能所有存入Cookie,Cookie對長度有必定的限制。

相關文章
相關標籤/搜索