194°

Shiro总结一会话,缓存管理,RememberMe

Shiro 会话管理

所谓会话,即用户访问应用时保持的连接关系,在多次交互中应用能够识别出当前访问的用户是谁,且可以在多次交互中保存一些数据。如访问一些网站时登录成功后,网站可以记住用户,且在退出之前都可以识别当前用户是谁。

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

登录成功后使用 Subject.getSession() 即可获取会话;其等价于 Subject.getSession(true),即如果当前没有创建 Session 对象会创建一个;另外 Subject.getSession(false),如果当前没有创建 Session 则返回 null(不过默认情况下如果启用会话存储功能的话在创建 Subject 时会主动创建一个 Session)。

//获取当前会话的唯一标识。
session.getId();
//获取当前 Subject 的主机地址,该地址是通过 HostAuthenticationToken.getHost() 提供的。
session.getHost();
//获取 ,设置当前 Session 的过期时间;如果不设置默认是会话管理器的全局过期时间。
session.getTimeout();
session.setTimeout(毫秒)
//获取会话的启动时间及最后访问时间
session.touch();
session.stop()

更新会话最后访问时间及销毁会话;当 Subject.logout() 时会自动调用 stop 方法来销毁会话。

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

设置 / 获取 / 删除会话属性;在整个会话范围内都可以对这些属性进行操作。

Shiro 提供的会话可以用于 JavaSE/JavaEE 环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。

会话管理器

会话管理器管理着应用中所有 Subject 的会话的创建、维护、删除、失效、验证等工作。是 Shiro 的核心组件,顶层组件 SecurityManager 直接继承了SessionManager,且提供了SessionsSecurityManager 实现直接把会话管理委托给相应的 SessionManager,DefaultSecurityManager 及 DefaultWebSecurityManager 默认 SecurityManager 都继承了 SessionsSecurityManager。

Shiro 提供了三个默认实现:

DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;

ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 Web 环境,其直接使用 Servlet 容器的会话;

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对长度有一定的限制。

本文由【大笨象会跳舞吧】发布于开源中国,原文链接:https://my.oschina.net/jiansin/blog/3024947

全部评论: 0

    我有话说: