Spring Boot Validation

验证表单输入(接口参数)是Web 开发 (API 接口开发)中不可缺少的一个部分。


@NotBlank Validation

首先我们从最简单的不能为空判断开始,介绍 Spring Boot Validation。

假设我们需要提供一个登陆接口,输入用户名密码,返回是否登陆成功。

在去数据库查询用户名密码是否正确之前,我们可以做一些简单的验证。

在 LoginRequest 的 username 和 password 参数上加上 @NotBlank。

package com.example.validation.request;

import javax.validation.constraints.NotBlank;

public class LoginRequest {
    @NotBlank(message = "用户名不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    private String password;

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

在 UserController::login() 方法接收输入的 loginRequest 前面加上 @Valid 。

package com.example.validation.controllers;

import com.example.validation.request.LoginRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;

@RestController
@RequestMapping("/user")
public class UserController {
    @PostMapping("/login")
    public LoginRequest login(@RequestBody @Valid LoginRequest loginRequest) {
        return loginRequest;
    }
}

这样当用户输入的用户名或则密码为空的时候,就不会继续进入到下面的业务逻辑,在验证参数的阶段就可以直接返回 400 错误。

Bean Validation

使用 @NotBlank 完成第一个参数验证的例子的时候,会有这样一个问题:@NotBlank 注解是不是 Spring 框架自带的,这样的注解一共有多少个。

这个时候我们就需要了解一个 Bean Validation。

Bean Validation: JSR 303 – JSR 349 – JSR 380

Bean Validation 是一套通过注解来表示对象模型的约束的 Java 规范,最新版本是 Bean Validation 2.0。

定义的内置约束注解有:

Build-in Constraint definitions

Hibernate Validator 是 Bean Validation 规范的实现,除了Bean Validation 定义的 22 种内置约束注解之外,还实现了另外 25 种内置约束注解。

Build-in Constraint definitions

Creating custom constraints

虽然 Hibernate Validator 已经内置了这么多约束注解,但是需求千变万化,总有不能满足的时候,这时候我们就需要定义并实现自己的一套约束注解。

假设我们需要提供一个注册接口,输入用户名、手机号码、邮箱、密码,返回是否注册成功。

除了需要验证用户名密码是否为空之外,还需要验证手机号码和邮箱至少要有一个是必填的。

MinimumRequiredParameters.java

package com.example.validation;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

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

@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = MinimumRequiredParametersValidator.class)
@Documented
public @interface MinimumRequiredParameters {

    String message() default "{minimum.required.parameters.message}";

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

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

    String[] value();

    int minimum() default 1;
}

MinimumRequiredParametersValidator.java

package com.example.validation;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;

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

public class MinimumRequiredParametersValidator implements ConstraintValidator<MinimumRequiredParameters, Object> {

    private String[] fields;

    private int minimum;

    private int count;

    @Override
    public void initialize(MinimumRequiredParameters constraintAnnotation) {
        this.fields = constraintAnnotation.value();
        this.minimum = constraintAnnotation.minimum();
        this.count = 0;
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        BeanWrapper wrapper = PropertyAccessorFactory.forBeanPropertyAccess(value);

        for (String field: fields) {
            if (wrapper.getPropertyValue(field) != null) { count++; }
        }

        return count >= minimum;
    }
}

resources/ValidationMessages.properties

minimum.required.parameters.message=at least {minimum} of {value} is required

在 RegisterRequest 中使用 @MinimumRequiredParameters 注解判断手机号码和邮箱至少有一个必填。

package com.example.validation.request;

import com.example.validation.MinimumRequiredParameters;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

@MinimumRequiredParameters(value = {"phone", "email"}, message = "手机号码和邮箱至少有一个必填")
public class RegisterRequest {
    @NotBlank(message = "用户名不能为空")
    private String username;

    @Pattern(regexp = "\\d{11}", message = "手机号码格式不正确")
    private String phone;

    @Email(message = "邮箱格式不正确")
    private String email;

    @NotBlank(message = "密码不能为空")
    private String password;

    public String getUsername() {
        return username;
    }

    public String getPhone() {
        return phone;
    }

