滑块验证码Java实现
- 滑块验证码的引入
- 滑块验证码原理
- 滑块验证码的Java实现
- 说明
- 依赖
- 项目框架
- java代码
- 结果验证
- 参考
滑块验证码的引入
最近滑动验证码在很多网站逐步流行起来,一方面对用户体验来说,比较新颖,操作简单,另一方面相对图形验证码来说,安全性并没有很大的降低。所以在项目中将登陆验证码方式改为滑块验证码。
滑块验证码原理
很多网站使用滑块验证码提高网站安全性,为了做到真正的验证,必须要走后台服务器。
下面是java实现滑块验证的核心步骤:
- 从服务器随机取一张图片,并对图片上的随机x,y坐标和宽高一块区域抠图;
- 根据步骤一的坐标和宽高,使用二维数组保存原图上抠图区域的像素点坐标;
- 根据步骤二的坐标点,对原图的抠图区域的颜色进行处理。
- 完成以上步骤之后得到两张图(扣下来的方块图,带有抠图区域阴影的原图),将这两张图和抠图区域的y坐标传到前台,前端在移动方块验证时,将移动后的x坐标传递到后台与原来的x坐标作比较,如果在阈值内则验证通过。
- 请求验证的步骤:前台向后台发起请求,后台随机一张图片做处理将处理完的两张图片的base64,抠图y坐标和token(token为后台缓存验证码的唯一token,可以用缓存和分布式缓存)返回给前台。
- 前台滑动图片将x坐标和token作为参数请求后台验证,服务器根据token取出x坐标与参数的x进行比较。
滑块验证码的Java实现
说明
项目是基于SpringBoot实现,前端是vue实现
依赖
无
项目框架
common包中存放验证码的常量;
controller包中存放验证码的controller类;
entity包中存放实体类;
tools包中存放验证码的工具类;
java代码
- CaptchaConstant
public class CaptchaConstant {
/**
* key
*/
public static final String TOKEN = "token";
/**
* 拼图所在x坐标名称
*/
public static final String X = "x";
/**
* 拼图允许误差
*/
public static final Integer SLICE_DIFF_LIMIT = 3;
/**
* redis最长存储时间5分钟
*/
public static final int MINUTES_5 = 5;
}
- ConfigConstant 拼图验证码配置
public class ConfigConstant {
/**
* 小图的宽 (SQUARE_W + CIRCLE_R * 2 + LIGHT * 2)
*/
public static final int SMALL_IMG_W = 64;
/**
* 小图的高 (SQUARE_H + CIRCLE_R * 2 + LIGHT * 2)
*/
public static final int SMALL_IMG_H = 64;
/**
* 正方形的宽
*/
public static final int SQUARE_W = 40;
/**
* 正方形的高
*/
public static final int SQUARE_H = 40;
/**
* 小图突出圆的直径 (CIRCLE_D * 2)
*/
public static final int CIRCLE_D = 16;
/**
* 小图突出圆的半径
*/
public static final int CIRCLE_R = 8;
/**
* 小图阴影宽度
*/
public static final int SHADOW = 4;
/**
* 小图边缘高亮宽度
*/
public static final int LIGHT = 4;
/**
* 小图边缘高亮颜色
*/
public static final Color CIRCLE_GLOW_I_H = new Color(253, 239, 175, 148);
public static final Color CIRCLE_GLOW_I_L = new Color(255, 209, 0);
public static final Color CIRCLE_GLOW_O_H = new Color(253, 239, 175, 124);
public static final Color CIRCLE_GLOW_O_L = new Color(255, 179, 0);
}
- HDCaptchaController 拼图验证码controller类
@RestController
@RequestMapping("/hdcaptcha")
@Slf4j
public class HDCaptchaController {
@Value("${xxx.path}") // 在配置文件中配置原图存放路径
private String captchaPath;
@Autowired
private RedisTool redisTool;
/**
* 注册验证码
*
* @param request
* @return 验证码图片信息
*/
@GetMapping("/register")
public String register(HttpServletRequest request) {
if (captchaPath == null) {
ResultTool.Error(21);
}
Captcha captcha = new CaptchaUtil().createCaptcha(captchaPath);
if (captcha == null) {
ResultTool.Error(21);
}
// 生成注册token
String token = captcha.getToken();
// redis中保存x偏移量,保存时间5分钟
redisTool.set(token, captcha.getX(), TimeUnit.MINUTES.toSeconds(CaptchaConstant.MINUTES_5));
return ResultTool.Success(captcha);
}
/**
* 拼图校验与登录
*
* @param request
* @param form 拼图校验与登录信息
* @return
*/
@PostMapping("/check")
public String check(HttpServletRequest request, @RequestBody CaptchaCheck form) {
if (StringUtils.isBlank(form.getAccountName())) {
return ResultToolUser.Error(13);
}
if (StringUtils.isBlank(form.getPassword())) {
return ResultToolUser.Error(11);
}
if (StringUtils.isBlank(form.getToken())) {
return ResultTool.Error(22);
}
if (null == form.getSliceX()) {
return ResultTool.Error(23);
}
int x = (int) redisTool.get(form.getToken().trim());
if (x < 0) {
return ResultTool.Error(23);
} else {
int diff = x - form.getSliceX();
if (diff < -CaptchaConstant.SLICE_DIFF_LIMIT || diff > CaptchaConstant.SLICE_DIFF_LIMIT) {
return ResultTool.Error(23);
}
}
// 验证码的验证和登录做了分离,若想做到一起可以在下边写登录的逻辑
return ResultTool.Success("success!");
}
}
- Captcha 拼图验证码实体类
@Data
public class Captcha {
/**
* 滑动拼图块
*/
private String sliceImg;
/**
* 背景图
*/
private String bgImg;
/**
* 注册token
*/
private String token;
private Integer x;
/**
* 拼图所在y坐标
*/
private Integer y;
}
- CaptchaCheck 滑块验证码核验form
@Data
public class CaptchaCheck {
/**
* 登录名
*/
private String accountName;
/**
* 登录密码
*/
private String password;
/**
* 注册token
*/
private String token;
/**
* 滑动x坐标
*/
private Integer sliceX;
}
- CaptchaUtil 滑块验证码的工具类
@Slf4j
public class CaptchaUtil {
/**
* 创建验证码
*
* @param captchaPath 验证码背景图保存路径
* @return
*/
public Captcha createCaptcha(String captchaPath) {
File sourceFile = FileUtil.getSourceImage(captchaPath);
// 原图图层
BufferedImage sourceImg;
try {
sourceImg = ImageIO.read(sourceFile);
} catch (IOException e) {
log.error("读取验证码背景图出错", e);
return null;
}
// 生成随机坐标
Random random = new Random();
// 滑动拼图x坐标范围为 [(0+40),(260-40)],y坐标范围为 [0,(160-40))
int x = random.nextInt(sourceImg.getWidth() - 2 * ConfigConstant.SMALL_IMG_W) + ConfigConstant.SMALL_IMG_W;
int y = random.nextInt(sourceImg.getHeight() - ConfigConstant.SMALL_IMG_H);
log.info("滑动拼图坐标为({},{})", x, y);
// 小图图层
BufferedImage smallImg;
try {
smallImg = ImageUtil.cutSmallImg(sourceFile, x, y);
} catch (IOException e) {
log.error("创建验证码出错", e);
return null;
}
// 创建shape区域
List<Shape> shapes = createSmallShape();
// 创建用于小图阴影和大图凹槽的图层
List<BufferedImage> effectImgs = createEffectImg(shapes, smallImg);
// 处理图片的边缘高亮及其阴影效果
BufferedImage sliceImg = dealLightAndShadow(effectImgs.get(0), shapes.get(0));
// 将灰色图当做水印印到原图上
BufferedImage bgImg = ImageUtil.createBgImg(effectImgs.get(1), sourceImg, x, y);
Captcha captchaDTO = new Captcha();
captchaDTO.setBgImg(Base64Util.getImageBase64(bgImg, true));
captchaDTO.setSliceImg(Base64Util.getImageBase64(sliceImg, false));
captchaDTO.setX(x);
captchaDTO.setY(y);
captchaDTO.setToken(TokenUtil.createToken());
return captchaDTO;
}
/**
* 处理小图,在4个方向上随机找到2个方向添加凸出
*
* @return
*/
private static List<Shape> createSmallShape() {
int face1 = RandomUtils.nextInt(4);
int face2;
//使凸出1 与 凸出2不在同一个方向
do {
face2 = RandomUtils.nextInt(4);
} while (face1 == face2);
Shape shape1 = createShape(face1, 0);
Shape shape2 = createShape(face2, 0);
// 因为后边图形需要生成阴影,所以生成的小图shape + 阴影宽度 = 灰度化的背景小图shape(即大图上的凹槽)
Shape bigShape1 = createShape(face1, ConfigConstant.SHADOW);
Shape bigShape2 = createShape(face2, ConfigConstant.SHADOW);
// 生成中间正方体Shape,(具体边界 + 弧半径 = x坐标位)
int xStart = ConfigConstant.CIRCLE_R + ConfigConstant.LIGHT;
int yStart = ConfigConstant.CIRCLE_R + ConfigConstant.LIGHT;
Shape center = new Rectangle2D.Float(xStart, yStart, ConfigConstant.SQUARE_W, ConfigConstant.SQUARE_H);
Shape bigCenter = new Rectangle2D.Float(xStart - (float) ConfigConstant.SHADOW / 2,
yStart - (float) ConfigConstant.SHADOW / 2, ConfigConstant.SQUARE_W + ConfigConstant.SHADOW,
ConfigConstant.SQUARE_H + ConfigConstant.SHADOW);
// 合并Shape
Area area = new Area(center);
area.add(new Area(shape1));
area.add(new Area(shape2));
// 合并大Shape
Area bigArea = new Area(bigCenter);
bigArea.add(new Area(bigShape1));
bigArea.add(new Area(bigShape2));
List<Shape> list = new ArrayList<>();
list.add(area);
list.add(bigArea);
return list;
}
/**
* 创建圆形区域,半径为5
* 由于小图边缘阴影的存在,坐标需加上此宽度
*
* @param type 0=上,1=左,2=下,3=右
* @param size 圆外接矩形边长
* @return
*/
private static Shape createShape(int type, int size) {
if (type < 0 || type > 3) {
type = 0;
}
int x;
int y;
if (type == 0) {
x = ConfigConstant.SQUARE_W / 2 + ConfigConstant.SHADOW;
y = ConfigConstant.SHADOW;
} else if (type == 1) {
x = ConfigConstant.SHADOW;
y = ConfigConstant.SQUARE_H / 2 + ConfigConstant.SHADOW;
} else if (type == 2) {
x = ConfigConstant.SQUARE_W / 2 + ConfigConstant.SHADOW;
y = ConfigConstant.SQUARE_H + ConfigConstant.SHADOW;
} else {
x = ConfigConstant.SQUARE_W + ConfigConstant.SHADOW;
y = ConfigConstant.SQUARE_H / 2 + ConfigConstant.SHADOW;
}
int halfSize = size / 2;
int wSide = ConfigConstant.CIRCLE_D + size;
return new Arc2D.Float(x - halfSize, y - halfSize, wSide, wSide, 90 * type, 190, Arc2D.CHORD);
}
/**
* 创建用于小图阴影和大图凹槽的图层
*
* @param shapes
* @param smallImg 小图原图
* @return
*/
private static List<BufferedImage> createEffectImg(List<Shape> shapes, BufferedImage smallImg) {
Shape area = shapes.get(0);
Shape bigArea = shapes.get(1);
// 创建图层用于处理小图的阴影
BufferedImage bfm1 = new BufferedImage(ConfigConstant.SMALL_IMG_W, ConfigConstant.SMALL_IMG_H,
BufferedImage.TYPE_INT_ARGB);
// 创建图层用于处理大图的凹槽
BufferedImage bfm2 = new BufferedImage(ConfigConstant.SMALL_IMG_W, ConfigConstant.SMALL_IMG_H,
BufferedImage.TYPE_INT_ARGB);
for (int i = 0; i < ConfigConstant.SMALL_IMG_W; i++) {
for (int j = 0; j < ConfigConstant.SMALL_IMG_W; j++) {
if (area.contains(i, j)) {
bfm1.setRGB(i, j, smallImg.getRGB(i, j));
}
if (bigArea.contains(i, j)) {
bfm2.setRGB(i, j, Color.black.getRGB());
}
}
}
List<BufferedImage> list = new ArrayList<>();
list.add(bfm1);
list.add(bfm2);
return list;
}
/**
* 处理小图的边缘灯光及其阴影效果
*
* @param bfm
* @param shape
* @return
*/
private static BufferedImage dealLightAndShadow(BufferedImage bfm, Shape shape) {
//创建新的透明图层,该图层用于边缘化阴影, 将生成的小图合并到该图上
BufferedImage buffimg = ((Graphics2D) bfm.getGraphics()).getDeviceConfiguration()
.createCompatibleImage(ConfigConstant.SMALL_IMG_W, ConfigConstant.SMALL_IMG_H,
Transparency.TRANSLUCENT);
Graphics2D graphics2d = buffimg.createGraphics();
Graphics2D g2 = (Graphics2D) bfm.getGraphics();
//原有小图,边缘亮色处理
paintBorderGlow(g2, shape);
//新图层添加阴影
paintBorderShadow(graphics2d, shape);
graphics2d.drawImage(bfm, 0, 0, null);
return buffimg;
}
/**
* 处理边缘亮色
*
* @param g2
* @param clipShape
*/
private static void paintBorderGlow(Graphics2D g2, Shape clipShape) {
int gw = ConfigConstant.LIGHT * 2;
for (int i = gw; i >= 2; i -= 2) {
float pct = (float) (gw - i) / (gw - 1);
Color mixHi = getMixedColor(ConfigConstant.CIRCLE_GLOW_I_H, pct, ConfigConstant.CIRCLE_GLOW_O_H,
1.0f - pct);
Color mixLo = getMixedColor(ConfigConstant.CIRCLE_GLOW_I_L, pct, ConfigConstant.CIRCLE_GLOW_O_L,
1.0f - pct);
g2.setPaint(new GradientPaint(0.0f, 35 * 0.25f, mixHi, 0.0f, 35, mixLo));
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, pct));
g2.setStroke(new BasicStroke(i));
g2.draw(clipShape);
}
}
/**
* 处理阴影
*
* @param g1
* @param clipShape
*/
private static void paintBorderShadow(Graphics2D g1, Shape clipShape) {
g1.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
int sw = ConfigConstant.SHADOW * 2;
for (int i = sw; i >= 2; i -= 2) {
float pct = (float) (sw - i) / (sw - 1);
//pct<03. 用于去掉阴影边缘白边, pct>0.8用于去掉过深的色彩, 如果使用Color.lightGray. 可去掉pct>0.8
if (pct < 0.3 || pct > 0.8) {
continue;
}
g1.setColor(getMixedColor(new Color(54, 54, 54), pct, Color.WHITE, 1.0f - pct));
g1.setStroke(new BasicStroke(i));
g1.draw(clipShape);
}
}
private static Color getMixedColor(Color c1, float pct1, Color c2, float pct2) {
float[] clr1 = c1.getComponents(null);
float[] clr2 = c2.getComponents(null);
for (int i = 0; i < clr1.length; i++) {
clr1[i] = (clr1[i] * pct1) + (clr2[i] * pct2);
}
return new Color(clr1[0], clr1[1], clr1[2], clr1[3]);
}
}
- ImageUtil 图片工具类
class ImageUtil {
/**
* 创建小块拼图
*
* @param file 背景原图
* @param x 小块拼图x坐标
* @param y 小块拼图y坐标
* @return
*/
static BufferedImage cutSmallImg(File file, int x, int y) throws IOException {
Iterator<ImageReader> iterator = ImageIO.getImageReadersByFormatName("png");
ImageReader render = iterator.next();
ImageInputStream in = ImageIO.createImageInputStream(new FileInputStream(file));
render.setInput(in, true);
BufferedImage bufferedImage;
try {
ImageReadParam param = render.getDefaultReadParam();
Rectangle rect = new Rectangle(x, y, ConfigConstant.SMALL_IMG_W, ConfigConstant.SMALL_IMG_H);
param.setSourceRegion(rect);
bufferedImage = render.read(0, param);
} finally {
if (in != null) {
in.close();
}
}
return bufferedImage;
}
/**
* 创建一个灰度化图层, 将生成的小图,覆盖到该图层,使其灰度化,用于作为一个水印图
*
* @param smallImage 小图
* @param originImg 原图
* @param x x坐标
* @param y y坐标
* @return
*/
static BufferedImage createBgImg(BufferedImage smallImage, BufferedImage originImg, int x, int y) {
// 将灰度化之后的图片,整合到原有图片上
Graphics2D graphics2d = originImg.createGraphics();
graphics2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.6F));
graphics2d.drawImage(smallImage, x, y, null);
// 释放
graphics2d.dispose();
return originImg;
}
/**
* 压缩图片
*
* @param originImg
* @return
*/
static byte[] compressImg(BufferedImage originImg) {
ImageWriter imageWriter = null;
ByteArrayOutputStream outputStream = null;
try {
int width = originImg.getWidth();
int height = originImg.getHeight();
BufferedImage newBufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_USHORT_555_RGB);
Graphics2D graphics2d = newBufferedImage.createGraphics();
// graphics2D.setBackground(new Color(255, 255, 255));
graphics2d.clearRect(0, 0, width, height);
graphics2d.drawImage(originImg.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null);
imageWriter = ImageIO.getImageWritersByFormatName("png").next();
outputStream = new ByteArrayOutputStream();
imageWriter.setOutput(ImageIO.createImageOutputStream(outputStream));
imageWriter.write(new IIOImage(newBufferedImage, null, null));
outputStream.flush();
return outputStream.toByteArray();
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
if (imageWriter != null) {
imageWriter.abort();
}
IOUtils.closeQuietly(outputStream);
}
}
}
其他的工具类这里不再给出。
结果验证- PostMan调接口的返回结果
- 为了美观,base64部分做了删减,其中sliceImg为切好的小图的base64,bgImg为切图后背景图的base64。
- 前端页面的显示