7

出于此问题中提到的原因,我正在生成随机令牌,这些令牌放置在 a 中java.util.List,并且List保存在会话范围内。

经过一些谷歌搜索后,我决定每小时删除List会话中包含的所有元素(令牌)。

我可以考虑使用Quartz API,但这样做似乎无法操纵用户的会话。我在 Spring 中使用 Quartz API(1.8.6、2.x 与我正在使用的 Spring 3.2 不兼容)尝试的内容如下所示。

package quartz;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

public final class RemoveTokens extends QuartzJobBean
{    
    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException
    {
        System.out.println("QuartzJobBean executed.");
    }
}

它在application-context.xml文件中配置如下。

<bean name="removeTokens" class="org.springframework.scheduling.quartz.JobDetailBean">
    <property name="jobClass" value="quartz.RemoveTokens" />
    <property name="jobDataAsMap">
        <map>
            <entry key="timeout" value="5" />
        </map>
    </property>
</bean>

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
      <property name="jobDetail" ref="removeTokens"/>
      <property name="startDelay" value="10000"/>
      <property name="repeatInterval" value="3600000"/>
</bean>

<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
  <property name="triggers">
      <list>
          <ref bean="simpleTrigger" />
      </list>
  </property>
</bean>

类中的重写方法RemoveTokens每小时执行一次,初始间隔为 10 秒,如 XML 中配置的那样,但无法执行某些类的某些方法来删除List存储在用户会话中的可用令牌。可能吗?

List以定义的时间间隔(每小时)将其删除存储在会话范围中的公平方法是什么?如果能通过使用这个 Quartz API 来实现,那就更好了。


编辑:

根据下面的答案,我尝试了以下方法,但不幸的是,它没有任何区别。

application-context.xml文件中,

<task:annotation-driven executor="taskExecutor" scheduler="taskScheduler"/>
<task:executor id="taskExecutor" pool-size="5"/>
<task:scheduler id="taskScheduler" pool-size="10"/>

这需要以下额外的命名空间,

xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/task
                    http://www.springframework.org/schema/task/spring-task-3.2.xsd"

以下 bean 已注册为会话范围 bean。

package token;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.apache.commons.lang.StringUtils;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

@Service
//@Scope("session")
public final class SessionToken implements SessionTokenService
{
    private List<String> tokens;

    private static String nextToken()
    {
        long seed = System.currentTimeMillis(); 
        Random r = new Random();
        r.setSeed(seed);
        return Long.toString(seed) + Long.toString(Math.abs(r.nextLong()));
    }

    @Override
    public boolean isTokenValid(String token)
    {        
        return tokens==null||tokens.isEmpty()?false:tokens.contains(token);
    }

    @Override
    public String getLatestToken()
    {
        if(tokens==null)
        {
            tokens=new ArrayList<String>(0);
            tokens.add(nextToken());            
        }
        else
        {
            tokens.add(nextToken());
        }

        return tokens==null||tokens.isEmpty()?"":tokens.get(tokens.size()-1);
    }

    @Override
    public boolean unsetToken(String token)
    {                
        return !StringUtils.isNotBlank(token)||tokens==null||tokens.isEmpty()?false:tokens.remove(token);
    }

    @Override
    public void unsetAllTokens()
    {
        if(tokens!=null&&!tokens.isEmpty())
        {
            tokens.clear();
        }
    }
}

以及它实现的接口,

package token;

import java.io.Serializable;

public interface SessionTokenService extends Serializable
{
    public boolean isTokenValid(String token);
    public String getLatestToken();
    public boolean unsetToken(String token);
    public void unsetAllTokens();
}

这个bean在application-context.xml文件中配置如下。

<bean id="sessionTokenCleanerService" class="token.SessionToken" scope="session">
    <aop:scoped-proxy proxy-target-class="false"/>
</bean>

现在,我在下面的课程中注入了这项服务。

package token;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@Service
public final class PreventDuplicateSubmission
{    
    @Autowired
    private final SessionTokenService sessionTokenService=null;

    @Scheduled(fixedDelay=3600000)
    public void clearTokens()
    {
        System.out.println("Scheduled method called.");
        sessionTokenService.unsetAllTokens();            
    }
}

application-context.xml文件中,

<bean id="preventDuplicateSubmissionService" class="token.PreventDuplicateSubmission"/>

上述两个 bean 都带有注释,@Service它们应该是文件的一部分context:component-scandispatacher-servelt.xml或任何名称)。

