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