標籤:
先說下背景,項目包含一個管理系統(web)和門戶網站(web),還有一個手機APP(包括Android和IOS),三個系統共用一個後端,在後端使用shiro進行登入認證和許可權控制。好的,那麼問題來了web和APP都可以用shiro認證嗎?兩者有什麼區別?如果可以,解決方案是什嗎?看著大家焦急的小眼神,接下來挨個解決上面的問題。
web和APP可以用shiro統一登入認證嗎?
可以。假如web和APP都使用密碼登入的話,那沒的說肯定是可以的,因為對於shiro(在此不會介紹shiro詳細知識,只介紹本文章必要的)來說,不管是誰登入,用什麼登入(使用者名稱密碼、驗證碼),只要通過subject.login(token)中的token告訴shiro,然後在自己定義的Realm裡面給出自己的認證欄位就可以了,好吧說的雲裡霧裡,看看代碼
// 在自己登入的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了,比如自訂UserRealm
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進行一個完整的登入過程
所以由以上代碼看出目前我們還沒有發現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,下次登入使用使用者名稱和密碼登入
這種方法存在的問題:
- cookie儲存在APP本地,安全性較低,可以通過加密cookie增加安全性
- 每次伺服器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.daoimport 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(session.getId()); if(entity != null){ session = (Session) entity.getSession(); } } else{ // 如果session沒有被刪除,則判斷session是否到期 if(isExpire(session)){ // session到期 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; } }
我卡UI被自己蠢哭了,在繼承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泄露了,也只是在某一段時間內有用(當然了,對於“有心人”來說“這段時間”已經足夠做一些事兒了)
在考慮這些問題的時候我意識到:
- 安全只是相對的(攻與防本來就是一件你強我更強的事,有攻擊,防禦就會增強,防禦增強了,攻擊要想成功就得更強)
- 安全不是在技術上越安全越好,要考慮實際應用場合、投入的成本(往往不是技術不能實現,而是要考慮實際情況,包括成本、資訊的重要程度等等,這就是一種工程思維)
shiro實現APP、web統一登入認證和許可權管理