• 密码安全策略


      前言

      几乎所有的系统都有密码安全要求,这是基础的安全策略,本文记录常用密码安全策略并编写策略校验工具类

      常用密码安全策略

        密文存储,通常为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 pwdFailedMap = new ConcurrentHashMap<>(10);
        // 用户连续密码输入错误次数达上限后锁定时长
        private final static ConcurrentHashMap 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、码云:

      GitHub:https://github.com/huanzi-qch/base-admin

      码云:https://gitee.com/huanzi-qch/base-admin

  • 相关阅读:
    X光胸片器官图像分割系统:前端交互展示
    01_大数据导论与Linux基础
    mongodb 基本概念
    熟悉c语言结构体
    sap 进行dubgger看表数据
    C++并发与多线程笔记六:单例模式下的数据共享
    uniapp本地打包到Android Studio生成APK文件
    数据结构篇:链表
    从 MySQL 迁移到 TiDB:使用 SQL-Replay 工具进行真实线上流量回放测试 SOP
    33 WEB漏洞-逻辑越权之水平垂直越权全解
  • 原文地址:https://www.cnblogs.com/huanzi-qch/p/16613636.html