先说下背景,项目包含一个管理系统(web)和门户网站(web),还有一个手机APP(包括Android和IOS),三个系统共用一个后端,在后端使用shiro进行登录认证和权限控制。好的,那么问题来了:
先说web端
1.因为一般网页主需要记住7天密码(或者稍微更长)的功能就可以了,可以使用cookie实现,而且shiro也提供了记住密码的功能,在服务器端session不需要保存过长时间。
再说app端
2.因为APP免密码登录时间需要较长(在用户不主动退出的时候,应该一直保持登录状态),这样子在服务器端就得把session保存很长时间,给服务器内存和性能上造成较大的挑战,存在的矛盾是:APP需要较长时间的免密码登录,而服务器不能保存过长时间的session。
解决办法:
这种方法存在的问题:
import java.io.Serializable;
import java.util.Date;
/**
* 用户session
*
* @author flyingTiger
* @email [email protected]
* @date 2018-03-05 15:44:08
*/
public class SysUserSessionEntity implements Serializable {
private static final long serialVersionUID = 1L;
//id
private String id;
//session
private String session;
//cookie
private String cookie;
//user_id
private Long userId;
//创建时间
private Date createTime;
//最后更新时间
private Date lastUpTime;
//状态
private String status;
/**
* 设置:id
*/
public void setId(String id) {
this.id = id;
}
/**
* 获取:id
*/
public String getId() {
return id;
}
/**
* 设置:session
*/
public void setSession(String session) {
this.session = session;
}
/**
* 获取:session
*/
public String getSession() {
return session;
}
/**
* 设置:cookie
*/
public void setCookie(String cookie) {
this.cookie = cookie;
}
/**
* 获取:cookie
*/
public String getCookie() {
return cookie;
}
/**
* 设置:user_id
*/
public void setUserId(Long userId) {
this.userId = userId;
}
/**
* 获取:user_id
*/
public Long getUserId() {
return userId;
}
/**
* 设置:创建时间
*/
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
/**
* 获取:创建时间
*/
public Date getCreateTime() {
return createTime;
}
/**
* 设置:最后更新时间
*/
public void setLastUpTime(Date lastUpTime) {
this.lastUpTime = lastUpTime;
}
/**
* 获取:最后更新时间
*/
public Date getLastUpTime() {
return lastUpTime;
}
/**
* 设置:状态
*/
public void setStatus(String status) {
this.status = status;
}
/**
* 获取:状态
*/
public String getStatus() {
return status;
}
}
3. 在此之前我已经实现过sessionDao ,是将session放到redis里面
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
/**
* shiro session dao
*
* @author flyingTiger
* @email [email protected]
* @date 2017/9/27 21:35
*/
@Component
public class RedisShiroSessionDAO extends EnterpriseCacheSessionDAO {
@Autowired
private RedisTemplate redisTemplate;
//创建session
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.doCreate(session);
final String key = RedisKeys.getShiroSessionKey(sessionId.toString());
setShiroSession(key, session);
return sessionId;
}
//获取session
@Override
protected Session doReadSession(Serializable sessionId) {
Session session = super.doReadSession(sessionId);
if(session == null){
final String key = RedisKeys.getShiroSessionKey(sessionId.toString());
session = getShiroSession(key);
}
return session;
}
//更新session
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
final String key = RedisKeys.getShiroSessionKey(session.getId().toString());
setShiroSession(key, session);
}
//删除session
@Override
protected void doDelete(Session session) {
super.doDelete(session);
final String key = RedisKeys.getShiroSessionKey(session.getId().toString());
redisTemplate.delete(key);
}
private Session getShiroSession(String key) {
return (Session)redisTemplate.opsForValue().get(key);
}
private void setShiroSession(String key, Session session){
redisTemplate.opsForValue().set(key, session);
//60分钟过期
redisTemplate.expire(key, 60, TimeUnit.MINUTES);
}
}
4. 那么现在我即要实现将session写入到数据库中,又要将sesion存储到redis里面。我还不想大量修改redisDao的代码,我该怎么做呢?
2. 然后让redisDao继承DataBaseDao
DataBaseSessionDao的代码如下
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.ValidatingSession;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.HashMap;
/**
* 存放session 到数据库
*
* @author FlyingTiger
* @version 1.0
* @since 2018/03/05 14:52
*/
@Component
public class DataBaseSessionDao extends EnterpriseCacheSessionDAO {
public static final String COOKIE = "cookie";
// 此处虽然不符合三层设计,但有效避免sql注入和减少了更新的维护管理,是合理的。 at FlyingTiger by 2018年3月5日
@Autowired
private SysUserSessionService sysUserSessionService;
//创建session
@Override
protected Serializable doCreate(Session session) {
Serializable cookie = super.doCreate(session);
// 保存session到数据库
SysUserSessionEntity sysUserSession = new SysUserSessionEntity();
sysUserSession.setCookie(cookie.toString());
sysUserSession.setSession(SerializableUtils.serializ(session));
sysUserSessionService.save(sysUserSession);
return cookie;
}
//获取session
@Override
protected Session doReadSession(Serializable sessionId) {
Session session = super.doReadSession(sessionId);
if (session == null) {
//final String key = RedisKeys.getShiroSessionKey(sessionId.toString());
HashMap param = new HashMap<>();
param.put(COOKIE, sessionId);
SysUserSessionEntity sysUserSessionEntity = sysUserSessionService.queryObjectByMap(param);
// 如果不为空
if (sysUserSessionEntity != null) {
String sessionStr64 = sysUserSessionEntity.getSession();
session = SerializableUtils.deserializ(sessionStr64);
}
}
return session;
}
//更新session
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
//当是ValidatingSession 无效的情况下,直接退出
if (session instanceof ValidatingSession &&
!((ValidatingSession) session).isValid()) {
return;
}
//检索到用户名
// String username = String.valueOf(session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY));
HashMap param = new HashMap<>();
param.put(COOKIE, session.getId());
SysUserSessionEntity sysUserSessionEntity = sysUserSessionService.queryObjectByMap(param);
sysUserSessionEntity.setSession(SerializableUtils.serializ(session));
// 如果登录成功,更新用户id
if (ShiroUtils.getSubject().isAuthenticated()){
SysUserEntity sysUser = (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
sysUserSessionEntity.setUserId(sysUser.getUserId());
}
sysUserSessionService.update(sysUserSessionEntity);
}
//删除session
@Override
protected void doDelete(Session session) {
super.doDelete(session);
HashMap param = new HashMap<>();
param.put(COOKIE, session.getId());
SysUserSessionEntity sysUserSessionEntity = sysUserSessionService.queryObjectByMap(param);
if (sysUserSessionEntity != null) {
sysUserSessionService.delete(sysUserSessionEntity.getId());
}
}
然后让redisSessionDao 继承 DataBaseSessionDao (除了继承关系,其他都不变 序列化类参考标题7):
import org.apache.shiro.session.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
/**
* shiro session dao
*
* @author flyingTiger
* @email [email protected]
* @date 2017/9/27 21:35
*/
@Component
public class RedisShiroSessionDAO extends DataBaseSessionDao {
@Autowired
private RedisTemplate redisTemplate;
//创建session
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.doCreate(session);
final String key = RedisKeys.getShiroSessionKey(sessionId.toString());
setShiroSession(key, session);
return sessionId;
}
//获取session
@Override
protected Session doReadSession(Serializable sessionId) {
Session session = super.doReadSession(sessionId);
if(session == null){
final String key = RedisKeys.getShiroSessionKey(sessionId.toString());
session = getShiroSession(key);
}
return session;
}
//更新session
@Override
protected void doUpdate(Session session) {
super.doUpdate(session);
final String key = RedisKeys.getShiroSessionKey(session.getId().toString());
setShiroSession(key, session);
}
//删除session
@Override
protected void doDelete(Session session) {
super.doDelete(session);
final String key = RedisKeys.getShiroSessionKey(session.getId().toString());
redisTemplate.delete(key);
}
private Session getShiroSession(String key) {
return (Session)redisTemplate.opsForValue().get(key);
}
private void setShiroSession(String key, Session session){
redisTemplate.opsForValue().set(key, session);
//60分钟过期
redisTemplate.expire(key, 60, TimeUnit.MINUTES);
}
}
5. 这里用到的是shiro bean 配置 (配置文件请参考标题6)
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Shiro的配置文件
*
* @author flyingTiger
* @email [email protected]
* @date 2017/9/27 22:02
*/
@Configuration
public class ShiroConfig {
@Bean("sessionManager")
public SessionManager sessionManager(RedisShiroSessionDAO redisShiroSessionDAO, DataBaseSessionDao dataBaseSessionDao, @Value("${renren.redis.open}") boolean redisOpen,
@Value("${renren.shiro.redis}") boolean shiroRedis) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//设置session过期时间为1小时(单位:毫秒),默认为30分钟
sessionManager.setGlobalSessionTimeout(60 * 60 * 1000);
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);
//如果开启redis缓存且shiro.redis=true,则shiro session存到redis里
if (redisOpen && shiroRedis) {
sessionManager.setSessionDAO(redisShiroSessionDAO);
} else {
// 将session保存到数据库
sessionManager.setSessionDAO(dataBaseSessionDao);
}
return sessionManager;
}
}
6. 配置spring-shiro.xml配置文件(如果不是用bean配置)
注意这个sessionDao 里面配置了activeSessionsCacheName 这个属性,这个在ecache.xml里面必须也配置一个shiro-activeSessionCache节点,用于存激活的session,简单来讲,就是登录的用户。
其中还有一部分是关于Shiro生命周期的,存储在了Spring-mvc中,因为生命周期配置在spring-shiro.xml中不生效
== Shiro Components ==
spring-mvc的配置
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;
import org.apache.shiro.session.Session;
/**
* 序列和反序列Session对象,只有将session对象序列化成字符串,才可以存储到Mysql上,不能直接存
*
* @author FlyingTiger
* @version 1.0
* @since 2018/03/05 15:59
*/
public class SerializableUtils {
/**
* 创建日期:2017/12/21
* 创建时间:9:25:30
* 创建用户:FlyingTiger
* 机能概要:将Session序列化成String类型
*
* @param session
* @return
*/
public static String serializ(Session session) {
try {
//ByteArrayOutputStream 用于存储序列化的Session对象
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//将Object对象输出成byte数据
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(session);
//将字节码,编码成String类型数据
return Base64.getEncoder().encodeToString(bos.toByteArray());
} catch (Exception e) {
throw new RuntimeException("序列化失败");
}
}
/**
* 创建日期:2017/12/21
* 创建时间:9:26:19
* 创建用户:FlyingTiger
* 机能概要:将一个Session的字符串序列化成字符串,反序列化
*
* @param sessionStr
* @return
*/
public static Session deserializ(String sessionStr) {
try {
//读取字节码表
ByteArrayInputStream bis = new ByteArrayInputStream(Base64.getDecoder().decode(sessionStr));
//将字节码反序列化成 对象
ObjectInputStream in = new ObjectInputStream(bis);
Session session = (Session) in.readObject();
return session;
} catch (Exception e) {
throw new RuntimeException("反序列化失败");
}
}
}
8. 用户登录成功后,将JSESSIONID 下发到终端,存储到移动端应用层变量。 每次访问的时候带上此sessionId 即可保持 用户登录。
PS:
shiro实现APP、web统一登录认证和权限管理
Shiro之保存Session到数据库中-yellowcong