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泄露了,也只是在某一段時間內有用(固然了,對於「有心人」來講「這段時間」已經足夠作一些事兒了)

在考慮這些問題的時候我意識到:

  • 安全只是相對的(攻與防原本就是一件你強我更強的事,有攻擊,防護就會加強,防護加強了,攻擊要想成功就得更強)
  • 安全不是在技術上越安全越好,要考慮實際應用場合、投入的成本(每每不是技術不能實現,而是要考慮實際狀況,包括成本、信息的重要程度等等,這就是一種工程思惟)
相關文章
相關標籤/搜索