登录提示“密码错误”,点忘记密码,重新设置密码提示“新密码不可与旧密码相同”??返回登录,输入密码提示“密码错误”??

前言

不知道大家有没有遇到过这种情况:登录某个网站或app的时候,输入密码,提示“密码错误”,然后点击“忘记密码”,重新设置新密码,结果提示“新密码不可与旧密码相同”???嗯?这什么情况?不信邪返回登录页面,再次输入密码,还是提示“密码错误”???不是,不是“新密码不可与旧密码相同”嘛,怎么用旧密码登录还是提示错误??这什么情况??

我之前就遇到过好几次这种情况,我一直想不太明白这到底怎么做到 “新密码不可与旧密码相同,旧密码错误” 的?

后面直到我自己做项目,遇到了需要 “给用户设置历史密码” 这种功能,我才想明白这是怎么一回事了。

因为之前用户密码一直都是直接放在用户信息表的,存的都是用户当前使用的密码,也想不到还有历史密码这回事,所以一直想不通怎么做到的。

其实啊,只要把“旧密码”换成“历史密码”看,就很容易理解了,这里的“旧密码”不是指前一次的密码,而是指前面几次用过的历史密码(比如前三次、前五次)。因为这个系统有存用户每一次用过的密码,如果新密码跟前面几次的历史密码中的任意一个相同,就会提示“新密码不可与旧密码相同”

假如系统的规则是新密码不能和前五次密码中的任意一个相同。这个系统的用户1更改过五次密码,每一次更改后的密码都被记录了,当第六次需要再次更改密码时,忘记了之前用过哪些密码,就输入了一个之前用过的密码,比如输入的是第二次的密码,就会出现“新密码不可与旧密码相同”的错误提示


所以这个“旧密码”并不一定是你最近一次用过的密码,而是近几次用过的历史密码(这里近几次包括了最近一次)

现在,我们就来看看这个困扰我n久的问题是怎么实现的。

开始

需求

记录2次用户历史密码(可根据自己需要设置次数)。

思路

密码采用的是盐值加密,用户信息表存储密码有这两个字段:password(密码加密密文)、salt(盐值)。

历史密码我们需要单独的一张表存储,必须要有这几个字段:用户id、创建时间、password(密码加密密文)、salt(盐值)。因为用户每次修改密码,都将密码记录到这张表中,然后我们校验历史密码时,就需要按照创建时间倒叙。比如我的需求是校验两次历史密码,那就是 创建时间倒叙,取最新的两条,也就是 limit 2


有了历史密码表,用户信息表的 password、salt 这两个字段也可以去掉,登录时校验密码是否正确,改成从历史密码表拿登录用户最新的那个密码和盐值进行校验。然后我这里的话选择了保留这两个字段,登录那一块的逻辑就不用变,只需要在新增用户、修改密码这两个地方需要改一下。

校验新密码和设置历史密码流程:用户每次修改密码,先校验新密码(校验格式、密码是否正确、是否和历史密码相同),校验通过,将新密码设置到历史密码表中(同时更新用户信息表的密码和盐值);新增用户时,先校验密码格式,校验通过,新增用户,同时将密码设置到历史密码表中。

历史密码表

用户信息表我选择保留 password(密码加密密文)、salt(盐值)这两个字段,所以这个表不做更改。

历史密码表如下:

