hey-girl东拼西凑原创文章,若有歧义可留言,若需转载需标明出处
验证码是什么,作用又是啥?
验证码(CAPTCHA)是“Completely Automated Public Turing test to tell Computers and Humans Apart”(全自动区分计算机和人类的图灵测试)的缩写,是一种区分用户是计算机还是人的公共全自动程序。(来自百度百科)
可以防止:恶意破解密码、刷票、论坛灌水,有效防止某个黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试,实际上用验证码是现在很多网站通行的方式,我们利用比较简易的方式实现了这个功能。这个问题可以由计算机生成并评判,但是必须只有人类才能解答。由于计算机无法解答CAPTCHA的问题,所以回答出问题的用户就可以被认为是人类
常见验证码有哪些形式?
- 中文字符验证码
- 英文字符加数字类型的验证码
- 加减乘除类的验证码
- 滑动的验证码
- 拼图类的验证码
不光只限于我上述说的这些。
网上验证码的方案也是层出不穷。今天我从2个方面讲述下,在springboot项目中使用验证码。
- 第一种实现英文字符加数字类型的验证码+中文字符。思路就是通过自定义验证码工具。生成验证码图片。保存验证码信息在session返回展示。
RandomValidateCodeUtil 工具类
有部分相同代码未封装,只为展示更加直观
@UtilityClass
public class RandomValidateCodeUtil {
/**
* 验证码字符集
*/
private final char[] chars = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
private String str="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
/**
* 字符数量
*/
private final int SIZE = 4;
/**
* 干扰线数量
*/
private final int LINES = 5;
/**
* 宽度
*/
private final int WIDTH = 80;
/**
* 高度
*/
private final int HEIGHT = 40;
/**
* 字体大小
*/
private final int FONT_SIZE = 30;
/**
* 生成随机验证码及图片
* Object[0]:验证码字符串;
* Object[1]:验证码图片。
*/
public Object[] createImage() {
StringBuffer sb = new StringBuffer();
// 1.创建空白图片 创建一个不带透明色的对象
BufferedImage image = new BufferedImage(
WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
// 2.获取图片画笔
Graphics graphic = image.getGraphics();
// 3.设置画笔颜色
graphic.setColor(Color.LIGHT_GRAY);
// 4.绘制矩形背景
graphic.fillRect(0, 0, WIDTH, HEIGHT);
// 5.画随机字符
Random ran = new Random();
for (int i = 0; i <SIZE; i++) {
// 取随机字符索引
int n = ran.nextInt(chars.length);
// 设置随机颜色
graphic.setColor(getRandomColor());
// 设置字体大小
graphic.setFont(new Font(
null, Font.BOLD + Font.ITALIC, FONT_SIZE));
// 画字符
graphic.drawString(
chars[n] + "", i * WIDTH / SIZE, HEIGHT*2/3);
// 记录字符
sb.append(chars[n]);
}
// 6.画干扰线
for (int i = 0; i < LINES; i++) {
// 设置随机颜色
graphic.setColor(getRandomColor());
// 随机画线
graphic.drawLine(ran.nextInt(WIDTH), ran.nextInt(HEIGHT),
ran.nextInt(WIDTH), ran.nextInt(HEIGHT));
}
// 7.返回验证码和图片
return new Object[]{sb.toString(), image};
};
/**
* 随机取色
*/
public static Color getRandomColor() {
Random ran = new Random();
Color color = new Color(ran.nextInt(256),
ran.nextInt(256), ran.nextInt(256));
return color;
}
/**
* 随机产生字母或者数字的验证码
*/
public Object[] createMumAndChar() {
// 这里用来存储
StringBuffer sb = new StringBuffer();
// 1.创建空白图片
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics graphic = image.getGraphics();
// 3.设置画笔颜色,绘制背景色
graphic.setColor(Color.LIGHT_GRAY);
// 4.绘制矩形背景
graphic.fillRect(0, 0, WIDTH, HEIGHT);
// 生成4个验证码,随机字母或者数字
Random random = new Random();
for (int i = 0; i < SIZE; i++) {
int index = random.nextInt(str.length());
char val = str.charAt(index);
graphic.setColor(getRandomColor());
graphic.setFont(new Font(
null, Font.BOLD + Font.ITALIC, FONT_SIZE));
graphic.drawString(String.valueOf(val),i * WIDTH / SIZE, HEIGHT*2/3);
sb.append(val);
}
// 6.画干扰线
for (int i = 0; i < LINES; i++) {
// 设置随机颜色
graphic.setColor(getRandomColor());
// 随机画线
graphic.drawLine(random.nextInt(WIDTH), random.nextInt(HEIGHT),
random.nextInt(WIDTH), random.nextInt(HEIGHT));
}
// 7.返回验证码和图片
return new Object[]{sb.toString(), image};
}
/**
* 生成随机字符和数字 65-90 97-122 48-57
*/
public Object[] createMumAndCharTwo() {
StringBuilder sb = new StringBuilder();
// 1.创建空白图片
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics graphic = image.getGraphics();
// 3.设置画笔颜色,绘制背景色
graphic.setColor(Color.LIGHT_GRAY);
// 4.绘制矩形背景
graphic.fillRect(0, 0, WIDTH, HEIGHT);
Random random = new Random();
long val = 0L;
for (int i = 0; i < SIZE; i++) {
int type = random.nextInt(3);
// Math.random()*25 取值0-1 也就是0-25
switch (type){
case 0:
val=Math.round(Math.random()*25+65);
break;
case 1:
val=Math.round(Math.random()*25+97);
break;
default:
val=Math.round(Math.random()*9+48);
}
graphic.setColor(getRandomColor());
graphic.setFont(new Font(
null, Font.BOLD + Font.ITALIC, FONT_SIZE));
graphic.drawString(String.valueOf((char)val),i * WIDTH / SIZE, HEIGHT*2/3);
sb.append(val);
}
// 6.画干扰线
for (int i = 0; i < LINES; i++) {
// 设置随机颜色
graphic.setColor(getRandomColor());
// 随机画线
graphic.drawLine(random.nextInt(WIDTH), random.nextInt(HEIGHT),
random.nextInt(WIDTH), random.nextInt(HEIGHT));
}
// 7.返回验证码和图片
return new Object[]{sb.toString(), image};
}
}
其实仔细看看,上面代码换汤不换药,只是说生成的验证码的内容方式稍微不同而已。核心关键就是BufferedImage这个类。
关于BufferedImage这个类,有兴趣的伙伴可以看看这位大大写的文章添加链接描述
利用BufferedImage。获取图片画笔。然后把要验证的字符画上去。在随机画写干扰的线条。
在写个CaptchaController控制层
@RestController
@RequestMapping("/captcha")
public class CaptchaController {
/**
* 通过工具类自己写验证码
*/
@GetMapping("/base")
public void getBaseCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
HttpSession session=request.getSession();
//利用图片工具生成图片
//第一个参数是生成的验证码,第二个参数是生成的图片
Object[] objs = RandomValidateCodeUtil.createMumAndCharTwo();
//将验证码存入Session
session.setAttribute("imageCode",objs[0]);
//将图片输出给浏览器
BufferedImage image = (BufferedImage) objs[1];
response.setContentType("image/png");
OutputStream os = response.getOutputStream();
ImageIO.write(image, "png", os);
}
}
这里其实也没啥重要的。就是把BufferedImage以图片的形式传递出去即可。然后把code放session中。
使用老演员postman测试。结果如下:
生成以后验证码,验证也就是在登录的时候处理。session里面取code和传入的参数对比。
上述就是一个最简单的验证码生成逻辑。
- 说完第一种,在说说第二种,利用插件easy-captcha生成验证码。并且在微服务中通过网关验证。验证码也根据随机字符或者手机号等存入redis中校验。这种也是比较适合前后端分离开发中使用的常见做法。
我这边写的微服务网关使用的gateway。
首先我们自定义验证码endPoint,方便访问。gateway基于WebFlux。所以这里使用函数式编程的写法。
具体关于WebFlux可以看看这位大大写的。
为方便理解 。我稍微截个图理解下:
首先引入插件
<dependencies>
<dependency>
<groupId>com.pig4cloud.plugin</groupId>
<artifactId>captcha-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>
</dependencies>
接着我们定义个endpoint,我们的先注册个RouterFunction到容器中管理起来。
在网关服务中配置RouterFunctionConfiguration
@Slf4j
@Configuration(proxyBeanMethods = false)
@RequiredArgsConstructor
public class RouterFunctionConfiguration {
private final ImageCodeHandler imageCodeHandler;
@Bean
public RouterFunction<ServerResponse> routerFunction() {
return RouterFunctions.route(
RequestPredicates.path("/code").and(RequestPredicates.accept(MediaType.TEXT_PLAIN)), imageCodeHandler);
}
}
上述代码就是指定path和handler.
接着写ImageCodeHandler
实现HandlerFunction
@Slf4j
@RequiredArgsConstructor
public class ImageCodeHandler implements HandlerFunction<ServerResponse> {
private static final Integer DEFAULT_IMAGE_WIDTH =100;
private static final Integer DEFAULT_IMAGE_HEIGHT =40;
private final RedisTemplate<String, Object> redisTemplate;
@Override
public Mono<ServerResponse> handle(ServerRequest serverRequest) {
// 生成计算类型验证码
ArithmeticCaptcha captcha = new ArithmeticCaptcha(DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT);
// 获取运算结果
String result = captcha.text();
// 保存验证码信息
Optional<String> randomStr = serverRequest.queryParam("randomStr");
redisTemplate.setKeySerializer(new StringRedisSerializer());
randomStr.ifPresent(s -> redisTemplate.opsForValue().set(CacheConstants.DEFAULT_CODE_KEY + s, result,
SecurityConstants.CODE_TIME, TimeUnit.SECONDS));
// 转换流信息写出
FastByteArrayOutputStream os = new FastByteArrayOutputStream();
captcha.out(os);
return ServerResponse.status(HttpStatus.OK).contentType(MediaType.IMAGE_JPEG)
.body(BodyInserters.fromResource(new ByteArrayResource(os.toByteArray())));
}
}
上述代码,首先生成一个计算类型的验证码。获取请求参数randomStr, randomStr就是随机一个字符 如果randomStr存在就加前缀为key, val为计算结果。存放redis.并设置过期时间。在返回。注意ImageCodeHandler
是需要注册的。我这里直接写配置文件了。就没展示。
这会就可以测试了
获取到验证码以后,我们考虑下个问题。验证。
前面说了我这里使用的是gateway为网关服务。所以只需写个过滤器。拦截验证就好。安全服务用的是auth2.也就是说大致流程是请求到网关,网关先check验证码。通过了才会往下走。
自定义过滤器ValidateCodeGatewayFilter
@Slf4j
@RequiredArgsConstructor
public class ValidateCodeGatewayFilter extends AbstractGatewayFilterFactory<Object> {
private final GatewayConfigProperties configProperties;
private final ObjectMapper objectMapper;
private final RedisTemplate<String, Object> redisTemplate;
/**
* GatewayFilter是一个接口
* Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
*/
@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) ->{
ServerHttpRequest request = exchange.getRequest();
// 验证是不是登录请求路径。这里登录路径为auth2 提供的/oauth/token
boolean isAuthToken = CharSequenceUtil.containsAnyIgnoreCase(request.getURI().getPath(),
SecurityConstants.OAUTH_TOKEN_URL);
// 不是登录请求,直接向下执行其他的过滤器
if (!isAuthToken) {
return chain.filter(exchange);
}
// 判断是不是需要验证 验证码的客户端
boolean isIgnoreClient = configProperties.getIgnoreClients().contains(WebUtils.getClientId(request));
try {
// only oauth and the request not in ignore clients need check code.
if (!isIgnoreClient) {
checkCode(request);
}
}
catch (Exception e) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.PRECONDITION_REQUIRED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
final String errMsg = e.getMessage();
return response.writeWith(Mono.create(monoSink -> {
try {
byte[] bytes = objectMapper.writeValueAsBytes(R.failed(errMsg));
DataBuffer dataBuffer = response.bufferFactory().wrap(bytes);
monoSink.success(dataBuffer);
}
catch (JsonProcessingException jsonProcessingException) {
log.error("对象输出异常", jsonProcessingException);
monoSink.error(jsonProcessingException);
}
}));
}
return chain.filter(exchange);
};
}
/**
* 校验code
* ServerHttpRequest 请求参数
* 获取参数的code,根据randomStr或者mobile字段,取redis存的值。并删除存的数据
* 然后作比较
*/
private void checkCode (ServerHttpRequest request) throws Exception {
String code = request.getQueryParams().getFirst("code");
if(CharSequenceUtil.isBlank(code)){
throw new ValidateCodeException("验证码不能为空");
}
String randomStr = request.getQueryParams().getFirst("randomStr");
if(CharSequenceUtil.isBlank(randomStr)){
randomStr = request.getQueryParams().getFirst("mobile");
}
String key = CacheConstants.DEFAULT_CODE_KEY + randomStr;
Object codeObj = redisTemplate.opsForValue().get(key);
redisTemplate.delete(key);
if(ObjectUtil.isEmpty(codeObj) || !code.equals(codeObj)){
throw new ValidateCodeException("验证码不合法");
}
}
}
这个过滤的代码如上。只需在config配置bean
@Bean
public ValidateCodeGatewayFilter validateCodeGatewayFilter(GatewayConfigProperties configProperties,
ObjectMapper objectMapper, RedisTemplate redisTemplate) {
return new ValidateCodeGatewayFilter(configProperties, objectMapper, redisTemplate);
}
最后一步就是在配置文件yml中加上
使用插件就是如此简单。
总结:上述2种方法是2种不同实现思路。后面会慢慢完善其他的验证码使用和大概模式。今天到此为止啦!