Skip to content

随机密码生成器

Posted on:March 21, 2024 at 08:46 AM

今天我们来实现这样一个随机密码生成器的前端网站:

image-20240321133808691

项目灵感来自 50projects50days-Password Generator

Live Demo

1. 分析需求

需要实现功能:

  1. 用户可指定密码长度,在 0 -20 之间
  2. 可定义是否包含以下类型的字符:
    1. 大写字母
    2. 小写字母
    3. 数字
    4. 符号
  3. 复制到剪贴板

这篇博客的内容主要关注在 JavaScript 部分逻辑的实现,html 和 css 的部分都比较简单。

2. 设计代码逻辑

捋代码逻辑时,我喜欢通过一个具体的测试用例来辅助思考,因为这样可以把抽象的东西变得具体,就像我们使用代码编辑器的 debug 功能一样。

而这个测试用例的设计也有一定讲究,比如我们可以考虑:

Use Case 1:

  1. 密码长度为 20
  2. 包含所有类型字符

这个可能是最容易想到的一个 Use Case,但是让我们再看看

Use Case 2:

  1. 密码长度为 16
  2. 包含其中 2 类字符,不包含另外 2 类字符

写出来之后我想大家都能看出,使用 Use Case 2 来设计逻辑,大概率要比使用 Use Case 1 来设计逻辑,更不容易出错。

现在我们进一步把 Use Case 来具体化,我们可以将要实现的方法定义为以下形式:

function generatePassword(lower, upper, number, symbol, length) {}

我们的 Use Case 便可以设计为:

generatePassword(true, false, true, false, 16);

现在我们再一次回头来看看需求:

  1. 用户可指定密码长度,在 0 -20 之间
  2. 可定义是否包含以下类型的字符:
    1. 大写字母
    2. 小写字母
    3. 数字
    4. 符号

再把我们的 Use Case 套进去,于是变成:

  1. 生成一个包含 16 个字符的字符串
  2. 包含小写字母和数字
  3. 不包含大写字母或符号

Note:如果我们捋逻辑的时候选择了 Use Case 1:生成包含所有字符类型的字符串,那么这里的第 3 点就非常容易被忽略掉。

这样捋清楚之后,我们很容易可以想到以下实现逻辑:

  1. 随机生成一个小写字母 和 一个数字
  2. 剩下从第 3 - 16 个字符:
    1. 随机生成一个小写字母或一个数字
    2. 直至生成了 16 个字符
  3. 打乱这个字符串里面各字符的顺序

现在我们再退回一步,增加一些条件判断,来使得整个逻辑更加完成,修改成:

  1. 初始化变量 let password = ""
  2. 遍历参数:[lower, upper, number, symbol],对于值为 true 的参数,生成相应的字符,并追加到变量 password
  3. 假设 password.length=2,那么就应该从 i=3开始生成剩下的字符,直到 i=16。且剩下的字符,必须是 lowercasenumber。因此我们可以在第二步中,存储一下值为 true 的参数或存储与之相应的方法。
  4. 打乱这个字符串里面各字符的顺序

这个逻辑看起来已经比较完整了,接下来:

3. 代码实现

我们从第 2 步开始:

遍历参数:[lower, upper, number, symbol],对于值为 true 的参数,生成相应的字符,并追加到变量 password

  1. 遍历参数:[lower, upper, number, symbol],对于值为 true 的参数,生成相应的字符

    首先我们先来看看随机生成一个 小写字母 / 大写字母 / 数字 / 符号 怎样实现。

​ 怎么实现呢?最简单的方法,当然是:

image-20240321145915755

这里就不详细解释每一行代码了,如果有不懂的部分可以叫 ChatGPT 帮忙解释,或者自己在 MDN Web Docs 中查阅下相关的部分。

然后我们需要通过判断 [lower, upper, number, symbol] 中每一个值,来判断是否调用相应的方法来生成相应的字符。

最简单直观的做法便是:

if (upper) {
  password += getRandomUpper();
}