使用前面代码片段中的注释注释的方法以@Scheduled给定的速率定期调用,但它会导致以下明显的异常被抛出。

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.sessionTokenCleanerService': Scope 'session' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:343)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
    at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:33)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:184)
    at $Proxy79.unsetAllTokens(Unknown Source)
    at token.PreventDuplicateSubmission.clearTokens(PreventDuplicateSubmission.java:102)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:64)
    at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:53)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
    at java.util.concurrent.FutureTask$Sync.innerRunAndReset(FutureTask.java:351)
    at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:178)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:178)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1110)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:603)
    at java.lang.Thread.run(Thread.java:722)
Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
    at org.springframework.web.context.request.SessionScope.get(SessionScope.java:90)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:329)
    ... 19 more

要清除存储在用户会话中的数据,应在每个用户会话的定义时间间隔内定期调用执行此任务的方法,但本次尝试并非如此。方法是什么?问题可能很简单:如何从每个用户的会话中触发一个固定的时间间隔?Spring 或 Servlet API 是否支持完成此任务?

4

7 回答 7

9

注释

恭喜您使用令牌来防止重新提交(“引入同步令牌”重构自 Core J2EE 模式一书)。:^)

Quartz 对于复杂或精确的调度很有价值。但是您的要求不需要 Quartz。学习 CDI、java.util.Timer、ScheduledExecutor 和/或 EJB 计时器可能更有用。

如果使用调度程序,正如您所提到的,最好让所有用户共享一个单例调度程序,而不是每个用户会话都有一个调度程序和线程实例。

如果您曾经存储对 HttpSession 的引用或将其作为参数传递,请小心。当会话完成时,存储引用可以防止垃圾收集 - 导致(大?)内存泄漏。尝试使用 HttpSessionListener 和其他技巧清理引用可能无法正常工作并且很混乱。将 HttpSession 作为参数传递给方法会使它们人为地依赖 Servlet 容器并且难以进行单元测试,因为您必须模拟复杂的 HttpSession 对象。将所有会话数据包装在单个对象 UserSessionState 中会更干净 - 将对此的引用存储在会话和对象实例变量中。这也称为上下文对象模式——即将所有范围数据存储在一个/几个 POJO 上下文对象中,独立于 HTTP 协议类。

回答

我建议您的问题有两种替代解决方案:

  1. 使用java.util.Timer单例实例

    您可以将java.util.Timer(Java SE 1.3 中引入)替换为ScheduledExecutor(Java SE 5 中引入)以获得几乎相同的解决方案。

    这在 JVM 中可用。它不需要 jar 设置和配置。你调用timerTask.schedule,传入一个TimerTask实例。当计划到期时,timerTask.run被调用。您通过删除计划任务timerTask.canceltimerTask.purge释放已使用的内存。

    您对 , 包括它的构造函数进行编码TimerTask,然后创建一个新实例并将其传递给 schedule 方法,这意味着您可以在TimerTask;中存储您需要的任何数据或对象引用。您可以保留对它的引用并在以后随时调用 setter 方法。

    我建议你创建两个全局单例:一个Timer实例和一个TimerTask实例。在您的自定义TimerTask实例中,保留所有用户会话的列表(以 POJO 形式UserSessionState或 Spring/CDI bean 形式,而不是 形式HttpSession)。向该类添加两个方法:addSessionObjectremoveSessionObject,带有UserSessionState或类似的参数。在该TimerTask.run方法中,遍历集合UserSessionState并清除数据。

    创建自定义 HttpSessionListener - 从 sessionCreated 中,将新的 UserSessionState 实例放入会话中并调用 TimerTask.addUserSession;从 sessionDestroyed 调用 TimerTask.removeUserSession。

    调用全局范围的单例timer.schedule来调度 TimerTask 实例以清除会话范围引用的内容。

  2. 使用具有上限大小的令牌列表(无清理)

    不要根据经过的时间清理令牌。而是限制列表大小(例如 25 个标记)并存储最近生成的标记。

    这可能是最简单的解决方案。当您将元素添加到列表时,请检查您是否超过了最大大小,如果是,则从列表中最早的索引开始插入:

    if (++putIndex > maxSize) {
        putIndex = 0;
    }
    list.put(putIndex, newElement);
    

    这里不需要调度程序,也不需要形成和维护一组所有用户会话。

于 2013-04-23T06:02:33.143 回答
3

我的想法是这样的:

精简版:

  1. 将任务间隔更改为 1 分钟;
  2. addunsetAllTokensIfNeeded方法SessionTokenService将每分钟调用一次,但如果确实需要它只会清理令牌列表(在这种情况下,决定是根据时间完成的)。

详细版本:

您的计划任务将每分钟运行一次,unsetAllTokensIfNeeded调用SessionToken. 该方法实现将检查最后一次令牌列表是干净的,unsetAllTokens如果是一小时前就调用。

