先說下背景,項目包含一個管理系統(tǒng)(web)和門戶網(wǎng)站(web),還有一個手機(jī)APP(包括Android和IOS),三個系統(tǒng)共用一個后端,在后端使用shiro進(jìn)行登錄認(rèn)證和權(quán)限控制。好的,那么問題來了web和APP都可以用shiro認(rèn)證嗎?兩者有什么區(qū)別?如果可以,解決方案是什么?看著大家焦急的小眼神,接下來挨個解決上面的問題。
可以。假如web和APP都使用密碼登錄的話,那沒的說肯定是可以的,因為對于shiro(在此不會介紹shiro詳細(xì)知識,只介紹本文章必要的)來說,不管是誰登錄,用什么登錄(用戶名密碼、驗證碼),只要通過subject.login(token)中的token告訴shiro,然后在自己定義的Realm里面給出自己的認(rèn)證字段就可以了,好吧說的云里霧里,看看代碼
// 在自己登錄的rest里面寫,比如UserRest里面的login方法中,user為傳遞過來的參數(shù) Subject currentUser = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword()); // 開始進(jìn)入shiro的認(rèn)證流程 currentUser.login(token);
上面的代碼是開始使用shiro認(rèn)證,調(diào)用subject.login(token)之后就交給shiro去認(rèn)證了,接下來和我們相關(guān)的就是自定認(rèn)證的Realm了,比如自定義UserRealm
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
//獲取基于用戶名和密碼的令牌
//實際上這個token是從UserResource面currentUser.login(token)傳過來的
//兩個token的引用都是一樣的
UsernamePasswordToken token = (UsernamePasswordToken)authcToken;
System.out.println("驗證當(dāng)前Subject時獲取到token為" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE));
// 從數(shù)據(jù)庫中獲取還用戶名對應(yīng)的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進(jìn)行一個完整的登錄過程
所以由以上代碼看出目前我們還沒有發(fā)現(xiàn)APP和web登錄d區(qū)別,那么區(qū)別是什么呢?
好吧,標(biāo)題不太準(zhǔn)確,應(yīng)該是登錄的時候和登陸之后會話保持在web和APP之間的區(qū)別,先說登錄:
APP和PC web所需的設(shè)備不同很大程度上決定了兩者之間的區(qū)別,web一般在PC上瀏覽,登錄的時候使用用戶名和密碼,如果使用了記住密碼就是用cookie認(rèn)證,web登錄有以下情況
APP在移動設(shè)備上查看,第一次登錄的時候使用用戶名和密碼,但是以后如果不是用戶主動退出,都應(yīng)該保持登錄狀態(tài),這樣才會有更好的用戶體驗,但是不可能一直保留該APP的會話,也不可能把密碼保存在本地,所以APP應(yīng)該以下的過程
貌似沒有看出什么區(qū)別,唯一的不同就是第二點(diǎn):怎么不用密碼登錄,web使用的是cookie(由瀏覽器自動維護(hù)的),APP怎么登陸呢?由于APP本地不保存密碼,那么也參考web,使用類似cookie的東西,我們叫他token吧,那問題就解決了,APP本地保存token,為了安全性,定期更新token,那再來看看會話的保持。
如果用戶登錄了,怎么保持登錄狀態(tài)呢,web有cookie和session配合解決這個問題,下面先簡單說一下我對這兩個東西的理解,因為APP會話就是參考這個原理設(shè)計的。
cookie:是由瀏覽器維護(hù)的,每次請求瀏覽器都會把cookie放在header里面(如果有的話),也可以看做js的可以訪問本地存儲數(shù)據(jù)的位置之一(另一個就是local storage)
session:由于http是無狀態(tài)的,但是有時候服務(wù)器需要把這次請求的數(shù)據(jù)保存下來留給下一次請求使用,即需要維護(hù)連續(xù)請求的狀態(tài),這個時候服務(wù)器就借助cookie,當(dāng)瀏覽器發(fā)送請求來服務(wù)器的時候,服務(wù)器會生成一個唯一的值,寫到cookie中返回給瀏覽器,同時生成一個session對象,這樣session和cookie值就有了一一對應(yīng)關(guān)系了,瀏覽下一次訪問的時候就會帶著這個cookie值,這個時候服務(wù)器就會獲得cookie的值,然后在自己的緩存里面查找是否存在和該cookie關(guān)聯(lián)的session
因為cookie和session的配合,shiro可以本身很好的支持web的登錄和會話保持,對于APP來說也可以借鑒cookie和session的這種實現(xiàn)方式,唯一存在的問題,就是web的cookie是由瀏覽器維護(hù)的,自動將cookie放在header里面,那我們APP只要把服務(wù)器返回的cookie放在header里面,每次訪問服務(wù)器的時候帶上就可以了。
解決了登錄和會話保持的問題,還剩一個免密碼登陸:
web:因為一般網(wǎng)頁主需要記住7天密碼(或者稍微更長)的功能就可以了,可以使用cookie實現(xiàn),而且shiro也提供了記住密碼的功能,在服務(wù)器端session不需要保存過長時間
APP:因為APP免密碼登錄時間需要較長(在用戶不主動退出的時候,應(yīng)該一直保持登錄狀態(tài)),這樣子在服務(wù)器端就得把session保存很長時間,給服務(wù)器內(nèi)存和性能上造成較大的挑戰(zhàn),存在的矛盾是:APP需要較長時間的免密碼登錄,而服務(wù)器不能保存過長時間的session,解決辦法:
這種方法存在的問題:
這里給出另外一種實現(xiàn)方式:
實現(xiàn)自己的SessionDao,將session保存在數(shù)據(jù)庫,這樣子的好處是,session不會大量堆積在內(nèi)存中,就不需要考慮session的過期時間了,對于APP這種需要長期保存session的情況來說,就可以無限期的保存session了,也就不用APP在每次session過期之后重新發(fā)送登錄請求了。實現(xiàn)方式如下:
為了使用Hibernate將Session保存到數(shù)據(jù)庫,新建一個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">
<!-- 標(biāo)識 -->
<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也是支持把對象保存在數(shù)據(jù)庫中的,但是該實體要實現(xiàn)Serializable,在取出來的時候強(qiáng)轉(zhuǎn)為對應(yīng)的對象即可,所以這里session的類型為Serializable
新建session緩存的方式的類,這里繼承自EnterpriseCacheSessionDAO,可以使用ehcache作為二級緩存,一定要記得實現(xiàn)save、update、readSession、delete方法,特別是save方法只是保存一個基本的session,重要的attribute都是update的,在readSession中從數(shù)據(jù)庫中讀取即可
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,然后保存到數(shù)據(jù)庫
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已經(jīng)被刪除,則從數(shù)據(jù)庫中查詢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 只實現(xiàn)了readSession,妄想自己新建一個SimpleSession來返回給shiro使用,嘗試過很多次之后不行,跟著調(diào)試了很多shiro源碼,發(fā)現(xiàn)在SimpleSession中Shiro不僅設(shè)置了基本的屬性,更重要的是設(shè)置了Attribute,但是我自己新建的SimpleSession沒有,所以認(rèn)證是失敗的,所以在此敬告各位一定要記得實現(xiàn)save和update方法。
雖然走了很多彎路,但是隨著對shiro源碼的調(diào)試學(xué)習(xí),對shiro了解更深了,不再僅僅停留在只會使用的地步上,有深入。
好了到此為止,正文完了,我們開頭提出的問題都解決完了,記下來掰扯掰扯在做APP登錄過程中遇到的問題以及一些自己的體會。
在考慮APP登錄的時候考慮了很多安全因素
在考慮這些問題的時候我意識到:
上一篇:微信小程序音樂播放器
下一篇:iOS App上架流程