if (lower) {
  password += getRandomLower();
}

if (number) {
  password += getRandomNumber();
}

if (symbol) {
  password += getRandomSymbol();
}

这个代码当然看起来就不是很 DRY,于是我们再一次请教 ChatGPT,让它帮我们简化这部分代码,ChatGPT 的回复:

// 添加必须包含的字符串
function addCharacterIfNeeded(condition, characterFunction) {
  if (condition) {
    password += characterFunction();
  }
}

addCharacterIfNeeded(upper, getRandomUpper);
addCharacterIfNeeded(lower, getRandomLower);
addCharacterIfNeeded(number, getRandomNumber);
addCharacterIfNeeded(symbol, getRandomSymbol);

Ok, 看着还行

继续下一步:

假设 password.length=2,那么就应该从 i=3开始生成剩下的字符,直到 i=16。且剩下的字符,必须是 lowercasenumber。因此我们可以在第二步中,存储一下值为 true 的参数或存储与之相应的方法。

首先我们在上一步中,存储一下需要调用的生成字符方法

// 增加下面一行
const includeFuncs = [];

function addCharacterIfNeeded(condition, characterFunction) {
  if (condition) {
    password += characterFunction();
    // 增加下面一行
    includeFuncs.push(characterFunction);
  }
}

然后随机生成剩下的字符

// 生成剩下的字符串
for (let i = password.length; i < length; i++) {
  // 随机挑选一个方法
  const randomFuncIndex = Math.floor(Math.random() * includeFuncs.length);
  password += includeFuncs[randomFuncIndex]();
}

Ok,现在最后一步:

打乱这个字符串里面各字符的顺序

感谢 🙏 ChatGPT

/* 使用 Fisher-Yates 洗牌算法打乱字符串中字符的顺序
Fisher-Yates 洗牌算法是一种用于随机排列数组的算法。
该算法的工作原理:
从数组的末尾开始,依次遍历每个元素。
对于每个元素,随机选择一个前面的元素。
将两个元素交换位置。 
*/

function shuffleString(str) {
  const chars = str.split("");
  for (let i = chars.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [chars[i], chars[j]] = [chars[j], chars[i]];
  }
  return chars.join("");
}

到目前为止的完整代码:

function generatePassword(lower, upper, number, symbol, length) {
  let password = "";
  const includeFuncs = [];

  // 添加必须包含的字符串
  function addCharacterIfNeeded(condition, characterFunction) {
    if (condition) {
      password += characterFunction();
      includeFuncs.push(characterFunction);
    }
  }

  addCharacterIfNeeded(upper, getRandomUpper);
  addCharacterIfNeeded(lower, getRandomLower);
  addCharacterIfNeeded(number, getRandomNumber);
  addCharacterIfNeeded(symbol, getRandomSymbol);

  // 生成剩下的字符串
  for (let i = password.length; i < length; i++) {
    // 随机挑选一个方法
    const randomFuncIndex = Math.floor(Math.random() * includeFuncs.length);
    password += includeFuncs[randomFuncIndex]();
  }

  return shuffleString(password);
}

function shuffleString(str) {
  const chars = str.split("");
  for (let i = chars.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [chars[i], chars[j]] = [chars[j], chars[i]];
  }
  return chars.join("");
}

// 生成随机小写字母
function getRandomLower() {
  return String.fromCharCode(Math.floor(Math.random() * 26) + 97);
}

// 生成随机大写字母
function getRandomUpper() {
  return String.fromCharCode(Math.floor(Math.random() * 26) + 65);
}

// 生成随机数字
function getRandomNumber() {
  return Math.floor(Math.random() * 10);
}

// 生成随机符号
function getRandomSymbol() {
  const symbols = "!@#$%^&*()_+[]{}|;:,.<>?";
  return symbols[Math.floor(Math.random() * symbols.length)];
}

下面我们来写几个 case 来测试一下

//          generatePassword(lower, upper, number, symbol, length)
console.log(
  generatePassword((lower = true), false, (number = true), false, 16)
); // w62x0ci7y7624514