    public String getEmail() {
        return email;
    }

    public String getPassword() {
        return password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

参考:

https://spring.io/guides/gs/validating-form-input/

https://beanvalidation.org

https://beanvalidation.org/2.0/spec/#builtinconstraints

http://hibernate.org/validator/

https://docs.jboss.org/hibernate/stable/validator/reference/en-US/html_single/#validator-customconstraints

https://stackoverflow.com/questions/9284450/jsr-303-validation-if-one-field-equals-something-then-these-other-fields-sho

222 total views, no views today

java.lang.IllegalArgumentException: Invalid character found in the request target.

最近在使用 Spring Boot 开发项目的时候遇到了这样一个错误:

java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986

请求中包含无效字符,有效字符的定义请查看 RFC 7230 和 RFC 3986。

查看了一下我的请求参数,应该是 [] 字符被 Tomcat 当作无效字符处理了。

Request Params:
expressNumber[]=111&expressNumber[]=222&expressNumber[]=333

在网上找了一下解决方案,需要在 Tomcat 配置文件中添加 relaxedQueryChars。

Tomcat >= 8.5
relaxedQueryChars = "[]"

由于 Spring Boot 使用的是 Embedded Tomcat ,可以参考以下设置方式。

@Component
public class MyTomcatWebServerCustomizer
        implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        factory.addConnectorCustomizers(connector -> connector.setAttribute("relaxedQueryChars", "[]"));
    }
}

问题已经成功解决了,但是还有另外一种思路。

再来看一下我的请求参数,是要传递一个数组。

Request Params:
expressNumber[]=111&expressNumber[]=222&expressNumber[]=333

Java Code:
@RequestParam(name = "expressNumber[]") String[] expressNumber

有没有其他方式呢?有的,而且还有两种。

Request Params:
expressNumber=111&expressNumber=222&expressNumber=333

Java Code:
@RequestParam String[] expressNumber
Request Params:
expressNumber=111,222,333

Java Code:
@RequestParam String[] expressNumber

参考:

https://stackoverflow.com/questions/48655291/java-lang-illegalargumentexception-invalid-character-found-in-the-request-targe

https://stackoverflow.com/questions/41053653/tomcat-8-is-not-able-to-handle-get-request-with-in-query-parameters/44005213#44005213

https://stackoverflow.com/questions/51703746/setting-relaxedquerychars-for-embedded-tomcat

https://docs.spring.io/spring-boot/docs/current/reference/html/howto-embedded-web-servers.html

885 total views, no views today

Spring Boot Security

开发一个 RESTful 接口的过程中,权限认证是必不可少的一个重要功能。

这里我们以 Spring Boot Security 为例,实现一个自定义的 Token 认证。

大致流程如下:

  1. 用户 -> 登录页面 -> 用户中心 -> 获取token
  2. 用户 -> token-> 当前应用 -> token-> 用户中心 -> 认证

首先我们新建配置文件 SecurityConfig。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public TokenAuthenticationFilter tokenAuthenticationFilterBean() throws Exception {
        return new TokenAuthenticationFilter();
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();

        http.csrf().disable();

        http.addFilterBefore(tokenAuthenticationFilterBean(), UsernamePasswordAuthenticationFilter.class);
    }
}

然后实现 TokenAuthenticationFilter 拦截器。

public class TokenAuthenticationFilter extends OncePerRequestFilter
{
    @Autowired
    private UserService userService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException
    {
        //extract token from header
        final String token = request.getHeader("x-auth-token");

        if (null != token) {
            //get and check whether token is valid ( from DB or file wherever you are storing the token)
            final User user = userService.getTokenUser(token);

            if (null != user) {
                //Populate SecurityContextHolder by fetching relevant information using token
                final UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        filterChain.doFilter(request, response);
    }
}

最后实现用户中心认证的业务逻辑。

@Service
public class UserService {

    @Autowired
    private OrderService orderService;

    public User getTokenUser(String token) {
        //TODO
    }
}

参考:

https://stackoverflow.com/questions/42354138/spring-security-token-based-authentication

https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-security.html

https://www.lefer.cn/posts/55880

216 total views, no views today