基于Spring Security的Grails应用:如何记录密码历史防止密码复用
Grails中实现禁止密码复用的完整方案
针对你的需求——统一拦截所有密码修改场景(管理员编辑、自助重置),阻止用户复用历史密码,且在Spring Security密码编码环节生效,以下是具体实现步骤:
1. 扩展用户域类存储历史密码
首先在User域类中添加字段,保存用户用过的加密后密码,同时限制存储数量避免冗余:
class User { String username String password // 存储历史加密密码,默认空集合 Set<String> previousPasswords = [] as Set static constraints = { previousPasswords nullable: true } static mapping = { previousPasswords type: 'text' // 用text类型存储集合数据 } }
2. 封装密码校验与历史更新逻辑
创建服务类统一处理密码复用检查和历史密码维护:
import org.springframework.security.crypto.password.PasswordEncoder class PasswordValidationService { PasswordEncoder passwordEncoder /** * 检查明文新密码是否匹配历史加密密码 * @param user 目标用户 * @param newRawPassword 明文新密码 * @throws IllegalArgumentException 密码复用时抛出异常 */ void checkForReusedPassword(User user, String newRawPassword) { if (!user.previousPasswords) return boolean isReused = user.previousPasswords.any { encodedOldPwd -> passwordEncoder.matches(newRawPassword, encodedOldPwd) } if (isReused) { throw new IllegalArgumentException("不能使用之前用过的密码") } } /** * 更新用户历史密码列表,自动截断超出限制的旧密码 * @param user 目标用户 * @param oldEncodedPassword 旧加密密码 * @param maxHistoryCount 最大保存数量,默认5个 */ void updatePreviousPasswords(User user, String oldEncodedPassword, int maxHistoryCount = 5) { user.previousPasswords.add(oldEncodedPassword) if (user.previousPasswords.size() > maxHistoryCount) { user.previousPasswords.remove(user.previousPasswords.first()) } } }
3. 自定义密码编码器实现统一拦截
通过自定义PasswordEncoder,在密码编码前自动检查复用情况,确保所有修改密码的操作都经过校验:
3.1 实现自定义编码器
import org.springframework.security.crypto.password.DelegatingPasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder class ReusePreventingPasswordEncoder implements PasswordEncoder { DelegatingPasswordEncoder delegateEncoder PasswordValidationService passwordValidationService // 用ThreadLocal临时存储当前修改密码的用户 private final ThreadLocal<User> currentUserHolder = new ThreadLocal<>() @Override String encode(CharSequence rawPassword) { User user = currentUserHolder.get() if (user) { passwordValidationService.checkForReusedPassword(user, rawPassword.toString()) } return delegateEncoder.encode(rawPassword) } @Override boolean matches(CharSequence rawPassword, String encodedPassword) { return delegateEncoder.matches(rawPassword, encodedPassword) } @Override boolean upgradeEncoding(String encodedPassword) { return delegateEncoder.upgradeEncoding(encodedPassword) } // 设置当前操作的用户 void setCurrentUser(User user) { currentUserHolder.set(user) } // 清除ThreadLocal中的用户,避免内存泄漏 void clearCurrentUser() { currentUserHolder.remove() } }
3.2 配置替换默认编码器
在grails-app/conf/spring/resources.groovy中配置自定义编码器,替换Spring Security默认的编码器:
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.password.DelegatingPasswordEncoder import com.yourpackage.ReusePreventingPasswordEncoder beans = { // 基础BCrypt编码器 bcryptPasswordEncoder(BCryptPasswordEncoder) // 默认的DelegatingPasswordEncoder,兼容多种编码方式 delegatePasswordEncoder(DelegatingPasswordEncoder, "bcrypt") { defaultPasswordEncoderForMatches = ref('bcryptPasswordEncoder') idToPasswordEncoder = ['bcrypt': ref('bcryptPasswordEncoder')] } // 自定义的防复用编码器 passwordEncoder(ReusePreventingPasswordEncoder) { delegateEncoder = ref('delegatePasswordEncoder') passwordValidationService = ref('passwordValidationService') } }
4. 在业务Service中集成逻辑
不管是管理员编辑还是自助重置,都需要在修改密码的Service方法中绑定当前用户,并更新历史密码:
管理员编辑用户密码
class UserAdminService { ReusePreventingPasswordEncoder passwordEncoder PasswordValidationService passwordValidationService void updateUserPassword(User user, String newRawPassword) { try { passwordEncoder.setCurrentUser(user) // 保存旧加密密码 String oldEncodedPwd = user.password // 设置新密码,Spring Security会自动调用编码器加密 user.password = newRawPassword // 更新历史密码列表 passwordValidationService.updatePreviousPasswords(user, oldEncodedPwd) user.save(flush: true) } finally { passwordEncoder.clearCurrentUser() } } }
自助密码重置
class PasswordResetService { ReusePreventingPasswordEncoder passwordEncoder PasswordValidationService passwordValidationService void resetUserPassword(User user, String newRawPassword) { try { passwordEncoder.setCurrentUser(user) String oldEncodedPwd = user.password user.password = newRawPassword passwordValidationService.updatePreviousPasswords(user, oldEncodedPwd) user.save(flush: true) } finally { passwordEncoder.clearCurrentUser() } } }
5. 控制器异常处理
在控制器中捕获密码复用的异常,返回友好提示:
class UserController { UserAdminService userAdminService def updatePassword(Long userId, String newPassword) { User user = User.get(userId) if (!user) { render status: 404, text: "用户不存在" return } try { userAdminService.updateUserPassword(user, newPassword) render status: 200, text: "密码修改成功" } catch (IllegalArgumentException e) { render status: 400, text: e.message } } }
内容的提问来源于stack exchange,提问作者SGT Grumpy Pants
相关产品推荐
相关产品推荐

