使用shiro的会话管理和redis缓存管理来构建登录模块spring+struts+hibernate(SSH)

     shiro是一个很好用的安全框架,主要表现在用户认证,权限认证,会话管理,如果想优化还可以做Cache管理,我们不需要做太多工作在用户身份token安全方面(记录shiro及用redis开发的步骤及一些问题,因为网上很多资料都不给全代码让小白没法理解,这里我整合了一下,在最后给上项目资源链接,这篇文章是我两个星期实践后的体会,大牛不喜勿喷)。

        这篇是关于用shiro提供的会话接口和缓存接口去实现会话管理和缓存管理,优化登录模块,就不讲shiro基础怎么搭建了,如果想了解shiro的基础和使用请看我的上一篇’shiro基本配置‘(URL)。

        有些人可能会发现如果我在登录后将身份信息和角色以及权限信息存入session,每次我要访问其他页面时,都要从session里拿出来,这样的效率并不高,redis存储存结构化数据,存取都很快,虽然还有其他更适用于缓存的技术,shiro也可以用他自己家的EHCache啦,不过在这里我就说redis(因为我对redis有点迷恋,网站计数,消息啊都在用它,回到主题哈),用户模块实现redis缓存后快了很多。

        首先我们先搞定Cache管理(缓存)

spring-shiro.xml

shiro的安全管理器的配置,其他的包括过滤器zanshi不用理

 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm" /><!--Realm配置(基础)-->
        <property name="sessionManager" ref="sessionManager"/><!--这是session配置-->
        <property name="cacheManager" ref="customShiroCacheManager"/><!--这是我们自定义的Cache配置-->
 </bean>    

下面这部分代码最主要的就是定义了CustomShiroCacheManager这个bean了,因为他实现了shiro 提供的cacheManager接口,而其他都是被调用或注入的bean,


spring-shiro.xml