DROP TABLE IF EXISTS `sys_history_pwd`;
CREATE TABLE `sys_history_pwd`  (
  `id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'ID编号',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `user_id` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户id',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
  `salt` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '盐值',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `user_id`(`user_id` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '存储用户历史密码' ROW_FORMAT = Dynamic;

新增用户

/**
 * 新增
 */
@PostMapping("/insert")
public ResultUtil insert(@RequestBody SysUser user) throws NoSuchAlgorithmException, InvalidKeySpecException {
	// 省略其他代码......
	// 注意密码传输应该进行加密,而不能直接用明文传输,这里我省略了密码解密这一步,项目中要注意
	// 校验用户名和密码不可相同
    if(user.getUserName().equals(user.getPassword())){
        return ResultUtil.error("密码不可与用户名称相同");
    }
    String salt = EncryptionUtil.generateSalt();
    // 根据明文密码和盐值进行加密,得到密文
    String password = EncryptionUtil.getEncryptedPassword(password, salt);
    user.setId(IdUtil.fastSimpleUUID());
    user.setSalt(salt);
    user.setPassword(password);
    sysUserService.save(user);
    // 记录历史密码
    sysHistoryPwdService.insertHistoryPwd(user.getId(),password,salt);
    return ResultUtil.success();
}

修改密码

/**
 * 修改密码
 */
@PostMapping("/updatePassword")
public ResultUtil updatePassword(String oldPassword,String newPassword,String userId) throws InvalidKeySpecException, NoSuchAlgorithmException {
	// 省略其他代码......
	// 注意密码传输应该进行加密,而不能直接用明文传输
    SysUser user = sysUserService.getById(userId);
    //对旧密码进行加密对比:先将用户输入的旧密码(明文)根据盐值加密,对比用户输入的旧密码的密文是否和数据库中的密文一致
    boolean authenticate = EncryptionUtil.authenticate(oldPassword, user.getPassword(), user.getSalt());
    if (!authenticate){
        return ResultUtil.error("旧密码错误");
    }
    // 校验历史密码
    if (sysHistoryPwdService.checkNewPwd(userId, newPassword)){
        return ResultUtil.error("新密码不可与旧密码相同");
    }
    String salt = EncryptionUtil.generateSalt();
    // 根据明文密码和盐值进行加密,得到密文
    String password = EncryptionUtil.getEncryptedPassword(newPassword, salt);
    UpdateWrapper<SysUser> updateWrapper = new UpdateWrapper<>();
    updateWrapper.set("password", password);
    updateWrapper.set("salt", salt);
    updateWrapper.eq("id",user.getId());
    sysUserService.update(updateWrapper);
    // 记录历史密码
    sysHistoryPwdService.insertHistoryPwd(userId,password,salt);
    return ResultUtil.success();
}

修改密码这里,我只做了在记得旧密码的情况下修改密码,没有做忘记密码的功能,不过“校验新密码和历史密码是否相同”都是一样的。

SysHistoryPwdService:

/**
 * 根据用户id获取该用户的历史密码
 */
public List<SysHistoryPwd> historyPwdByUserId(String userId){
    QueryWrapper<SysHistoryPwd> query = new QueryWrapper<>();
    query.eq("user_id",userId);
    query.orderByDesc("create_time"); // 创建时间倒叙
    query.last("limit 2"); // 只取最近的两个密码
    return baseMapper.selectList(query);
}

/**
 * 校验新密码是否和前两次历史密码相同
 */
public boolean checkNewPwd(String userId,String newPwd) throws NoSuchAlgorithmException, InvalidKeySpecException {
    List<SysHistoryPwd> historys = historyPwdByUserId(userId);
    boolean flag = false; // false 新密码与历史密码不同 true 新密码与历史密码相同
    for (SysHistoryPwd item : historys) {
        boolean authenticate = EncryptionUtil.authenticate(newPwd, item.getPassword(), item.getSalt());
        if (authenticate){
            flag = true;
            break;
        }
    }
    return flag;
}

/**
 * 新增一条历史密码数据
 */
public void insertHistoryPwd(String userId,String password,String salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
    SysHistoryPwd historyPwd = new SysHistoryPwd();
    if (StrUtil.isEmpty(salt)){
        salt = EncryptionUtil.generateSalt();
        password = EncryptionUtil.getEncryptedPassword(password, salt);
    }
    historyPwd.setId(IdUtil.fastSimpleUUID());
    historyPwd.setUserId(userId);
    historyPwd.setSalt(salt);
    historyPwd.setPassword(password);
    baseMapper.insert(historyPwd);
}

EncryptionUtil密码盐值加密工具类:

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Random;

/**
 * 密码盐值加密工具类
 */
public class EncryptionUtil {
    private EncryptionUtil(){}

    public static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA256";

    /**
     * 盐的长度
     */
    public static final int SALT_BYTE_SIZE = 32;

    /**
     * 生成密文的长度
     */
    public static final int HASH_BIT_SIZE = 128;

    /**
     * 迭代次数
     */
    public static final int PBKDF2_ITERATIONS = 2000;

    /**
     * 验证输入的password是否正确
     * @param attemptedPassword 待验证的password
     * @param encryptedPassword 密文
     * @param salt 盐值
     */
    public static boolean authenticate(String attemptedPassword, String encryptedPassword, String salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
        // 用同样的盐值对用户输入的password进行加密
        String encryptedAttemptedPassword = getEncryptedPassword(attemptedPassword, salt);
        // 把加密后的密文和原密文进行比較,同样则验证成功。否则失败
        return encryptedAttemptedPassword.equals(encryptedPassword);
    }

    /**
     * 生成密文
     * @param password 明文password
     * @param salt 盐值
     */
    public static String getEncryptedPassword(String password, String salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
        KeySpec spec = new PBEKeySpec(password.toCharArray(), fromHex(salt), PBKDF2_ITERATIONS, HASH_BIT_SIZE);
        SecretKeyFactory f = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
        return toHex(f.generateSecret(spec).getEncoded());
    }

    /**
     * 通过提供加密的强随机数生成器 生成盐
     */
    public static String generateSalt() throws NoSuchAlgorithmException {
        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
        byte[] salt = new byte[SALT_BYTE_SIZE];
        random.nextBytes(salt);
        return toHex(salt);
    }

    /**
     * 十六进制字符串转二进制字符串
     * @param hex the hex string
     */
    private static byte[] fromHex(String hex) {
        byte[] binary = new byte[hex.length() / 2];
        for (int i = 0; i < binary.length; i++) {
            binary[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
        }
        return binary;
    }

    /**
     * 二进制字符串转十六进制字符串
     * @param array the byte array to convert
     */
    private static String toHex(byte[] array) {
        BigInteger bi = new BigInteger(1, array);
        String hex = bi.toString(16);
        int paddingLength = (array.length * 2) - hex.length();
        if (paddingLength > 0) return String.format("%0" + paddingLength + "d", 0) + hex;
        else return hex;
    }
}

最后

密码加密、校验是否正确这些,根据自己的加密方式调整,反正道理就是这么个道理。

这样我们就实现了历史密码,快点把这个功能加进自己的项目里吧,这样说不定以后别人用你的系统也会有我开头的那种疑问呢😂

(欢迎大家多多给博主点赞或关注~)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

符华-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值