密碼錯誤次數管理和圖形驗證碼管理介面和實現
阿新 • • 發佈:2018-12-14
在開發中,登入介面一般會校驗密碼,當密碼錯誤次數達到一定次數(閥值)就啟用圖形驗證碼校驗,此舉的目的主要是為了防止暴力破解密碼。基於此,我抽取了密碼次數管理介面和驗證碼校驗。
錯誤次數管理器:
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;
}
}