提交 78f3a0ea 编写于 作者: dong.an's avatar dong.an

图形验证码

上级 4cff1d55
流水线 #13049 已失败 于阶段
......@@ -3,6 +3,7 @@ package com.pica.cloud.account.account.server.controller;
import com.pica.cloud.account.account.server.entity.Account;
import com.pica.cloud.account.account.server.req.AccountReq;
import com.pica.cloud.account.account.server.service.AccountService;
import com.pica.cloud.account.account.server.service.CaptchaService;
import com.pica.cloud.foundation.entity.PicaException;
import com.pica.cloud.foundation.entity.PicaResponse;
import com.pica.cloud.foundation.entity.PicaResultCode;
......@@ -41,6 +42,8 @@ public class AccountController extends AccountBaseController {
@Autowired
private AccountService accountService;
@Autowired
private CaptchaService captchaService;
@Autowired
@Qualifier("cacheMigrateClient")
private ICacheClient redisClient;
......@@ -50,7 +53,13 @@ public class AccountController extends AccountBaseController {
@ApiOperation("获取登录验证码")
@GetMapping("/authCode")
public PicaResponse<String> getAuthCode(@ApiParam(value = "手机号", required = true) @RequestParam("mobilePhone") String mobilePhone,
@ApiParam(value = "验证码类型 0默认 1注册 2修改密码 4微信登录绑定手机 5修改手机 6重置密码") @RequestParam(value = "flag", defaultValue = "0") String flag) {
@ApiParam(value = "验证码类型 0默认 1注册 2修改密码 4微信登录绑定手机 5修改手机 6重置密码") @RequestParam(value = "flag", defaultValue = "0") String flag,
@ApiParam(value = "图形验证码token", required = true) @RequestParam("token") String token,
@ApiParam(value = "图形验证码答案", required = true) @RequestParam("answer") String answer) {
//校验图形验证码
if (!captchaService.acknowledge(token, answer)) {
return PicaResponse.toResponse(null, PicaResultCode.PARAM_IS_INVALID.code(), "图形验证码答案错误!");
}
this.checkMobilePhone(mobilePhone);
String authCode = CommonUtil.createValidateCode(); //随机生成验证码
String message = "您的验证码是" + authCode + ",在10分钟内有效。如非本人操作,请忽略本短信!";
......
package com.pica.cloud.account.account.server.controller;
import com.pica.cloud.account.account.server.service.CaptchaService;
import com.pica.cloud.account.account.server.util.captcha.CaptchaToken;
import com.pica.cloud.foundation.entity.PicaResponse;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author andong
* @create 2019/8/9
*/
@Api(description = "图形验证码")
@RestController
@RequestMapping("/account")
public class CaptchaController {
@Autowired
private CaptchaService captchaService;
@ApiOperation("获取图形验证码")
@GetMapping("/captcha")
public PicaResponse<CaptchaToken> captcha(@ApiParam("图片宽度") @RequestParam(value = "width", required = false) Integer width,
@ApiParam("图片高度") @RequestParam(value = "height", required = false) Integer height) {
width = (width == null || width.intValue() <= 0) ? 160 : width;
height = (height == null || height.intValue() <= 0) ? 40 : height;
CaptchaToken captchaToken = captchaService.generateToken(width, height);
return PicaResponse.toResponse(captchaToken);
}
@ApiOperation("校验图形验证码")
@GetMapping("/acknowledge")
public PicaResponse<Boolean> acknowledge(@ApiParam("token") @RequestParam("token") String token,
@ApiParam("answer") @RequestParam("answer") String answer) {
boolean valid = captchaService.acknowledge(token, answer);
return PicaResponse.toResponse(valid);
}
}
package com.pica.cloud.account.account.server.service;
import com.pica.cloud.account.account.server.util.captcha.CaptchaToken;
/**
* @author Laurence Cao
*
*/
public interface CaptchaService {
/**
* 生成验证码的令牌及图片(json时为base64编码)
*
* @return
*/
CaptchaToken generateToken(int width, int height);
/**
* 确认令牌与回答字符串是一致的,无论成功与否都将失效令牌
*
* @param token
* @param answer
* @return
*/
boolean acknowledge(String token, String answer);
}
package com.pica.cloud.account.account.server.service.impl;
import com.pica.cloud.account.account.server.service.CaptchaService;
import com.pica.cloud.account.account.server.util.captcha.CaptchaGenerator;
import com.pica.cloud.account.account.server.util.captcha.CaptchaToken;
import com.pica.cloud.foundation.redis.ICacheClient;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* @author Laurence Cao
*
*/
@Component
public class CaptchaServiceImpl implements CaptchaService {
private final int size = 5;
private final int EXPIRE = 5 * 60; // 5 minutes
private final String CAPTCHA_PREFIX = "captcha-";
@Autowired
private CaptchaGenerator agent;
@Autowired
@Qualifier("cacheMigrateClient")
private ICacheClient redisClient;
@Override
public CaptchaToken generateToken(int width, int height) {
String origin = agent.generateChars(size);
byte[] buf = agent.createCaptcha(origin, width, height);
CaptchaToken ret = new CaptchaToken();
ret.setBuf(buf);
ret.setOrigin(origin);
ret.setToken(DigestUtils.md5Hex(UUID.randomUUID().toString()));
redisClient.set(CAPTCHA_PREFIX + ret.getToken(), origin, EXPIRE);
return ret;
}
@Override
public boolean acknowledge(String token, String answer) {
String origin = redisClient.get(CAPTCHA_PREFIX + token);
redisClient.del(CAPTCHA_PREFIX + token);
if (origin != null && answer != null) {
return origin.compareToIgnoreCase(answer) == 0;
}
return false;
}
}
package com.pica.cloud.account.account.server.util.captcha;
import java.awt.*;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
/**
* @author Laurence Cao
*
*/
public class CaptchaContext {
final Random rnd = ThreadLocalRandom.current();
final int fontCount = 4;
public int width;
public int height;
public float fontSize;
public boolean showGrid;
public int gridSize = 1 + rnd.nextInt(10);
public int rotationAmplitude = rnd.nextInt(10);
public int scaleAmplitude = 2 * rotationAmplitude;
public Font font;
public CaptchaContext(int width, int height, int fontSize, boolean showGrid) throws FontFormatException, IOException {
this.width = width;
this.height = height;
this.fontSize = fontSize;
this.showGrid = showGrid;
String name = "/" + (ThreadLocalRandom.current().nextInt(0, fontCount) + 1) + ".ttf";
this.font = Font.createFont(Font.TRUETYPE_FONT, CaptchaGenerator.class.getResourceAsStream(name));
}
public int rndGridSize() {
return 1 + rnd.nextInt(10);
}
public int rndRotationAmplitude() {
return rnd.nextInt(10);
}
public int rndScaleAmplitude() {
return rnd.nextInt(20);
}
public float rndFontSize() {
return fontSize + rnd.nextFloat();
}
}
package com.pica.cloud.account.account.server.util.captcha;
import com.google.common.collect.Range;
import com.google.common.collect.RangeMap;
import com.google.common.collect.TreeRangeMap;
import java.awt.*;
import java.io.IOException;
/**
* @author Laurence Cao
*
*/
public abstract class CaptchaGenerator {
protected RangeMap<Integer, CaptchaContext> ctxs = TreeRangeMap.create();
public CaptchaGenerator(boolean showGrid) throws FontFormatException, IOException {
int h = 40;
ctxs.put(Range.closed(0, h), new CaptchaContext(h * 4, h, h / 5 * 4, showGrid));
h *= 2;
ctxs.put(Range.closed(h / 2, h), new CaptchaContext(h * 4, h, h / 5 * 4, showGrid));
h *= 2;
ctxs.put(Range.closed(h / 2, Integer.MAX_VALUE), new CaptchaContext(h * 4, h, h / 5 * 4, showGrid));
}
@SuppressWarnings("unused")
public byte[] createCaptcha(String text, int width, int height) {
CaptchaContext ctx = ctxs.get(height);
if (ctx == null) {
ctx = ctxs.get(0);
}
return CaptchaUtil.generateImage(text, ctx);
}
public abstract String generateChars(int size);
}
package com.pica.cloud.account.account.server.util.captcha;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.Base64;
/**
* @author Laurence Cao
*
*/
public class CaptchaToken {
/**
* 代表此验证码的唯一标识符,超时和一次使用均会失效
*/
protected String token;
/**
* 代表此验证码的实际图片,以字节码方式提供
*/
@JsonIgnore
protected byte[] buf;
/**
* 原始随机字符串
*/
@JsonIgnore
protected String origin;
/**
* base64编码的图片
*/
protected String content;
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public byte[] getBuf() {
return buf;
}
public void setBuf(byte[] buf) {
this.buf = buf;
setContent(Base64.getEncoder().encodeToString(this.buf));
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getOrigin() {
return origin;
}
public void setOrigin(String origin) {
this.origin = origin;
}
}
/**
*
*/
package com.pica.cloud.account.account.server.util.captcha;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
/**
* @author Laurence Cao
*
*/
public class CaptchaUtil {
final static int MAXSIZE = UUID.randomUUID().toString().replace("_", "").length();
public static String generateUUIDText(int count) {
int pos = count < MAXSIZE ? count : MAXSIZE;
pos = pos <= 0 ? 5 : pos;
return UUID.randomUUID().toString().replace("_", "").substring(0, pos);
}
public static byte[] generateImage(String text, CaptchaContext ctx) {
byte[] ret = null;
try {
BufferedImage img = createCaptcha(text, ctx);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(img, "jpg", baos);
baos.flush();
ret = baos.toByteArray();
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
return ret;
}
public static BufferedImage createCaptcha(String src, CaptchaContext ctx) {
char[] text = src == null ? new char[0] : src.toCharArray();
if (text == null || text.length == 0) {
throw new IllegalArgumentException("No captcha text given");
}
BufferedImage image = new BufferedImage(ctx.width, ctx.height, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = image.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setBackground(Color.WHITE);
g2d.setColor(Color.BLACK);
clearCanvas(g2d, ctx);
if (ctx.showGrid) {
drawGrid(g2d, ctx);
}
int charMaxWidth = ctx.width / text.length;
int xPos = 0;
for (char ch : text) {
drawCharacter(g2d, ctx, ch, xPos, charMaxWidth);
xPos += charMaxWidth;
}
g2d.dispose();
return image;
}
/**
* Clears the canvas.
*/
private static void clearCanvas(Graphics2D g2d, CaptchaContext ctx) {
g2d.clearRect(0, 0, ctx.width, ctx.height);
}
/**
* Draws the background grid.
*/
private static void drawGrid(Graphics2D g2d, CaptchaContext ctx) {
for (int y = 2; y < ctx.height; y += ctx.gridSize) {
g2d.drawLine(0, y, ctx.width - 1, y);
}
for (int x = 2; x < ctx.width; x += ctx.gridSize) {
g2d.drawLine(x, 0, x, ctx.height - 1);
}
}
/**
* Draws a single character.
*
* @param g2d {@link Graphics2D} context
* @param ch character to draw
* @param x left x position of the character
* @param boxWidth width of the box
*/
private static void drawCharacter(Graphics2D g2d, CaptchaContext ctx, char ch, int x, int boxWidth) {
ThreadLocalRandom rnd = ThreadLocalRandom.current();
double degree = (rnd.nextDouble() * ctx.rndRotationAmplitude() * 2) - ctx.rndRotationAmplitude();
double scale = 1 - (rnd.nextDouble() * ctx.rndScaleAmplitude() / 100);
Graphics2D cg2d = (Graphics2D)g2d.create();
cg2d.setFont(ctx.font.deriveFont(ctx.rndFontSize()));
cg2d.translate(x + (boxWidth / 2), ctx.height / 2);
cg2d.rotate(degree * Math.PI / 90);
cg2d.scale(scale, scale);
FontMetrics fm = cg2d.getFontMetrics();
int charWidth = fm.charWidth(ch);
int charHeight = fm.getAscent() + fm.getDescent();
cg2d.drawString(String.valueOf(ch), -(charWidth / 2), fm.getAscent() - (charHeight / 2));
cg2d.dispose();
}
}
package com.pica.cloud.account.account.server.util.captcha;
import org.springframework.stereotype.Component;
import java.awt.*;
import java.io.IOException;
/**
* @author Laurence Cao
*
*/
@Component
public class NumberLetterGenerator extends CaptchaGenerator {
public NumberLetterGenerator() throws FontFormatException, IOException {
super(true);
}
@Override
public String generateChars(int size) {
return CaptchaUtil.generateUUIDText(size);
}
}
Markdown 格式
0% or
您添加了 0 到此讨论。请谨慎行事。
先完成此消息的编辑!
想要评论请 注册