本次写这篇文章是系统中有几处登录时没有添加验证码校验,导致安全等保时扫描出高危漏洞。尽管接口已经做了限制登录(5次密码错误,锁定账号)。既然扫出了高危,那也是要想办法解决,这里选择在登录时添加后端验证码校验,这样就可以防止登录时暴力破解
验证码的作用
验证码是为了区分计算机程序(如自动化机器人、脚本等)和真实的人类用户。其目的是保护网站和服务免受自动化攻击和滥用,确保交互是由真正的用户发起的。
-
1.防止自动化攻击:
- 防止垃圾邮件: 验证码可以阻止自动化程序(如垃圾邮件机器人)发送大量垃圾邮件。
- 防止恶意注册: 通过验证码,可以防止自动化程序批量注册虚假账户。
- 防止恶意登录: 验证码可以增加暴力破解密码的难度,防止自动化程序尝试大量密码组合。
-
2.保护用户隐私:
- 防止数据抓取: 验证码可以防止自动化程序抓取网站上的敏感数据。
- 防止恶意爬虫: 验证码可以阻止恶意爬虫抓取网站内容,保护网站的知识产权。
-
3.提高安全性:
- 防止DDoS攻击: 验证码可以增加攻击者发起分布式拒绝服务(DDoS)攻击的难度。
- 防止自动化脚本滥用: 验证码可以防止自动化脚本滥用网站功能,如自动发布评论、点赞等。
虽然验证码可能会给用户带来一些不便,但它提供了一种简单且有效的方式来验证用户身份
使用
这里借鉴若依的验证码,若依的验证码是比较简单且好用的一个功能,而且基本上所有项目都能很容易引入
1. 引入pom依赖
<!-- 验证码 -->
<dependency>
<groupId>pro.fessional</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.3</version>
<exclusions>
<exclusion>
<artifactId>servlet-api</artifactId>
<groupId>javax.servlet</groupId>
</exclusion>
</exclusions>
</dependency>
2. 导入配置类
package com.sun3d.why.config;
import java.util.Random;
import com.google.code.kaptcha.text.impl.DefaultTextCreator;
/**
* 验证码文本生成器
*
* @author shangtf
*/
public class KaptchaTextCreator extends DefaultTextCreator
{
private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(",");
@Override
public String getText()
{
Integer result = 0;
Random random = new Random();
int x = random.nextInt(10);
int y = random.nextInt(10);
StringBuilder suChinese = new StringBuilder();
int randomoperands = random.nextInt(3);
if (randomoperands == 0)
{
result = x * y;
suChinese.append(CNUMBERS[x]);
suChinese.append("*");
suChinese.append(CNUMBERS[y]);
}
else if (randomoperands == 1)
{
if ((x != 0) && y % x == 0)
{
result = y / x;
suChinese.append(CNUMBERS[y]);
suChinese.append("/");
suChinese.append(CNUMBERS[x]);
}
else
{
result = x + y;
suChinese.append(CNUMBERS[x]);
suChinese.append("+");
suChinese.append(CNUMBERS[y]);
}
}
else
{
if (x >= y)
{
result = x - y;
suChinese.append(CNUMBERS[x]);
suChinese.append("-");
suChinese.append(CNUMBERS[y]);
}
else
{
result = y - x;
suChinese.append(CNUMBERS[y]);
suChinese.append("-");
suChinese.append(CNUMBERS[x]);
}
}
suChinese.append("=?@" + result);
return suChinese.toString();
}
}
package com.sun3d.why.config;
import java.util.Properties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import static com.google.code.kaptcha.Constants.*;
/**
* 验证码配置
*
* @author shangtf
*/
@Configuration
public class CaptchaConfig
{
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean()
{
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "38");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCode");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
@Bean(name = "captchaProducerMath")
public DefaultKaptcha getKaptchaBeanMath()
{
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty(KAPTCHA_BORDER, "yes");
// 边框颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_BORDER_COLOR, "105,179,90");
// 验证码文本字符颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue");
// 验证码图片宽度 默认为200
properties.setProperty(KAPTCHA_IMAGE_WIDTH, "160");
// 验证码图片高度 默认为50
properties.setProperty(KAPTCHA_IMAGE_HEIGHT, "60");
// 验证码文本字符大小 默认为40
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_SIZE, "35");
// KAPTCHA_SESSION_KEY
properties.setProperty(KAPTCHA_SESSION_CONFIG_KEY, "kaptchaCodeMath");
// 验证码文本生成器
properties.setProperty(KAPTCHA_TEXTPRODUCER_IMPL, "com.sun3d.why.config.KaptchaTextCreator");
// 验证码文本字符间距 默认为2
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_SPACE, "3");
// 验证码文本字符长度 默认为5
properties.setProperty(KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "6");
// 验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty(KAPTCHA_TEXTPRODUCER_FONT_NAMES, "Arial,Courier");
// 验证码噪点颜色 默认为Color.BLACK
properties.setProperty(KAPTCHA_NOISE_COLOR, "white");
// 干扰实现类
properties.setProperty(KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
// 图片样式 水纹com.google.code.kaptcha.impl.WaterRipple 鱼眼com.google.code.kaptcha.impl.FishEyeGimpy 阴影com.google.code.kaptcha.impl.ShadowGimpy
properties.setProperty(KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
注意: 在这个配置类中需要在验证码文本生成器中指定上面那个文本生成器类的路径!!!
3. 业务中使用
3.1 引入bean
将上述两个bean引入,一个是算术验证码,一个图形验证码
import com.google.code.kaptcha.Producer;
@Autowired
@Qualifier("captchaProducer")
private Producer captchaProducer;
@Autowired
@Qualifier("captchaProducerMath")
private Producer captchaProducerMath;
3.2 获取验证码接口
import javax.imageio.ImageIO;
import com.sun3d.why.util.Base64;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
/**
* 生成验证码
*/
@RequestMapping("/captchaImage")
@ResponseBody
public Map getCode() throws IOException
{
Map result = new HashMap();
// 保存验证码信息
String uuid = UUIDUtils.createUUId();
String verifyKey = "captcha_codes:" + uuid;
String capStr = null, code = null;
BufferedImage image = null;
// 此值可以配置为math或char
String captchaType = "char";
if ("math".equals(captchaType))
{
// 生成验证码
String capText = captchaProducerMath.createText();
capStr = capText.substring(0, capText.lastIndexOf("@"));
code = capText.substring(capText.lastIndexOf("@") + 1);
image = captchaProducerMath.createImage(capStr);
}
else if ("char".equals(captchaType))
{
capStr = code = captchaProducer.createText();
image = captchaProducer.createImage(capStr);
}
ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
opsForValue.set(verifyKey, code, 2, TimeUnit.MINUTES);
// 转换流信息写出
ByteArrayOutputStream os = new ByteArrayOutputStream();
try
{
ImageIO.write(image, "jpg", os);
}
catch (IOException e)
{
result.put("code", "fail");
return result;
}
result.put("code", "success");
result.put("uuid", uuid);
result.put("img", Base64.encode(os.toByteArray()));
return result;
}
1. 这里将验证码的结果存入到了redis中,过期时间为2分钟。并将uuid,以及图片返回给了前端。
2. 切换 captchaType 的值可以改变验证码的类型,将此值配置到配置文件中,可灵活改变验证码类型
上面使用ByteArrayOutputStream 类来输出图片,若依中使用的效率更好的FastByteArrayOutputStream类,但是由于我的项目过于老旧,就没有使用这个
FastByteArrayOutputStream类是始于springframework框架4.2版本之后的,很明显我的项目不允许使用。
3.3 校验验证码
在添加验证码的业务中校验验证码,先校验验证码,如果验证码校验通过才去执行后面的逻辑,校验失败直接返回,不在执行逻辑
/**
* 校验验证码
*
* @param code 验证码
* @param uuid 唯一标识
* @return 结果
*/
public boolean validateCaptcha(String code, String uuid) {
if (StringUtils.isEmpty(uuid)) {
uuid = "";
}
String verifyKey = "captcha_codes:" + uuid;
ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
String captcha = opsForValue.get(verifyKey);
// 删除验证码
redisTemplate.delete(verifyKey);
if (captcha == null) {
return false;
}
if (!code.equalsIgnoreCase(captcha)) {
return false;
}
return true;
}
这里使用过之后做了删除redis的操作很重要,可见若依想的还是比较全面!
使用上面方法校验
// 登录方法
public Map terminalLogin(CmsTerminalUser user,String code,String uuid) {
Map<String, Object> map = new HashMap<String, Object>();
boolean flag = validateCaptcha(code, uuid);
if (!flag){
map.put("status", RESULT_STR_FAILURE);
map.put("msg", "验证码错误");
return map;
}
//...登录逻辑......
}
3.4 前端使用时注意
前端使用时注意:
<img id="captchaImage" onclick="getCaptchaImage()" style="display: inline" src="">
function getCaptchaImage(){
$.ajax({
type: "POST",
url: '${path}/frontTerminalUser/captchaImage.do',
success: function (data) {
$("#captchaImage").attr("src","data:image/gif;base64,"+data.img);
$("#uuid").val(data.uuid);
},
error: function () {
}
});
}
这里注意src的值拼接时,需要拼接
"data:image/gif;base64,"
字符串。上面是jsp页面的写法,如果是vue按照vue的方式给img标签赋值方式即可!
注意:在提交登录接口时要将刚才请求验证码时返回的uuid以及用户输入的结果传给后端。
˚‧º·(˚ ˃̣̣̥᷄⌓˂̣̣̥᷅ )‧º·˚ 唉,使用若依框架的人真幸福