<!-- 这部分引用github的sojson的方法(这部分配置太麻烦了,有时间改为注解扫描注入 -->
    <!-- redis缓存管理器(用户缓存) *test-->
   	<bean id="customShiroCacheManager" class="com.usersAc.shiro.cache.impl.CustomShiroCacheManager">
	    <property name="shiroCacheManager" ref="jedisShiroCacheManager"/>
	</bean>
	
	<!-- shiro用redis实现缓存管理器 *test -->
	<bean id="jedisShiroCacheManager" class="com.usersAc.shiro.cache.impl.JedisShiroCacheManager">
	    <property name="jedisManager" ref="jedisManager"/>
	</bean>

    <!-- Redis缓存 *test-->  
	<bean id="jedisManager" class="com.usersAc.shiro.cache.JedisManager">
	    <property name="jedisPool" ref="jedisPool"/>
	</bean>



jedisPool > jedisManager > jedisShiroCacheManager > customShiroCacheManager >securityManager.cacheManager


jedisPool-----就是我们jedis的连接池(配置在下面)

jedisManager -----是我们jedis管理器(自定义),用来定义对redis的操作

jedisShiroCacheManager-----调用getCache()返回JedisShiroCache(权限操作类)

JedisShiroCache-----实现了ache接口,将权限信息存入redis缓存或从redis缓存取出

customShiroCacheManager-----实现了shiro 提供的cacheManager接口,作为Cache管理器

(这些类我也会贴在下面供理解)


下面部分是redis的配置,这里没有用redisTemplate,用了一般的配置方法,没有太多封装好的方法,有需求就可以自己定义

spring-shiro.xml

<!-- redis池的配置 -->
    <bean id="jedisPoolConfig"  
        class="redis.clients.jedis.JedisPoolConfig">  
        <property name="maxIdle" value="${redis.maxIdle}" />  
        <property name="testOnBorrow" value="${redis.testOnBorrow}" />  
    </bean>
    <!-- 我们上面说的jedisPool的配置(配置host,端口,超时,其他默认)-->  
 	<bean id="jedisPool" class="redis.clients.jedis.JedisPool">  
  		<constructor-arg name="poolConfig" ref="jedisPoolConfig" />
        <constructor-arg name="host" value="${redis.host}" />
        <constructor-arg name="port" value="${redis.port}" type="int" />
        <constructor-arg name="timeout" value="${redis.timeout}" type="int" />
	</bean>  
<!-- redis的context:placeholder ,扫描redis.properties的参数-->
    <bean id="propertyConfigurerRedis"  
        class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">  
        <property name="order" value="1" />  
        <property name="ignoreUnresolvablePlaceholders" value="true" />  
        <property name="systemPropertiesMode" value="1" />  
        <property name="searchSystemEnvironment" value="true" />  
        <property name="locations">  
        <list>  
            <value>classpath:redis.properties</value>  
        </list>  
        </property>  
    </bean>

redis.properties

redis.host=127.0.0.1   
redis.port=6379 
redis.default.db=1  
redis.timeout=100000  
redis.maxActive=300  
redis.maxIdle=100  
redis.maxWait=1000  
redis.testOnBorrow=true 

贴部分注入或调用的bean

customShiroCacheManager 

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.util.Destroyable;
import com.usersAc.shiro.cache.ShiroCacheManager;

/**
 * 这里的shiroCacheManager会被(jedisShiroCacheManager)注入,
 * jedisPool > jedisManager > jedisShiroCacheManager > customShiroCacheManager >securityManager.cacheManager
 * 
 */
public class CustomShiroCacheManager implements CacheManager, Destroyable {

    private ShiroCacheManager shiroCacheManager;//实际注入了JedisShiroCacheManager,而ShiroCacheManager是解耦接口
    
    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        return getShiroCacheManager().getCache(name);
    }
    
    @Override
    public void destroy() throws Exception {
        shiroCacheManager.destroy();
    }

    public ShiroCacheManager getShiroCacheManager() {
        return shiroCacheManager;
    }

    public void setShiroCacheManager(ShiroCacheManager shiroCacheManager) {
        this.shiroCacheManager = shiroCacheManager;
    }

}

