所谓会话,即用户访问应用时保持的链接关系,在屡次交互中应用可以识别出当前访问的用户是谁,且能够在屡次交互中保存一些数据。如访问一些网站时登陆成功后,网站能够记住用户,且在退出以前均可以识别当前用户是谁。html
Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession();
登陆成功后使用 Subject.getSession() 便可获取会话;其等价于 Subject.getSession(true),即若是当前没有建立 Session 对象会建立一个;另外 Subject.getSession(false),若是当前没有建立 Session 则返回 null(不过默认状况下若是启用会话存储功能的话在建立 Subject 时会主动建立一个 Session)。java
//获取当前会话的惟一标识。 session.getId(); //获取当前 Subject 的主机地址,该地址是经过 HostAuthenticationToken.getHost() 提供的。 session.getHost(); //获取 ,设置当前 Session 的过时时间;若是不设置默认是会话管理器的全局过时时间。 session.getTimeout(); session.setTimeout(毫秒) //获取会话的启动时间及最后访问时间 session.touch(); session.stop()
更新会话最后访问时间及销毁会话;当 Subject.logout() 时会自动调用 stop 方法来销毁会话。git
session.setAttribute("key", "123"); Assert.assertEquals("123", session.getAttribute("key")); session.removeAttribute("key");
设置 / 获取 / 删除会话属性;在整个会话范围内均可以对这些属性进行操做。web
Shiro 提供的会话能够用于 JavaSE/JavaEE 环境,不依赖于任何底层容器,能够独立使用,是完整的会话模块。redis
会话管理器管理着应用中全部 Subject 的会话的建立、维护、删除、失效、验证等工做。是 Shiro 的核心组件,顶层组件 SecurityManager 直接继承了SessionManager,且提供了SessionsSecurityManager 实现直接把会话管理委托给相应的 SessionManager,DefaultSecurityManager 及 DefaultWebSecurityManager 默认 SecurityManager 都继承了 SessionsSecurityManager。spring
Shiro 提供了三个默认实现:数据库
DefaultSessionManager:DefaultSecurityManager 使用的默认实现,用于 JavaSE 环境;apache
ServletContainerSessionManager:DefaultWebSecurityManager 使用的默认实现,用于 Web 环境,其直接使用 Servlet 容器的会话;json
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();
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; } }
安全性要求高的网站不建议有记住密码功能,由于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对长度有必定的限制。