Java中單點登入的實現——類似QQ“頂號”操作
簡介
對於目前的網路環境而言,在開發的系統中建立一個完善的賬號系統尤為重要。而其中的一個手段就是進行多點登入的限制。類似於騰訊qq應用軟體的機制,在其他裝置上登入自己賬號的時候,當前登入會被踢出。這樣就避免了一些自己本身的失誤或者一些惡意的賬號攻擊。
而這樣的多點登陸限制一般有兩種情況:一就是上述的機制,將上一次的登入踢出,另一種就是如果當前賬號已經登入,則限制當前登入。
比較而言,限制當前登入的做法在一定程度上會出現很大的問題,例如當用戶剛剛登陸,但是出現掉線或者其他不可預料的情況後,再次登陸的時候會被限制,只能等到登入超時後才能再次登陸,而且這種情況是無法解決的。因此我們一般會在系統中採用另一種限制措施。
原理
目前的Web系統中,對於登入資訊一般都使用session來進行保持,對於限制多點登陸的實現,可以將所有儲存登入資訊的session存到一個靜態變數中,對於已經存在的登入資訊,在進行新的登陸時,需要把儲存當前登入資訊的session銷燬,或者清空其登入資訊,然後從儲存登入資訊的所有session的靜態變數中將原始登入的session移除,完成踢出操作。
重要介面分析
通過對session的監聽,可以在session儲存使用者登入資訊的時候將此session儲存到線上列表中,此處需要使用到HttpSessionAttributeListener介面和HttpSessionListener。接下來先來分析HttpSessionListener介面
public interface HttpSessionListener extends EventListener {
void sessionCreated(HttpSessionEvent var1);
void sessionDestroyed(HttpSessionEvent var1);
}
介面HttpSessionListener中共有兩個方法:
void sessionCreated(HttpSessionEvent var1):在session建立時呼叫;
void sessionDestroyed(HttpSessionEvent var1):在session銷燬時呼叫。
這裡主要用到sessionDestroyed方法,以便在使用者登入超時的時候將使用者session從線上列表中移除
接下來是對session屬性監聽的介面HttpSessionAttributeListener:
public interface HttpSessionAttributeListener extends EventListener {
void attributeAdded(HttpSessionBindingEvent var1);
void attributeRemoved(HttpSessionBindingEvent var1);
void attributeReplaced(HttpSessionBindingEvent var1);
}
此介面中有三個方法:
void attributeAdded(HttpSessionBindingEvent var1):在向session中新增屬性是呼叫;
void attributeRemoved(HttpSessionBindingEvent var1):在移除session屬性是呼叫;
void attributeReplaced(HttpSessionBindingEvent var1):在替換session某屬性值的時候呼叫
我們主要用到attributeAdded和attributeRemoved兩個方法,attributeAdded主要用於在session中儲存登入資訊時將使用者session儲存到線上列表中,在在將儲存原始登入資訊的session踢出線上列表之前,許喲先將session中儲存的登入資訊清空,因此呼叫attributeRemoved方法,在清空登入資訊之後,呼叫相應的方法踢出原始登入。
實現
當用戶在輸入登入資訊並提交後,首先需要對使用者資訊的正確性進行驗證,否則會出現只輸入使用者名稱但是點選登入之後,會出現非法踢掉上一次登入。
在進行使用者名稱和密碼的正確性驗證之後,需要進行重複登陸驗證,判斷是否出現重複登陸,如果非重複登陸,就將當前登入的資訊存入session,並通過觸發監聽來講儲存當前登入資訊的session存入到線上列表中。當檢測到是重複登陸時,將上一次登入的session的登入資訊清空,並通過觸發監聽來踢出線上列表中已登陸的session,即可完成異地登入的踢出操作。
因此首先需要一個物件來儲存所有的線上使用者session的集合。因此 需要定義一個相應的session容器來存放:
import javax.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class OnlineUserMap {
public static List<HttpSession> sessionList=new ArrayList<HttpSession>();
public List<HttpSession> getSession() {
return sessionList;
}
public void setSession(List<HttpSession> session) {
this.sessionList = session;
}
public void addOnLine(HttpSession se){
List<HttpSession> selist=this.getSession();
selist.add(se);
this.setSession(selist);
}
public void removeOnLine(String seid){
List<Integer> listIndex=new ArrayList<Integer>();
for (HttpSession session:sessionList)
{
if(session.getId().equals(seid))
{
listIndex.add(sessionList.indexOf(session));
}
}
for (int j = 0; j < listIndex.size(); j++) {
sessionList.get(listIndex.get(j)).removeAttribute("curUser");
sessionList.remove(listIndex.get(j));
}
}
}
可以看到在上述容器的定義中,分別定義了線上使用者的新增和移除操作方法。次容器可用來儲存所有當前線上的使用者的session。
如下是登陸時需要做的驗證:
public Map<String,Object> Login(HttpServletRequest request)
{
Map<String,Object> result=new HashMap<String,Object>();
String userId =request.getParameter("name");
String userPassword =request.getParameter("pwd");
Map<String,Object> map=new HashMap<String,Object>();
map.put("NAME",userId);
map.put("PWD",userPassword);
if (userId!=null && userPassword!=null) {
User appUser = this.userService.checkLogin(map);
if (appUser!=null)
{
try {
//重複登陸驗證
new checkMulitLogin().checkSuccess(appUser.getId());
result.put("isok",true);
//儲存當前登入資訊到session
request.getSession().setAttribute("curUser",appUser);
} catch (Exception e) {
result.put("isok",false);
result.put("errorInfo","強制下線失敗");
}
}
}
else
{
result.put("isok",false);
result.put("errorInfo","使用者名稱密碼輸入錯誤!");
}
return result;
}
上述示例中,首先通過和資料庫中儲存的資訊進行對比完成使用者名稱密碼的正確性驗證,緊接著呼叫重複性驗證工具類中的驗證方法,判斷是否為重複登陸。在判斷登入資訊正確性和重複登入之後,需要對線上列表和session進行操作,來做相應的操作。
驗證重複登陸的工具類:
import com.javafeng.entity.OnlineUserMap;
import com.javafeng.entity.User;
import javax.servlet.http.HttpSession;
import java.util.List;
public class checkMulitLogin {
public void checkSuccess(int id) throws Exception{
List<HttpSession> list = new OnlineUserMap().getSession();
int index=-1;
for (HttpSession session:list) {
if (((User)session.getAttribute("curUser")).getId()==id)
{
index=new OnlineUserMap().getSession().indexOf(session);
}
}
if (index!=-1)
new OnlineUserMap().removeOnLine(list.get(index).getId());
}
}
可以看到,當判斷線上列表中已存在當前想要登入的使用者資訊時,呼叫容器的移除方法,將儲存當前登入資訊的session從線上列表中移除。
在做出相應的操作之後,會將登入資訊儲存到當前的session中,此時會觸發監聽器,將session新增到線上列表中,若使用者主動做出登出的操作時,只需從session中移除當前的登入資訊,便會觸發監聽器,將儲存登入資訊的session從線上列表中移除。
以下是監聽器的示例:
public class LoginListener implements HttpSessionAttributeListener,HttpSessionListener{
//session新增屬性時觸發,呼叫新增方法,將登入新增至線上列表
@Override
public void attributeAdded(HttpSessionBindingEvent httpSessionBindingEvent) {
String username = httpSessionBindingEvent.getName();
if (username == "curUser")
{
new OnlineUserMap().addOnLine(httpSessionBindingEvent.getSession());
}
}
@Override
public void attributeRemoved(HttpSessionBindingEvent httpSessionBindingEvent) {
}
@Override
public void attributeReplaced(HttpSessionBindingEvent httpSessionBindingEvent) {
}
@Override
public void sessionCreated(HttpSessionEvent httpSessionEvent) {
}
//session移除屬性時觸發,呼叫移除方法,將登入踢出線上列表
@Override
public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
String sessionid = httpSessionEvent.getSession().getId();
new OnlineUserMap().removeOnLine(sessionid);
}
}
監聽器在編寫完成後,程式無法自動將其啟動,需要在web.xml做相應的配置,讓程式在啟動的過程中將監聽器啟動。web.xml配置如下:
<listener>
<listener-class>com.test.listener.LoginListener</listener-class>
</listener>
通過上述的步驟即可實現web專案登入時的“頂號”操作,被頂號後,因為原登入失效,因此在原登入方重新整理介面或者其他需要獲取登入資訊的操作時,因為登入資訊已經為空,因此會提示使用者重新登入並跳轉到登入介面。
接下來是登入頁面的程式碼示例:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<%
String path = request.getContextPath();
String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/";
%>
<head>
<script src="<%=basePath%>static/js/jq.js"></script>
<title>Title</title>
</head>
<body>
<p id="errorInfo"></p>
<input id="name" type="text" name="name"/>
<input id="pwd" type="password" name="pwd"/>
<span onclick="javascript:submit()" style="cursor: pointer">登入</span>
<script>
function submit() {
var name = $("#name").val();
var pwd = $("#pwd").val();
var data={
name:name,
pwd:pwd
}
$.post("/user/login",data,function (result) {
console.log(result);
if(result.isok==true)
{
window.location.href="<%=basePath%>user/list";
}
else
{
$("#errorInfo").append(result.errorInfo)
}
});
}
</script>
</body>
</html>
上述的程式碼中,通過JQuery的方式提交資料,若登陸驗證不成功,則返回相對應的錯誤資訊並顯示在登入頁面中。
以下是登陸後的測試主頁面:
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<%if(session.getAttribute("curUser")==null){%>
<script type="text/javascript">
alert("登入已失效,請重新登入!");
window.top.location.href='<%=basePath%>user/toLogin';
</script>
<%}%>
測試用主頁面
</body>
</html>
在主頁面中進行了登入資訊的驗證,若驗證不通過,則提示使用者登入已失效,需要重複登陸,然後跳轉至登入頁面,讓使用者重新登入。
因為Spring MVC會攔截所有的請求,因此程式無法直接訪問到Jsp頁面。一次需要在控制器中做一定的操作。這裡通過訪問控制頁面跳轉的控制器,在控制器中實現頁面跳轉。
首先是跳轉到測試主頁的控制器方法:
@RequestMapping(value = "/list")
public String list(HttpServletRequest request)
{
return "/show";
}
跳轉到登入頁面的控制器方法:
@RequestMapping(value = "/toLogin")
public String toLogin(HttpServletRequest request)
{
return "/account/Login";
}
可以看到,在控制器方法中返回了需要跳轉的路勁。在Spring MVC的配置中提到了檢視解析器的配置,而檢視解析器會處理控制器返回的字串,為返回的字串新增配置好的字首和字尾,組成一個有效的Jsp檔案路徑並進行跳轉。
例如如下的檢視解析器配置:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
prefix屬性為字首,suffix屬性為字尾,則處理後的連結地址就變成了“/WEB-INF/jsp/控制器返回的字串.jsp”,即一個Jsp檔案的路徑。通過這種方法可以完成Jsp頁面的訪問。