整合spring、shiro、redis实现会话共享
什么是Shiro?
Apache旗下强大灵活的开源安全框架
提供认证、授权、会话管理、安全加密等功能
Shiro整体架构
SecurityManager Shiro的核心,Shiro通过SecurityManager 提供安全服务;
Authenticator 认证器,管理主体的登录、登出;
Authorizer 授权器,赋予主体有哪些权限;
SessionManager Session管理器,可以脱离web容器;
SessionDao 提供Session的增删改查操作;
CacheManager 缓存管理器,可以缓存主体的角色、权限数据;
Realm Shiro和数据源之间的桥梁,通过Realm获取认证、角色和权限数据等
主体提交请求到SecurityManager,SecurityManager调用Authenticator对主体进行认证。Authenticator通过Realm来获取认证信息,和主体提交的信息进行比对;
Shiro认证过程:
1、创建SecurityManager;
2、主体提交认证;
3、SecurityManager认证;
4、Authenticator认证;
5、Realm认证;
代码实现:
1、pom中添加依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- aop -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.5</version>
</dependency>
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.1</version>
</dependency>
2、web.xml中配置shiro拦截器
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-context.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- shiro权限校验 拦截器 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 字符转码拦截器 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>dispatherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-servlet.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
3、自定义认证Realm,查询数据库认证信息
package com.huatech.support.shiro;
import java.util.HashSet;
import java.util.Set;
import javax.annotation.Resource;
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.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.stereotype.Component;
import com.huatech.core.domain.User;
import com.huatech.core.service.PermService;
import com.huatech.core.service.RoleService;
import com.huatech.core.service.UserService;
@Component
public class SystemAuthorizingRealm extends AuthorizingRealm {
@Resource UserService userService;
@Resource RoleService roleService;
@Resource PermService permService;
/**
* 角色、权限认证
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
Set<String> roles = new HashSet<String>(roleService.findByUsername(username));
Set<String> perms = new HashSet<String>();
if(roles != null && roles.size() > 0) {
perms = new HashSet<String>(permService.findByRoles(roles));
}
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(roles);
authorizationInfo.addStringPermissions(perms);
return authorizationInfo;
}
/**
* 身份认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
User user = userService.findByUsername(username);
if(user == null) {
throw new UnauthenticatedException();
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username, user.getPassword(), getName());
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
return authenticationInfo;
}
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher("md5");
matcher.setHashIterations(1);
super.setCredentialsMatcher(matcher);
}
@Override
public String getName() {
return this.getClass().getSimpleName();
}
}
4、继承AbstractSessionDAO,实现Redis Session的增刪改查操作
package com.huatech.support.shiro; import java.io.Serializable; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.annotation.Resource; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.eis.AbstractSessionDAO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ScanOptions; /** * 继承AbstractSessionDAO,实现Redis Session的增刪改查操作 * @author [email protected] * @since 2019-01-28 * */ public class RedisSessionDao extends AbstractSessionDAO { private static final Logger LOGGER = LoggerFactory.getLogger(RedisSessionDao.class); public static final String SESSION_PREFIX = "shiro_session:"; public static final int DEFAILT_TIME_OUT = 30; @Resource RedisTemplate<String, Object> redisTemplate; //@Resource RedisUtil redisUtil; private int timeout = DEFAILT_TIME_OUT; public void setTimeout(int timeout) { this.timeout = timeout; } @Override protected Serializable doCreate(Session session) { Serializable id = generateSessionId(session); LOGGER.debug("id:{}", id.toString()); assignSessionId(session, id);//将session 和 sessionId捆绑在一起 saveSession(session); return id; } public void update(Session session) throws UnknownSessionException { LOGGER.debug("id:{}", session.getId().toString()); saveSession(session); } public void delete(Session session) { LOGGER.debug("id:{}", session.getId().toString()); if(session == null || session.getId() == null){ return; } redisTemplate.delete(getKey(session.getId())); } public Collection<Session> getActiveSessions() { Set<String> keys = keys(); Set<Session> sessions = new HashSet<Session>(); if(keys.size() == 0){ return sessions; } for (String id : keys) { Session _session = getSession(id); if(_session == null){ continue; } sessions.add(_session); } return sessions; } @Override protected Session doReadSession(Serializable sessionId) { if(sessionId == null){ return null; } LOGGER.debug("id:{}", sessionId.toString()); return getSession(sessionId); } private static String getKey(Serializable id){ return SESSION_PREFIX + id.toString(); } private void saveSession(Session session){ if(session != null && session.getId() != null){ Serializable id = session.getId(); redisTemplate.opsForValue().set(getKey(id), session, timeout, TimeUnit.MINUTES); } } private Session getSession(Serializable id){ return (Session)redisTemplate.boundValueOps(getKey(id)).get(); } private Set<String> keys(){ return redisTemplate.execute(new RedisCallback<Set<String>>() { public Set<String> doInRedis(RedisConnection connection) throws DataAccessException { Set<String> binaryKeys = new HashSet<String>(); Cursor<byte[]> cursor = connection.scan( new ScanOptions.ScanOptionsBuilder().match(SESSION_PREFIX + "*").count(1000).build()); while (cursor.hasNext()) { binaryKeys.add(new String(cursor.next())); } connection.close(); return binaryKeys; } }); } }
5、实现用户角色、权限缓存管理
package com.huatech.support.shiro; import java.util.Collection; import java.util.Set; import javax.annotation.Resource; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.CacheManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; import com.alibaba.fastjson.JSONObject; /** * 实现用户角色、权限缓存管理 * @author [email protected] * */ public class RedisCacheManager implements CacheManager { @Resource RedisTemplate<String, Object> redisTemplate; private static final Logger LOGGER = LoggerFactory.getLogger(RedisCacheManager.class); //@Resource RedisUtil redisUtil; @Override public <K, V> Cache<K, V> getCache(String arg0) throws CacheException { return new RedisCache<K, V>(); } class RedisCache<K, V> implements Cache<K, V>{ private static final String CACHE_KEY = "redis-cache"; @Override public void clear() throws CacheException { redisTemplate.delete(CACHE_KEY); } private String toString(Object obj){ if(obj instanceof String){ return obj.toString(); }else{ return JSONObject.toJSONString(obj); } } @SuppressWarnings("unchecked") @Override public V get(K k) throws CacheException { LOGGER.info("get field:{}", toString(k)); return (V)redisTemplate.boundHashOps(CACHE_KEY).get(k); } @SuppressWarnings("unchecked") @Override public Set<K> keys() { LOGGER.info("keys"); return (Set<K>)redisTemplate.boundHashOps(CACHE_KEY).keys(); } @Override public V put(K k, V v) throws CacheException { LOGGER.info("put field:{}, value:{}", toString(k), toString(v)); redisTemplate.boundHashOps(CACHE_KEY).put(k, v); return v; } @Override public V remove(K k) throws CacheException { LOGGER.info("remove field:{}", toString(k)); V v = get(k); redisTemplate.boundHashOps(CACHE_KEY).delete(k); return v; } @Override public int size() { int size = redisTemplate.boundHashOps(CACHE_KEY).size().intValue(); LOGGER.info("size:{}", size); return size; } @SuppressWarnings("unchecked") @Override public Collection<V> values() { LOGGER.info("values"); return (Collection<V>)redisTemplate.boundHashOps(CACHE_KEY).values(); } public String getCacheKey() { return "RedisCache"; } } }
6、重写DefaultWebSessionManager的retrieveSession方法,防止一个接口重复读取redis的session
package com.huatech.support.shiro; import java.io.Serializable; import javax.servlet.ServletRequest; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.SessionKey; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.session.mgt.WebSessionKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 重写DefaultWebSessionManager的retrieveSession方法,防止一个接口重复读取redis的session * @author [email protected] * */ public class RedisSessionManager extends DefaultWebSessionManager { private static final Logger LOGGER = LoggerFactory.getLogger(RedisSessionManager.class); @Override protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException { Serializable sessionId = getSessionId(sessionKey); ServletRequest request = null; if(sessionKey instanceof WebSessionKey){ request = ((WebSessionKey)sessionKey).getServletRequest(); } Session session = null; if(request != null && sessionId != null){ session = (Session) request.getAttribute(sessionId.toString()); } if(session != null){ return session; } try{ session = super.retrieveSession(sessionKey); }catch(UnknownSessionException e){ LOGGER.error(e.getMessage()); } if(request != null && sessionId != null && session != null){ request.setAttribute(sessionId.toString(), session); } return session; } }
7、spirng-servlet配置,启用shiro注解
<?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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
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/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">
<context:component-scan base-package="com.huatech.core.controller"/>
<mvc:annotation-driven >
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<constructor-arg value="UTF-8" />
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
<mvc:resources mapping="/**" location="/"/>
<aop:config proxy-target-class="true"/>
<!-- 保证实现了Shiro内部lifecycle函数的bean执行 -->
<bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>
8、Shiro配置
<?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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd" default-lazy-init="false">
<!-- 安全认证过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="login.jsp"/>
<property name="unauthorizedUrl" value="403.jsp"/>
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/doLogin = anon
/rolesOr = rolesOr["admin","admin1"]
/roleAll = roles["admin", "admin1"]
/* = authc
</value>
</property>
<property name="filters">
<util:map>
<entry key="rolesOr" value-ref="rolesOrFilter"/>
</util:map>
</property>
</bean>
<bean id="rolesOrFilter" class="com.huatech.support.shiro.RolesOrFilter"/>
<!-- 定义Shiro安全管理配置 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="realm" />
<property name="sessionManager" ref="sessionManager" />
<property name="cacheManager" ref="cacheManager" />
<property name="rememberMeManager" ref="rememberMeManager"/>
</bean>
<bean id="realm" class="com.huatech.support.shiro.SystemAuthorizingRealm"/>
<bean id="sessionManager" class="com.huatech.support.shiro.RedisSessionManager">
<property name="sessionDAO" ref="redisSessionDao"/>
</bean>
<!-- 用户会话 操作 -->
<bean id="redisSessionDao" class="com.huatech.support.shiro.RedisSessionDao">
<property name="timeout" value="${redis.session.timeout}"/>
</bean>
<!-- 用户角色、权限缓存管理 -->
<bean id="cacheManager" class="com.huatech.support.shiro.RedisCacheManager">
</bean>
<!-- 记住我 -->
<bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager">
<property name="cookie" ref="simpleCookie"/>
</bean>
<bean id="simpleCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<property name="name" value="remeberMe"/>
</bean>
</beans>
9、Redis配置
<?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:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd"
default-lazy-init="false">
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="${redis.pool.maxIdle}" />
<property name="maxTotal" value="${redis.pool.maxTotal}" />
<property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
</bean>
<bean id="jedisConnFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" >
<property name="hostName" value="${redis.host}" />
<property name="port" value="${redis.port}" />
<property name="timeout" value="${redis.timeout}" />
<property name="password" value="${redis.password}" />
<property name="poolConfig" ref="jedisPoolConfig" />
</bean>
<bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer" />
<bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate"
p:connectionFactory-ref="jedisConnFactory" />
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
p:connectionFactory-ref="jedisConnFactory"
p:keySerializer-ref="stringRedisSerializer"
p:hashKeySerializer-ref="stringRedisSerializer" />
</beans>
代码已全部上传到gitee,完整代码请参考:https://gitee.com/hualee/spring4-shiro-redis-demo