【问题标题】:Thread safe input validation线程安全输入验证
【发布时间】:2016-01-27 15:47:55
【问题描述】:

我正在构建一个 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;
    }
}

验证器 sn-p:

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();
    }

    ...
}

【问题讨论】:

  • 据我所知,假设几个validate* 方法和doesEmailOrusernameExist(...) 也是线程安全的,那么您的所有代码都已经是线程安全的。
  • 您好,我在验证器中看不到任何非线程安全的逻辑。您遇到了什么问题。
  • 你不应该在服务层预先验证数据库容器
  • @Edd 每个数据完整性约束必须由数据库层处理。即使不能以声明方式定义约束,也可以使用事务以编程方式进行。
  • @Edd 例如,如果您需要实现一个没有声明约束的唯一电子邮件地址,您可以使用start transaction->lock the table->check whether the email exists in table->insert new record->unlock table->end transaction。但如果你想保持理智,这应该尽可能靠近数据库。

标签: java multithreading spring hibernate


【解决方案1】:

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

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

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

【讨论】:

  • 所以我猜这个例子的最佳选择 - 如果我希望用户允许创建具有大写/小写用户名和电子邮件的帐户,那么将为这些属性存储两个附加字段和总是强制它们是相同的情况,而是对它们执行唯一的验证。
  • 是的,或者如果您的数据库支持,则使用某种独特的功能索引。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-05
  • 2019-04-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多