密码安全策略
前言
几乎所有的系统都有密码安全要求,这是基础的安全策略,本文记录常用密码安全策略并编写策略校验工具类
常用密码安全策略
密文存储,通常为MD5加盐
需包含数字、大写字母、小写字母、特殊字符,且有长度限制
设置有效期,超期强制要求修改密码,或禁止登陆
连续输入密码错误锁定账号
代码编写
PwdUtil.java
package cn.huanzi.qch.util; import java.util.Calendar; import java.util.Date; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; /** * 密码安全策略 */ public class PwdUtil { // 用户连续密码输入错误次数 private final static ConcurrentHashMap<String,Integer> pwdFailedMap = new ConcurrentHashMap<>(10); // 用户连续密码输入错误次数达上限后锁定时长 private final static ConcurrentHashMap<String, Date> banTimeMap = new ConcurrentHashMap<>(10); // 允许连续密码输入错误次数 private final static Integer maxTryLogin = 3; // 连续失败后锁定时长(分钟) private final static Integer banMinute = 5; // 密码长度限制:[最少,最多] private final static Integer[] passwordLength = {6,12}; /** * 密码复杂度校验 * 满足要求返回1 */ public String pwdCheck(String password){ if(password == null || "".equals(password)){ return "密码不能为空!"; } //密码长度限制 if(password.length() < passwordLength[0]){ return "密码长度不能小于"+passwordLength[0]+"位数!"; } if(password.length() > passwordLength[1]){ return "密码长度不能大于"+passwordLength[1]+"位数!"; } //数字 Pattern pat = Pattern.compile("[0-9]+"); if(!pat.matcher(password).find()){ return "密码需包含数字!"; } //小写字母 Pattern pat2 = Pattern.compile("[a-z]+"); if(!pat2.matcher(password).find()){ return "密码需包含小写字母!"; } //大写字母 Pattern pat3 = Pattern.compile("[A-Z]+"); if(!pat3.matcher(password).find()){ return "密码需包含大写字母!"; } //特殊字符:~!@#$%^&*()_+/-[]{}\|;':"<>?,. Pattern pat4 = Pattern.compile("[~!@#$%^&*()_+/\\-\\[\\]{}\\\\|;':\"<>?,.]+"); if(!pat4.matcher(password).find()){ return "密码需包含特殊字符!"; } /* 也可以直接使用下面这个正则,效果一样,只是错误提示没那么详细 */ //数字:(?=.*\d) //小写字母:(?=.*[a-z]) //大写字母:(?=.*[A-Z]) //特殊字符:(?=.*[~!@#$%^&*()_+/\-\[\]{}\\|;':"<>?,.]) //长度限制:.{6,12} //Pattern pat5 = Pattern.compile("^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[~!@#$%^&*()_+/\\-\\[\\]{}\\\\|;':\"<>?,.]).{"+passwordLength[0]+","+passwordLength[1]+"}$"); //if(!pat5.matcher(password).find()){ // return "密码需包含数字、小写字母、大写字母、特殊字符且长度在"+passwordLength[0]+"-"+passwordLength[1]+"之间!"; //} return "1"; } /** * 密码错误一次,并返回可剩余次数提示 */ public String addPwdFailedCount(String username) { Integer result = 0; if (pwdFailedMap.containsKey(username)) { result = pwdFailedMap.get(username); //校验锁定时间是否到期 if (banTimeMap.containsKey(username)) { Date banTime = banTimeMap.get(username); long diff = banTime.getTime() - new Date().getTime(); if(diff <= 0){ banTimeMap.remove(username); pwdFailedMap.remove(username); result = 0; } } } ++result; if (result >= maxTryLogin) { //当前时间 + banMinute Calendar cal = Calendar.getInstance(); cal.add(Calendar.MINUTE, banMinute); Date banTime = cal.getTime(); banTimeMap.put(username, banTime); return "密码已经输错" + maxTryLogin + "次," + checkBanTimeByUser(username); } else { pwdFailedMap.put(username, result); return "密码已经输错" + result + "次,输错" + (maxTryLogin - result) + "次后帐号将会被锁定!"; } } /** * 检查用户是否已经被锁定 * 未被锁定返回1 */ public String checkBanTimeByUser(String username) { if (banTimeMap.containsKey(username)) { Date banTime = banTimeMap.get(username); long diff = banTime.getTime() - new Date().getTime(); //校验锁定时间是否到期 if(diff <= 0){ banTimeMap.remove(username); pwdFailedMap.remove(username); return "1"; } //毫秒转分钟 1000 * 60 long minute = (long)Math.ceil((double)diff / 60000); return "帐号已被锁定,请" + minute + "分钟后,再登录系统!"; } return "1"; } /** * 登陆成功,清除对应集合内容 */ public void removeMapDataByUser(String username){ pwdFailedMap.remove(username); banTimeMap.remove(username); } /** * 定时器调用,清除所有集合内容 */ public void removeMapDataByAll(){ pwdFailedMap.clear(); banTimeMap.clear(); } /** * 随机六位数密码:两个数字 + 三个大/小写字母 + 一个特殊字符 */ public String randomPassword(){ final char[] char0 = "0123456789".toCharArray(); final char[] char1 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); final char[] char2 = "abcdefghijklmnopqrstuvwxyz".toCharArray(); final char[] char3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); final char[] char4 = "~!@#$%^&*()_+/-[]{}\\|;':\"<>?,.".toCharArray(); //随机六位数密码:两个数字 + 三个大/小写字母 + 一个特殊字符 Random random = new Random(); return String.valueOf(char0[random.nextInt(char0.length - 1)]) + char0[random.nextInt(char0.length - 1)] + char1[random.nextInt(char1.length - 1)] + char2[random.nextInt(char2.length - 1)] + char3[random.nextInt(char3.length - 1)] + char4[random.nextInt(char4.length - 1)]; } }
简单效果演示
密码复杂度校验
/* 密码长度小于6位:密码长度不能小于6位数! 密码长度大于12位:密码长度不能大于12位数! 密码缺少数字:密码需包含数字! 密码缺少小写字母:密码需包含小写字母! 密码缺少大写字母:密码需包含大写字母! 密码缺少特殊字符:密码需包含特殊字符! 密码包含数字、小写字母、大写字母、特殊字符且长度在6-12之间:1 */ public static void main(String[] args) { PwdUtil pwdUtil = new PwdUtil(); System.out.println("密码长度小于6位:"+pwdUtil.pwdCheck("1qQ#")); System.out.println("密码长度大于12位:"+pwdUtil.pwdCheck("1qQ#111111111")); System.out.println("密码缺少数字:"+pwdUtil.pwdCheck("qqQQ##")); System.out.println("密码缺少小写字母:"+pwdUtil.pwdCheck("11QQ##")); System.out.println("密码缺少大写字母:"+pwdUtil.pwdCheck("11qq##")); System.out.println("密码缺少特殊字符:"+pwdUtil.pwdCheck("11qqQQ")); System.out.println("密码包含数字、小写字母、大写字母、特殊字符且长度在6-12之间:"+pwdUtil.pwdCheck("11qqQQ##")); }
模拟登陆,密码错误3次后锁定账号5分钟
工具类新增两个测试方法
/** * 模拟登陆 * 登录成功,返回1 */ public String login(String username, String password){ //查库,获取用户信息 //是否已被锁定 String checkBanTime = checkBanTimeByUser(username); if(!"1".equals(checkBanTime)){ return checkBanTime; } /* 模拟数据 */ //密码错误 if(!getMd5("123456").equals(getMd5(password))){ return addPwdFailedCount(username); } //密码正确 else{ removeMapDataByUser(username); return "1"; } } /** * 生成MD5加密串 */ public String getMd5(String password) { String md5 = ""; try { //创建一个md5算法对象 MessageDigest md = MessageDigest.getInstance("MD5"); //MD5加盐规则(例如 123456 -> 1234564):原字符串 + 原字符串倒数第三个字符 byte[] messageByte = (password + password.charAt(password.length() - 3)).getBytes(StandardCharsets.UTF_8); //获得MD5字节数组,16*8=128位 byte[] md5Byte = md.digest(messageByte); //转换为16进制字符串 StringBuilder hexStr = new StringBuilder(md5Byte.length); int num; for (byte aByte : md5Byte) { num = aByte; if (num < 0) { num += 256; } if (num < 16) { hexStr.append("0"); } hexStr.append(Integer.toHexString(num)); } md5 = hexStr.toString().toUpperCase(); } catch (Exception e) { e.printStackTrace(); } return md5; }
main测试
/* ---模拟前两次密码错误,最后一次密码正确--- 密码已经输错1次,输错2次后帐号将会被锁定! 密码已经输错2次,输错1次后帐号将会被锁定! 1 pwdFailedMap.size:0 banTimeMap.size():0 ---模拟前三次密码错误,最后一次密码正确--- 密码已经输错1次,输错2次后帐号将会被锁定! 密码已经输错2次,输错1次后帐号将会被锁定! 密码已经输错3次,帐号已被锁定,请5分钟后再登录系统! 帐号已被锁定,请5分钟后再登录系统! pwdFailedMap.size:1 banTimeMap.size():1 */ public static void main(String[] args) { PwdUtil pwdUtil = new PwdUtil(); System.out.println("---模拟前两次密码错误,最后一次密码正确---"); System.out.println(pwdUtil.login("zs","123451")); System.out.println(pwdUtil.login("zs","123452")); System.out.println(pwdUtil.login("zs","123456")); System.out.println("pwdFailedMap.size:"+pwdFailedMap.size()); System.out.println("banTimeMap.size():"+banTimeMap.size()); System.out.println("---模拟前三次密码错误,最后一次密码正确---"); System.out.println(pwdUtil.login("zs","123453")); System.out.println(pwdUtil.login("zs","123454")); System.out.println(pwdUtil.login("zs","123455")); System.out.println(pwdUtil.login("zs","123456")); System.out.println("pwdFailedMap.size:"+pwdFailedMap.size()); System.out.println("banTimeMap.size():"+banTimeMap.size()); }
base-admin整合
base-admin是可以在设置中开启、关闭密码安全策略,因此在策略类中要先判断是否启用安全策略,如果系统不需要这个功能,可以不设置
密码错误时,错误次数+1
密码正确时,进一步校验是否已被锁定(密码错误次数达上限)
登录成功后,清除用户错误次数,同时可以对密码过期时间进行校验,如果密码过期,可弹窗强制用户修改密码
修改密码时,对新密码进行复杂度校验
定时器,定时清除密码安全策略集合数据,以免死数据堆积占用内存
整合效果演示
连续输入3次错误密码,账号被锁定5分钟
如果在被锁定时间内用正确密码尝试登陆仅提示账号被锁定,过了锁定时间方可继续登陆直到登陆成功
如果在被锁定时间内继续用错误密码尝试登陆会不断更新锁定时间,过了锁定时间方可继续登陆直到登陆成功
过了锁定时间方可继续登陆直到登陆成功
登陆成功后检查密码过期时间,弹窗提示修改密码或强制修改密码
后记
密码安全策略暂时先记录到这,后续再进行补充
代码开源
代码已经开源、托管到我的GitHub、码云:
版权声明
作者:huanzi-qch
若标题中有“转载”字样,则本文版权归原作者所有。若无转载字样,本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利.
捐献、打赏
请注意:相应的资金支持能更好的持续开源和创作,如果喜欢这篇文章,请随意打赏!
支付宝
微信
交流群
有事请加群,有问题进群大家一起交流!