1. 程式人生 > >密碼錯誤次數管理和圖形驗證碼管理介面和實現

密碼錯誤次數管理和圖形驗證碼管理介面和實現

在開發中,登入介面一般會校驗密碼,當密碼錯誤次數達到一定次數(閥值)就啟用圖形驗證碼校驗,此舉的目的主要是為了防止暴力破解密碼。基於此,我抽取了密碼次數管理介面和驗證碼校驗。

錯誤次數管理器:

import cn.palmte.anfang.model.Member;

/**
 * @author xiongshiyan
 * 密碼錯誤管理器
 * 可以使用Reids實現,可以使用db實現
 */
public interface ErrorCountManager {
    /**
     * 當前此人密碼錯誤次數
     * @param member 人員
     * @return 錯誤次數
     */
    int currentErrorCount(Member member);

    /**
     * 密碼錯誤次數加 1
     * @param member 人員
     * @return 當前的錯誤次數
     */
    int incrErrorCount(Member member);

    /**
     * 是否達到閥值
     * @param currentErrorCount 當前錯誤次數
     * @return 是否達到錯誤閥值
     */
    boolean reachThreshold(int currentErrorCount);

    /**
     * 密碼正確的時候清除錯誤次數
     * @param member 人
     */
    void clearErrorCount(Member member);
}

其Redis實現:

import cn.palmte.anfang.model.Member;
import cn.palmte.anfang.redis.RedisUtil;

/**
 * redis實現錯誤管理器
 * @author xiongshiyan at 2018/10/8 , contact me with email [email protected] or phone 15208384257
 */
public class RedisErrorCountManager implements ErrorCountManager{
    private static final String REDIS_PREFIX = "errorCount:";

    private int threshold = 3;
    private RedisUtil redisUtil;

    public RedisErrorCountManager(int threshold){
        this.threshold = threshold;
    }

    public RedisErrorCountManager(){

    }

    public void setThreshold(int threshold) {
        this.threshold = threshold;
    }

    public void setRedisUtil(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }

    @Override
    public int currentErrorCount(Member member) {
        Integer currentErrorCount = (Integer) redisUtil.get(key(member));
        return null == currentErrorCount ? 0 : currentErrorCount;
    }

    @Override
    public int incrErrorCount(Member member) {
        String key = key(member);
        Integer currentErrorCount = (Integer) redisUtil.get(key);
        int errorCount = null == currentErrorCount ? 1 : currentErrorCount + 1;
        redisUtil.set(key, errorCount, 1800);
        return errorCount;
    }

    @Override
    public boolean reachThreshold(int currentErrorCount) {
        return currentErrorCount > threshold;
    }

    @Override
    public void clearErrorCount(Member member) {
        redisUtil.del(key(member));
    }

    private String key(Member member){
        return REDIS_PREFIX + member.getPhone();
    }
}

圖形驗證碼管理器:

import cn.palmte.anfang.model.Member;

/**
 * 驗證碼管理器,可由redis或者db實現
 * @author xiongshiyan at 2018/10/8 , contact me with email [email protected] or phone 15208384257
 */
public interface CaptchaManger {
    /**
     * 更新驗證碼
     * @param member 人
     * @param captcha 驗證碼
     */
    void updateCaptcha(Member member , String captcha);

    /**
     * 驗證驗證碼是否正確
     * @param member 人
     * @param captcha 驗證碼
     * @return 是否正確
     */
    boolean verifyCaptcha(Member member , String captcha);

    /**
     * 刪除驗證碼
     * @param member 人
     */
    void deleteCaptcha(Member member);
}

其Redis實現:

import cn.palmte.anfang.model.Member;
import cn.palmte.anfang.redis.RedisUtil;

/**
 * @author xiongshiyan at 2018/10/8 , contact me with email [email protected] or phone 15208384257
 */
public class RedisCaptchaManager implements CaptchaManger{
    private static final String REDIS_PREFIX = "captcha:";
    private RedisUtil redisUtil;
    public void setRedisUtil(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }
    @Override
    public void updateCaptcha(Member member, String captcha) {
        redisUtil.set(key(member) , captcha , 1800);
    }

    @Override
    public boolean verifyCaptcha(Member member, String captcha) {
        String cap = (String) redisUtil.get(key(member));
        return null != cap && cap.equalsIgnoreCase(captcha);
    }

    @Override
    public void deleteCaptcha(Member member) {
        redisUtil.del(key(member));
    }

    private String key(Member member){
        return REDIS_PREFIX + member.getPhone();
    }
}

校驗邏輯:每次校驗密碼前先校驗密碼錯誤次數,達到閥值就校驗驗證碼。其主要邏輯如下:

/// 密碼錯誤次數超限校驗
        int errorCount = errorCountManager.currentErrorCount(member);
        boolean reachThreshold = errorCountManager.reachThreshold(errorCount);
        if(reachThreshold){
            //到達閥值的話就需要校驗驗證碼
            String imgCode = object.getString("imgCode");
            Map<String , Integer> map = new HashMap<>(1);
            map.put("errorCount" , errorCount);
            if(null == imgCode || "".equals(imgCode)){
                return ResponseMsg.buildMsg(3 , "錯誤次數超限,驗證碼錯誤" , map);
            }else {
                boolean verifyCaptcha = captchaManger.verifyCaptcha(member, imgCode);
                captchaManger.deleteCaptcha(member);
                if(!verifyCaptcha){
                    return ResponseMsg.buildMsg(3 , "錯誤次數超限,驗證碼錯誤" , map);
                }
            }
        }


        boolean verifyPassword = memberService.verifyPassword(password, member);
        if(!verifyPassword){
            //更新錯誤次數
            int count = errorCountManager.incrErrorCount(member);
            Map<String , Integer> map = new HashMap<>(1);
            map.put("errorCount" , count);
            return ResponseMsg.buildMsg(3 , "密碼錯誤" , map);
        }

        //清除錯誤次數
        errorCountManager.clearErrorCount(member);

驗證碼產生service

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.QuadCurve2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random;

/**
 * @author xiongshiyan at 2018/1/15
 */
@Service
public class PhoneCaptchaService {

    @Autowired
    private CaptchaManger captchaManger;

    /** 預設的驗證碼大小 */
    public static final String WIDTH = "108", HEIGHT = "40", NUMBERS = "4";
    /** 驗證碼隨機字元陣列 */
    private static final String[] STR_ARR = {"3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y"};
    /** 驗證碼字型 */
    private static final Font[] RANDOM_FONT = new Font[] {
            new Font("nyala", Font.BOLD, 38),
            new Font("Arial", Font.BOLD, 32),
            new Font("Bell MT", Font.BOLD, 32),
            new Font("Credit valley", Font.BOLD, 34),
            new Font("Impact", Font.BOLD, 32),
            new Font(Font.MONOSPACED, Font.BOLD, 40)
    };

    public String drawGraphic(BufferedImage image,int numbers){
        // 獲取圖形上下文
        Graphics2D g = image.createGraphics();

        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
        // 圖形抗鋸齒
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        // 字型抗鋸齒
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        // 設定背景色
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, image.getWidth(), image.getHeight());

        //生成隨機類
        Random random = new Random();
        //設定字型
        g.setFont(RANDOM_FONT[random.nextInt(RANDOM_FONT.length)]);

        // 畫蛋蛋,有蛋的生活才精彩
        Color color;
        for(int i = 0; i < 10; i++){
            color = getRandColor(120, 200);
            g.setColor(color);
            g.drawOval(random.nextInt(image.getWidth()), random.nextInt(image.getHeight()), 5 + random.nextInt(10), 5 + random.nextInt(10));
            color = null;
        }

        // 取隨機產生的認證碼(numbers位數字)
        StrBuilder sRand = new StrBuilder();
        for (int i = 0; i < numbers; i++){
            String rand = String.valueOf(STR_ARR[random.nextInt(STR_ARR.length)]);
            sRand.append(rand);
            //旋轉度數 最好小於45度
            int degree = random.nextInt(28);
            if (i % 2 == 0) {
                degree = degree * (-1);
            }
            //定義座標
            int x = 22 * i, y = 21;
            //旋轉區域
            g.rotate(Math.toRadians(degree), x, y);
            //設定字型顏色
            color = getRandColor(20, 130);
            g.setColor(color);
            //將認證碼顯示到圖象中
            g.drawString(rand, x + 8, y + 10);
            //旋轉之後,必須旋轉回來
            g.rotate(-Math.toRadians(degree), x, y);
            color = null;
        }
        //圖片中間線
        g.setColor(getRandColor(0, 60));
        //width是線寬,float型
        BasicStroke bs = new BasicStroke(3);
        g.setStroke(bs);
        //畫出曲線
        QuadCurve2D.Double curve = new QuadCurve2D.Double(0d, random.nextInt(image.getHeight() - 8) + 4, image.getWidth() / 2.0, image.getHeight() / 2.0, image.getWidth(), random.nextInt(image.getHeight() - 8) + 4);
        g.draw(curve);
        // 銷燬影象
        g.dispose();
        return sRand.toString();
    }

    /**
     * 給定範圍獲得隨機顏色
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255){
            fc = 255;}
        if (bc > 255){
            bc = 255;}
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }


    /**
     * 產生圖形驗證碼圖片二進位制
     */
    public byte[] generateCodeImage(Member member, int width, int height, int numbers) throws IOException {
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        String captcha = drawGraphic(image,numbers);

        captchaManger.updateCaptcha(member , captcha);

        ByteArrayOutputStream stream = new ByteArrayOutputStream();
        ImageIO.write(image,"jpeg",stream);
        byte[] bytes = stream.toByteArray();
        stream.close();
        return bytes;
    }
}