Spring MVC: Làm thế nào để thực hiện xác nhận?


156

Tôi muốn biết đâu là cách sạch nhất và tốt nhất để thực hiện xác nhận mẫu của đầu vào người dùng. Tôi đã thấy một số nhà phát triển thực hiện org.springframework.validation.Validator. Một câu hỏi về điều đó: Tôi thấy nó xác nhận một lớp. Có phải lớp phải được điền thủ công với các giá trị từ đầu vào của người dùng, và sau đó được chuyển đến trình xác nhận?

Tôi bối rối về cách sạch nhất và tốt nhất để xác nhận đầu vào của người dùng. Tôi biết về phương pháp sử dụng truyền thống request.getParameter()và sau đó kiểm tra thủ công nulls, nhưng tôi không muốn thực hiện tất cả xác thực trong đó Controller. Một số lời khuyên tốt về lĩnh vực này sẽ được đánh giá rất cao. Tôi không sử dụng Hibernate trong ứng dụng này.


Câu trả lời:


322

Với Spring MVC, có 3 cách khác nhau để thực hiện xác nhận: sử dụng chú thích, thủ công hoặc kết hợp cả hai. Không có một "cách sạch nhất và tốt nhất" duy nhất để xác nhận, nhưng có lẽ có một cách phù hợp với dự án / vấn đề / bối cảnh của bạn hơn.

Hãy có một Người dùng:

public class User {

    private String name;

    ...

}

Phương pháp 1: Nếu bạn có Spring 3.x + và xác thực đơn giản để thực hiện, hãy sử dụng javax.validation.constraintscác chú thích (còn được gọi là chú thích JSR-303).

public class User {

    @NotNull
    private String name;

    ...

}

Bạn sẽ cần một nhà cung cấp JSR-303 trong các thư viện của mình, như Trình xác thực Hibernate là người triển khai tham chiếu (thư viện này không liên quan gì đến cơ sở dữ liệu và ánh xạ quan hệ, nó chỉ thực hiện xác nhận :-).

Sau đó, trong bộ điều khiển của bạn, bạn sẽ có một cái gì đó như:

@RequestMapping(value="/user", method=RequestMethod.POST)
public createUser(Model model, @Valid @ModelAttribute("user") User user, BindingResult result){
    if (result.hasErrors()){
      // do something
    }
    else {
      // do something else
    }
}

Lưu ý @Valid: nếu người dùng tình cờ có tên null, result.hasErrors () sẽ đúng.

Phương pháp 2: Nếu bạn có xác thực phức tạp (như logic xác thực doanh nghiệp lớn, xác thực có điều kiện trên nhiều trường, v.v.) hoặc vì một số lý do bạn không thể sử dụng phương pháp 1, hãy sử dụng xác thực thủ công. Đó là một cách thực hành tốt để tách mã của bộ điều khiển khỏi logic xác thực. Không tạo (các) lớp xác thực của bạn từ đầu, Spring cung cấp org.springframework.validation.Validatorgiao diện tiện dụng (kể từ Mùa xuân 2).

Vì vậy, hãy nói rằng bạn có

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

và bạn muốn thực hiện một số xác thực "phức tạp" như: nếu tuổi của người dùng dưới 18 tuổi, người dùng có trách nhiệm không được rỗng và tuổi của người có trách nhiệm phải trên 21 tuổi.

Bạn sẽ làm một cái gì đó như thế này

public class UserValidator implements Validator {

    @Override
    public boolean supports(Class clazz) {
      return User.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
      User user = (User) target;

      if(user.getName() == null) {
          errors.rejectValue("name", "your_error_code");
      }

      // do "complex" validation here

    }

}

Sau đó, trong bộ điều khiển của bạn, bạn sẽ có:

@RequestMapping(value="/user", method=RequestMethod.POST)
    public createUser(Model model, @ModelAttribute("user") User user, BindingResult result){
        UserValidator userValidator = new UserValidator();
        userValidator.validate(user, result);

        if (result.hasErrors()){
          // do something
        }
        else {
          // do something else
        }
}

