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