You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 19:54:53