在开发中,登录接口一般会校验密码,当密码错误次数达到一定次数(阀值)就激活图形验证码校验,此举的目的主要是为了防止暴力破解密码。基于此,我抽取了密码次数管理接口和验证码校验。
错误次数管理器:
/**
* @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实现:
/**
* redis实现错误管理器
* @author xiongshiyan at 2018/10/8 , contact me with email yanshixiong@126.com 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();
}
}
图形验证码管理器:
/**
* 验证码管理器,可由redis或者db实现
* @author xiongshiyan at 2018/10/8 , contact me with email yanshixiong@126.com or phone 15208384257
*/
public interface CaptchaManger {
/**
* 更新验证码
* @param member 人
* @param captcha 验证码
*/
void updateCaptcha(Member member , String captcha);
/**
* 产生一个验证码
* @param numbers 字符数
* @return 验证码
*/
String generateCode(int numbers);
/**
* 根据验证码生成图片
* @param width 宽度
* @param height 高度
* @param code 验证码
* @return 图片字节流
* @throws IOException IOException
*/
byte[] generateCodeImage(int width, int height, String code) throws IOException;
/**
* 验证验证码是否正确
* @param member 人
* @param captcha 验证码
* @return 是否正确
*/
boolean verifyCaptcha(Member member , String captcha);
/**
* 删除验证码
* @param member 人
*/
void deleteCaptcha(Member member);
/**
* 校验及删除验证码
* @param member 人
* @param code 验证码
* @return 校验是否通过
*/
boolean verify(Member member , String code);
/**
* 产生验证码、保存、生成图片
* @param member 人
* @param width 宽
* @param height 高
* @param numbers 个数
* @return 验证码图形
* @throws IOException IO
*/
byte[] generate(Member member , int width, int height, int numbers) throws IOException;
}
其对外暴露的方法实现放到基类中,使用的时候调用这两个方法即可。
/**
* 使用的时候一般使用这两个方法即可
* @author xiongshiyan at 2018/10/8 , contact me with email yanshixiong@126.com or phone 15208384257
*/
public abstract class AbstractCaptchaManager implements CaptchaManger{
/**
* 校验之后删除验证码
*/
@Override
public boolean verify(Member member , String code){
boolean verifyCaptcha = verifyCaptcha(member, code);
deleteCaptcha(member);
return verifyCaptcha;
}
/**
* 产生验证码、保存、生成图片
*/
@Override
public byte[] generate(Member member , int width, int height, int numbers) throws IOException{
String code = generateCode(numbers);
updateCaptcha(member , code);
return generateCodeImage(width , height , code);
}
}
其Redis实现:
import java.io.IOException;
/**
* @author xiongshiyan at 2018/10/8 , contact me with email yanshixiong@126.com or phone 15208384257
*/
public class RedisCaptchaManager extends AbstractCaptchaManager implements CaptchaManger{
private static final String REDIS_PREFIX = "captcha:";
private RedisUtil redisUtil;
private CaptchaCodeService captchaCodeService;
public void setRedisUtil(RedisUtil redisUtil) {
this.redisUtil = redisUtil;
}
public void setCaptchaCodeService(CaptchaCodeService captchaCodeService) {
this.captchaCodeService = captchaCodeService;
}
@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));
}
@Override
public byte[] generateCodeImage(int width, int height, String code) throws IOException {
return captchaCodeService.generateCodeImage(width, height, code);
}
@Override
public String generateCode(int numbers) {
return captchaCodeService.generateCode(numbers);
}
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 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
*/
public class CaptchaCodeService {
/** 默认的验证码大小 */
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)
};
private BufferedImage drawGraphic(int width, int height, String code){
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 获取图形上下文
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位数字)
for (int i = 0 , len = code.length(); i < len; i++){
//旋转度数 最好小于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(String.valueOf(code.charAt(i)), 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 image;
}
/**
* 给定范围获得随机颜色
*/
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(int width, int height, String code) throws IOException {
BufferedImage image = drawGraphic(width , height, code);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
ImageIO.write(image,"jpeg",stream);
byte[] bytes = stream.toByteArray();
stream.close();
return bytes;
}
public String generateCode(int numbers){
Random random = new Random();
StringBuilder sRand = new StringBuilder();
for (int i = 0; i < numbers; i++) {
sRand.append(STR_ARR[random.nextInt(STR_ARR.length)]);
}
return sRand.toString();
}
}
其中Member就是一个实体类的JavaBean。