// 只包含 小写 / 大写 / 数字 / 符号
console.log(generatePassword((lower = true), false, false, false, 20)); // nlluhttpbpeahwalhgbs
console.log(generatePassword(false, (upper = true), false, false, 20)); // IWTTXSSCORFVYMEMGPHB
console.log(generatePassword(false, false, (number = true), false, 20)); // 15324415325690076136
console.log(generatePassword(false, false, false, (symbol = true), 20)); // +_;>;,%^:+]!!],([}?<
// 不包含 小写 / 大写 / 数字 / 符号
console.log(generatePassword((low = false), true, true, true, 20)); // 523VNQ&I2S3<89R;P#?S
console.log(generatePassword(true, (upper = false), true, true, 20)); // 9}sfaf*wf1jk36^2u:xc
console.log(generatePassword(true, true, (number = false), true, 20)); // ),*&?eGjW+RC}Q!Sux+f
console.log(generatePassword(true, true, true, (symbol = false), 20)); // 9sokn7P1lIh0Mib8498P
// 包含全部 和 全不包含
console.log(generatePassword(true, true, true, true, 20)); // 2yfD#7N^y?+jI.|ErFi4
console.log(generatePassword(false, false, false, false, 20));

这里我们看到最后一条执行失败了

Uncaught TypeError TypeError: includeFuncs[randomFuncIndex] is not a function
    at generatePassword (/Users/shiyuwang/Downloads/code/temp/debug.js:22:46)
    at <anonymous> (/Users/shiyuwang/Downloads/code/temp/debug.js:77:13)
    at Module._compile (internal/modules/cjs/loader:1241:14)
    at Module._extensions..js (internal/modules/cjs/loader:1295:10)
    at Module.load (internal/modules/cjs/loader:1091:32)
    at Module._load (internal/modules/cjs/loader:938:12)
    at executeUserEntryPoint (internal/modules/run_main:83:12)
    at <anonymous> (internal/main/run_main_module:23:47)

通过报错很容易猜到,是因为 includeFuncs 这个数组为空,因此我们需要处理一下这种情况

function generatePassword(lower, upper, number, symbol, length) {
  ...

  addCharacterIfNeeded(upper, getRandomUpper);
  addCharacterIfNeeded(lower, getRandomLower);
  addCharacterIfNeeded(number, getRandomNumber);
  addCharacterIfNeeded(symbol, getRandomSymbol);
  // 添加以下代码:
  if (includeFuncs.length === 0) return ''
	...
}

接下来我们再测试一下 length < 4 的情况

console.log(generatePassword(true, true, true, true, 0)); // [F8x
console.log(generatePassword(true, true, true, true, 1)); // F+c5
console.log(generatePassword(true, true, true, true, 2)); // 2|fO
console.log(generatePassword(true, true, true, true, 3)); // 0Wb.

又发现一个 bug。很明显这个 bug 是因为在第 2 步中:

遍历参数:[lower, upper, number, symbol],对于值为 true 的参数,生成相应的字符,并追加到变量 password

如果 4 个参数都为 true,我们会添加 4 个字符,此时字符串长度会大于用户指定的长度。因此在这一步我们需要修改一下代码:

// 添加必须包含的字符串
function addCharacterIfNeeded(condition, characterFunction) {
  // 修改以下代码:
  if (condition && password.length < length) {
    password += characterFunction();
    includeFuncs.push(characterFunction);
  }
}

重新测试:

console.log(generatePassword(true, true, true, true, 0)); // 空字符串
console.log(generatePassword(true, true, true, true, 1)); // F
console.log(generatePassword(true, true, true, true, 2)); // Sm
console.log(generatePassword(true, true, true, true, 3)); // Uw7

到目前为止的完整代码:

function generatePassword(lower, upper, number, symbol, length) {
  let password = "";
  const includeFuncs = [];

  // 添加必须包含的字符串
  function addCharacterIfNeeded(condition, characterFunction) {
    if (condition && password.length < length) {
      password += characterFunction();
      includeFuncs.push(characterFunction);
    }
  }

  addCharacterIfNeeded(upper, getRandomUpper);
  addCharacterIfNeeded(lower, getRandomLower);
  addCharacterIfNeeded(number, getRandomNumber);
  addCharacterIfNeeded(symbol, getRandomSymbol);

  if (includeFuncs.length === 0) return "";

  // 生成剩下的字符串
  for (let i = password.length; i < length; i++) {
    // 随机挑选一个方法
    const randomFuncIndex = Math.floor(Math.random() * includeFuncs.length);
    password += includeFuncs[randomFuncIndex]();
  }

  return shuffleString(password);
}

function shuffleString(str) {
  const chars = str.split("");
  for (let i = chars.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [chars[i], chars[j]] = [chars[j], chars[i]];
  }
  return chars.join("");
}

// 生成随机小写字母
function getRandomLower() {
  return String.fromCharCode(Math.floor(Math.random() * 26) + 97);
}

// 生成随机大写字母
function getRandomUpper() {
  return String.fromCharCode(Math.floor(Math.random() * 26) + 65);
}

// 生成随机数字
function getRandomNumber() {
  return Math.floor(Math.random() * 10);
}

// 生成随机符号
function getRandomSymbol() {
  const symbols = "!@#$%^&*()_+[]{}|;:,.<>?";
  return symbols[Math.floor(Math.random() * symbols.length)];
}

4. 添加 js 和 DOM 的交互

主要的逻辑实现了,接下来我们添加和 DOM 的交互

初始代码:CodePen - Starter

  1. select elements
const resultEl = document.getElementById("result");
const lengthEl = document.getElementById("length");
const uppercaseEl = document.getElementById("uppercase");
const lowercaseEl = document.getElementById("lowercase");
const numbersEl = document.getElementById("numbers");
const symbolsEl = document.getElementById("symbols");
const generateEl = document.getElementById("generate");
const clipboardEl = document.getElementById("clipboard");
  1. get length
let passwordLen = lengthEl.value;

lengthEl.addEventListener("input", e => {
  passwordLen = e.target.value;
});
  1. 监听 button 的点击事件,生成随机密码
generateEl.addEventListener("click", () => {
  const includeLower = lowercaseEl.checked;
  const includeUpper = uppercaseEl.checked;
  const includeNumbers = numbersEl.checked;
  const includeSymbols = symbolsEl.checked;

  resultEl.innerText = generatePassword(
    includeLower,
    includeUpper,
    includeNumbers,
    includeSymbols,
    passwordLen
  );
});
  1. 复制到剪贴板
clipboardEl.addEventListener("click", () => {
  const result = resultEl.innerText;
  writeToClipboard(result);
});

async function writeToClipboard(text) {
  try {
    await navigator.clipboard.writeText(text);
    alert("Password copied to clipboard!");
  } catch (error) {
    alert(error.message);
  }
}

最后完整的代码:CodePen - Finished

5. 总结

这篇博客记录了实现这个网页过程中的一些具体步骤和方法,其中有一些代码是 AI 生成的,AI 在解决一些常见算法问题时已经成为非常好用的工具。本文中没有详细解释那一部分的代码,但是这里还是建议大家在写代码的过程中还是要深入地去学习自己不懂的语法,比如这里我们用到了

  1. 使用 String.fromCharCode()方法来分别生成随机的大写字母、小写字母和数字
  2. 使用 Fisher-Yates 洗牌算法打乱字符串中字符的顺序
  3. 将内容写入系统剪贴板

每次当我们从 AI 那里学到新东西的时候,记得记录下来,同时也推荐再去看一下官方文档中关于那一部分的解释,因为基于 AI 算法生成的解并不一定是最优解,有时候甚至不是正确的解,而查询官方文档能让我们学得更加深入和全面。

最后,谢谢有朋友看到这里,希望你有所获得。