但是为了在每个单独的会话范围内调用它,SessionTokenService您将需要现有SessionTokenServices 的列表,这可以通过在创建时将其注册到将清理它的服务来实现(这里您将使用 aWeakHashMap来避免硬引用,这将避免被垃圾收集器收集的对象)。

实现将是这样的:

会话令牌:

界面:

public interface SessionTokenService extends Serializable {
    public boolean isTokenValid(String token);
    public String getLatestToken();
    public boolean unsetToken(String token);
    public void unsetAllTokens();
    public boolean unsetAllTokensIfNeeded();
}

执行:

@Service
@Scope("session")
public final class SessionToken implements SessionTokenService, DisposableBean, InitializingBean {

    private List<String> tokens;
    private Date lastCleanup = new Date();

// EDIT {{{

    @Autowired
    private SessionTokenFlusherService flusherService;

    public void afterPropertiesSet() {
        flusherService.register(this);
    }

    public void destroy() {
        flusherService.unregister(this);
    }

// }}}

    private static String nextToken() {
        long seed = System.currentTimeMillis(); 
        Random r = new Random();
        r.setSeed(seed);
        return Long.toString(seed) + Long.toString(Math.abs(r.nextLong()));
    }

    @Override
    public boolean isTokenValid(String token) {        
        return tokens == null || tokens.isEmpty() ? false : tokens.contains(token);
    }

    @Override
    public String getLatestToken() {
        if(tokens==null) {
            tokens=new ArrayList<String>(0);
        }
        tokens.add(nextToken());            

        return tokens.isEmpty() ? "" : tokens.get(tokens.size()-1);
    }

    @Override
    public boolean unsetToken(String token) {                
        return !StringUtils.isNotBlank(token) || tokens==null || tokens.isEmpty() ? false : tokens.remove(token);
    }

    @Override
    public void unsetAllTokens() {
        if(tokens!=null&&!tokens.isEmpty()) {
            tokens.clear();
            lastCleanup = new Date();
        }
    }

    @Override
    public void unsetAllTokensIfNeeded() {
        if (lastCleanup.getTime() < new Date().getTime() - 3600000) {
           unsetAllTokens();
        }
    }

}

会话令牌刷新器:

界面:

public interface SessionTokenFlusherService {
    public void register(SessionToken sessionToken);
    public void unregister(SessionToken sessionToken);
}

执行:

@Service
public class DefaultSessionTokenFlusherService implements SessionTokenFlusherService {

    private Map<SessionToken,Object> sessionTokens = new WeakHashMap<SessionToken,Object>();

    public void register(SessionToken sessionToken) {
        sessionToken.put(sessionToken, new Object());
    }

    public void unregister(SessionToken sessionToken) {
        sessionToken.remove(sessionToken);
    }

    @Scheduled(fixedDelay=60000) // each minute
    public void execute() {
        for (Entry<SessionToken, Object> e : sessionToken.entrySet()) {
            e.getKey().unsetAllTokensIfNeeded();
        }
    }

}

在这种情况下,您将使用 Spring 的注释驱动任务功能:

<task:annotation-driven executor="taskExecutor" scheduler="taskScheduler"/>
<task:scheduler id="taskScheduler" pool-size="10"/>

从我的角度来看,这将是一个简单而好的解决方案。

编辑

使用这种方法,您或多或少SessionTokenFlusher在 spring 结束 bean 配置时注册并在 spring 销毁 bean 时将其删除,这是在会话终止时完成的。

于 2013-04-23T09:50:26.873 回答
3

我认为这应该简单得多。

关于同步器令牌实现

  1. 同步器令牌模式中的令牌并不意味着可重用。令牌仅对一次提交有效。不多不少。

  2. 在任何给定时间点,您都应该只为会话保存一个令牌。

  3. 当向用户显示表单时,令牌应作为隐藏的表单元素包含在表单中。

  4. 提交时,您所要做的就是检查表单中的令牌和会话是否匹配。如果他们这样做,允许表单提交,并在会话中重置令牌的值。

  5. 现在,如果用户重新提交相同的表单,(使用旧令牌)令牌将不匹配,您可以检测到重复提交或过时提交

  6. 另一方面,如果用户重新加载表单本身,更新的令牌现在将出现在隐藏的表单元素中。

结论 - 无需为用户保存令牌列表。每个会话一个令牌正是需要的。这种模式被广泛用作防止 CSRF 攻击的安全措施。页面上的每个链接只能调用一次。即便如此,每个会话只需一个令牌即可完成。作为参考,您可以查看 CSRF Guard V3 的工作原理https://www.owasp.org/index.php/Category:OWASP_CSRFGuard_Project#Source_Code