JedisShiroCacheManager(实现了ShiroCacheManager(作为解耦的接口)

import org.apache.shiro.cache.Cache;
import com.usersAc.shiro.cache.JedisManager;
import com.usersAc.shiro.cache.JedisShiroCache;
import com.usersAc.shiro.cache.ShiroCacheManager;

/**
 * 注入JedisManager(redis底层操作类)
 * 身份信息由sessionManager处理
 * 返回JedisShiroCache(权限操作类)
 */
public class JedisShiroCacheManager implements ShiroCacheManager {


    private JedisManager jedisManager;   //注入了JedisManager
@Override
    public <K, V> Cache<K, V> getCache(String name) {
        return new JedisShiroCache<K, V>(name, getJedisManager());
    }
    @Override
    public void destroy() {
    }
    public JedisManager getJedisManager() {
        return jedisManager;
    }
    public void setJedisManager(JedisManager jedisManager) {
        this.jedisManager = jedisManager;
    }
}


JedisManager

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import org.apache.shiro.session.Session;

import com.usersAc.common.utils.LoggerUtils;
import com.usersAc.common.utils.SerializeUtil;
import com.usersAc.common.utils.StringUtils;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.exceptions.JedisConnectionException;



/**
 * Redis Manager Utils
 * 
 * 这部分用来定义对redis的操作(伪底层,即还有上层调用)
 */
public class JedisManager {
	/*注入连接池的bean*/
    private JedisPool jedisPool;

    public Jedis getJedis() {
        Jedis jedis = null;
        try {
        	/*获取连接池资源*/
            jedis = getJedisPool().getResource();
        } catch (JedisConnectionException e) {
        	String message = StringUtils.trim(e.getMessage());
        	if("Could not get a resource from the pool".equalsIgnoreCase(message)){
        		System.out.println("检查redis是否启动");
        		System.exit(0);//停止项目
        	}
        	throw new JedisConnectionException(e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return jedis;
    }
    /*
     * 
     * 返回资源--资源释放
     * 
     */
    public void returnResource(Jedis jedis, boolean isBroken) {
        if (jedis == null)
            return;
        /**
         * @deprecated starting from Jedis 3.0 this method will not be exposed.
         * Resource cleanup should be done using @see {@link redis.clients.jedis.Jedis#close()}
        if (isBroken){
            getJedisPool().returnBrokenResource(jedis);
        }else{
            getJedisPool().returnResource(jedis);
        }
        */
        /* 这里本来是
         * jedis.close();
         * 但现在我的jedis版本太低,要至少2.9
         * close是将连接返回,使多次使用的redis的连接都是同一个,不会产生在连接数限制数那么多连接
         * 下面这段是quit掉连接,并且如果isConnected(),则socket.close()<!--disconnect()-->关闭socket
         * socket的close和shutdown
         * close-----关闭本进程的socket id,但链接还是开着的,用这个socket id的其它进程还能用这个链接,能读或写这个socket id
         * shutdown--则破坏了socket 链接,读的时候可能侦探到EOF结束符,写的时候可能会收到一个SIGPIPE信号,这个信号可能直到
         */
        if (isBroken)
        	getJedisPool().returnBrokenResource(jedis);
        else
        	getJedisPool().returnResource(jedis);
       /* jedis.quit();
        jedis.disconnect();*/
    }

    public byte[] getValueByKey(int dbIndex, byte[] key) throws Exception {
        Jedis jedis = null;
        byte[] result = null;
        boolean isBroken = false;
        try {
            jedis = getJedis();
            jedis.select(dbIndex);
            result = jedis.get(key);
        } catch (Exception e) {
            isBroken = true;
            throw e;
        } finally {
            returnResource(jedis, isBroken);
        }
        return result;
    }

    public void deleteByKey(int dbIndex, byte[] key) throws Exception {
        Jedis jedis = null;
        boolean isBroken = false;
        try {
            jedis = getJedis();
            jedis.select(dbIndex);
            Long result = jedis.del(key);
            LoggerUtils.fmtDebug(getClass(), "删除Session结果:%s" , result);
        } catch (Exception e) {
            isBroken = true;
            throw e;
        } finally {
            returnResource(jedis, isBroken);
        }
    }

    public void saveValueByKey(int dbIndex, byte[] key, byte[] value, int expireTime)
            throws Exception {
        Jedis jedis = null;
        boolean isBroken = false;
        try {
            jedis = getJedis();
            jedis.select(dbIndex);
            jedis.set(key, value);
            if (expireTime > 0)
                jedis.expire(key, expireTime);
        } catch (Exception e) {
            isBroken = true;
            throw e;
        } finally {
            returnResource(jedis, isBroken);
        }
    }

    public JedisPool getJedisPool() {
        return jedisPool;
    }

    public void setJedisPool(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

	/**
	 * 获取所有Session
	 * @param dbIndex
	 * @param redisShiroSession
	 * @return
	 * @throws Exception
	 */
	@SuppressWarnings("unchecked")
	public Collection<Session> AllSession(int dbIndex, String redisShiroSession) throws Exception {
		Jedis jedis = null;
        boolean isBroken = false;
        Set<Session> sessions = new HashSet<Session>();
		try {
            jedis = getJedis();
            jedis.select(dbIndex);
            
            Set<byte[]> byteKeys = jedis.keys((JedisShiroSessionRepository.REDIS_SHIRO_ALL).getBytes());  
            if (byteKeys != null && byteKeys.size() > 0) {  
                for (byte[] bs : byteKeys) {  
                	Session obj = SerializeUtil.deserialize(jedis.get(bs),  
                    		 Session.class);  
                     if(obj instanceof Session){
                    	 sessions.add(obj);  
                     }
                }  
            }  
        } catch (Exception e) {
            isBroken = true;
            throw e;
        } finally {
            returnResource(jedis, isBroken);
        }
        return sessions;
	}
}

代码量比较多,我都放在项目的我都放在我项目里的com.userAc.shiro.cache文件夹下了(有需要文章下面取)


Cache的配置工作基本做完,可能会有人觉得配置麻烦,后面还有会话管理的配置和cookie的配置,不过为了展示好整个shiro准备工作,方便理解,我下次再用注解或Template去简化。


接下来讲Session的配置

jedisShiroSessionRepository------使用jedis管理器,这部分主要是用户身份的token的缓存存取,这里的JedisManager在上面Cache那有,可以自己看下。

customShiroSessionDAO------继承了shiro 提供的AbstractSessionDAO接口作为监听用的DAO

customSessionManager------手动操作session,暂时不需要用到,可以获取有效session用户或用户的所有权限再用,现在我们仅仅是做session存取登录的token

customSesssionListener------shiro的监听类,监听AuthorizingRealmd类的继承实现Realm(其实是监听CachingRealm类,而AuthorizingRealmd类是CachingRealm类的子类)

sessionManager------实现会话管理的主配置,要配置在shiro的securityManager

<property name="sessionManager" ref="sessionManager"/><!--这是session配置-->

spring-shiro.xml

<!-- (自定义)session 操作。。。创建、删除、查询 -->
	<bean id="jedisShiroSessionRepository" class="com.usersAc.shiro.cache.JedisShiroSessionRepository" >
		 <property name="jedisManager" ref="jedisManager"/>
	</bean>
	
    <!-- Shiro对应(自定义)session的监听 -->
	<bean id="customShiroSessionDAO" class="com.usersAc.shiro.CustomShiroSessionDAO">
	    <property name="shiroSessionRepository" ref="jedisShiroSessionRepository"/>
	    <property name="sessionIdGenerator" ref="sessionIdGenerator"/>
	</bean>
	
	<!-- 手动操作Session,管理Session(暂时不需要用到)-->
	<bean id="customSessionManager" class="com.usersAc.shiro.session.CustomSessionManager">
		<property name="shiroSessionRepository" ref="jedisShiroSessionRepository"/>
		<property name="customShiroSessionDAO" ref="customShiroSessionDAO"/>
	</bean>
	
	<bean id="customSessionListener" class="com.usersAc.shiro.listenter.CustomSessionListener">
	    <property name="shiroSessionRepository" ref="jedisShiroSessionRepository"/>
	</bean>
	
	<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
		<!-- 相隔多久检查一次session的有效性   -->
	 	<property name="sessionValidationInterval" value="1800000"/>  
	 	<!-- session 有效时间为半小时 (毫秒单位)-->  
		<property name="globalSessionTimeout" value="1800000"/>
	   	<property name="sessionDAO" ref="customShiroSessionDAO"/>
	   	<!-- session 监听,可以多个。 -->
	   	<property name="sessionListeners">                 <!--这里是监听类-->
	       <list>
	           <ref bean="customSessionListener"/>
	       </list>
	   	</property>
	</bean>
	

会话管理的配置基本可以了

这里不贴代码了,在我项目里看会好点,我主要是把配置的问题解释清楚

如果要使用redis缓存记得打开redisfuwu



最后是Cookie配置

这部分主要是为了用shiro 的rememberMe来管理Cookie,比如让客户端在几天或半个月记住登录状态,原本客户端登录时只需要存sessionId在Cookie里就行了,如果rememberMe则会存用户用户名密码权限等信息在客户端,如果没有加密的话会不安全,所以下面这部分除了加密都是版式的

spring-shiro.xml

<!-- 用户信息记住我功能的相关配置 -->
	<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
	    <constructor-arg value="v_v-re-baidu"/>
	    <property name="httpOnly" value="true"/>
	    <!-- 配置存储rememberMe Cookie的domain为 一级域名
	    <property name="domain" value=".itboy.net"/>
	     -->
	    <property name="maxAge" value="2592000"/><!-- 30天时间,记住我30天 -->
	</bean>
    <!-- rememberMe管理器 -->
    <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
        <!-- rememberMe cookie加密的密钥 建议每个项目都不一样,自定义加密-->
        <property name="cipherKey"
                  value="#{T(org.apache.shiro.codec.Base64).decode('3AvVhmFLUs0KTA3Kprsdag==')}"/>
        <property name="cookie" ref="rememberMeCookie"/>
    </bean>
	

用户开启记住我选项后

UsernamePasswordToken token=new UsernamePasswordToken(usern,passd);
token.setRememberMe(true);

这里我是写在Action里的,如果用户选择记住我为true,则setRememberMe(true)

实现后可以在浏览器的Cookie里看到rememberme的cookie,像我上面设的有效时间为30天,则会在30天后才失效



此处为项目资源(URL)https://github.com/Sirenes/shiro-redis-SSH


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
本项目详细介绍请看:http://www.sojson.com/shiro (强烈推荐) Demo已经部署到线上,地址是http://shiro.itboy.net, 管理员帐号:admin,密码:sojson.com 如果密码错误,请用sojson。 PS:你可以注册自己的帐号,然后用管理员赋权限给你自己的帐号,但是,每20分钟会把数据初始化一次。建议自己下载源码,让Demo跑起来,然后跑的更快,有问题加群解决。 声明: 本人提供这个Shiro + SpringMvc + Mybatis + Redis 的Demo 本着学习的态度,如果有欠缺和不足的地方,给予指正,并且多多包涵。 “去其糟粕取其精华”。如果觉得写的好的地方就给个赞,写的不好的地方,也请多多包涵。 使用过程: 1.创建数据库。 创建语句 :tables.sql 2.插入初始化数据 插入初始化数据:init.data.sql 3.运行。 管理员帐号:admin 密码:sojson ps:定时任务的sql会把密码改变为sojson.com 新版本说明:http://www.sojson.com/blog/164.html 和 http://www.sojson.com/blog/165.html 主要解决是之前说的问题:Shiro 教程,关于最近反应的相关异常问题,解决方法合集。 项目在本页面的附件中提取。 一、Cache配置修改。 配置文件(spring-cache.xml )中已经修改为如下配置: <!-- redis 配置,也可以把配置挪到properties配置文件中,再读取 --> <!-- 这种 arguments 构造的方式,之前配置有缺点。 这里之前的配置有问题,因为参数类型不一致,有时候jar和环境的问题,导致参数根据index对应,会处理问题, 理论上加另一个 name,就可以解决,现在把name 和type都加上,更保险。 --> 二、登录获取上一个URL地址报错。 当没有获取到退出前的request ,为null 的时候会报错。在(UserLoginController.java )135行处有所修改。 /** * shiro 获取登录之前的地址 * 之前0.1版本这个没判断空。 */ SavedRequest savedRequest = WebUtils.getSavedRequest(request); String url = null ; if(null != savedRequest){ url = savedRequest.getRequestUrl(); } /** * 我们平常用的获取上一个请求的方式,在Session不一致的情况下是获取不到的 * String url = (String) request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE); */ 三、删除了配置文件中的cookie写入域的问题。 在配置文件里(spring-shiro.xml )中的配置有所修改。 <!-- 会话Cookie模板 --> <!--cookie的name,我故意取名叫xxxxbaidu --> <!--cookie的有效时间 --> <!-- 配置存储Session Cookie的domain为 一级域名 --> 上面配置是去掉了 Session 的存储Key 的作用域,之前设置的.itboy.net ,是写到当前域名的 一级域名 下,这样就可以做到N 个 二级域名 下,三级、四级....下 Session 都是共享的。 <!-- 用户信息记住我功能的相关配置 --> <!-- 配置存储rememberMe Cookie的domain为 一级域名 --> <!-- 30天时间,记住我30天 --> 记住我登录的信息配置。和上面配置是一样的道理,可以在相同 一级域名 下的所有域名都可以获取到登录的信息。 四、简单实现了单个帐号只能在一处登录。 我们在其他的系统中可以看到,单个帐号只允许一人使用,在A处登录了,B处再登录,那A处就被踢出了。如下图所示。 但是此功能不是很完美,当A处被踢出后,再重新登录,这时候B处反应有点慢,具体我还没看,因为是之前加的功能,现在凌晨了,下次我有空再瞧瞧,同学你也可以看看,解决了和我说一声,我把功能修复。 五、修复功能(BUG) 1.修复权限添加功能BUG。 之前功能有问题,每当添加一个权限的时候,默认都给角色为“管理员”的角色默认添加当前新添加的权限。这样达到管理员的权限永远是最大的。由于代码有BUG ,导致所有权限删除了。现已修复。 2.修复项目只能部署到Root目录下的问题。 问题描述:之前项目只能部署到Root 下才能正常运行,目前已经修复,可以带项目路径进行访问了,之前只能这样访问,http://localhost:8080 而不能http://localhost:8080/shiro.demo/ 访问,目前是可以了。 解决方案:在 FreeMarkerViewExtend.java 33行处 增加了BasePath ,通过BasePath 来控制请求目录,在 Freemarker 中可以自由使用,而 JSP 中是直接在 JSP 中获取BasePath 使用。 解决后遗症:因为我们的权限是通过URL 来控制的,那么增加了项目的目录,导致权限不能正确的判断,再加上我们的项目名称(目录)可以自定义,导致更不好判断。 后遗症解决方案:PermissionFilter.java 50行处 解决了这个问题,详情请看代码和注释,其实就是replace 了一下。 HttpServletRequest httpRequest = ((HttpServletRequest)request); /** * 此处是改版后,为了兼容项目不需要部署到root下,也可以正常运行,但是权限没设置目前必须到root 的URI, * 原因:如果你把这个项目叫 ShiroDemo,那么路径就是 /ShiroDemo/xxxx.shtml ,那另外一个人使用,又叫Shiro_Demo,那么就要这么控制/Shiro_Demo/xxxx.shtml * 理解了吗? * 所以这里替换了一下,使用根目录开始的URI */ String uri = httpRequest.getRequestURI();//获取URI String basePath = httpRequest.getContextPath();//获取basePath if(null != uri && uri.startsWith(basePath)){ uri = uri.replace(basePath, ""); } 3.项目启动的时候报错,关于JNDI的错误提示。 其实也不是错,但是看着不舒服,所以还得解决这个问题。解决这个问题需要在web.xml 中的开始部位加入以下代码。 spring.profiles.active dev spring.profiles.default dev spring.liveBeansView.mbeanDomain dev 4.项目Maven打包问题。 打包的时候,不同版本的 Eclipse 还有IDEA 会有打包打不进去Mapper.xml 文件,这个时候要加如下代码(群里同学提供的)。 src/main/java **/*.properties **/*.xml false 在 标签内加入即可,如果还是不能解决,那么请你加群(改名后)说明你的问题,有人会回答你。 5.Tomcat7以上在访问JSP页面的时候,提示JSTL错误。 这个错误是因为Tomcat7 中没有 JSTL 的jar包,现在已经在项目pom.xml 中增加了如下 jar 的引入管理。 javax.servlet jstl 1.2 javax.servlet jsp-api 2.0 provided 如果还是不能解决问题,请在官方群(群号:259217951)内搜索“jstl” 如图下载依赖包。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值