Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Running @HandleBeforeCreate after entity validation in Spring Boot Data REST

I'm using Spring Boot Data REST to persist my User entities

@Entity
public class User {

    @Id
    @GeneratedValue
    private long id;

    @NotEmpty
    private String firstName;

    @NotEmpty
    private String lastName;

    @NotEmpty
    private String email;

    @Size(min = 5, max = 20)
    private String password;

    // getters and setters
}

using the repository:

public interface UserRepository extends CrudRepository<User, Long> {}

What I want to do is validate the POSTed users first:

@Configuration
public class CustomRestConfiguration extends SpringBootRepositoryRestMvcConfiguration {

    @Autowired
    private Validator validator;

    @Override
    protected void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
        validatingListener.addValidator("beforeCreate", validator);
    }

}

and only later hash the user's password before storing it in the DB:

@Component
@RepositoryEventHandler(User.class)
public class UserRepositoryEventHandler {

    private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @HandleBeforeCreate
    public void handleUserCreate(User user) {
         user.setPassword(passwordEncoder.encode(user.getPassword()));
    }
}

As it turns out though, validation is performed after the password hashing and as a result it fails due to the hashed password being too long.

Is there any way to instruct Spring to perform validation first and only then hash the password? I know I could write a controller myself and specify everything in a fine-grained manner, but I'd rather leave it as my last resort.

like image 308
Wojtek Avatar asked Oct 11 '15 12:10

Wojtek


1 Answers

As I investigated in the debugger it turned out that an incoming entity is processed in the following order:

  1. Spring performs bean validation when deserializing JSON in SpringValidatorAdapter::validate. The password here is in plain text.
  2. @HandleBeforeCreate is invoked and the password is hashed.
  3. JPA performs entity validation before saving it in the DB. The password here is already hashed and validation fails. In my case (Hibernate implementation of JPA) the validation was performed in BeanValidationEventListener::validate.

Solution 1 (full validation in both phases)

One solution I found was to relax the constraint on the password field by just using @NotEmpty (so that both validation phases passed and still the incoming JSON was checked for emptiness/nullity) and perform the size validation of the raw password in @HandleBeforeCreate (and throw appropriate exception from there if needed).

The problem with this solution is that it required me to write my own exception handler. To keep up with the high standards set by Spring Data REST with respect to error response body, I would have to write lots of code for this one simple case. The way to do this is described here.

Solution 2 (Spring bean validation without JPA entity validation)

As hinted by Bohuslav Burghardt, it is possible to disable the second validation phase done by JPA. This way you can keep the min and max constrains and at the same time avoid writing any additional code. As always it's a trade-off between simplicity and safety. The way to disable JPA is described here.

Solution 3 (preserving only min password length constraint)

Another solution, at least valid in my case, was to leave the max password length unbounded. This way in the first validation phase the password was checked whether it wasn't too short and in the second phase it effectively validated every time (because encrypted password was already long enough).

The only caveat to this solution is that @Size(min = 5) does not seem to check for nullity so I had to add @NotNull to handle this case. All in all the field is annotated as:

@NotNull
@Size(min = 5)
private String password;
like image 151
Wojtek Avatar answered Nov 10 '22 15:11

Wojtek