shiro实现APP保持登录状态,以及web统一登录认证和权限管理,会话保持在web和APP之间。

先说下背景,项目包含一个管理系统(web)和门户网站(web),还有一个手机APP(包括Android和IOS),三个系统共用一个后端,在后端使用shiro进行登录认证和权限控制。好的,那么问题来了

先说web端

1.因为一般网页主需要记住7天密码(或者稍微更长)的功能就可以了,可以使用cookie实现,而且shiro也提供了记住密码的功能,在服务器端session不需要保存过长时间。

再说app端

2.因为APP免密码登录时间需要较长(在用户不主动退出的时候,应该一直保持登录状态),这样子在服务器端就得把session保存很长时间,给服务器内存和性能上造成较大的挑战,存在的矛盾是:APP需要较长时间的免密码登录,而服务器不能保存过长时间的session。

 解决办法:

  • APP第一次登录,使用用户名和密码,如果登录成功,将cookie保存在APP本地(比如sharepreference),后台将cookie值保存到user表里面
  • APP访问服务器,APP将cookie添加在heade里面,服务器session依然存在,可以正常访问
  • APP访问服务器,APP将cookie添加在heade里面,服务器session过期,访问失败,由APP自动带着保存在本地的cookie去服务器登录,服务器可以根据cookie和用户名进行登录,这样服务器又有session,会生成新的cookie返回给APP,APP更新本地cookie,又可以正常访问
  • 用户手动退出APP,删除APP本次存储的cookie,下次登录使用用户名和密码登录

这种方法存在的问题:

  1. cookie保存在APP本地,安全性较低,可以通过加密cookie增加安全性
  2. 每次服务器session失效之后,得由APP再次发起登录请求(虽然用户是不知道的),但是这样本身就会增加访问次数,好在请求数量并不是很大,不过这种方式会使cookie经常更新,反而增加了安全性。

这里给出另外一种实现方式:
实现自己的SessionDao,将session保存在数据库,这样子的好处是,session不会大量堆积在内存中,就不需要考虑session的过期时间了,对于APP这种需要长期保存session的情况来说,就可以无限期的保存session了,也就不用APP在每次session过期之后重新发送登录请求了。实现方式如下:

1. 数据库设计 (需要的话自己扩展)

                       shiro实现APP保持登录状态,以及web统一登录认证和权限管理,会话保持在web和APP之间。_第1张图片

2. 实体类
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的代码,我该怎么做呢?
答案就是,
    继承,
      1. 首先自己重新定义一个SessionDao比如叫做DataBaseSessionDao.

      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的配置


   

      
      
          
      

      
          
         

配置ecache (可选session缓存)




    
    

    
    

    
    

    
    

    
    

    

    
  
    

    


7.  SerializableUtils

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:

  • 在用户使用用户名和密码登录的时候,对密码进行加密
  • 会话保持如果使用cookie这种技术的话,存在被别人截取cookie之后就可以认证登录了
  • 在本地保存密码肯定是不合适的,如果保存cookie(token)的话,手机被root之后,很容易就可以看得到了,比如Android的就只是一个xml文件,所以cookie保存要加密,加密之后提高了破解门槛,加密就涉及到秘钥的问题了,秘钥如果写在代码里面,java被反编译之后就很容易秘钥找得到了,当然了google早就已经开始支持NDK(即Android原生开发,这个原生是指使用C/C++开发,编译成为so文件,在java中调用),这样又加大了破解难度,使用Hybrid就更不用说了,直接解压安装包就可以看到了。
  • cookie如果保存在本地,更新的时机(频率)是什么,这样就算是cookie泄露了,也只是在某一段时间内有用(当然了,对于“有心人”来说“这段时间”已经足够做一些事儿了)
  • 可以在session表中添加ip地址来进行再次判断,这样可以解决上述问题。
    参考:

   shiro实现APP、web统一登录认证和权限管理

    Shiro之保存Session到数据库中-yellowcong

你可能感兴趣的:(java框架,database,javaEE,Android,login,session)