Nếu có lỗi xác thực, result.hasErrors () sẽ đúng.

Lưu ý: Bạn cũng có thể đặt trình xác nhận theo phương thức @InitBinder của bộ điều khiển, với "binder.setValidator (...)" (trong trường hợp đó, không thể sử dụng kết hợp phương thức 1 và 2, vì bạn thay thế mặc định người xác nhận). Hoặc bạn có thể khởi tạo nó trong hàm tạo mặc định của bộ điều khiển. Hoặc có một UserValidator @ Thành phần / @ Dịch vụ mà bạn tiêm (@Autowired) trong bộ điều khiển của bạn: rất hữu ích, bởi vì hầu hết các trình xác nhận là singletons + thử nghiệm đơn vị trở nên dễ dàng hơn + trình xác nhận của bạn có thể gọi các thành phần Spring khác.

Phương pháp 3: Tại sao không sử dụng kết hợp cả hai phương pháp? Xác thực các công cụ đơn giản, như thuộc tính "tên", với các chú thích (việc này được thực hiện nhanh chóng, súc tích và dễ đọc hơn). Giữ các xác nhận hợp lệ cho các trình xác nhận (khi phải mất hàng giờ để mã các chú thích xác thực phức tạp tùy chỉnh hoặc chỉ khi không thể sử dụng các chú thích). Tôi đã làm điều này trên một dự án cũ, nó hoạt động như một sự quyến rũ, nhanh chóng và dễ dàng.

Cảnh báo: bạn không được nhầm lẫn xử lý xác thực để xử lý ngoại lệ . Đọc bài này để biết khi nào nên sử dụng chúng.

Người giới thiệu :


bạn có thể cho tôi biết servlet.xml của tôi nên có gì cho cấu hình này không. Tôi muốn chuyển các lỗi trở lại chế độ xem
devdar

@dev_darin Ý bạn là cấu hình để xác thực JSR-303?
Jerome Dalbert

2
@dev_marin Để xác thực, trong Spring 3.x +, không có gì đặc biệt trong "servlet.xml" hoặc "[tên servlet] -servlet.xml. Bạn chỉ cần jar trình xác thực hibernate trong thư viện dự án của bạn (hoặc thông qua Maven). Đó là tất cả, nó sẽ hoạt động. Cảnh báo nếu bạn sử dụng Phương pháp 3: theo mặc định, mỗi bộ điều khiển có quyền truy cập vào trình xác nhận hợp lệ JSR-303, vì vậy hãy cẩn thận không ghi đè lên bằng "setValidator". Nếu bạn muốn thêm trình xác nhận tùy chỉnh vào trên cùng, chỉ cần khởi tạo nó và sử dụng nó hoặc tiêm nó (nếu nó là thành phần Spring). Nếu bạn vẫn gặp vấn đề sau khi kiểm tra google và Spring doc, bạn nên đăng một câu hỏi mới.
Jerome Dalbert

2
Để sử dụng kết hợp phương pháp 1 & 2, có một cách sử dụng @InitBinder. Thay vì "binder.setValidator (...)", có thể sử dụng "binder.addValidators (...)"
jasonfungsing

1
Sửa lỗi cho tôi nếu tôi sai, nhưng bạn có thể kết hợp xác thực qua các chú thích JSR-303 (Phương thức 1) và xác thực tùy chỉnh (Phương pháp 2) khi sử dụng chú thích @InitBinder. Chỉ cần sử dụng binder.addValidators (userValidator) thay vì binder.setValidator (userValidator) và cả hai phương thức Xác thực sẽ có hiệu lực.
SebastianRiemer

31

