
templates.plugins.spincast-attempts-limiter.spincast-attempts-limiter.html Maven / Gradle / Ivy
Show all versions of spincast-website Show documentation
{% extends "../../layout.html" %}
{% block sectionClasses %}plugins hasBreadCrumb plugins-spincast-attempts-limiter{% endblock %}
{% block meta_title %}Plugins - Spincast Attempts Limiter{% endblock %}
{% block meta_description %}Spincast Attempts Limiter plugin - Limit the number of attempts a user can make at something.{% endblock %}
{% block scripts %}
{% endblock %}
{% block body %}
Overview
This plugin lets you limit the number of attempts users can make
trying to perform some actions.
For each kind of action you want to protect, you specify the number
of allowed attempts, a duration and some criteria. For example:
Limit the number of attempts at log in to the site
(the action to protect) to maximum 10 times
(the maximum number
of attempts) per 15 minutes
(the duration) given requests
from the same IP address
(the criteria).
With this rule in place, multiple requests coming from the same IP address and
trying to log in more than 10 times in 15 minutes would be blocked.
Usage
Registering your Attempt Rules
Once the plugin is properly installed, the first thing to do
is to register your attempt rules.
An AttemptRule contains three things:
-
The
name
of the action to protect. For example "login", "confirm order", etc.
-
The
maximum number of attempts
allowed for a duration. For example "3".
-
The
duration
to consider. For example "15 minutes", "2 days", etc.
You register those rules anywhere in your code, using the
AttemptsManager
provided by the plugin. In general, you may want to register a rule in the same class where the
action to protect occurs (in a controller, for example). You also probably want to perform those registrations in
an init method
so the rules are registered as soon as the application starts:
{% verbatim %}
public class LoginController {
private final AttemptsManager attemptsManager;
protected AttemptsManager getAttemptsManager() {
return this.attemptsManager;
}
@Inject
public LoginController(AttemptsManager attemptsManager) {
this.attemptsManager = attemptsManager;
}
@Inject
protected void init() {
getAttemptsManager().registerAttempRule("login",
10,
Duration.of(15, ChronoUnit.MINUTES));
}
//...
}
{% endverbatim %}
Explanation :
-
14-15 : The
init
method
is called as soon as the controller instance is available.
-
16 : Using the
AttemptsManager
,
we start registering an Attempt Rule
for the action to protect
"login
".
-
17 : This rule will allow 10 attempts...
-
18 : ... for a duration of 15 minutes.
Note that you can also use the AttemptFactory
to create an AttemptRule
instance by yourself:
{% verbatim %}
@Injected
private AttemptFactory attemptFactory;
//...
AttemptRule changePasswordAttemptRule =
attemptFactory.createAttemptRule("changePassword",
5,
Duration.of(1, ChronoUnit.HOURS));
getAttemptsManager().registerAttempRule(changePasswordAttemptRule);
{% endverbatim %}
Validating an attempt
Let's continue with our "login" example.
When a login request enters the route handler
in
charge of handling it, we call the
attempt(...)
method of the AttemptsManager
object. This method returns an
Attempt instance
representing the current attempt.
With this Attempt
instance, we're able to know if the associated action
(here "trying to log in on the site") must be allowed or be denied:
{% verbatim %}
public class LoginController {
// ...
public void loginPost(AppRequestContext context) {
Attempt attempt =
getAttemptsManager().attempt("login",
AttemptCriteria.of("ip", context.request().getIp()));
if (attempt.isMaxReached()) {
// Attempt denied!
// Manage this as you want : throw an exception, display
// a warning message, etc.
}
}
}
{% endverbatim %}
Explanation :
-
5 : The "loginPost" method is the
route handler managing a login request.
-
6-7 : We call the "
attempt(...)
"
method of the AttemptsManager
object. We first specify the name of the
action we are interested in ("login"). This name must match the name
used in a registered AttemptRule
!
-
8 : We specify the
criteria
we
want to use to validate the request. Here, we use the IP address of the request.
The AttemptCriteria#of(...)
method is an easy way to create such criteria
.
-
9 : You call the
isMaxReached()
method on the
Attempt
object to see if the associated action should be allowed or not!
-
10-12 : If the action should be denied,
you can throw an Exception, display a warning message to the user, etc.
Note that you can specify as many criteria
as you want! For example, you may want to
limit the number of attempts for the "send contact message" action not only by IP address, but also
by user id:
{% verbatim %}
Attempt attempt =
getAttemptsManager().attempt("send contact",
AttemptCriteria.of("ip", context.request().getIp()),
AttemptCriteria.of("userId", user.getId()));
if (attempt.isMaxReached()) {
// ...
{% endverbatim %}
The only important thing is that you use consistent names for your
criteria, as the plugin will use those to find the correct number of attempts made
with them! But, otherwise, the plugin doesn't care: it will use any criteria names and
values you provide at runtime to group attempts together in order to
determine if the maximum was reached or not.
Also note that if you try to validate an attempt using the name of an action which isn't found in any
registered AttemptRules
, the attempt.isMaxReached()
method
will always return true
, so the action will be denied!
Incrementing the number of attempts
By default, the number of attempts will be automatically incremented, when you call
the attempt(...)
method. This means that without any extra code,
attempt.isMaxReached()
will automatically become true when too many
attempts are made.
But sometimes you may need more control. You may want to manage by yourself when
to increment the number of attempts! For example, you may want to allow
an action to be performed as many times as a user want, as long as he provides a correct
password. If the user always sends the correct password, each time, he can perform the action as many times as
he wants. In that situation, you don't want the attempts to be automatically incremented,
since the action would be denied after a while...
You can configure how the auto-increment is done (or not done) by passing a
AttemptsAutoIncrementType
parameter when calling the .attempt(...)
method:
{% verbatim %}
Attempt attempt =
getAttemptsManager().attempt("login",
AttemptsAutoIncrementType.NEVER,
AttemptCriteria.of("ip", context.request().getIp()));
if (attempt.isMaxReached()) {
// ...
{% endverbatim %}
This parameter can be:
-
ALWAYS
: the method will always automatically increment the number of attempts
(this is the default).
-
NEVER
: the method will never increment the number of attempts.
-
IF_MAX_REACHED
: the method will only increment the number of attempts
automatically if the current attempt is denied.
-
IF_MAX_NOT_REACHED
: the method will only increment the number of attempts
automatically if the current attempt is allowed.
If you don't let the .attempt(...)
method increment the number of attempts, you
are responsible to do it by yourself. You do so by calling
incrementAttemptsCount()
on the Attempt
instance. For example:
{% verbatim %}
Attempt attempt =
getAttemptsManager().attempt("login",
AttemptsAutoIncrementType.NEVER,
AttemptCriteria.of("ip", context.request().getIp()));
if (attempt.isMaxReached()) {
// ...
}
// We only increment the number of attempts if
// the password provided by the user is invalid!
if(!passwordValid) {
attempt.incrementAttemptsCount();
}
//...
{% endverbatim %}
Note that even if .incrementAttemptsCount()
is called multiple times,
the attempts will only be incremented once.
Dependencies
The Spincast Attempts Limiter plugin depends on the Spincast Scheduled Tasks plugin, a plugin which is not
provided by default by the spincast-default
package. This dependency plugin will be automatically installed,
you don't need to install it by yourself in your application (but you can).
Just don't be surprised if you see transitive dependencies being added to your application!
Also, note that is you want to use the provided repository implementation example,
as is, you will need to install the Spincast JDBC plugin.
Installation
The plugin itself
1.
Add this Maven artifact to your project:
<dependency>
<groupId>org.spincast</groupId>
<artifactId>spincast-plugins-attempts-limiter</artifactId>
<version>{{spincast.spincastCurrrentVersion}}</version>
</dependency>
2. Add an instance of the SpincastAttemptsLimiterPlugin
plugin to your Spincast Bootstrapper:
{% verbatim %}
Spincast.configure()
.plugin(new SpincastAttemptsLimiterPlugin())
// ...
{% endverbatim %}
The repository implementation
This plugin is agnostic on what database is used to save the information about the attempts. Therefore you
need to bind a custom implementation of the
SpincastAttemptsLimiterPluginRepository
in the Guice context to specify how it should be done.
There are four methods to implement in that repository:
-
void saveNewAttempt(String actionName, AttemptCriteria... criterias)
Called by the plugin to save a new attempt, with its
associated criteria.
-
Map<String,Integer> getAttemptsNumberPerCriteriaSince(String actionName, Instant sinceDate, AttemptCriteria... criterias)
Called to get a Map consisting of the criteria
and the number of
attempts associated with them currently in the database, since the specified date
.
-
void deleteAttempts(String actionName, AttemptCriteria... criterias)
Called to delete all attempts saved in the database, given the action name
and a set of criteria
.
-
void deleteAttemptsOlderThan(String actionName, Instant date)
Called to delete the attempts for the action name
and that are older
than the specified date
.
Those methods are not totally trivial to implement if you don't know
what they must do exactly. But you can use this as a reference:
repository implementation example
[GitHub].
This implementation uses a H2 database (the database we use to test the plugin).
We also tested this implementation using PostgreSQL, and very few
modifications were required
("TIMESTAMPTZ
" instead of "TIMESTAMP WITH TIME ZONE
" for the "creation_date " column was one, for example).
In addition to the methods required by the repository interface, this example also contains the SQL required to
create the "attempts
" table and its indexes, in the
createAttemptTable()
method.
When your implementation of the repository is ready, you bind it in your application's Guice module:
{% verbatim %}
bind(SpincastAttemptsLimiterPluginRepository.class)
.to(AppAttemptsLimiterPluginRepository.class)
.in(Scopes.SINGLETON);
{% endverbatim %}
Configurations
The configuration interface for this plugin is
SpincastAttemptsLimiterPluginConfig.
To change the default configurations, you can bind an implementation of
that interface, extending the default
SpincastAttemptsLimiterPluginConfigDefault
implementation if you don't want to start from scratch.
Options:
-
boolean isValidationEnabled()
To enable/disable the validation.
By default it is enabled except in development mode
(when SpincastConfig#isDevelopmentMode
is true
)
-
AttemptsAutoIncrementType getDefaultAttemptAutoIncrementType()
The default type auto-increment performed by the
attempt(...)
method.
Defaults to ALWAYS
.
-
boolean isAutoBindDeleteOldAttemptsScheduledTask()
Should a scheduled task to delete old attempts in the database be automatically registered?
If you disable this option, you are responsible to clean up your database of old attempts entries.
Defaults to true
.
-
int getDeleteOldAttemptsScheduledTaskIntervalMinutes()
The number of minutes between two launches of the scheduled task that will clean the database from old attempts,
if isAutoBindDeleteOldAttemptsScheduledTask() is enabled.
Defaults to 10
.
{% endblock %}