Commit 2704d43f by 刘红梅

滑动验证码功能

parent 6d117c30
package com.cnooc.expert.auth.service;
import com.cnooc.expert.system.entity.pojo.SlideImageResult;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.concurrent.ThreadLocalRandom;
@Service
public class SlideCaptchaService {
private static final int SLIDER_WIDTH = 50;
private static final int SLIDER_HEIGHT = 50;
@Value("classpath:images/backgrounds/*")
private Resource[] backgroundResources;
/**
* 使用预设背景图片生成验证码
*/
public SlideImageResult generateSlideImages() throws IOException {
// 随机选择背景图片
Resource bgResource = backgroundResources[
ThreadLocalRandom.current().nextInt(backgroundResources.length)
];
BufferedImage background = ImageIO.read(bgResource.getInputStream());
// 调整背景图尺寸
background = resizeImage(background, 300, 200);
// 生成滑块位置
int sliderX = ThreadLocalRandom.current().nextInt(50, 250);
int sliderY = ThreadLocalRandom.current().nextInt(10, 150);
// 创建滑块
BufferedImage sliderImage = createSliderWithShadow(background, sliderX, sliderY);
drawSliderHole(background, sliderX, sliderY);
// 转换为Base64
String bgBase64 = imageToBase64(background, "png");
String sliderBase64 = imageToBase64(sliderImage, "png");
SlideImageResult result = new SlideImageResult();
result.setBackgroundBase64(bgBase64);
result.setSlideBase64(sliderBase64);
result.setSliderX(sliderX);
result.setSliderY(sliderY);
return result;
}
/**
* 图片转换为Base64
*/
private String imageToBase64(BufferedImage image, String format) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(image, format, baos);
byte[] imageBytes = baos.toByteArray();
return Base64.getEncoder().encodeToString(imageBytes);
}
/**
* 在背景图上绘制滑块挖空效果
*/
private void drawSliderHole(BufferedImage background, int x, int y) {
Graphics2D g2d = background.createGraphics();
// 设置挖空区域
g2d.setComposite(AlphaComposite.Clear);
g2d.fillRect(x, y, SLIDER_WIDTH, SLIDER_HEIGHT);
// 重置合成规则,绘制边框
g2d.setComposite(AlphaComposite.SrcOver);
g2d.setColor(Color.GRAY);
g2d.setStroke(new BasicStroke(2));
g2d.drawRect(x, y, SLIDER_WIDTH, SLIDER_HEIGHT);
g2d.dispose();
}
/**
* 创建带阴影效果的滑块
*/
private BufferedImage createSliderWithShadow(BufferedImage background, int x, int y) {
int width = 50;
int height = 50;
int shadowSize = 3;
BufferedImage slider = new BufferedImage(
width + shadowSize, height + shadowSize, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2d = slider.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 绘制阴影
g2d.setColor(new Color(0, 0, 0, 100));
g2d.fillRoundRect(shadowSize, shadowSize, width, height, 10, 10);
// 绘制滑块主体
Shape sliderShape = new RoundRectangle2D.Double(0, 0, width, height, 10, 10);
g2d.setClip(sliderShape);
BufferedImage subImage = background.getSubimage(x, y, width, height);
g2d.drawImage(subImage, 0, 0, null);
g2d.setClip(null);
// 绘制边框
g2d.setColor(new Color(64, 158, 255));
g2d.setStroke(new BasicStroke(2));
g2d.draw(sliderShape);
g2d.dispose();
return slider;
}
/**
* 调整图片尺寸
*/
private BufferedImage resizeImage(BufferedImage originalImage, int targetWidth, int targetHeight) {
BufferedImage resizedImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = resizedImage.createGraphics();
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.drawImage(originalImage, 0, 0, targetWidth, targetHeight, null);
g2d.dispose();
return resizedImage;
}
}
package com.cnooc.expert.auth.service;
import com.cnooc.expert.controller.auth.model.response.SlideCaptchaVO;
import com.cnooc.expert.system.entity.vo.SysCaptchaVO;
public interface SysCaptchaService {
......@@ -9,6 +10,7 @@ public interface SysCaptchaService {
*/
SysCaptchaVO generate();
SlideCaptchaVO generateSlide();
/**
* 验证码效验
*
......@@ -18,6 +20,8 @@ public interface SysCaptchaService {
*/
boolean validate(String key, String code);
boolean validateSlide(String code, Integer moveX);
/**
* 是否开启登录验证码
*
......
......@@ -26,6 +26,7 @@ import org.springframework.stereotype.Service;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
......@@ -173,7 +174,9 @@ public class LoginServiceImpl implements LoginService {
boolean isAccountLocked = accountLockService.isAccountLocked(expertInfoResp.getZhuanJiaGuid());
if(isAccountLocked){
//如果账号锁定了,返回错误信息
throw new BusinessException(GlobalErrorCodeConstants.USER_LOCKED.getCode(),GlobalErrorCodeConstants.USER_LOCKED.getMsg());
Map<String, Object> errorData = new HashMap<>();
errorData.put("dueDate", accountLockService.getLockRemainingTime(expertInfoResp.getZhuanJiaGuid()));
throw new BusinessException(GlobalErrorCodeConstants.USER_LOCKED.getCode(),GlobalErrorCodeConstants.USER_LOCKED.getMsg(), "user_login", errorData);
}
ExpertInfoAppResp expertInfoAppResp = loginServicesClient.getZhuanJiaInfoAppById(expertInfoResp.getZhuanJiaGuid());
if(expertInfoAppResp == null){
......@@ -228,7 +231,9 @@ public class LoginServiceImpl implements LoginService {
boolean isAccountLocked = accountLockService.isAccountLocked(expertInfoResp.getZhuanJiaGuid());
if(isAccountLocked){
//如果账号锁定了,返回错误信息
throw new BusinessException(GlobalErrorCodeConstants.USER_LOCKED.getCode(),GlobalErrorCodeConstants.USER_LOCKED.getMsg());
Map<String, Object> errorData = new HashMap<>();
errorData.put("dueDate", accountLockService.getLockRemainingTime(expertInfoResp.getZhuanJiaGuid()));
throw new BusinessException(GlobalErrorCodeConstants.USER_LOCKED.getCode(),GlobalErrorCodeConstants.USER_LOCKED.getMsg(), "user_login", errorData);
}
//2.存在校验验证码
if (!smsService.verifySmsCode(loginVO.getPhoneNumber(), loginVO.getPhoneCode())) {
......
......@@ -2,15 +2,19 @@ package com.cnooc.expert.auth.service.impl;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.cnooc.expert.common.cache.RedisKeys;
import com.cnooc.expert.auth.service.SysCaptchaService;
import lombok.AllArgsConstructor;
import com.cnooc.expert.auth.service.*;
import com.cnooc.expert.controller.auth.model.response.SlideCaptchaVO;
import com.cnooc.expert.system.entity.pojo.CaptchaData;
import com.cnooc.expert.system.entity.pojo.SlideImageResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import com.cnooc.expert.system.entity.vo.SysCaptchaVO;
import com.wf.captcha.SpecCaptcha;
import com.wf.captcha.base.Captcha;
import org.apache.commons.lang3.StringUtils;
import java.util.concurrent.TimeUnit;
......@@ -19,6 +23,9 @@ public class SysCaptchaServiceImpl implements SysCaptchaService {
private static final int EXPIRE_MINUTES = 5;
@Autowired
private SlideCaptchaService slideCaptchaService;
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public SysCaptchaVO generate() {
......@@ -44,6 +51,43 @@ public class SysCaptchaServiceImpl implements SysCaptchaService {
}
@Override
public SlideCaptchaVO generateSlide(){
try {
// 1. 生成背景图和滑块图
SlideImageResult imageResult = slideCaptchaService.generateSlideImages();
// 2. 生成随机位置
// int x = ThreadLocalRandom.current().nextInt(50, 250);
// int y = ThreadLocalRandom.current().nextInt(10, 150);
int x = imageResult.getSliderX();
int y = imageResult.getSliderY();
// 3. 生成唯一标识
String token = UUID.randomUUID().toString();
// 4. 存储验证数据到Redis (5分钟过期)
CaptchaData captchaData = new CaptchaData(x, y, System.currentTimeMillis());
redisTemplate.opsForValue().set(
"captcha:" + token,
JSON.toJSONString(captchaData),
5, TimeUnit.MINUTES
);
// 5. 返回前端所需数据
SlideCaptchaVO vo = new SlideCaptchaVO();
vo.setToken(token);
vo.setBackgroundImage(imageResult.getBackgroundBase64());
vo.setSlideImage(imageResult.getSlideBase64());
vo.setStartY(y);
return vo;
} catch (Exception e) {
return null;
}
}
@Override
public boolean validate(String key, String code) {
// 如果关闭了验证码,则直接效验通过
if (!isCaptchaEnabled()) {
......@@ -62,6 +106,41 @@ public class SysCaptchaServiceImpl implements SysCaptchaService {
}
@Override
public boolean validateSlide(String token, Integer moveX) {
// 如果关闭了验证码,则直接效验通过
if (!isCaptchaEnabled()) {
return true;
}
if (StrUtil.isBlank(token)) {
return false;
}
// 1. 从Redis获取验证数据
String newkey = "captcha:" + token;
String dataStr = redisTemplate.opsForValue().get(newkey);
if (StringUtils.isEmpty(dataStr)) {
return false;
}
// 2. 解析数据
CaptchaData captchaData = JSON.parseObject(dataStr, CaptchaData.class);
// 3. 验证滑动距离 (允许±3像素的误差)
int expectedX = captchaData.getX();
int actualX = moveX;
if (Math.abs(actualX - expectedX) <= 3) {
// 验证成功,删除Redis中的key
redisTemplate.delete(newkey);
return true;
} else {
return false;
}
}
@Override
public boolean isCaptchaEnabled() {
return true;
}
......
......@@ -51,6 +51,10 @@ public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ApiResult<String> handleBusinessException(BusinessException e, HttpServletRequest request) {
log.error("业务异常: {},请求URL: {}", e.getMessage(), request.getRequestURI(), e);
if(e.getErrorCode()==GlobalErrorCodeConstants.USER_LOCKED.getCode()){
//这个需要特殊处理
return ApiResult.errorWithResult(GlobalErrorCodeConstants.USER_LOCKED.getCode(),GlobalErrorCodeConstants.USER_LOCKED.getMsg(),String.valueOf(e.getErrorData().get("dueDate")));
}
return ApiResult.error(e.getErrorCode(), e.getMessage());
}
......
......@@ -7,6 +7,8 @@ import com.cnooc.expert.common.response.ApiResult;
import com.cnooc.expert.auth.service.LoginService;
import com.cnooc.expert.common.utils.JwtUtils;
import com.cnooc.expert.common.utils.KafkaProducerUtil;
import com.cnooc.expert.controller.auth.model.request.VerifyRequest;
import com.cnooc.expert.controller.auth.model.response.SlideCaptchaVO;
import com.cnooc.expert.external.expert.model.response.ExpertInfoResp;
import com.cnooc.expert.system.entity.pojo.ZhuanJiaUser;
import com.cnooc.expert.system.entity.vo.LoginVO;
......@@ -146,6 +148,32 @@ public class LoginController {
return ApiResult.successWithResult(response);
}
@GetMapping("/slideCaptcha")
public ApiResult<Map<String, String>> slideCaptcha() {
SlideCaptchaVO captchaVO = sysCaptchaService.generateSlide();
// 5. 构建响应
Map<String, String> response = new HashMap<>();
response.put("backgroundImage", captchaVO.getBackgroundImage());
response.put("slideImage", captchaVO.getSlideImage());
response.put("token", captchaVO.getToken());
response.put("startY",String.valueOf(captchaVO.getStartY()));
return ApiResult.successWithResult(response);
}
@PostMapping("/verifySlideCaptcha")
public ApiResult<Map<String, Object>> verifySlideCaptcha(
@RequestBody VerifyRequest request) {
Map<String, Object> result = new HashMap<>();
boolean flag = sysCaptchaService.validateSlide(request.getToken(),request.getMoveX());
String msg = "验证成功";
if (!flag) {
msg = "验证失败";
}
result.put("success", flag);
result.put("message", msg);
return ApiResult.successWithResult(result);
}
/**
* 验证图片验证码
*/
......
package com.cnooc.expert.controller.auth.model.request;
import lombok.Data;
@Data
public class VerifyRequest {
private String token;
private Integer moveX; // 滑块移动的X距离
}
package com.cnooc.expert.controller.auth.model.response;
import lombok.Data;
@Data
public class SlideCaptchaVO {
private String token;
private String backgroundImage; // base64格式
private String slideImage; // base64格式
private Integer startY; // 滑块初始Y坐标
}
package com.cnooc.expert.controller.auth.model.response;
import lombok.Data;
@Data
public class VerifyResult {
private Boolean success;
private String message;
public VerifyResult(Boolean success, String message) {
this.success = success;
this.message = message;
}
}
\ No newline at end of file
package com.cnooc.expert.system.entity.pojo;
import lombok.Data;
@Data
public class CaptchaData {
private Integer x;
private Integer y;
private Long timestamp;
public CaptchaData(Integer x, Integer y, Long timestamp) {
this.x = x;
this.y = y;
this.timestamp = timestamp;
}
}
\ No newline at end of file
package com.cnooc.expert.system.entity.pojo;
import lombok.Data;
@Data
public class SlideImageResult {
private String backgroundBase64;
private String slideBase64;
private Integer sliderX;
private Integer sliderY;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment