一、背景
在实现登录功能时,为了防止特定的程序暴力破解,一般为了安全都会在用户登录时增加otp动态验证码录。otp验证码 otp全称叫One-time Password,也称动态口令,是指计算机系统或其他数字设备上只能使用一次的密码,有效期为只有一次登录会话或很短。
常见验证码分为图片验证码和短信验证码,还有滑动窗口模块和选中指定物体验证方式。下面通过Java来实现图片验证码示例,效果如下:
二、实现步骤
1、maven中加入依赖
pom.xml引入依赖:
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
2、CaptchaController.java
/**
* 验证码
*/
@GetMapping("/captcha/digit")
@ApiOperation(value = "获取数字验证码", notes = "获取数字验证码", tags = "验证码相关")
@ApiImplicitParams({
@ApiImplicitParam(name = "uuid", value = "uuid", required = true, paramType = "query")
})
@PassToken
public void captcha(HttpServletResponse response, String uuid) throws IOException {
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");
log.info("获取验证码,uuid:{}", uuid);
//获取图片验证码
BufferedImage image = captchaService.getCaptcha(uuid);
log.info("获取验证码,uuid:{},return:{}", uuid, JSON.toJSONString(image));
ServletOutputStream out = response.getOutputStream();
ImageIO.write(image, "jpg", out);
IOUtils.closeQuietly(out);
}
@GetMapping("/captcha/graphics")
@ApiOperation(value = "获取图形验证码", notes = "获取图形验证码", tags = "验证码相关")
@ApiImplicitParams({
@ApiImplicitParam(name = "uuid", value = "uuid", required = true, paramType = "query"),
@ApiImplicitParam(name = "type", value = "类型 png:png gif:gif cn:中文 cngif:中文gif arithmeti:算术", required = false, paramType = "query")
})
public void captcha(HttpServletRequest request, HttpServletResponse response,
@RequestParam String uuid,
@RequestParam(defaultValue = "arithmeti", required = false) String type) throws Exception {
// 设置请求头为输出图片类型
response.setContentType("image/gif");
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
Captcha captcha = null;
switch (type) {
case "png":
captcha = new SpecCaptcha(130, 48);
break;
case "gif":
// gif类型
captcha = new GifCaptcha(130, 48);
break;
case "cn":
// 中文类型
captcha = new ChineseCaptcha(130, 48, 5, new Font("楷体", Font.PLAIN, 28));
break;
case "cngif":
// 中文gif类型
captcha = new ChineseGifCaptcha(130, 48, 5, new Font("楷体", Font.PLAIN, 28));
break;
case "arithmeti":
// 算术类型
ArithmeticCaptcha arithmeticCaptcha = new ArithmeticCaptcha(130, 48);
arithmeticCaptcha.setLen(3); // 几位数运算,默认是两位
arithmeticCaptcha.getArithmeticString(); // 获取运算的公式:3+2=?
arithmeticCaptcha.text(); // 获取运算的结果:5
captcha = arithmeticCaptcha;
break;
default:
new SpecCaptcha(130, 48);
break;
}
log.info("验证码:{}", captcha.text());
// 设置字体
// captcha.setFont(new Font("Verdana", Font.PLAIN, 32)); // 有默认字体,可以不用设置
// 设置类型,纯数字、纯字母、字母数字混合
captcha.setCharType(Captcha.TYPE_DEFAULT);
//缓存验证码
redisService.set(AuthKeys.AUTH_CAPTCHA, uuid, captcha.text().toLowerCase());
// 输出图片流
captcha.out(response.getOutputStream());
}
}
3、生成验证码配置
/**
* 生成验证码配置
*
*/
@Configuration
public class KaptchaConfig {
@Bean
public DefaultKaptcha producer() {
Properties properties = new Properties();
//图片边框
properties.setProperty("kaptcha.border", "no");
//文本集合,验证码值从此集合中获取
properties.setProperty("kaptcha.textproducer.char.string", "ABCDEGHJKLMNRSTUWXY23456789");
//字体颜色
properties.setProperty("kaptcha.textproducer.font.color", "0,84,144");
//干扰颜色
properties.setProperty("kaptcha.noise.color", "0,84,144");
//字体大小
properties.setProperty("kaptcha.textproducer.font.size", "30");
//背景颜色渐变,开始颜色
properties.setProperty("kaptcha.background.clear.from", "247,255,234");
//背景颜色渐变,结束颜色
properties.setProperty("kaptcha.background.clear.to", "247,255,234");
//图片宽
properties.setProperty("kaptcha.image.width", "125");
//图片高
properties.setProperty("kaptcha.image.height", "35");
properties.setProperty("kaptcha.session.key", "code");
//验证码长度
properties.setProperty("kaptcha.textproducer.char.length", "4");
//字体
properties.setProperty("kaptcha.textproducer.font.names", "Arial,Courier,cmr10,宋体,楷体,微软雅黑");
properties.put("kaptcha.textproducer.char.space", "5");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
4、CaptchaService.java接口
public interface CaptchaService {
boolean validate(String uuid, String code);
BufferedImage getCaptcha(String uuid);
}
5、CaptchaServiceImpl.java实现类
@Service
@Slf4j
public class CaptchaServiceImpl implements CaptchaService {
@Autowired
private Producer producer;
@Autowired
private RedisService redisService;
@Value("${default-captcha}")
private String defaultCaptcha;
/**
* 生成并缓存验证码,返给前端图片
*/
@Override
public BufferedImage getCaptcha(String uuid) {
if (StringUtils.isEmpty(uuid)) {
throw new GlobalException(BasicCodeMsg.PARAMETER_ERROR.setMsg("uuid不能为空"));
}
//生成文字验证码
String code = producer.createText();
log.info("uuid:{},验证码:{}",uuid,code);
//缓存验证码
redisService.set(AuthKeys.AUTH_CAPTCHA, uuid, code);
return producer.createImage(code);
}
}
/**
* 校验验证码
*/
@Override
public boolean validate(String uuid, String code) {
//测试环境123456通过验证(可不加)
if (EnvEnum.dev.name().equals(env) && code.equals(defaultCaptcha)) {
return true;
}
String cacheCode = redisService.get(AuthKeys.AUTH_CAPTCHA, uuid, String.class);
if (StringUtils.isEmpty(cacheCode)) {
return false;
}
//删除缓存验证码
redisService.delete(AuthKeys.AUTH_CAPTCHA, uuid);
if (cacheCode.equalsIgnoreCase(code)) {
return true;
}
return false;
}
6、增加验证码校验
在登录授权验证的地方添加验证码相关校验,也就是原来校验用户名密码的地方增加。
if ("captcha".equals(type)) {
LoginVo loginVo = LoginVo.builder().captcha(captcha)
.loginName(username)
.uuid(uuid).build();
boolean result = captchaService.validate(uuid, captcha);
if (!result) {
throw new OAuth2Exception("验证码不正确");
}
return;