图形验证码使用:
- 用户名密码登录
防止用户重复输入用户名密码强行破解登录 - 短信发送
某些时候短信API的限量是无效的,此时需要在短信发送接口前进行图形验证码校验,防止短信盗刷(APP模块开发) - 类似于12306,利用图形验证码限流
此处图形验证码按照视频教程做,笔记不完整,因为后面开发到APP模块之后会对图形验证码进行重构(不使用session,图形验证码放入Redis缓存,APP场景适用于当前浏览器模式的开发)
验证码类关系图:
ImageCode.java:
package com.cong.security.core.code.image;
import com.cong.security.core.code.ValidateCode;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
public class ImageCode extends ValidateCode {
private BufferedImage BufferImage;
/*过期时间*/
private LocalDateTime expireTime;
/**
* 设置在多少秒之后过期
* @param bufferImage
* 图片流
* @param code
* 验证码
* @param expireIn
* 秒数
*/
public ImageCode(BufferedImage bufferImage, String code, int expireIn) {
super(code);
BufferImage = bufferImage;
// 当前时间+过期时间长度(使用缓存之后此配置即不需要)
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
public BufferedImage getBufferImage() {
return BufferImage;
}
public void setBufferImage(BufferedImage bufferImage) {
BufferImage = bufferImage;
}
public boolean isExpried() {
return LocalDateTime.now().isAfter(expireTime);
}
}
图形验证码后续会进行修改,不会存储在session中。
ValidateCode只有一个code属性(存储验证码实际内容)。
图形验证码生成实现类ImageCodeGenerator:
代码有点长,当做一个图形验证码生成工具类即可。
package com.cong.security.core.code.image;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.util.Random;
import com.cong.security.core.code.ValidateCodeGenerator;
import com.cong.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component("imageCodeGenerator")
public class ImageCodeGenerator implements ValidateCodeGenerator {
private static char code[] = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789".toCharArray();
@Autowired
private SecurityProperties securityProperties;
@Override
public ImageCode generate() {
// 在内存中创建图象
BufferedImage image = new BufferedImage(securityProperties.getCode().getImage().getWidth(),
securityProperties.getCode().getImage().getHeight(), BufferedImage.TYPE_INT_RGB);
// 获取图形上下文
Graphics g = image.getGraphics();
// 生成随机类
Random random = new Random();
// 设定背景色
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, securityProperties.getCode().getImage().getWidth(),
securityProperties.getCode().getImage().getHeight());
// 设定字体
g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
// 设置颜色
g.setColor(getRandColor(160, 200));
// 随机产生100条干扰线,使图象中的认证码不易被其它程序探测到
for (int i = 0; i < 100; i++) {
int x = random.nextInt(securityProperties.getCode().getImage().getWidth());
int y = random.nextInt(securityProperties.getCode().getImage().getHeight());
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
// 取随机产生的认证码(4位数字)
String sRand = "";
for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
String rand = String.valueOf(code[random.nextInt(code.length)]);
sRand += rand;
// 将认证码显示到图象中
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
g.drawString(rand, 13 * i + random.nextInt(6), random.nextInt(7) + 15);
}
// shear(g, securityProperties.getCode().getImage().getWidth(),
// securityProperties.getCode().getImage().getHeight(), getRandColor(200, 200));// 使图片扭曲
// 赋值验证码
// 图象生效
g.dispose();
ImageCode imageCode = new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());
return imageCode;
}
/*
* 给定范围获得随机颜色
*/
private static 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);
}
/**
* 使图形扭曲
*
* @param g
* @param w1
* @param h1
* @param color
* @author single-聪
* @date 2019年11月12日
* @version 1.0.1
*/
private static void shear(Graphics g, int w1, int h1, Color color) {
shearX(g, w1, h1, color);
shearY(g, w1, h1, color);
}
/**
* 扭曲
*
* @param g
* @param w1
* @param h1
* @param color
* @author single-聪
* @date 2019年11月12日
* @version 1.0.1
*/
private static void shearX(Graphics g, int w1, int h1, Color color) {
Random random = new Random();
int period = random.nextInt(2);
boolean borderGap = true;
int frames = 1;
int phase = random.nextInt(2);
for (int i = 0; i < h1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
g.copyArea(0, i, w1, 1, (int) d, 0);
if (borderGap) {
g.setColor(color);
g.drawLine((int) d, i, 0, i);
g.drawLine((int) d + w1, i, w1, i);
}
}
}
/**
* 扭曲
*
* @param g
* @param w1
* @param h1
* @param color
* @author single-聪
* @date 2019年11月12日
* @version 1.0.1
*/
private static void shearY(Graphics g, int w1, int h1, Color color) {
Random random = new Random();
int period = random.nextInt(40) + 10; // 50;
boolean borderGap = true;
int frames = 20;
int phase = 7;
for (int i = 0; i < w1; i++) {
double d = (double) (period >> 1)
* Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
g.copyArea(i, 0, 1, h1, 0, (int) d);
if (borderGap) {
g.setColor(color);
g.drawLine(i, (int) d, i, 0);
g.drawLine(i, (int) d + h1, i, h1);
}
}
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
短信验证码生成实现类SmsCodeGenerator:
package com.cong.security.core.code.sms;
import com.cong.security.core.code.ValidateCode;
import com.cong.security.core.code.ValidateCodeGenerator;
import com.cong.security.core.properties.SecurityProperties;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component("smsCodeGenerator")
public class SmsCodeGenerator implements ValidateCodeGenerator {
@Autowired
private SecurityProperties securityProperties;
@Override
public ValidateCode generate() {
// 随机数字
String code = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());
ValidateCode validateCode = new ValidateCode(code);
return validateCode;
}
}
只是生成一个配置文件中指定长度的数字,过期时间等的设置需要在短信发送接口中设置(需要调用第三方短信接口,存在发送失败情况)
验证码调用接口CodeController(目前仅写图形验证码接口,后续在此控制器类中添加短信验证码接口):
package com.cong.security.core.code;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.cong.security.core.code.image.ImageCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
/**
* 验证码接口,图形验证码,短信验证码之类
*
* @author single-聪
*
*/
@RestController
@RequestMapping("code")
public class CodeController {
public static String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
// 生成验证码接口
@Autowired
private ValidateCodeGenerator imageCodeGenerator;
// 短信验证码接口
@Autowired
private ValidateCodeGenerator smsCodeGenerator;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* @Description 图形验证码生成
* @Param [request, response]
* @Author single-聪
* @Date 20:12 2020/1/10
* @Version 1.0.1
* @return void
**/
@RequestMapping(value = "image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 强转成imageCode
ImageCode imageCode = (ImageCode) imageCodeGenerator.generate();
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
// 图片写入IO响应中
ImageIO.write(imageCode.getBufferImage(), "JPEG", response.getOutputStream());
}
}
在安全配置中将该接口权限放开,允许所有人访问。
此处因为后续还会在此类中添加短信接口,所以放开以/code
开始的所有接口,需要相应权限才能访问的接口应避免以/code
开头。
编写HTML页面调用:
<table>
<tr>
<td>用户名</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>
图形验证码
</td>
<td>
<input type="text" name="imageCode"/>
<img src="/code/image">
</td>
</tr>
<tr>
<td>
<button type="submit">登录</button>
</td>
</tr>
</table>
运行项目即可看到图形验证码
开始使用验证码。
图形验证码过滤器ValidateCodeFilter.java:
package com.cong.security.core.code;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.cong.security.core.code.image.ImageCode;
import com.cong.security.core.properties.SecurityProperties;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import lombok.extern.slf4j.Slf4j;
/**
* 继承OncePerRequestFilter,保证该过滤器只会被调用一次
*
* @author single-聪
*
*/
@Slf4j
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
/* 失败处理器,成功直接进入下一过滤器,所以不需要成功处理器 */
private AuthenticationFailureHandler myAuthenticationFailureHandler;
/* 存放需要拦截的Url */
private Set<String> urls = new HashSet<>();
private SecurityProperties securityProperties;
public void setMyAuthenticationFailureHandler(AuthenticationFailureHandler myAuthenticationFailureHandler) {
this.myAuthenticationFailureHandler = myAuthenticationFailureHandler;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
log.info("配置图形验证码拦截路径[{}] ", securityProperties.getCode().getImage().getUrl());
// 放入配置文件中配置
String[] configUrls = StringUtils
.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(), ",");
for (String string : configUrls) {
urls.add(string);
}
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 登录请求才起作用且必须是post请求
log.info("图形验证码请求路径[{}] ", request.getRequestURI());
// 判断用户登录请求是否需要先进行验证码校验
boolean match = false;
// 循环判断当前请求是否需要进行图形验证码校验
for (String url : urls) {
if (antPathMatcher.match(url, request.getRequestURI())) {
match = true;
}
}
// 如果需要进行验证码校验
if (match) {
try {
log.info("开始校验图形验证码");
validate(new ServletWebRequest(request));
} catch (CodeException e) {
// 图形验证码校验出现问题,走失败处理器(验证码成功是敲门砖,验证码失败不会走用户名密码登录,一方面降低数据库请求次数,一方面防止别人拿到登陆请求恶意攻击,造成数据库压力过大)
myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
// 获取图形验证码信息,APP中的验证码值会从缓存中获取,完全抛弃session
ImageCode imageCode = (ImageCode) sessionStrategy.getAttribute(request, CodeController.SESSION_KEY);
//
String code = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
log.info("imageCode值为[{}] ", imageCode);
// 用户未输入值
if (StringUtils.isBlank(code)) {
throw new CodeException("图形验证码的值不能为空");
}
// session中不存在数据
if (imageCode == null) {
throw new CodeException("图形验证码不存在");
}
// 验证码超时过期,缓存中不存在也是过期
if (imageCode.isExpried()) {
sessionStrategy.removeAttribute(request, CodeController.SESSION_KEY);
throw new CodeException("图形验证码已过期");
}
// 验证码是否匹配
if (!StringUtils.equalsIgnoreCase(imageCode.getCode(), code)) {
throw new CodeException("图形验证码不匹配");
}
// 验证成功,将验证码从session中移除
sessionStrategy.removeAttribute(request, CodeController.SESSION_KEY);
}
}
将ValidateCodeFilter放入过滤器链中,并且在UsernamePasswordAuthenticationFilter
之后。
配置拦截请求:my.security.code.image.url:/login
,以,分割,
即可拦截多个请求
此时调用登录页面输入错误的用户名密码,错误的图形验证码,返回值为图形验证码比配错误,并没有进入用户名密码校验模块,目的达成。