參考連接:
單一用戶登陸實現:https://www.cnblogs.com/leechenxiang/p/6171151.html
shiro攔截器介紹:http://jinnianshilongnian.iteye.com/blog/2025656
shiro全配置文件:http://www.cnblogs.com/leechenxiang/p/6136017.html
需求:
現在項目應用在web端和app端,一個用戶要麼登陸在web端要麼app端,並且不能重複登陸,如果重複登陸,新登陸的客戶端會踢掉已經登陸的客戶端用戶。即一個用戶只能同時在線一個客戶端
實現:
第一種:不適用框架,待補充
第二種:使用shiro框架來實現
如果使用shiro框架來實現,系統的認證授權模塊都由shiro框架來實現,可能需要重構權限框架
直接看如下連接:
https://www.cnblogs.com/leechenxiang/p/6171151.html
權限相關知識和shiro框架介紹見shiro權限
sso單點登陸見單點登陸
單一用戶登陸實現步驟如下:
- 導包,maven或者直接下載,直接百度
- 利用shiro框架搭建權限模塊,見上文
- 利用shiro新建自定義攔截器
自定義攔截器可以繼承shiro相關攔截器,擴展AccessControlFilter,其繼承了PathMatchingFilter,並擴展了了兩個方法:
isAccessAllowed:即是否允許訪問,返回true表示允許;
onAccessDenied:表示isAccessAllowed方法返回爲false時,即用戶訪問拒絕時,是否自己處理,如果返回true表示自己不處理且繼續攔截器鏈執行,返回false表示自己已經處理了(比如重定向到另一個頁面)。
//導包省略
//自定義單一登陸shiro攔截器,可以繼承AccessControlFilter類
public class SessionKickoutControlFilter extends AccessControlFilter{
//緩存,存入登陸用戶的sessionId,參數string爲kickout+用戶名+web/app,Deque<Serializable>爲sessionId集合(可能同時多個)
private Cache<String, Deque<Serializable>> sessionKickoutCache;
private boolean kickoutAfter = false;
private int maxSession = 1;//最大登陸session人數
private SessionManager sessionManager;
private static final String keyPrefix = "kickout";
@Autowired
AdminService adminService;
@Autowired
UserLogService userLogService;
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//request獲得當前用戶
Subject subject = getSubject(request,response);
if(!subject.isAuthenticated() && !subject.isRemembered()) {
//如果沒有登錄,返回true登陸即可
return true;
}
Session session = subject.getSession();
String username = (String) subject.getPrincipal();
Serializable sessionId = session.getId();
String client = null;
String[] clients = request.getParameterValues("client");
if(clients != null && clients.length > 0){
client = "1".equals(clients[0]) ? "APP" : "WEB";
}else {
client = ShiroUtil.getLoginClient();
}
//需要同步緩存值
synchronized(this.sessionKickoutCache){
Deque<Serializable> deque = sessionKickoutCache.get(keyPrefix + client + username);
if(deque == null || deque.size() == 0) {
deque = new LinkedList<Serializable>();
}
//如果隊列裏沒有此sessionId,且用戶沒有被踢出;放入隊列
if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
deque.push(sessionId);
}
//如果隊列裏的sessionId數超出最大會話數,開始踢人
while(deque.size() > maxSession) {
Serializable kickoutSessionId = null;
if(kickoutAfter) { //如果踢出後者
kickoutSessionId = deque.removeFirst();
} else { //否則踢出前者
kickoutSessionId = deque.removeLast();
}
try {
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if(kickoutSession != null) {
//設置會話的kickout屬性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {//ignore exception
}
}
sessionKickoutCache.put(keyPrefix + client + username, deque);
}
//如果被踢出了,直接退出,重定向到踢出後的地址
if (session.getAttribute("kickout") != null && (Boolean)session.getAttribute("kickout")) {
session.setAttribute("kickout", false);
//會話被踢出了
try {
String loginIp = BaseUtil.getIpAddr((HttpServletRequest)request);
userLogService.insertUserLog(ShiroUtil.getUserNo(),ShiroUtil.getUserName(), ShiroUtil.getLoginClient(), loginIp, UserLogService.USER_LOG_LOGOUT);
subject.logout();
} catch (Exception e) { //ignore
}
saveRequest(request);
response.getWriter().write(JSONObject.toJSONString(adminService.kickoutLogin()));
//如果踢出以前登陸用戶,可以讓他重定向kickoutUrl(登陸頁面)
WebUtils.issueRedirect(request, response, kickoutUrl);
return false;
}
return true;
}
//返回false不允許直接訪問,要進行攔截,返回true,則可以直接訪問,一般繼承這個類就是爲了不能直接訪問,要進行攔截所以要返回false
@Override
protected boolean isAccessAllowed(ServletRequest request,
ServletResponse response, Object mappedValue) throws Exception {
return false;
}
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}
public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
}
public void setCacheManager(CacheManager cacheManager){
this.sessionKickoutCache = cacheManager.getCache("sessionKickoutCache");
}
}
- 配置SessionKickoutControlFilter攔截器
第一種:未整合spring
在shrio.ini文件中加入
[filters]
kickout=cn.paic.rep.pare.shiro.SessionKickoutControlFilter
[urls]
/**=kickout//表示對所有的連接都需要進行攔截判斷
第二種:spring整合(常用)
在application-shiro.xml文件中配置
<!--第一步:在spring中注入自定義的攔截器
樓主定義了多個攔截器:先判斷單點登陸,然後單一用戶登陸,最後權限驗證,形成一個責任鏈
-->
<bean id="ssoFilter" class="cn.paic.rep.pare.shiro.SSOFilter" />
<bean id="_permissionsFilter" class="cn.paic.rep.pare.shiro._PermissionsFilter"></bean>
<!--這是的單一用戶登陸配置項-->
<bean id="sessionKickoutControlFilter" class="cn.paic.rep.pare.shiro.SessionKickoutControlFilter">
<property name="cacheManager" ref="cacheManager"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="kickoutAfter" value="false"/>
<property name="maxSession" value="1"/>
<!--跳轉頁面 <property name="kickoutUrl" value="/login"/>
--> </bean>
<!-- 配置shiro的過濾器工廠類,id- shiroFilter要和我們在web.xml中配置的過濾器一致 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- 調用我們配置的權限管理器 -->
<property name="securityManager" ref="securityManager" />
<!-- 配置我們的登錄請求地址 -->
<!-- <property name="loginUrl" value="/login" /> -->
<!-- 配置我們在登錄頁登錄成功後的跳轉地址,如果你訪問的是非/login地址,則跳到您訪問的地址 -->
<!-- <property name="successUrl" value="/home" /> -->
<!-- 如果您請求的資源不再您的權限範圍,則跳轉到/403請求地址 -->
<!-- <property name="unauthorizedUrl" value="/api/admin/unauthorized" /> -->
<!-- 權限配置 -->
<!-- 第二步:以下爲配置自定義的攔截器 -->
<property name="filters">
<map>
<entry key="ssoFilter" value-ref="ssoFilter" />
<entry key="perms" value-ref="_permissionsFilter" />
<entry key="kickout" value-ref="sessionKickoutControlFilter"/>
</map>
</property>
</bean>
<!-- 第三步:權限資源配置 -->
<bean id="filterChainDefinitionsService" class="cn.paic.rep.pare.shiro.SimpleFilterChainDefinitionsService">
<property name="definitions">
<value>
<!--不需要登陸和攔截即可訪問-->
/api/file/getBbsImg/*=anon
<!--左邊對api下的請求都要進行攔截判斷-->
/api/** =kickout,ssoFilter
</value>
</property>