Java中单点登录的实现——类似QQ“顶号”操作
2019年8月14日 13:59 Java框架技术 评论

简介

  对于目前的网络环境而言,在开发的系统中建立一个完善的账号系统尤为重要。而其中的一个手段就是进行多点登录的限制。类似于腾讯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页面的访问。

评论
评论已暂时关闭。