SSM+Shrio+redis对session管理的实现
1.整合shrio + redis
<!--shiro权限控制器-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${
shiro-version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>${
shiro-version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${
shiro-version}</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${
shiro-version}</version>
</dependency>
<!-- Redis 相关依赖 -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.6.1.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
2.resources目录配置 添加spring-shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"
default-lazy-init="true">
<description>Shiro Configuration</description>
<!-- 自定义退出路径 -->
<bean id="logout" class="org.apache.shiro.web.filter.authc.LogoutFilter">
<property name="redirectUrl" value="/api/login/logout_success"/>
</bean>
<!-- 安全认证过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/api/login/loginAgain"/>
<property name="unauthorizedUrl" value="/api/login/unauthorized_err"/>
<property name="filterChainDefinitions">
<value>
<!--对静态资源设置匿名访问-->
<!--/images/**=anon
/js/**=anon
/style/**=anon-->
<!--swagger相关配置不拦截-->
<!--/swagger-ui.html=anon
/swagger-resources=anon
/v2/api-docs=anon
/webjars/** = anon
/swagger/**=anon
/configuration/**=anon-->
<!--druid连接池不拦截-->
<!--/druid = anon-->
/api/login/verifycode = anon
/api/login/channellogin = anon
/api/login/managelogin = anon
/api/login/logout_success = anon
/api/logout = logout
<!--/**=anon 表示所有的url都可以匿名访问,anon是shiro中一个过滤器的简写,authc表示拦截-->
/api/** = authc
</value>
</property>
<property name="filters">
<map>
<!--定义登出过滤器的重定向url-->
<entry key="logout" value-ref="logout" />
</map>
</property>
</bean>
<!-- 配置Shiro安全管理配置 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="myRealm"/>
<property name="cacheManager" ref="redisCacheManager"/>
<property name="sessionManager" ref="redisSessionManager"/>
</bean>
<!--开启aop对类的代理-->
<aop:config proxy-target-class="true"></aop:config>
<!--开启shiro注解支持-->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"></property>
</bean>
<!--shiro统一异常处理-->
<!--<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<!–没有权限异常–>
<prop key="org.apache.shiro.authz.UnauthorizedException">redirect:/nopermission</prop>
</props>
</property>
</bean>-->
<!--自定义凭证匹配器-->
<bean id="myCredentialsMatcher" class="com.rjwl.api.security.MyHashedCredentialsMatcher">
<property name="hashAlgorithmName" value="md5"></property>
<property name="hashIterations" value="3"></property>
</bean>
<!--配置自定义realm-->
<bean id="myRealm" class="com.rjwl.api.security.MyRealm">
<!--配置凭证匹配器-->
<!--<property name="credentialsMatcher" ref="credentialsMatcher"/>-->
<property name="credentialsMatcher" ref="myCredentialsMatcher"/>
<property name="adminService" ref="adminServiceImpl"/>
<property name="menuService" ref="menuServiceImpl"/>
</bean>
<!--自定义RedisCacheManager-->
<bean id="redisCacheManager" class="com.rjwl.api.security.RedisCacheManager">
<!--Authorization cache过期时间 单位s-->
<property name="timeout" value="1800"/>
</bean>
<!--redis来管理session-->
<bean id="redisSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionDAO" ref="redisSessionDAO"/>
<property name="sessionIdCookie" ref="shareSession"/>
<property name="sessionIdUrlRewritingEnabled" value="false"/>
<!--删除失效session-->
<property name="deleteInvalidSessions" value="true"/>
<!--超时ms-->
<property name="globalSessionTimeout" value="1800000"/>
<!-- 定时检查失效的session -->
<property name="sessionValidationSchedulerEnabled" value="true"/>
<!--隔多久检查一次失效的session-->
<property name="sessionValidationInterval" value="900000"/>
</bean>
<!--自定义redisSessionDAO-->
<bean id="redisSessionDAO" class="com.rjwl.api.security.RedisSessionDao">
<constructor-arg ref="redisTemplate"/>
<property name="sessionIdGenerator">
<bean class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>
</property>
</bean>
<!--sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID-->
<bean id="shareSession" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg name="name" value="SHAREJSESSIONID"/>
<property name="path" value="/"/>
<!-- 浏览器关闭session失效,不计入cookie -->
<property name="maxAge" value="-1"/>
</bean>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
</beans>
3.resources 目录 下spring.xml配置 redis 配置
<!-- 引入jdbc、redis配置文件 -->
<context:property-placeholder ignore-unresolvable="true" location="classpath:properties/*.properties"/>
<!-- jedis连接池配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="${redis.maxIdle}"/>
<property name="maxWaitMillis" value="${redis.maxWait}"/>
<property name="testOnBorrow" value="${redis.testOnBorrow}"/>
<property name="maxTotal" value="${redis.maxTotal}"/>
<property name="blockWhenExhausted" value="true"/>
</bean>
<!-- jedis连接工程的配置 -->
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${redis.host}"/>
<property name="port" value="${redis.port}"/>
<property name="poolConfig" ref="jedisPoolConfig"/>
<property name="password" value="${redis.password}"/>
<property name="usePool" value="true"/>
</bean>
<!-- redisTemplate配置 -->
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="valueSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
<property name="hashKeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="hashValueSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
</bean>
<bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="jedisConnectionFactory"/>
</bean>
<bean id="redisUtil" class="com.rjwl.api.common.utils.RedisUtil">
<property name="redisTemplate" ref="stringRedisTemplate"/>
</bean>
4.security目录下
package com.rjwl.api.security;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
*
* 使用redis来管理session信息
*
*/
public class RedisSessionDao extends AbstractSessionDAO {
private static Logger LOGGER = LogManager.getLogger(RedisSessionDao.class);
/**
* key前缀
*/
private static final String SHIRO_REDIS_SESSION_KEY_PREFIX = "shiro:redis.session_";
private RedisTemplate redisTemplate;
private ValueOperations valueOperations;
public RedisSessionDao(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
this.valueOperations = redisTemplate.opsForValue();
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("shiro redis session create. sessionId={"+sessionId+"}");
}
this.assignSessionId(session, sessionId);
valueOperations.set(generateKey(sessionId), session, session.getTimeout(), TimeUnit.MILLISECONDS);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("shiro redis session read. sessionId={"+sessionId+"}");
}
return (Session) valueOperations.get(generateKey(sessionId));
}
@Override
public void update(Session session) throws UnknownSessionException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("shiro redis session update. sessionId={"+session.getId()+"}");
}
valueOperations.set(generateKey(session.getId()), session, session.getTimeout(), TimeUnit.MILLISECONDS);
}
@Override
public void delete(Session session) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("shiro redis session delete. sessionId={"+session.getId()+"}");
}
redisTemplate.delete(generateKey(session.getId()));
}
@Override
public Collection<Session> getActiveSessions() {
Set<Object> keySet = redisTemplate.keys(generateKey("*"));
Set<Session> sessionSet = new HashSet<>();
if (keySet==null||keySet.isEmpty()) {
return Collections.emptySet();
}
for (Object key : keySet) {
sessionSet.add((Session) valueOperations.get(key));
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("shiro redis session all. size={"+keySet.size()+"}");
}
return sessionSet;
}
/**
* 重组key
* 区别其他使用环境的key
*
* @param key
* @return
*/
private String generateKey(Object key) {
return SHIRO_REDIS_SESSION_KEY_PREFIX + this.getClass().getName() +"_"+ key;
}
}
package com.rjwl.api.security;
import com.rjwl.api.entity.Admin;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public class RedisCacheManager implements CacheManager {
private String cacheKeyPrefix = "shiro:";
private long timeout;
public void setTimeout(long timeout) {
this.timeout = timeout;
}
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
return new ShiroRedisCache<K, V>(cacheKeyPrefix + name,timeout);
}
/**
* 为shiro量身定做的一个redis cache,为Authorization cache做了特别优化
*/
public class ShiroRedisCache<K, V> implements Cache<K, V> {
private String cacheKey;
private long authorizationCacheTimeout;
public ShiroRedisCache(String cacheKey,long authorizationCacheTimeout) {
this.cacheKey = cacheKey;
this.authorizationCacheTimeout = authorizationCacheTimeout;
}
@Override
public V get(K key) throws CacheException {
BoundHashOperations<String, K, V> hash = redisTemplate.boundHashOps(cacheKey);
Object k = hashKey(key);
//获取Authorization cache 刷新过期时间
hash.expire(authorizationCacheTimeout, TimeUnit.SECONDS);
return hash.get(k);
}
@Override
public V put(K key, V value) throws CacheException {
BoundHashOperations<String, K, V> hash = redisTemplate.boundHashOps(cacheKey);
Object k = hashKey(key);
hash.put((K) k, value);
//为Authorization cache添加过期时间
hash.expire(authorizationCacheTimeout, TimeUnit.SECONDS);
return value;
}
@Override
public V remove(K key) throws CacheException {
BoundHashOperations<String, K, V> hash = redisTemplate.boundHashOps(cacheKey);
Object k = hashKey(key);
V value = hash.get(k);
hash.delete(k);
return value;
}
@Override
public void clear() throws CacheException {
redisTemplate.delete(cacheKey);
}
@Override
public int size() {
BoundHashOperations<String, K, V> hash = redisTemplate.boundHashOps(cacheKey);
return hash.size().intValue();
}
@Override
public Set<K> keys() {
BoundHashOperations<String, K, V> hash = redisTemplate.boundHashOps(cacheKey);
return hash.keys();
}
@Override
public Collection<V> values() {
BoundHashOperations<String, K, V> hash = redisTemplate.boundHashOps(cacheKey);
return hash.values();
}
protected Object hashKey(K key) {
//此处很重要,如果key是登录凭证,那么这是访问用户的授权缓存;将登录凭证转为admin对象,返回admin的id属性做为hash key,否则会以admin对象做为hash key,这样就不好清除指定用户的缓存了
if (key instanceof PrincipalCollection) {
PrincipalCollection pc = (PrincipalCollection) key;
Admin admin = (Admin) pc.getPrimaryPrincipal();
return admin.getAdminName();
}
return key;
}
}
}
package com.rjwl.api.security;
import com.rjwl.api.entity.Admin;
import com.rjwl.api.entity.Menu;
import com.rjwl.api.service.AdminService;
import com.rjwl.api.service.MenuService;
import lombok.Setter;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import java.util.ArrayList;
import java.util.List;
/**
* shiro 自定义realm
*/
public class MyRealm extends AuthorizingRealm {
@Setter
private AdminService adminService;
@Setter
private MenuService menuService;
//盐值 自定义
private static final String SALT = "RUIJING_QIANG";
/* private static final String AUTHORIZATION_CACHE_NAME = "authorization";
public MyRealm() {
super.setAuthorizationCacheName(AUTHORIZATION_CACHE_NAME);
}*/
@Override
public String getName() {
return "MyRealm";
}
/**
* 认证
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
Admin admin = adminService.getMapper().selectAdminByName(username);
if (admin == null) {
return null;
}
// clearAuthorizationInfoCache(admin);//用户登录后,清除用户缓存,以便重新加载用户权限
return new SimpleAuthenticationInfo(admin, admin.getPassword(), ByteSource.Util.bytes(SALT), getName());
}
/**
* 授权
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取当前操作用户信息
Admin admin = (Admin) principals.getPrimaryPrincipal();
//权限编码 user:edit
List<String> permissions = new ArrayList<>();
//角色简称 sysMgr
List<String> roles = new ArrayList<>();
//admin用户
if ("admin".equals(admin.getAdminName())) {
//拥有所有权限
permissions.add("*:*");
//查询所有角色
// roles 缺少编码字段 sysMgr
roles.add(admin.getRoleId().toString());
} else {
roles.add(admin.getRoleId().toString());
List<Menu> menus = menuService.getMapper().selectMenusByRoleId(admin.getRoleId());
for (Menu menu : menus) {
//menu 中权限表达式menuCode **:**格式 与 congtroller 中方法 @RequestPermission注解的value 对应
permissions.add(menu.getMenuCode());
}
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissions);
info.addRoles(roles);
return info;
}
/**
* 清除所有用户的缓存
*/
/* public void clearAuthorizationInfoCache() {
Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
if (cache != null) {
cache.clear();
}
}
*/
/**
* 清除指定用户的缓存
*
* @param
*/
/*private void clearAuthorizationInfoCache(Admin admin) {
Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
cache.remove(admin.getId());
}*/
/**
* 清除缓存
*/
public void clearCached() {
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
}
package com.rjwl.api.security;
import com.rjwl.api.common.utils.RedisUtil;
import com.rpframework.core.utils.MobileUtil;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SaltedAuthenticationInfo;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.crypto.hash.AbstractHash;
import org.apache.shiro.crypto.hash.Hash;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 自定义凭证匹配器
*/
public class MyHashedCredentialsMatcher extends HashedCredentialsMatcher {
/**
* @since 1.1
*/
private String hashAlgorithm;
private int hashIterations;
private boolean hashSalted;
private boolean storedCredentialsHexEncoded;
@Autowired
private RedisUtil redisUtil;
public MyHashedCredentialsMatcher() {
this.hashAlgorithm = null;
this.hashSalted = false;
this.hashIterations = 1;
//false means Base64-encoded
this.storedCredentialsHexEncoded = true;
}
public MyHashedCredentialsMatcher(String hashAlgorithmName) {
this();
if (!StringUtils.hasText(hashAlgorithmName)) {
throw new IllegalArgumentException("hashAlgorithmName cannot be null or empty.");
}
this.hashAlgorithm = hashAlgorithmName;
}
@Override
public String getHashAlgorithmName() {
return hashAlgorithm;
}
@Override
public void setHashAlgorithmName(String hashAlgorithmName) {
this.hashAlgorithm = hashAlgorithmName;
}
@Override
public boolean isStoredCredentialsHexEncoded() {
return storedCredentialsHexEncoded;
}
@Override
public void setStoredCredentialsHexEncoded(boolean storedCredentialsHexEncoded) {
this.storedCredentialsHexEncoded = storedCredentialsHexEncoded;
}
@Deprecated
public boolean isHashSalted() {
return hashSalted;
}
@Deprecated
public void setHashSalted(boolean hashSalted) {
this.hashSalted = hashSalted;
}
@Override
public int getHashIterations() {
return hashIterations;
}
@Override
public void setHashIterations(int hashIterations) {
if (hashIterations < 1) {
this.hashIterations = 1;
} else {
this.hashIterations = hashIterations;
}
}
@Deprecated
protected Object getSalt(AuthenticationToken token) {
return token.getPrincipal();
}
@Override
protected Object getCredentials(AuthenticationInfo info) {
Object credentials = info.getCredentials();
byte[] storedBytes = toBytes(credentials);
if (credentials instanceof String || credentials instanceof char[]) {
//account.credentials were a char[] or String, so
//we need to do text decoding first:
if (isStoredCredentialsHexEncoded()) {
storedBytes = Hex.decode(storedBytes);
} else {
storedBytes = Base64.decode(storedBytes);
}
}
AbstractHash hash = newHashInstance();
hash.setBytes(storedBytes);
return hash;
}
//自定义凭证匹配器 主要为适用当前业务 登录存在账号密码 和手机号 验证码 两种方式 具体 可以根据自己情况定 是否需要 此凭证匹配器 默认为HashedCredentialsMatcher
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//不存在密码
if (org.apache.commons.lang.StringUtils.isNotBlank((String) info.getCredentials())) {
Object tokenHashedCredentials = hashProvidedCredentials(token, info);
Object accountCredentials = getCredentials(info);
return equals(tokenHashedCredentials, accountCredentials);
} else {
//通过验证码方式登录
//查看登录账号是否符合 手机号格式
String phone = (String) token.getPrincipal();
String tVerifyCode = String.valueOf((char[])token.getCredentials());
if (!MobileUtil.isPhoneBase(phone)) {
return false;
}
//获取redis中验证码
String rVerifyCode = redisUtil.get(phone);
if (org.apache.commons.lang.StringUtils.isBlank(rVerifyCode)) {
return false;
}
//进行验证码比对
if (tVerifyCode.equals(rVerifyCode)) {
return true;
}
return false;
}
}
@Override
protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) {
Object salt = null;
if (info instanceof SaltedAuthenticationInfo) {
salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
} else {
//retain 1.0 backwards compatibility:
if (isHashSalted()) {
salt = getSalt(token);
}
}
return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations());
}
private String assertHashAlgorithmName() throws IllegalStateException {
String hashAlgorithmName = getHashAlgorithmName();
if (hashAlgorithmName == null) {
String msg = "Required 'hashAlgorithmName' property has not been set. This is required to execute " +
"the hashing algorithm.";
throw new IllegalStateException(msg);
}
return hashAlgorithmName;
}
@Override
protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) {
String hashAlgorithmName = assertHashAlgorithmName();
return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
}
@Override
protected AbstractHash newHashInstance() {
String hashAlgorithmName = assertHashAlgorithmName();
return new SimpleHash(hashAlgorithmName);
}
}
5.数据库 为RBAC权限模型 用户表-用户角色-角色表-角色菜单-菜单表