使用bcrypt加密时,如何更新用户信息而不修改数据库密码?
Hey, I've run into this exact issue before with Spring and AngularJS—let's break down what's going wrong and how to fix it:
Your core problem is two-fold:
- Your
setPassword()method encrypts every value passed to it, even if it's already the encrypted password from your database. That's why existing passwords get re-encrypted when you update other user details. - You're forced to pass the password every time, even when the user isn't changing it.
Here are three solid solutions, ordered by how clean they are for your use case:
1. Move password encryption logic to the Service layer (recommended)
The biggest issue right now is that your entity's setter is handling encryption, which mixes domain logic with data persistence. Let's fix that:
Step 1: Update the User entity's password setter
Make it a simple setter without encryption:
public String getPassword() { return password; } public void setPassword(String password) { // No encryption here—we'll handle that in the service this.password = password; }
Step 2: Handle encryption in your UserService
Only encrypt the password when creating a new user or when the user explicitly updates their password:
@Service public class UserService { private final UserRepository userRepository; private final BCryptPasswordEncoder passwordEncoder; // Constructor injection (preferred over @Autowired) public UserService(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } // For creating new users public User createUser(User newUser) { newUser.setPassword(passwordEncoder.encode(newUser.getPassword())); return userRepository.save(newUser); } // For updating user details public User updateUser(User updatedUser) { // Fetch the existing user from the database User existingUser = userRepository.findById(updatedUser.getId()) .orElseThrow(() -> new RuntimeException("User not found with ID: " + updatedUser.getId())); // Update all non-password fields existingUser.setFirstName(updatedUser.getFirstName()); existingUser.setMiddleName(updatedUser.getMiddleName()); existingUser.setLastName(updatedUser.getLastName()); existingUser.setEmailAddress(updatedUser.getEmailAddress()); existingUser.setBday(updatedUser.getBday()); existingUser.setContactNo(updatedUser.getContactNo()); existingUser.setAddress(updatedUser.getAddress()); existingUser.setGender(updatedUser.getGender()); existingUser.setUsername(updatedUser.getUsername()); existingUser.setRole(updatedUser.getRole()); // Only update password if a new one was provided (and it's not empty) if (updatedUser.getPassword() != null && !updatedUser.getPassword().trim().isEmpty()) { existingUser.setPassword(passwordEncoder.encode(updatedUser.getPassword())); } return userRepository.save(existingUser); } }
Step 3: Update your AngularJS code to only send password when needed
Don't include the password in the payload unless the user is changing it. Add a password input field for edits, and only attach it if the user entered something:
var profile = { "id": $scope.userData.id, "firstName": $scope.first, "middleName": $scope.mid, "lastName": $scope.last, "emailAddress": $scope.mail, "bday": $scope.bday, "contactNo": $scope.num, "address": $scope.add, "gender": $scope.gender, "username": $scope.userData.username, "role": $scope.userData.role }; // Only add password to the payload if user entered a new one if ($scope.newPassword && $scope.newPassword.trim() !== '') { profile.password = $scope.newPassword; } // Send the profile object to your Spring backend via $http or $resource
2. Modify the setter to skip encryption for existing BCrypt passwords
If you don't want to move the encryption logic right now, you can add a check in the setter to detect if the input is already a BCrypt-encrypted string (they start with $2a$, $2b$, or $2y$):
public void setPassword(String password) { BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); // Check if the password is already encrypted if (password != null && (password.startsWith("$2a$") || password.startsWith("$2b$") || password.startsWith("$2y$"))) { this.password = password; } else { // Encrypt only if it's a plaintext password this.password = passwordEncoder.encode(password); } }
Note: This works, but it's not the cleanest approach because it still ties encryption logic to the entity class.
3. Use partial updates with PATCH
If you're using Spring MVC, you can use @PatchMapping and update only the fields that were sent in the request. Combine this with a tool like ModelMapper to copy non-null fields from the request DTO to the existing entity:
@PatchMapping("/users/{id}") public ResponseEntity<User> updateUserPartial(@PathVariable Long id, @RequestBody UserUpdateDto updateDto) { User existingUser = userRepository.findById(id) .orElseThrow(() -> new RuntimeException("User not found")); // Use ModelMapper to copy only non-null fields ModelMapper modelMapper = new ModelMapper(); modelMapper.getConfiguration().setSkipNullEnabled(true); modelMapper.map(updateDto, existingUser); // Handle password encryption if needed if (updateDto.getPassword() != null && !updateDto.getPassword().trim().isEmpty()) { existingUser.setPassword(passwordEncoder.encode(updateDto.getPassword())); } return ResponseEntity.ok(userRepository.save(existingUser)); }
This is great for reducing payload size and keeping updates focused, but it requires creating a DTO class for partial updates.
Any of these approaches will let you update user details without re-encrypting the existing password or forcing users to re-enter their password every time. My top pick is the first one—it keeps your code organized and follows best practices for layered architecture.
内容的提问来源于stack exchange,提问作者Mark