Có hai cách để xác thực đầu vào của người dùng: chú thích và bằng cách kế thừa lớp Trình xác thực của Spring. Đối với các trường hợp đơn giản, các chú thích là tốt đẹp. Nếu bạn cần xác thực phức tạp (như xác thực trường chéo, ví dụ: trường "xác minh địa chỉ email") hoặc nếu mô hình của bạn được xác thực ở nhiều nơi trong ứng dụng của bạn với các quy tắc khác nhau hoặc nếu bạn không có khả năng sửa đổi mô hình đối tượng bằng cách đặt các chú thích trên đó, Trình xác thực dựa trên thừa kế của Spring là cách để đi. Tôi sẽ đưa ra ví dụ về cả hai.

Phần xác nhận thực tế là như nhau cho dù bạn đang sử dụng loại xác thực nào:

RequestMapping(value="fooPage", method = RequestMethod.POST)
public String processSubmit(@Valid @ModelAttribute("foo") Foo foo, BindingResult result, ModelMap m) {
    if(result.hasErrors()) {
        return "fooPage";
    }
    ...
    return "successPage";
}

Nếu bạn đang sử dụng các chú thích, Foolớp của bạn có thể trông như sau:

public class Foo {

    @NotNull
    @Size(min = 1, max = 20)
    private String name;

    @NotNull
    @Min(1)
    @Max(110)
    private Integer age;

    // getters, setters
}

Chú thích ở trên là javax.validation.constraintschú thích. Bạn cũng có thể sử dụng Hibernate org.hibernate.validator.constraints, nhưng có vẻ như bạn không sử dụng Hibernate.

Ngoài ra, nếu bạn triển khai Trình xác thực của Spring, bạn sẽ tạo một lớp như sau:

public class FooValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return Foo.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {

        Foo foo = (Foo) target;

        if(foo.getName() == null) {
            errors.rejectValue("name", "name[emptyMessage]");
        }
        else if(foo.getName().length() < 1 || foo.getName().length() > 20){
            errors.rejectValue("name", "name[invalidLength]");
        }

        if(foo.getAge() == null) {
            errors.rejectValue("age", "age[emptyMessage]");
        }
        else if(foo.getAge() < 1 || foo.getAge() > 110){
            errors.rejectValue("age", "age[invalidAge]");
        }
    }
}

Nếu sử dụng trình xác nhận ở trên, bạn cũng phải liên kết trình xác nhận với trình điều khiển Spring (không cần thiết nếu sử dụng chú thích):

@InitBinder("foo")
protected void initBinder(WebDataBinder binder) {
    binder.setValidator(new FooValidator());
}

Cũng xem tài liệu mùa xuân .

Mong rằng sẽ giúp.


Khi sử dụng Trình xác thực của Spring, tôi có phải đặt pojo từ bộ điều khiển và sau đó xác thực nó không?
devdar

Tôi không chắc là tôi hiểu câu hỏi của bạn. Nếu bạn thấy đoạn mã bộ điều khiển, Spring sẽ tự động ràng buộc biểu mẫu đã gửi với Footham số trong phương thức xử lý. Bạn có thể làm rõ?
stephen.hanson

ok điều tôi đang nói là khi người dùng gửi người dùng nhập vào Bộ điều khiển nhận yêu cầu http, từ đó điều gì xảy ra là bạn sử dụng request.getParameter () để lấy tất cả các tham số người dùng sau đó đặt các giá trị trong POJO rồi chuyển lớp đến đối tượng xác nhận. Lớp xác nhận sẽ gửi các lỗi trở lại khung nhìn với các lỗi nếu tìm thấy. Đây có phải là cách nó xảy ra?
devdar

1
Nó sẽ xảy ra như thế này nhưng có một cách đơn giản hơn ... Nếu bạn sử dụng JSP và gửi <form: form commandName = "user">, dữ liệu sẽ tự động được đưa vào người dùng @ModelAttribution ("user") trong bộ điều khiển phương pháp. Xem tài liệu: static.springsource.org/spring/docs/3.0.x/ Kẻ
Jerome Dalbert

