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

先说下背景,项目包含一个管理系统(web)和门户网站(web),还有一个手机APP(包括Android和IOS),三个系统共用一个后端,在后端使用shiro进行登陆认证和权限控制。好的,那么问题来了web和APP均可以用shiro认证吗?二者有什么区别?若是能够,解决方案是什么?看着你们焦急的小眼神,接下来挨个解决上面的问题。java

web和APP能够用shiro统一登陆认证吗?

能够。假如web和APP都使用密码登陆的话,那没的说确定是能够的,由于对于shiro(在此不会介绍shiro详细知识,只介绍本文章必要的)来讲,不论是谁登陆,用什么登陆(用户名密码、验证码),只要经过subject.login(token)中的token告诉shiro,而后在本身定义的Realm里面给出本身的认证字段就能够了,好吧说的云里雾里,看看代码web

// 在本身登陆的rest里面写,好比UserRest里面的login方法中,user为传递过来的参数
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword()); 
// 开始进入shiro的认证流程
currentUser.login(token);

上面的代码是开始使用shiro认证,调用subject.login(token)以后就交给shiro去认证了,接下来和咱们相关的就是自定认证的Realm了,好比自定义UserRealmspring

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {  
        //获取基于用户名和密码的令牌  
        //实际上这个token是从UserResource面currentUser.login(token)传过来的  
        //两个token的引用都是同样的
        UsernamePasswordToken token = (UsernamePasswordToken)authcToken;  
        System.out.println("验证当前Subject时获取到token为" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE));  
        // 从数据库中获取还用户名对应的user
        User user = userService.getByPhoneNum(token.getUsername());  
        if(null != user){  
            AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user.getPhoneNum(),user.getPassword(), getName());  
            return authcInfo;  
        }else{  
            return null;  
        }
    }  

再配一张图数据库

图中描述的是使用shiro进行一个完整的登陆过程apache

因此由以上代码看出目前咱们尚未发现APP和web登陆d区别,那么区别是什么呢?后端

web和APP登陆认证的区别

好吧,标题不太准确,应该是登陆的时候和登录以后会话保持在web和APP之间的区别,先说登陆:浏览器

登陆

APP和PC web所需的设备不一样很大程度上决定了二者之间的区别,web通常在PC上浏览,登陆的时候使用用户名和密码,若是使用了记住密码就是用cookie认证,web登陆有如下状况缓存

  • 第一次登陆,使用用户名和密码登陆
  • 关闭浏览器、session过时,从新使用密码登陆(若是有记住密码功能,可使用cookie登陆)
  • 用户删除cookie或者cookie过时,使用户名和密码登陆

APP在移动设备上查看,第一次登陆的时候使用用户名和密码,可是之后若是不是用户主动退出,都应该保持登陆状态,这样才会有更好的用户体验,可是不可能一直保留该APP的会话,也不可能把密码保存在本地,因此APP应该如下的过程安全

  • 第一次登陆,使用用户名密码
  • 之后用户打开应用以后,用户不需输入密码系统就能够自动登陆
  • 用户主动退出(重装等状况视为主动退出)以后,使用用户名和密码登陆

貌似没有看出什么区别,惟一的不一样就是第二点:怎么不用密码登陆,web使用的是cookie(由浏览器自动维护的),APP怎么登录呢?因为APP本地不保存密码,那么也参考web,使用相似cookie的东西,咱们叫他token吧,那问题就解决了,APP本地保存token,为了安全性,按期更新token,那再来看看会话的保持。服务器

会话(session)(保持状态)

若是用户登陆了,怎么保持登陆状态呢,web有cookie和session配合解决这个问题,下面先简单说一下我对这两个东西的理解,由于APP会话就是参考这个原理设计的。

cookie:是由浏览器维护的,每次请求浏览器都会把cookie放在header里面(若是有的话),也能够看作js的能够访问本地存储数据的位置之一(另外一个就是local storage)

session:因为http是无状态的,可是有时候服务器须要把此次请求的数据保存下来留给下一次请求使用,即须要维护连续请求的状态,这个时候服务器就借助cookie,当浏览器发送请求来服务器的时候,服务器会生成一个惟一的值,写到cookie中返回给浏览器,同时生成一个session对象,这样session和cookie值就有了一一对应关系了,浏览下一次访问的时候就会带着这个cookie值,这个时候服务器就会得到cookie的值,而后在本身的缓存里面查找是否存在和该cookie关联的session

 

由于cookie和session的配合,shiro能够自己很好的支持web的登陆和会话保持,对于APP来讲也能够借鉴cookie和session的这种实现方式,惟一存在的问题,就是web的cookie是由浏览器维护的,自动将cookie放在header里面,那咱们APP只要把服务器返回的cookie放在header里面,每次访问服务器的时候带上就能够了。

 

免密码登陆

解决了登陆和会话保持的问题,还剩一个免密码登录:

web:由于通常网页主须要记住7天密码(或者稍微更长)的功能就能够了,可使用cookie实现,并且shiro也提供了记住密码的功能,在服务器端session不须要保存过长时间

APP:由于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过时以后从新发送登陆请求了。实现方式以下:

为了使用Hibernate将Session保存到数据库,新建一个SimpleSessionEntity

package org.lack.entity;

import java.io.Serializable;

import org.apache.shiro.session.mgt.SimpleSession;

import com.phy.em.user.entity.User;

public class SimpleSessionEntity {

