如何使用 Spring "matches" 方法检查旧密码?
How to check for old passwords using Spring "matches" method?
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private OldPasswordsService oldPasswordsService;
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
OldPasswords value = list.get();
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches)
{
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
else
{
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
}
Table 旧密码:
@Table(name = "old_passwords")
public class OldPasswords implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", unique = true, updatable = false, nullable = false)
private int id;
@Column(name = "encrypted_password", length = 255)
private String encryptedPassword;
@Column(name = "password_owner_id", length = 4)
private Integer passwordOwnerId;
@Column(name = "created_at")
@Convert(converter = LocalDateTimeConverter.class)
private LocalDateTime createdAt;
但我得到 java.util.NoSuchElementException: No value present
。
你知道我如何实现比较旧密码和新密码的逻辑吗?
编辑:
我试过这个设计:
SQL查询:
public List<OldPasswords> findByOwnerId(Integer ownerId) {
String hql = "select e from " + OldPasswords.class.getName() + " e where e.passwordOwnerId = :passwordOwnerId ORDER BY e.createdAt DESC";
TypedQuery<OldPasswords> query = entityManager.createQuery(hql, OldPasswords.class).setMaxResults(3).setParameter("passwordOwnerId", ownerId);
List<OldPasswords> list = query.getResultList();
return list;
}
端点:
@PostMapping("reset_password")
public ResponseEntity<?> reset(@RequestBody PasswordResetDTO resetDTO) {
return this.userService.findByLogin(resetDTO.getName()).map(user -> {
Integer userId = user.getId();
List<OldPasswords> list = oldPasswordsService.findByOwnerId(userId);
if(!list.isEmpty() && !list.isEmpty()) {
for (int i = 0; i<list.size(); i++){
OldPasswords value = list.get(i);
boolean matches = passwordEncoder.matches(resetDTO.getPassword(), value.getEncryptedPassword());
if (matches) {
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
}
}
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswords.setPasswordOwnerId(userId);
oldPasswordsService.save(oldPasswords);
user.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
user.setResetPasswordToken(null);
userService.save(user);
return ok().build();
}).orElseGet(() -> notFound().build());
}
但是当我使用相同的密码多次更改代码时,错误 PASSWORD_ALREADY_USED
没有显示。
检查可选类型列表是否包含有效的必需值
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
OldPasswords value = list.orElse(null);
if(value != null) {
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches) {
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
return new ResponseEntity<>("New Password Saved" , HttpStatus.OK);
}
return new ResponseEntity<>("Incorrect Password Provided" , HttpStatus.BAD_REQUEST);
或像@Manuel 建议的那样
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
if(list.isPresent()) {
OldPasswords value = list.get();
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches) {
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
return new ResponseEntity<>("New Password Saved" , HttpStatus.OK);
}
return new ResponseEntity<>("Incorrect Password Provided" , HttpStatus.BAD_REQUEST);
我认为你的代码有几个问题。
1。 'passwordEncoder'
的类型
密码编码器有多种类型,具体取决于实际使用的编码算法。
如果 'passwordEncoder' 的类型是 MD5、SHA1 等,您很可能会遇到密码冲突,因为您希望密码是唯一的。
意味着如果一个用户有一个弱密码,例如"topSecret123",而另一个用户有相同的密码"topSecret123",你方法
oldPasswordsService.findEncryptedPassword(...)
将 return 多个条目,而不是一个条目。
这将导致例如 NonUniqueResultException
或其他内容。
1.1 可能的解决方案:
已将密码与用户名相关联。获取由 userId(或类似的东西)给出的用户,并使用该用户的密码检查密码。
1.2 另一种可能的解决方案
使用例如 BCryptPasswordEncoder
。这种类型 PasswordEncoder
负责为你的 has 添加盐。这样可以避免在您的数据库中出现可能的重复条目。
如果仅提供 "password",则这些类型的密码编码器无法计算密码或检查密码是否匹配。由于他们将 "salt" 与您的编码密码一起使用,因此这些类型的密码编码器需要(salt+hashed)密码作为输入,以检查提供的密码是否匹配。
2。实际问题
代码
OldPasswords value = list.get();
是问题所在。 Optional<OldPasswords>
可能包含 null
值。
对 Optional
的调用 .get()
将 null
值将导致 java.util.NoSuchElementException: No value present
.
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
if (!list.isPresent()) {
return new ResponseEntity<>("The old password value you've entered is incorrect", HttpStatus.UNAUTHORIZED);
}
OldPasswords value = list.get();
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches)
{
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
else
{
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
}
3。旧密码实体
您既不必将 @Id
列设置为 unique=true
,也不必 nullable=false
不设置 updateable=false
。
4。混合图层
您发布的代码使用服务并更新域对象。
并且它确实return一个ResponseEntity
。您显然将应用程序的不同层合二为一。
5。暴露信息
您暴露了信息,即所选的(新)密码已被其他用户使用!不要那样做!加起来是因为第 1 点。我已经列出了。
编辑:
问题更新后,我也想更新我的答案。
由于更新问题中的代码片段无法编译,我想根据我从代码片段中了解到的内容制作一个非常简单的基本示例。
我不评论问题中显示的 "reset password" 设计的概念,因为中间缺少很多代码。
包括测试在内的完整代码可以在这里找到:https://github.com/mschallar/so_oldpasswords_example
问题中请求的函数代码是:
@PostMapping("reset_password")
public ResponseEntity<?> reset(@RequestBody PasswordResetDTO resetDTO) {
Optional<User> findByLogin = this.userService.findByLogin(resetDTO.getName());
if (!findByLogin.isPresent()) {
return ResponseEntity.notFound().build();
}
User user = findByLogin.get();
Integer userId = user.getUserId();
String encodedPassword = passwordEncoder.encode(resetDTO.getPassword());
for (OldPasswords oldPasswords : oldPasswordsService.findByOwnerId(userId)) {
if (passwordEncoder.matches(resetDTO.getPassword(), oldPasswords.getEncryptedPassword())) {
// Information: Don't do that! Don't reveal that another user already has such a password!
log.info("Password already used.");
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
}
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(encodedPassword));
oldPasswords.setPasswordOwnerId(userId);
oldPasswordsService.save(oldPasswords);
user.setEncryptedPassword(encodedPassword);
user.setResetPasswordToken(null);
userService.save(user);
return ResponseEntity.ok().build();
}
我已经使用一个简单的解决方案解决了这个问题。
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
if (encoder.matches(newPassword, oldPassword))
{
System.out.println("Successfully logged in!");
}
else
{
System.out.println("Incorrect Password.");
}
newPassword
和 oldPassword
必须在 String 中,以便匹配两个密码。
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private OldPasswordsService oldPasswordsService;
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
OldPasswords value = list.get();
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches)
{
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
else
{
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
}
Table 旧密码:
@Table(name = "old_passwords")
public class OldPasswords implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", unique = true, updatable = false, nullable = false)
private int id;
@Column(name = "encrypted_password", length = 255)
private String encryptedPassword;
@Column(name = "password_owner_id", length = 4)
private Integer passwordOwnerId;
@Column(name = "created_at")
@Convert(converter = LocalDateTimeConverter.class)
private LocalDateTime createdAt;
但我得到 java.util.NoSuchElementException: No value present
。
你知道我如何实现比较旧密码和新密码的逻辑吗?
编辑: 我试过这个设计:
SQL查询:
public List<OldPasswords> findByOwnerId(Integer ownerId) {
String hql = "select e from " + OldPasswords.class.getName() + " e where e.passwordOwnerId = :passwordOwnerId ORDER BY e.createdAt DESC";
TypedQuery<OldPasswords> query = entityManager.createQuery(hql, OldPasswords.class).setMaxResults(3).setParameter("passwordOwnerId", ownerId);
List<OldPasswords> list = query.getResultList();
return list;
}
端点:
@PostMapping("reset_password")
public ResponseEntity<?> reset(@RequestBody PasswordResetDTO resetDTO) {
return this.userService.findByLogin(resetDTO.getName()).map(user -> {
Integer userId = user.getId();
List<OldPasswords> list = oldPasswordsService.findByOwnerId(userId);
if(!list.isEmpty() && !list.isEmpty()) {
for (int i = 0; i<list.size(); i++){
OldPasswords value = list.get(i);
boolean matches = passwordEncoder.matches(resetDTO.getPassword(), value.getEncryptedPassword());
if (matches) {
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
}
}
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswords.setPasswordOwnerId(userId);
oldPasswordsService.save(oldPasswords);
user.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
user.setResetPasswordToken(null);
userService.save(user);
return ok().build();
}).orElseGet(() -> notFound().build());
}
但是当我使用相同的密码多次更改代码时,错误 PASSWORD_ALREADY_USED
没有显示。
检查可选类型列表是否包含有效的必需值
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
OldPasswords value = list.orElse(null);
if(value != null) {
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches) {
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
return new ResponseEntity<>("New Password Saved" , HttpStatus.OK);
}
return new ResponseEntity<>("Incorrect Password Provided" , HttpStatus.BAD_REQUEST);
或像@Manuel 建议的那样
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
if(list.isPresent()) {
OldPasswords value = list.get();
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches) {
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
return new ResponseEntity<>("New Password Saved" , HttpStatus.OK);
}
return new ResponseEntity<>("Incorrect Password Provided" , HttpStatus.BAD_REQUEST);
我认为你的代码有几个问题。
1。 'passwordEncoder'
的类型密码编码器有多种类型,具体取决于实际使用的编码算法。 如果 'passwordEncoder' 的类型是 MD5、SHA1 等,您很可能会遇到密码冲突,因为您希望密码是唯一的。
意味着如果一个用户有一个弱密码,例如"topSecret123",而另一个用户有相同的密码"topSecret123",你方法
oldPasswordsService.findEncryptedPassword(...)
将 return 多个条目,而不是一个条目。
这将导致例如 NonUniqueResultException
或其他内容。
1.1 可能的解决方案:
已将密码与用户名相关联。获取由 userId(或类似的东西)给出的用户,并使用该用户的密码检查密码。
1.2 另一种可能的解决方案
使用例如 BCryptPasswordEncoder
。这种类型 PasswordEncoder
负责为你的 has 添加盐。这样可以避免在您的数据库中出现可能的重复条目。
如果仅提供 "password",则这些类型的密码编码器无法计算密码或检查密码是否匹配。由于他们将 "salt" 与您的编码密码一起使用,因此这些类型的密码编码器需要(salt+hashed)密码作为输入,以检查提供的密码是否匹配。
2。实际问题
代码
OldPasswords value = list.get();
是问题所在。 Optional<OldPasswords>
可能包含 null
值。
对 Optional
的调用 .get()
将 null
值将导致 java.util.NoSuchElementException: No value present
.
Optional<OldPasswords> list = oldPasswordsService.findEncryptedPassword(passwordEncoder.encode("new password entered form web reset form"));
if (!list.isPresent()) {
return new ResponseEntity<>("The old password value you've entered is incorrect", HttpStatus.UNAUTHORIZED);
}
OldPasswords value = list.get();
boolean matches = passwordEncoder.matches("new password entered form web reset form", value.getEncryptedPassword());
if (matches)
{
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
else
{
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(resetDTO.getPassword()));
oldPasswordsService.save(oldPasswords);
}
3。旧密码实体
您既不必将 @Id
列设置为 unique=true
,也不必 nullable=false
不设置 updateable=false
。
4。混合图层
您发布的代码使用服务并更新域对象。
并且它确实return一个ResponseEntity
。您显然将应用程序的不同层合二为一。
5。暴露信息
您暴露了信息,即所选的(新)密码已被其他用户使用!不要那样做!加起来是因为第 1 点。我已经列出了。
编辑:
问题更新后,我也想更新我的答案。 由于更新问题中的代码片段无法编译,我想根据我从代码片段中了解到的内容制作一个非常简单的基本示例。
我不评论问题中显示的 "reset password" 设计的概念,因为中间缺少很多代码。
包括测试在内的完整代码可以在这里找到:https://github.com/mschallar/so_oldpasswords_example
问题中请求的函数代码是:
@PostMapping("reset_password")
public ResponseEntity<?> reset(@RequestBody PasswordResetDTO resetDTO) {
Optional<User> findByLogin = this.userService.findByLogin(resetDTO.getName());
if (!findByLogin.isPresent()) {
return ResponseEntity.notFound().build();
}
User user = findByLogin.get();
Integer userId = user.getUserId();
String encodedPassword = passwordEncoder.encode(resetDTO.getPassword());
for (OldPasswords oldPasswords : oldPasswordsService.findByOwnerId(userId)) {
if (passwordEncoder.matches(resetDTO.getPassword(), oldPasswords.getEncryptedPassword())) {
// Information: Don't do that! Don't reveal that another user already has such a password!
log.info("Password already used.");
return new ResponseEntity<>("PASSWORD_ALREADY_USED", HttpStatus.BAD_REQUEST);
}
}
OldPasswords oldPasswords = new OldPasswords();
oldPasswords.setEncryptedPassword(passwordEncoder.encode(encodedPassword));
oldPasswords.setPasswordOwnerId(userId);
oldPasswordsService.save(oldPasswords);
user.setEncryptedPassword(encodedPassword);
user.setResetPasswordToken(null);
userService.save(user);
return ResponseEntity.ok().build();
}
我已经使用一个简单的解决方案解决了这个问题。
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
if (encoder.matches(newPassword, oldPassword))
{
System.out.println("Successfully logged in!");
}
else
{
System.out.println("Incorrect Password.");
}
newPassword
和 oldPassword
必须在 String 中,以便匹配两个密码。