+1 vì đó là ví dụ đầu tiên tôi tìm thấy sử dụng @ModelAttribution; không có nó, không có hướng dẫn nào tôi tìm thấy hoạt động.
Riccardo Cossu

12

Tôi muốn mở rộng câu trả lời tốt đẹp của Jerome Dalbert. Tôi thấy rất dễ dàng để viết các trình xác nhận chú thích của riêng bạn theo cách JSR-303. Bạn không bị giới hạn khi xác thực "một trường". Bạn có thể tạo chú thích của riêng mình ở cấp độ loại và xác thực phức tạp (xem ví dụ bên dưới). Tôi thích cách này vì tôi không cần kết hợp các loại xác thực khác nhau (Spring và JSR-303) như Jerome làm. Ngoài ra, trình xác nhận này là "Nhận biết mùa xuân", do đó bạn có thể sử dụng @ Tiêm / @ Autowire ngoài hộp.

Ví dụ về xác thực đối tượng tùy chỉnh:

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { YourCustomObjectValidator.class })
public @interface YourCustomObjectValid {

    String message() default "{YourCustomObjectValid.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

public class YourCustomObjectValidator implements ConstraintValidator<YourCustomObjectValid, YourCustomObject> {

    @Override
    public void initialize(YourCustomObjectValid constraintAnnotation) { }

    @Override
    public boolean isValid(YourCustomObject value, ConstraintValidatorContext context) {

        // Validate your complex logic 

        // Mark field with error
        ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate());
        cvb.addNode(someField).addConstraintViolation();

        return true;
    }
}

@YourCustomObjectValid
public YourCustomObject {
}

Ví dụ về đẳng thức chung:

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { FieldsEqualityValidator.class })
public @interface FieldsEquality {

    String message() default "{FieldsEquality.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * Name of the first field that will be compared.
     * 
     * @return name
     */
    String firstFieldName();

    /**
     * Name of the second field that will be compared.
     * 
     * @return name
     */
    String secondFieldName();

    @Target({ TYPE, ANNOTATION_TYPE })
    @Retention(RUNTIME)
    public @interface List {
        FieldsEquality[] value();
    }
}




import java.lang.reflect.Field;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;

public class FieldsEqualityValidator implements ConstraintValidator<FieldsEquality, Object> {

    private static final Logger log = LoggerFactory.getLogger(FieldsEqualityValidator.class);

    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(FieldsEquality constraintAnnotation) {
        firstFieldName = constraintAnnotation.firstFieldName();
        secondFieldName = constraintAnnotation.secondFieldName();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (value == null)
            return true;

        try {
            Class<?> clazz = value.getClass();

            Field firstField = ReflectionUtils.findField(clazz, firstFieldName);
            firstField.setAccessible(true);
            Object first = firstField.get(value);

            Field secondField = ReflectionUtils.findField(clazz, secondFieldName);
            secondField.setAccessible(true);
            Object second = secondField.get(value);

            if (first != null && second != null && !first.equals(second)) {
                    ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate());
          cvb.addNode(firstFieldName).addConstraintViolation();

          ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate());
          cvb.addNode(someField).addConstraintViolation(secondFieldName);

                return false;
            }
        } catch (Exception e) {
            log.error("Cannot validate fileds equality in '" + value + "'!", e);
            return false;
        }

        return true;
    }
}

@FieldsEquality(firstFieldName = "password", secondFieldName = "confirmPassword")
public class NewUserForm {

    private String password;

    private String confirmPassword;

}

1
Tôi cũng đã tự hỏi một bộ điều khiển thường có một trình xác nhận hợp lệ và tôi đã thấy nơi bạn có thể có nhiều trình xác nhận, tuy nhiên nếu bạn có một bộ xác thực được xác định cho một đối tượng tuy nhiên thao tác bạn muốn tạo trước trên đối tượng là khác nhau, ví dụ như lưu / cập nhật cho lưu một bộ xác nhận nhất định là bắt buộc và một bản cập nhật khác là bắt buộc. Có cách nào để cấu hình lớp trình xác nhận để giữ tất cả xác thực dựa trên hoạt động hay bạn sẽ phải sử dụng nhiều trình xác nhận?
devdar