关于会话

  1. 只有当执行线程以某种方式与 http 请求/响应对绑定时,会话对象才有意义。或者简单地说,只要用户正在浏览您的网站。

  2. 一旦用户离开,会话(来自 JVM)也会离开。所以你不能使用计时器来重置它

  3. 会话由服务器序列化,以确保当用户重新访问您的站点时它们可以活跃起来(jsessionid 用于标识哪个会话应该为哪个浏览器会话反序列化)

  4. 会话有一个超时,如果超时,当用户重新访问时,服务器会启动一个新的会话。

结论 - 没有合理的理由必须定期刷新用户的会话 - 也没有办法做到这一点。

如果我误解了什么,请告诉我,我希望这会有所帮助。

于 2013-04-23T10:41:34.807 回答
2

我对石英不太了解,但这就是我在你的情况下会做的事情:Spring 3.2,对吧?在您的应用程序上下文中:

<task:annotation-driven executor="taskExecutor" scheduler="taskScheduler"/>
<task:scheduler id="taskScheduler" pool-size="10"/>

您将需要任务命名空间...

在某些类中(例如 SessionCleaner.java):

    @Scheduled(fixedRate=3600000)
    public void clearSession(){
         //clear the session    
    }

我要做的是将会话数据用作由 Spring 管理的会话范围 bean:

<bean id="mySessionData" class="MySessionDataBean" scope="session">
</bean>

然后将其注入我需要的地方。然后氏族将看起来像这样。

class SessionCleaner{
        @Autowired
        private MySessionDataBean sessionData;

        @Scheduled(fixedRate=3600000)
        public void clearSession(){
            sessionData.getList().clear();//something like that    
        }
}
于 2013-04-21T10:32:28.503 回答
1

我对石英不太了解。但是,看到你的例外,我猜想调度是异步通信的一部分。所以多线程和事件驱动模型被用来实现它。因此,在您的情况下,您正在尝试创建一个 bean preventDuplicateSubmissionService 。容器将 bean 创建为对象的一部分。但是当您访问它时,Quartz 作为异步调度的一部分创建的线程正在尝试获取服务。但是,对于那个线程,bean 没有被实例化。可能是你正在使用 scope="request"> 。所以当范围是每个 http 请求的请求时,它的对象被创建。在你的情况下,当你尝试访问 bean 时,bean 请求没有创建作用域,因为 bean 的访问是在非 http 模式下进行的,所以当线程尝试访问服务时会抛出异常。当我尝试在 serviceImpl 中使用多线程访问 bean 时,我也遇到了同样的问题。在我的例子中,没有创建请求范围的 bean,因为它是为每个 http 请求模式创建的。我实现了这里给出的解决方案,它对我有用。 在 Threads 中访问作用域代理 bean

于 2013-04-29T06:11:07.087 回答
1

您设置的令牌移除器定期作业似乎没有在特定用户会话的上下文中运行 - 也不是所有用户会话。

我不确定您是否有任何机制可以扫描所有活动会话并对其进行修改,但是我建议的替代方法是:

在服务器端存储一对带有时间戳的唯一令牌。仅在您呈现表单时将令牌发送给用户(从不发送时间戳)。提交表单时,查找生成该令牌的时间——如果它超过了超时值,则拒绝它。

这样,您甚至不需要计时器来删除所有令牌。使用计时器也会删除新创建的令牌

于 2013-04-29T14:02:42.240 回答
1

在原始问题方法(按照 shcedule 正确运行)中,使用以下代码...

package quartz;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;

public final class RemoveTokens extends QuartzJobBean implements javax.sevlet.http.HttpSessionListener {

    static java.util.Map<String, HttpSession> httpSessionMap = new HashMap<String, HttpSession>();

    void sessionCreated(HttpSessionEvent se) {
        HttpSession ss = se.getSession();
        httpSessionMap.put(ss.getId(), ss);
    }

    void sessionDestroyed(HttpSessionEvent se) {
        HttpSession ss = se.getSession();
        httpSessionMap.remove(ss.getId());
    }

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        Set<String> keys = httpSessionMap.keySet();

        for (String key : keys) {
            HttpSession ss = httpSessionMap.get(key);
            Long date = (Long) ss.getAttribute("time");

            if (date == null) {
                date = ss.getCreationTime();
            }

            long curenttime = System.currentTimeMillis();
            long difference = curenttime - date;

            if (difference > (60 * 60 * 1000)) {
                /*Greater than 1 hour*/
                List l = ss.getAttribute("YourList");
                l.removeAll(l);
                ss.setAttribute("time", curenttime);
            }
        }
    }
}
于 2013-04-29T15:40:11.277 回答