线程安全输入验证

时间:2016-01-27 15:47:56

标签: java multithreading spring hibernate

我正在构建一个Spring Boot应用程序,它允许注册,提交需要验证的各种数据等。例如,我有这个实体设置与基本约束:

@Entity
public class Account extends BaseEntity {

    public static final int MAX_LENGTH = 64;

    @Column(unique = true, nullable = false, length = MAX_LENGTH)
    private String username;

    @Column(unique = true, nullable = false, length = MAX_LENGTH)
    private String email;

    ...
}

每次创建新帐户之前,都会在服务层上执行验证:

public Account register(String username, String email, String rawPassword) {

    if (!accountValidator.validate(username, email, rawPassword)) {
        return null;
    }

    Account account = new Account(username, email, passwordEncoder.encode(rawPassword));
    try {
        return accountRepository.save(account);
    } catch (DataIntegrityViolationException e) {
        return null;
    }
}

验证码片段:

public boolean validate(String username, String email, String password) {

    if (!validateUsernameStructure(username)) {
        return false;
    }

    if (!validateEmailStructure(email)) {
        return false;
    }

    if (!validatePasswordStructure(password)) {
        return false;
    }

    // Performs a query on all users to see if no such email or username
    // already exists. Before checking the email and username are set to
    // lowercase characters on the service layer.
    if (accountService.doesEmailOrUsernameExist(email, username)) {
        return false;
    }
    return true;
}

现在在这种情况下,如果我收到很多多个请求,并且其中一个请求设法通过验证,如果用户名或电子邮件首先被强制为小写,我将遇到异常。但是,例如,我想允许用户注册大写/小写用户名,电子邮件字符等。基于this问题,我可以向我的实体添加一个额外的字段或在数据库级别添加更复杂的约束但是我想在没有溢出数据和java代码的情况下这样做。

因此,例如,我得到这两个请求创建一个相隔毫秒的新帐户:

Request 1:
username=foo
email=foo@foo.com

Request 2:
username=foO
email=foO@foO.com

在此阶段我检查重复项(电子邮件和用户名设置为小写,但保存时我保持案例完好无损):

if (accountService.doesEmailOrUsernameExist(email, username)) {
    return false;
}

然而,由于请求彼此如此接近,第一个请求可能还没有创建新帐户,因此第二个帐户的检查通过,因此我得到两个数据库条目。

那么实际问题是,如何在我的服务层中对这些类型的操作执行线程安全验证而不会对性能产生巨大影响?

解决方案我已选择此示例

设置用户名和电子邮件时,也会将这些值应用于小写对应项,并对它们应用唯一约束。这样我就可以为这两个属性保留用户设置的大小写:

@Entity
public class Account extends BaseEntity {

    public static final int MAX_LENGTH = 64;

    @Column(unique = true, nullable = false, length = MAX_LENGTH)
    private final String username;

    @Column(unique = true, nullable = false, length = MAX_LENGTH)
    private String email;

    @Column(unique = true, nullable = false, length = MAX_LENGTH)
    private final String normalizedUsername;

    @Column(unique = true, nullable = false, length = MAX_LENGTH)
    private String normalizedEmail;

    public Account(String username, String email) {
        this.username = username;
        this.email = email;

        this.normalizedUsername = username.toLowerCase();
        this.normalizedEmail = email.toLowerCase();
    }

    ...
}

1 个答案:

答案 0 :(得分:2)

没有数据库的帮助就没有简单的解决方案,因为交易是相互隔离的。

另一种方法是将用户名/电子邮件临时存储在内存中并执行复杂的同步以执行检查。在集群环境中,事情会变得更加复杂。这非常难看并且难以维护(如果单个同步点的锁定保持太长时间,可能会显着影响性能)。

标准和直接的解决方案是使用数据库约束。