1
Bạn cũng có thể có một xác nhận chú thích trên phương thức. Vì vậy, bạn có thể tạo "xác thực tên miền" của riêng mình nếu tôi hiểu câu hỏi của bạn. Đối với điều này, bạn phải xác định ElementType.METHODtrong @Target.
michal.kreuzman

tôi hiểu những gì bạn đang nói, bạn cũng có thể chỉ cho tôi một ví dụ cho một bức tranh rõ ràng hơn.
devdar

4

Nếu bạn có cùng logic xử lý lỗi cho các trình xử lý phương thức khác nhau, thì bạn sẽ có rất nhiều trình xử lý có mẫu mã sau:

if (validation.hasErrors()) {
  // do error handling
}
else {
  // do the actual business logic
}

Giả sử bạn đang tạo các dịch vụ RESTful và muốn quay lại 400 Bad Requestcùng với các thông báo lỗi cho mọi trường hợp lỗi xác thực. Sau đó, phần xử lý lỗi sẽ giống nhau cho mọi điểm cuối REST yêu cầu xác thực. Lặp đi lặp lại logic rất giống nhau trong mỗi trình xử lý không phải là DRY !

Một cách để giải quyết vấn đề này là bỏ ngay lập tức BindingResultsau mỗi hạt đậu được xác thực . Bây giờ, xử lý của bạn sẽ như thế này:

@RequestMapping(...)
public Something doStuff(@Valid Somebean bean) { 
    // do the actual business logic
    // Just the else part!
}

Bằng cách này, nếu hạt đậu bị ràng buộc không hợp lệ, MethodArgumentNotValidExceptionsẽ bị ném bởi Spring. Bạn có thể định nghĩa một ControllerAdvicexử lý ngoại lệ này với logic xử lý lỗi tương tự:

@ControllerAdvice
public class ErrorHandlingControllerAdvice {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public SomeErrorBean handleValidationError(MethodArgumentNotValidException ex) {
        // do error handling
        // Just the if part!
    }
}

Bạn vẫn có thể kiểm tra các phương pháp cơ bản BindingResultbằng cách sử dụng .getBindingResultMethodArgumentNotValidException


1

Tìm ví dụ đầy đủ về Xác thực Spring Mvc

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.technicalkeeda.bean.Login;

public class LoginValidator implements Validator {
    public boolean supports(Class aClass) {
        return Login.class.equals(aClass);
    }

    public void validate(Object obj, Errors errors) {
        Login login = (Login) obj;
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName",
                "username.required", "Required field");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userPassword",
                "userpassword.required", "Required field");
    }
}


public class LoginController extends SimpleFormController {
    private LoginService loginService;

    public LoginController() {
        setCommandClass(Login.class);
        setCommandName("login");
    }

    public void setLoginService(LoginService loginService) {
        this.loginService = loginService;
    }

    @Override
    protected ModelAndView onSubmit(Object command) throws Exception {
        Login login = (Login) command;
        loginService.add(login);
        return new ModelAndView("loginsucess", "login", login);
    }
}

0

Đặt bean này trong lớp cấu hình của bạn.

 @Bean
  public Validator localValidatorFactoryBean() {
    return new LocalValidatorFactoryBean();
  }

và sau đó bạn có thể sử dụng

 <T> BindingResult validate(T t) {
    DataBinder binder = new DataBinder(t);
    binder.setValidator(validator);
    binder.validate();
    return binder.getBindingResult();
}

để xác nhận một cách thủ công. Sau đó, bạn sẽ nhận được tất cả kết quả trong BindingResult và bạn có thể truy xuất từ ​​đó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.