1. 程式人生 > >我的shiro之旅: 十二 shiro 踢出使用者(同一使用者只能一處登入)

我的shiro之旅: 十二 shiro 踢出使用者(同一使用者只能一處登入)

部落格已移至 http://blog.gogl.top

看了一下官網,沒有找到關於如何控制同一使用者只能一處登入的介紹,網上也沒有找到相關的文章。可能有些人會記錄使用者的登入資訊,然後達到踢出使用者的效果。這裡介紹一個更簡單的方法。

如果我們跟shiro的原始碼,我們可以看到。當用戶登入成功後,shiro會把使用者名稱放到session的attribute中,key為DefaultSubjectContext_PRINCIPALS_SESSION_KEY,這個key的定義是在shiro的org.apache.shiro.subject.support.DefaultSubjectContext中,這個類有三個public的靜態屬性,其他都為private。其中PRINCIPALS_SESSION_KEY這個屬性記錄的是使用者名稱,而AUTHENTICATED_SESSION_KEY屬性記錄的是使用者認證,當用戶登入成功後,這個attribute的值是true。

曾經我想把AUTHENTICATED_SESSION_KEY這個attribute的值設定為false,表示使用者是退出狀態,這樣達到退出使用者的目的,不過沒有成功,shiro判斷使用者是否是登入狀態並不從這裡判斷。不過既然我們可以通過使用者名稱可以找到使用者對應的session,也很容易將該session刪除,讓使用者重新建立一個新的sesison。這裡給出一個幫助類,帶有跳出使用者的功能。

package com.concom.security.infrastructure.helper;

import java.util.Collection;

import org.apache.commons.lang.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.concom.lang.helper.TimeHelper;
import com.concom.security.application.memcache.CurrentUserMemcacheService;
import com.concom.security.application.user.UserService;
import com.concom.security.domain.user.User;

/**
 * @author Dylan
 * @time 2013-8-12
 */
public class ShiroSecurityHelper {
	
	private final static Logger log = LoggerFactory.getLogger(ShiroSecurityHelper.class);

	private static UserService userService;

	private static CurrentUserMemcacheService currentUserMemcacheService;
	
	private static SessionDAO sessionDAO;

	/**
	 * 把user放到cache中
	 * 
	 * @param user
	 */
	public static void setUser(User user) {
		currentUserMemcacheService.save(user);
	}

	/**
	 * 清除當前使用者的快取
	 */
	public static void clearCurrentUserCache() {
		if (hasAuthenticated()) {
			currentUserMemcacheService.remove(getCurrentUsername());
		}
	}

	/**
	 * 從cache拿當前user
	 * 
	 * @return
	 */
	public static User getCurrentUser() {
		if (!hasAuthenticated()) {
			return null;
		}
		User user = currentUserMemcacheService.get(getCurrentUsername());
		try {
			if (null == user) {
				user = userService.getByUsername(getCurrentUsername());
				ShiroSecurityHelper.setUser(user);
			}
			return user;
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

	/**
	 * 獲得當前使用者名稱
	 * 
	 * @return
	 */
	public static String getCurrentUsername() {
		Subject subject = getSubject();
		PrincipalCollection collection = subject.getPrincipals();
		if (null != collection && !collection.isEmpty()) {
			return (String) collection.iterator().next();
		}
		return null;
	}

	/**
	 * 
	 * @return
	 */
	public static Session getSession() {
		return SecurityUtils.getSubject().getSession();
	}

	/**
	 * 
	 * @return
	 */
	public static String getSessionId() {
		Session session = getSession();
		if (null == session) {
			return null;
		}
		return getSession().getId().toString();
	}
	
	/**
	 * @param username
	 * @return
	 */
	public static Session getSessionByUsername(String username){
		Collection<Session> sessions = sessionDAO.getActiveSessions();
		for(Session session : sessions){
			if(null != session && StringUtils.equals(String.valueOf(session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY)), username)){
				return session;
			}
		}
		return null;
	}
	
	/**踢除使用者
	 * @param username
	 */
	public static void kickOutUser(String username){
		Session session = getSessionByUsername(username);
		if(null != session && !StringUtils.equals(String.valueOf(session.getId()), ShiroSecurityHelper.getSessionId())){
			ShiroAuthorizationHelper.clearAuthenticationInfo(session.getId());
			log.info("############## success kick out user 【{}】 ------ {} #################", username,TimeHelper.getCurrentTime());
		}
	}

	/**
	 * @param userService
	 * @param currentUserMemcacheService
	 * @param sessionDAO
	 */
	public static void initStaticField(UserService userService,CurrentUserMemcacheService currentUserMemcacheService,SessionDAO sessionDAO){
		ShiroSecurityHelper.userService = userService;
		ShiroSecurityHelper.currentUserMemcacheService = currentUserMemcacheService;
		ShiroSecurityHelper.sessionDAO = sessionDAO;
	}
	
	/**
	 * 判斷當前使用者是否已通過認證
	 * @return
	 */
	public static boolean hasAuthenticated() {
		return getSubject().isAuthenticated();
	}

	private static Subject getSubject() {
		return SecurityUtils.getSubject();
	}


}

再通過spring注入幫助類需要的靜態屬性。

	<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
		<property name="staticMethod" value="com.concom.security.infrastructure.helper.ShiroSecurityHelper.initStaticField" />
		<property name="arguments">
			<list>
				<ref bean="userService"/>
				<ref bean="currentUserMemcacheService"/>
				<ref bean="sessionDAO"/>
			</list>
		</property>
	</bean>


讀者可以不關注userService,currentUserMemcacheService,其中踢出功能用到了SessionDAO,定義可能看我的shiro之旅: 七 shiro session 共享,文章裡的spring配置檔案有介紹,這裡不再作描述。從這個類名我們就可以猜想到,這個類是使用者管理session的。ShiroAuthorizationHelper這個幫助類在我的shiro之旅: 九 shiro 清理快取的許可權資訊這篇文章介紹到。通過使用者名稱使用使用者對應的session,然後將該session刪除,這個kickOutUser方法應該在使用者登入之前呼叫。

session刪除後,當用戶再請求伺服器時,服務端shiro會丟擲there is no session的異常,然後從新為請求建立一個新的session,就像使用者很長時間沒有點選瀏覽器,shiro的定時器定時將失效的session清除的時候也丟擲這個異常一樣。不過這個對使用者是透明的,對使用者的體驗沒有影響。

這是其中的一種方式,也許有更好的實現方式。如果讀者有更好的實現方式,希望能與我分享。