I'm generating random tokens for the reason as mentioned in this question which are placed in a java.util.List
and this List
is kept in a session scope.
After going through some Google search, I have decided to remove all elements (tokens) every hour contained by this List
in session.
I could think of using the Quartz API but doing so, doesn't seem possible for manipulating a user's session. What I have tried with the Quartz API (1.8.6, 2.x is incompatible with Spring 3.2 which I'm using) in Spring can be seen below.
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.");
}
}
And it was configured in the application-context.xml
file as follows.
<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>
The overridden method in the RemoveTokens
class is executed every hour with the initial interval of 10 seconds as configured in XML but it is not possible to execute some method of some class to remove the tokens available in the List
which is stored in a user's session. Is it possible?
What is the fair way to remove this List
stored in a session scope with a defined time interval (every hour)? It would be far better, if it were to be made possible by using this Quartz API.
EDIT:
According to the answer below, I have tried the following but unfortunately, it didn't make a difference.
In the application-context.xml
file,
<task:annotation-driven executor="taskExecutor" scheduler="taskScheduler"/>
<task:executor id="taskExecutor" pool-size="5"/>
<task:scheduler id="taskScheduler" pool-size="10"/>
This requires the following additional namespaces,
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"
The following bean was registered as a session scoped 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();
}
}
}
And interface which it implements,
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();
}
And this bean was configured in the application-context.xml
file as follows.
<bean id="sessionTokenCleanerService" class="token.SessionToken" scope="session">
<aop:scoped-proxy proxy-target-class="false"/>
</bean>
Now, I'm injecting this service in the following class.
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();
}
}
And in the application-context.xml
file,
<bean id="preventDuplicateSubmissionService" class="token.PreventDuplicateSubmission"/>
Both of the above beans are annotated with the @Service
and they should be part of context:component-scan
in the dispatacher-servelt.xml
file (or whatever its name).
The method annotated with the @Scheduled
annotation in the preceding code snippet is invoked regularly at the given rate but it causes the following obvious exception to be thrown.
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
To clear data stored in a user's session, the method that does this task should be invoked regularly at a defined time interval from every user's session which is not the case with this attempt. What is the way? The question may simply be : how to trigger a regular time interval from every user's session? Does Spring or Servlet API support something to accomplish this?
Congrats on using tokens to prevent resubmits ("Introduce Synchronizing Token" refactoring from the book Core J2EE Patterns). :^)
Quartz is valuable for complicated or precise scheduling. But your requirements don't need Quartz. Might be more useful to learn CDI, java.util.Timer, ScheduledExecutor and/or EJB timers.
If using a scheduler, as you mention, it's good to have a singleton scheduler shared by all users, rather than having a scheduler & thread instance per user session.
Be careful if you ever store references to HttpSession or pass it as a parameter. Storing references prevents garbage collection when the session is complete - causing a (big?) memory leak. Attempts to clean up references using HttpSessionListener & other tricks, may not work & are messy. Passing HttpSession to methods as a parameter makes them artificially dependent on the Servlet container and difficult to unit test, because you have to mock complex HttpSession objects. It's cleaner to wrap all session data in a singular object UserSessionState - store refererences to this in your session & object instance variables. This is also known as the Context Object Pattern - i.e. store all your scoped data in one/few POJO context objects, independent from the HTTP protocol classes.
I suggest two alternative solutions to your problem:
Use a java.util.Timer
singleton instance
You could replace java.util.Timer
(introduced in Java SE 1.3) with ScheduledExecutor
(introduced in Java SE 5) for a nearly identical solution.
This is available with the JVM. It requires no jar setup and no configuration.
You call timerTask.schedule
, passing in a TimerTask
instance. When the schedule is due, timerTask.run
is called. You remove scheduled tasks via timerTask.cancel
and timerTask.purge
which releases used memory.
You code the TimerTask
, including it's constructor and you create a new instance and pass it to the schedule method, meaning you can store any data or object references you need within the TimerTask
; you can keep a reference to it and call setter methods anytime later.
I suggest you create two global singletons: a Timer
instance and a TimerTask
instance. In your custom TimerTask
instance, keep a list of all user sessions (in POJO form like UserSessionState
or Spring/CDI bean, not in form of HttpSession
). Add two methods to this class:addSessionObject
and removeSessionObject
, with a parameter of UserSessionState
or similar. In the TimerTask.run
method, iterate through the set of UserSessionState
and clear the data.
Create a custom HttpSessionListener - from sessionCreated, put a new UserSessionState instance into the session and call TimerTask.addUserSession; from sessionDestroyed, call TimerTask.removeUserSession.
Call the global-scoped singleton timer.schedule
to schedule the TimerTask instance to clear the session-scoped reference's contents.
Use Token List with Capped Size (no cleanup)
Don't cleanup tokens based on time elapsed. Instead restrict the list size (e.g. 25 tokens) and store the most recently generated tokens.
This is probably the simplest solution. When you add an element to the list, check whether you've exceeded the maximum size, if so wrap-around and insert from the earliest index in the list:
if (++putIndex > maxSize) {
putIndex = 0;
}
list.put(putIndex, newElement);
Here no scheduler is required, and there's no need to form & maintain a set of all user sessions.
I think this should be a lot simpler.
About Synchronizer Token Implementation
The tokens in the synchronizer token pattern are NOT meant to be re-usable. A token is considered valid only for one submission. No more no less.
At any given point of time, you should have to save only one token for a session.
When a form is shown to the user the token should be included in the form as a hidden form element.
On submission, all you have to do is check if the token in the form and the session match. If they do, allow the form submission, and reset the value of the token in the session.
Now if the user re-submits the same form, (with the old token) tokens will not match and you can detect a double submission or a stale submission
On the other hand, if the user reloads the form itself, the updated token will now be present in the hidden form element.
Conclusion - there is no need to save a list of tokens for a user. One token per session is just what is needed. This pattern is widely used as a security measure to prevent CSRF attacks. Where each link on the page can only be invoked once. Even this can be done with just one token per session. For reference you can see how the CSRF Guard V3 works https://www.owasp.org/index.php/Category:OWASP_CSRFGuard_Project#Source_Code
About sessions
A session object has meaning only as long as the thread of execution is somehow tied to an http request / response pair. Or simply put, as long as the user is browsing your site.
Once the user is gone, so does the session (from the JVM). So you cannot use a timer to reset it
Sessions are serialized by servers to make sure they can be sprung to life when a user re-visits your site (jsessionid is used to id which session should be deserialized for which browser session)
There is a timeout associated with a session, if the time out expires, the server starts a new session when the user revisits.
Conclusion - no plausible reason to have to flush users' sessions periodically - And no way to do it.
Let me know if I misunderstood something, and I hope this helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With