原文:开发基于JBoss AS 7.2.0的Java EE程序 - 09.如何实现用户在线状态查询以及踢人
概述
关于用户在线状态
我们通常需要判定用户是否在线,有以下好处:
- 统计在线用户数量峰值以及峰值出现时间,这样我们能够得知我们的服务器是否需要扩容,而且可以把一些备份等定时工作放在空闲时间段运行。
- 可以向在线用户发送站内消息,用户能够立刻看到,便于我们跟用户进行及时的沟通。
- 其它。
本站判定用户是否在线原理:
用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_18198if(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”页面吧。
以后再研究如何 重定向或覆盖 该页面。
...
Comments