Version 3

    原文:开发基于JBoss AS 7.2.0的Java EE程序 - 09.如何实现用户在线状态查询以及踢人

    英文:JBoss AS 7.2.0 - Java EE application development - 09.How to monitor online users and kick user off line

     

     

    概述

    关于用户在线状态

    我们通常需要判定用户是否在线,有以下好处:

    • 统计在线用户数量峰值以及峰值出现时间,这样我们能够得知我们的服务器是否需要扩容,而且可以把一些备份等定时工作放在空闲时间段运行。
    • 可以向在线用户发送站内消息,用户能够立刻看到,便于我们跟用户进行及时的沟通。
    • 其它。

     

    本站判定用户是否在线原理:

    用Servlet Filter,拦截用户的页面访问,然后在缓存中刷新用户访问站点的最新时间。如果 当前时间 - 该时间 小于某个值(比如4分钟),我们就能够判定用户处于在线状态。

     

    关于迫使用户离线(踢人)

    如果有用户在论坛里面做坏事,那么我们需要迫使其离线。

     

    本站实现原理:

    手工修改用户锁定属性为true,

    刷新JAAS中对该username的缓存内容。

    用户再次访问本站的时候,JAAS就会认为该用户是非法用户(invalid user)并抛出HTTP 500错误。注意,JAAS认证的时候需要用户的锁定属性为false,我们在链接中 定义过“principalsQuery”:"SELECT hashedPassword FROM User WHERE username=? and activated=true and locked=false"。我们再根据500错误将用户访问地址重定向到 退出URL 就可以了。

     

    1. 用户在线状态

     

    1.1 用Filter获取用户最新在线时间并在缓存中刷新

    package com.ybxiang.forum.servlet.filter;

     

    import java.io.IOException;

    import java.security.Principal;

    import java.util.logging.Logger;

     

    import javax.ejb.EJB;

    import javax.servlet.Filter;

    import javax.servlet.FilterChain;

    import javax.servlet.FilterConfig;

    import javax.servlet.ServletException;

    import javax.servlet.ServletRequest;

    import javax.servlet.ServletResponse;

    import javax.servlet.annotation.WebFilter;

    import javax.servlet.http.HttpServletRequest;

     

    import com.ybxiang.forum.ejb.session.core.ICacheService;

     

    @WebFilter("/*")

    public class UserOnlineStatusRefreshingFilter implements Filter {

        static final Logger logger = Logger.getLogger(UserOnlineStatusRefreshingFilter.class.getName());

     

        @EJB

        ICacheService cacheService;

       

        public void doFilter(ServletRequest r1, ServletResponse r2, FilterChain chain) throws ServletException,IOException{

            HttpServletRequest request = (HttpServletRequest)r1;

           

            try {

                Principal p = request.getUserPrincipal();

                if(p!=null){
                    cacheService.refreshUserOnlineLatestTime(p.getName());
                }

            } catch (Exception e) {

                logger.info(e.getMessage());

            }

           

            //WARNING: do NOT swallow any Exception!!! If container can not see this exception, it will not redirect client to the error page.

            //You can catch Exceptions and print them, and you must throw them at last!

            chain.doFilter(r1, r2);

        }

        @Override

        public void destroy() {

        }

        @Override

        public void init(FilterConfig arg0) throws ServletException {

        }

    }

     

     

    @Local(ICacheService.class)

    @Singleton

    @Startup

    public class CacheService implements ICacheService{

        ...
        private HashMap<String, Long> userOnlineLatestTime= new HashMap<String, Long>();
        ...

        @PermitAll()

        public void refreshUserOnlineLatestTime(String username){

            userOnlineLatestTime.put(username, System.currentTimeMillis());

        }

        ...
        public boolean isUserOnline(String username){
            Long t = userOnlineLatestTime.get(username);
            if(t==null){
                return false;
            }else{
                if(System.currentTimeMillis() - t >= ONLINE_VALID_X_MINUTES_MILLIS){
                    userOnlineLatestTime.remove(username);
                    return false;
                }else{
                    return true;
                }
            }   
        }
        ...
    }

    1.2 从缓存读取用户最新在线时间并判定是否在线

    参见上面的 CacheService.isUserOnline(String username) 方法。

     

     

     

     

     

    2. 迫使用户离线

    2.1 以管理员的身份锁定某用户

    @Stateless

    @Local(IUserSession.class)

    public class UserSession implements IUserSession{

        ...

        @PersistenceContext()

        private EntityManager em;

        ...

        public static final long oneDayMills = 86400000;

        /**

         * 如果我们调用了 jaasCacheSession.flushJavaarmForumSecurityDomainJaasCache(u.getUsername())

         *    或者 jaasCacheSession.flushJavaarmForumSecurityDomainJaasCache(),

         * 我们会看到:

         *        被锁定用户的浏览器会出现:JBWEB000065: HTTP Status 500 - JBAS013323: Invalid User(javax.ejb.EJBAccessException: JBAS013323: Invalid User),这正式我们期望的!

         * 原理:如果该用户被刷新了cache,或者整个JAAS Cache被清理了,

         *        那么JBoss会根据 <module-option name="principalsQuery" ..> 元素判定当前用户不应该处于登陆状态(lock为true的不能登陆)!

         *        因此抛出了 “JBWEB000065: HTTP Status 500 - JBAS013323: Invalid User(javax.ejb.EJBAccessException: JBAS013323: Invalid User)”!

         *

         * 但是该页面不够友好,如果我们不刷新JAAS cache,测试:

         *        (a) 如果用户刷新了页面,我们就用 com.ybxiang.forum.servlet.filter.UserLockStatusCheckFilter.forceOffLine(HttpServletRequest, HttpServletResponse)

         *            迫使该用户退出系统!

         *        (a-1) 如果这时用户刷新页面, com.ybxiang.forum.servlet.filter.UserLockStatusCheckFilter.forceOffLine(HttpServletRequest, HttpServletResponse)

         *                检测会迫使用户退出!

         *        (a-2) 如果这时用户不刷新页面,直接关闭浏览器,重新开启浏览器,重新登陆,由于cache 没有刷新,所以依旧能登陆成功!!!

         *            但是我们继续用 com.ybxiang.forum.servlet.filter.UserLockStatusCheckFilter.forceOffLine(HttpServletRequest, HttpServletResponse)

         *            检测,那么再次迫使用户退出!这样不完美,因为用户毕竟能够登陆成功。

         *   

         * 更好解决方案,刷新JAAS cache,而且 为"JBWEB000065: HTTP Status 500" 设置跳转页面:

         *        <error-page>

         *            <error-code>500</error-code>

         *            <location>/faces/lock-force-offline.xhtml</location>

         *        </error-page>

         *        在lock-force-offline.xhtml中,运行JavaScript代码自动跳转到 http://javaarm.com/logoutServlet

         */

        @RolesAllowed({KnownJaasRoles.ADMINISTRATOR})
        public void lockUser(Long userId, Long days){

            User u = em.find(User.class, userId);

            u.setLocked(true);

            u.setLockReleaseDate(new Date(System.currentTimeMillis()+days*86400000));//one day: 86400000 ms

            em.merge(u);

            //

            cacheService.updateUserCache_ONLY_by_UserSessionOrLocalEJB(u);

            jaasCacheSession.flushJavaarmForumSecurityDomainJaasCache(u.getUsername());//OK

            //jaasCacheSession.flushJavaarmForumSecurityDomainJaasCache();//OK

        }

       

        /**

         * 如果unlock执行成功,不刷新cache,而用户重新登陆,这个时候会重新建立session没有问题。

         *

         */

        @RolesAllowed({KnownJaasRoles.ADMINISTRATOR})

        public void unlockUser(Long userId){

            User u = em.find(User.class, userId);

            u.setLocked(false);

            u.setLockReleaseDate(new Date());

            em.merge(u);

            //

            cacheService.updateUserCache_ONLY_by_UserSessionOrLocalEJB(u);

            jaasCacheSession.flushJavaarmForumSecurityDomainJaasCache(u.getUsername());

            //jaasCacheSession.flushJavaarmForumSecurityDomainJaasCache();

        }

        ...

    }

     

    如果我们对某User执行了上述锁定动作,那么该用户再次访问我们的网站的时候就会看到下面的内容:

     

    JBWEB000065: HTTP Status 500 - JBAS013323: Invalid User


    JBWEB000309: type JBWEB000066: Exception report

    JBWEB000068: message JBAS013323: Invalid User

    JBWEB000069: description JBWEB000145: The server encountered an internal error that prevented it from fulfilling this request.

    JBWEB000070: exception

    javax.servlet.ServletException: JBAS013323: Invalid User javax.faces.webapp.FacesServlet.service(FacesServlet.java:606) com.ybxiang.forum.servlet.filter.SourceCodeHidingFilter.doFilter(SourceCodeHidingFilter.java:32) com.ybxiang.forum.servlet.filter.ServletRequestPrintingFilter.doFilter(ServletRequestPrintingFilter.java:74) com.ybxiang.forum.servlet.filter.UserUrltrackFilter.doFilter(UserUrltrackFilter.java:53) com.ybxiang.forum.servlet.filter.UserOnlineStatusRefreshingFilter.doFilter(UserOnlineStatusRefreshingFilter.java:40) com.ybxiang.forum.servlet.filter.EncodingFilter.doFilter(EncodingFilter.java:37) 

    JBWEB000071: root cause

    javax.ejb.EJBAccessException: JBAS013323: Invalid User org.jboss.as.ejb3.security.SecurityContextInterceptor$1.run(SecurityContextInterceptor.java:54) org.jboss.as.ejb3.security.SecurityContextInterceptor$1.run(SecurityContextInterceptor.java:45) java.security.AccessController.doPrivileged(Native Method) ... 

    JBWEB000072: note JBWEB000073: The full stack trace of the root cause is available in the JBoss Web/7.2.0.Final logs.


    JBoss Web/7.2.0.Final

     

     

    注意

    JBoss AS 7.2.0 在遇到 “JBAS013323: Invalid User”之外的 其它异常时,也会向客户端显示“JBWEB000065: HTTP Status 500”错误,只是错误的消息有所不同而已。因此,我们不能在web.xml中添加下面的代码将用户重定向到 强制退出页面

        <error-page>

            <error-code>500</error-code>

            <location>/faces/lock-force-offline.xhtml</location>

        </error-page>

     

     

    2.2 退出解决方案1 - Filter根据用户锁定状态调用退出代码(无效)

    我们本来期望 UserLockStatusCheckFilter 能够检测用户的锁定状态,然后判定是否调用退出代码,但是在执行下面

    User u = cacheService.getUserFromCache(p.getName());

    的时候,就会直接跳转到上面的“JBWEB000065: HTTP Status 500 - JBAS013323: Invalid User”页面。

     

    个人猜测:JBoss禁止非法用户访问任何EJB方法,即便该方法允许任何JAAS Role访问!因为非法用户根本就谈不上Role!

     

    package com.ybxiang.forum.servlet.filter;

     

    import java.io.IOException;

    import java.security.Principal;

    import java.util.logging.Logger;

     

    import javax.ejb.EJB;

    import javax.servlet.Filter;

    import javax.servlet.FilterChain;

    import javax.servlet.FilterConfig;

    import javax.servlet.ServletException;

    import javax.servlet.ServletRequest;

    import javax.servlet.ServletResponse;

    import javax.servlet.http.HttpServletRequest;

    import javax.servlet.http.HttpServletResponse;

     

    import com.ybxiang.forum.ejb.entity.User;

    import com.ybxiang.forum.ejb.session.core.ICacheService;

     

    /**

    * replaced by "error code 500 redirection" in web.xml.

    * Please refer to com.ybxiang.forum.ejb.session.UserSession.lockUser(Long, Long)

    * @author yingbinx

    *

    */

    @javax.servlet.annotation.WebFilter("/*")

    public class UserLockStatusCheckFilter implements Filter {

        static final Logger logger = Logger.getLogger(UserLockStatusCheckFilter.class.getName());

     

        @EJB

        ICacheService cacheService;

       

        public void doFilter(ServletRequest r1, ServletResponse r2, FilterChain chain)throws ServletException,IOException {

            HttpServletRequest request = (HttpServletRequest)r1;

            HttpServletResponse response = (HttpServletResponse)r2;

            //

            Principal p = request.getUserPrincipal();

            if(p!=null){

                User u = cacheService.getUserFromCache(p.getName());

                if(u==null){

                    throw new RuntimeException("User is NOT in cache! IMPOSSIBLE!");

                }else{

                    if(u.getLocked()){

                        //this user is locked, now we force it off-line.

                        forceOffLine(request,response);

                    }

                }

            }

           

            //WARNING: do NOT swallow any Exception!!! If container can not see this exception, it will not redirect client to the error page.

            //You can catch Exceptions and print them, and you must throw them at last!

            chain.doFilter(r1, r2);

        }

        @Override

        public void destroy() {

        }

        @Override

        public void init(FilterConfig arg0) throws ServletException {

        }

     

       
        /**
         * Copied from: com.ybxiang.forum.servlet.LogoutServlet.doGet(HttpServletRequest, HttpServletResponse)
         */
        private void forceOffLine(HttpServletRequest request,
                HttpServletResponse response){
            try{
                response.setHeader("Cache-Control", "no-cache, no-store");
                response.setHeader("Pragma", "no-cache");
                response.setHeader("Expires", new java.util.Date().toString());//http://www.coderanch.com/t/541412/Servlets/java/Logout-servlet-button 
                response.setHeader("Connection", "close");//http://javaarm.com/faces/display.xhtml?tid=2416&page=1#post_18198

     

                if(request.getSession(false)!=null){
                    request.getSession(false).invalidate();//remove session.
                }
                if(request.getSession()!=null){
                    request.getSession().invalidate();//remove session.
                }
               
                request.logout();//JAAS log out (from servlet specification)! It is a MUST!
            }catch(Exception e){
                logger.severe("Exception during forcing locked user offline:"+e.getMessage());
            }
           
            try{
                response.sendRedirect(request.getContextPath()+"/faces/lock-force-offline.xhtml");
            }catch(Exception e){
                logger.severe("Exception during forcing locked user offline:"+e.getMessage());
            }
        }
    }

    2.3 退出解决方案2 - ...

    在web.xml中,根据特殊异常进行跳转,比如:

        <error-page>

            <exception-type>com.sun.faces.context.FacesFileNotFoundException</exception-type>

            <location>/faces/error-page/unknown-resource.xhtml</location>

        </error-page>

        <error-page>

            <exception-type>java.lang.SecurityException</exception-type>

            <location>/faces/error-page/security-exception.xhtml</location>

        </error-page>

     

    可是,“JBWEB000065: HTTP Status 500 - JBAS013323: Invalid User”这个页面抛出的只是很一般的javax.servlet.ServletException异常(消息内容特殊而已):

    javax.servlet.ServletException: JBAS013323: Invalid User

     

     

    暂时向被锁定的用户显示“JBWEB000065: HTTP Status 500 - JBAS013323: Invalid User”页面吧。
    以后再研究如何 重定向或覆盖 该页面。

    ...