    private Long id;
    private String cookie;
    private Serializable session;
    
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public Serializable entity() {
        return session;
    }
    public void setSession(Serializable session) {
        this.session = session;
    }
    public String getCookie() {
        return cookie;
    }
    public void setCookie(String cookie) {
        this.cookie = cookie;
    }
    public Serializable getSession() {
        return session;
    }
}
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="org.lack.entity">
    <class name="SimpleSessionEntity" table="session">
        <!-- 标识 -->
        <id name="id">
            <column name="id"></column>
            <generator class="increment"></generator>
        </id>
        
        <property name="session">
            <column name="session"></column>
        </property>
        
        <property name="cookie">
            <column name="cookie"></column>
        </property>

    </class>
</hibernate-mapping>

以上贴出来的是SimpleSessionEntity的映射文件,特别要注意的是Hibernate也是支持把对象保存在数据库中的,可是该实体要实现Serializable,在取出来的时候强转为对应的对象便可,因此这里session的类型为Serializable

新建session缓存的方式的类,这里继承自EnterpriseCacheSessionDAO,可使用ehcache做为二级缓存,必定要记得实现save、update、readSession、delete方法,特别是save方法只是保存一个基本的session,重要的attribute都是update的,在readSession中从数据库中读取便可

package org.lack.dao

import java.io.Serializable;
import java.util.Date;
import org.apache.log4j.Logger;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SimpleSession;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.springframework.transaction.annotation.Transactional;
import com.phy.em.common.dao.IBaseDao;
import com.phy.em.common.shiro.entity.SimpleSessionEntity;
import com.phy.em.user.entity.User;
public class SessionEntityDao extends EnterpriseCacheSessionDAO {
    
    private IBaseDao<User> baseDao;
    private IBaseDao<SimpleSessionEntity> sessionDao;
    private Logger log = Logger.getLogger(SessionEntityDao.class);
    
    @Override
    public Serializable create(Session session) {
        // 先保存到缓存中
        Serializable cookie = super.create(session);
        // 新建一个SimpleSessionEntity,而后保存到数据库
        SimpleSessionEntity entity = new SimpleSessionEntity();
        entity.setSession((SimpleSession)session);
        entity.setCookie(cookie.toString());
        sessionDao.save(entity);
        
        return cookie;
    }
    
    @Override
    public void update(Session session) throws UnknownSessionException {
        super.update(session);
        SimpleSessionEntity entity = getEntity(session.getId());
        if(entity != null){
            entity.setSession((SimpleSession)session);    
            sessionDao.update(entity);
        }        
    }
    
    @Override
    public Session readSession(Serializable sessionId) throws UnknownSessionException {
        Session session = null;
        
        try{
            session = super.readSession(sessionId);
        } catch(Exception e){
            
        }
        
        // 若是session已经被删除,则从数据库中查询session
        if(session == null){
            SimpleSessionEntity entity = getEntity(sessionId);
            if(entity != null){
                session = (Session) entity.getSession();    
            } 
        }
     // 若是是APP则更新lastAccessTime
       User user = getUser(sessionId);
        if(user != null){
          // 若是该用户是APP用户(user不为空说明就是),则判断session是否过时,若是过时则修改最后访问时间
          ((SimpleSession)session).setLastAccessTime(new Date());
        }

     return session; } @Override public void delete(Session session) { super.delete(session); } private User getUser(Serializable sessionId){ String hql = "from User user where user.cookie ='" + sessionId + "'"; return baseDao.findUniqueByHQL(hql); } private SimpleSessionEntity getEntity(Serializable sessionId){ String hql = "from SimpleSessionEntity entity where entity.cookie ='" + sessionId + "'"; return sessionDao.findUniqueByHQL(hql); } private boolean isExpire(Session session){ long timeout = session.getTimeout(); long lastTime = session.getLastAccessTime().getTime(); long current = new Date().getTime(); if((lastTime + timeout) > current){ return false; } return true; } public void setBaseDao(IBaseDao<User> baseDao) { this.baseDao = baseDao; } public void setSessionDao(IBaseDao<SimpleSessionEntity> sessionDao) { this.sessionDao = sessionDao; } }

我快被本身蠢哭了,在继承EnterpriseCacheSessionDAO 只实现了readSession,妄想本身新建一个SimpleSession来返回给shiro使用,尝试过不少次以后不行,跟着调试了不少shiro源码,发如今SimpleSession中Shiro不只设置了基本的属性,更重要的是设置了Attribute,可是我本身新建的SimpleSession没有,因此认证是失败的,因此在此敬告各位必定要记得实现save和update方法。

虽然走了不少弯路,可是随着对shiro源码的调试学习,对shiro了解更深了,再也不仅仅停留在只会使用的地步上,有深刻。

 


 

 

好了到此为止,正文完了,咱们开头提出的问题都解决完了,记下来掰扯掰扯在作APP登陆过程当中遇到的问题以及一些本身的体会。

关于系统安全

在考虑APP登陆的时候考虑了不少安全因素

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

在考虑这些问题的时候我意识到:

  • 安全只是相对的(攻与防原本就是一件你强我更强的事,有攻击,防护就会加强,防护加强了,攻击要想成功就得更强)
  • 安全不是在技术上越安全越好,要考虑实际应用场合、投入的成本(每每不是技术不能实现,而是要考虑实际状况,包括成本、信息的重要程度等等,这就是一种工程思惟)
相关文章
相关标签/搜索