Browse Source

重构验证码删除文件的实现逻辑 (#479)

* 重构验证码删除文件的实现逻辑

* 移除未使用的依赖

* 微调描述信息
pull/205/MERGE
kl 1 year ago committed by GitHub
parent
commit
0a8be8ac95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 89
      server/src/main/java/cn/keking/utils/CaptchaUtil.java
  2. 26
      server/src/main/java/cn/keking/utils/DateUtils.java
  3. 90
      server/src/main/java/cn/keking/utils/RandomValidateCodeUtil.java
  4. 56
      server/src/main/java/cn/keking/utils/WebUtils.java
  5. 84
      server/src/main/java/cn/keking/web/controller/FileController.java
  6. 88
      server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java
  7. 10
      server/src/main/resources/web/main/index.ftl

89
server/src/main/java/cn/keking/utils/CaptchaUtil.java

@ -0,0 +1,89 @@
package cn.keking.utils;
import com.aspose.cad.Tuple;
import org.springframework.util.ObjectUtils;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class CaptchaUtil {
public static final String captcha_code = "captchaCode";
public static final String captcha_code_pic = "captchaCodePic";
public static final String captcha_generate_time = "captchaTime";
private static final int width = 100;// 定义图片的width
private static final int height = 30;// 定义图片的height
private static final int codeLength = 4;// 定义图片上显示验证码的个数
private static final int xx = 18;
private static final int fontHeight = 28;
private static final int codeY = 27;
private static final char[] codeSequence = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', '2', '3', '4', '5', '6', '7', '8', '9'};
/**
* 指定验证码或生成验证码
* @param captchaCode 指定验证码, 如果为 null则生成验证码否则使用指定的验证码
* @return 验证码和验证码图片
*/
public static Map<String, Object> generateCaptcha(String captchaCode) {
// 定义图像buffer
BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics gd = buffImg.getGraphics();
Random random = new Random();
// 将图像填充为白色
gd.setColor(Color.WHITE);
gd.fillRect(0, 0, width, height);
Font font = new Font("Times New Roman", Font.BOLD, fontHeight);
gd.setFont(font);
// 画边框。
gd.setColor(Color.BLACK);
gd.drawRect(0, 0, width - 1, height - 1);
// 随机产生40条干扰线,使图象中的认证码不易被其它程序探测到。
gd.setColor(Color.BLACK);
for (int i = 0; i < 30; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
gd.drawLine(x, y, x + xl, y + yl);
}
// randomCode用于保存随机产生的验证码,以便用户登录后进行验证。
int red, green, blue;
if (ObjectUtils.isEmpty(captchaCode)) {
captchaCode = generateCaptchaCode();
}
// 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。
red = random.nextInt(255);
green = random.nextInt(255);
blue = random.nextInt(255);
// 用随机产生的颜色将验证码绘制到图像中。
gd.setColor(new Color(red, green, blue));
gd.drawString(captchaCode, 18, codeY);
Map<String, Object> map = new HashMap<>();
map.put(captcha_code, captchaCode);
//存放生成的验证码BufferedImage对象
map.put(captcha_code_pic, buffImg);
return map;
}
/**
* 生成随机字符串
* @return 字符串
*/
private static String generateCaptchaCode() {
Random random = new Random();
StringBuilder randomCode = new StringBuilder();
for (int i = 0; i < codeLength; i++) {
randomCode.append(codeSequence[random.nextInt(52)]);
}
return randomCode.toString();
}
}

26
server/src/main/java/cn/keking/utils/DateUtils.java

@ -0,0 +1,26 @@
package cn.keking.utils;
import java.time.Instant;
/**
* @author kl (http://kailing.pub)
* @since 2023/8/11
*/
public class DateUtils {
/**
* 获取当前时间的秒级时间戳
* @return
*/
public static long getCurrentSecond() {
return Instant.now().getEpochSecond();
}
/**
* 计算当前时间与指定时间的秒级时间戳差值
* @param datetime 指定时间
* @return 差值
*/
public static long calculateCurrentTimeDifference(long datetime) {
return getCurrentSecond() - datetime;
}
}

90
server/src/main/java/cn/keking/utils/RandomValidateCodeUtil.java

@ -1,90 +0,0 @@
package cn.keking.utils;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class RandomValidateCodeUtil {
private static final int width = 100;// 定义图片的width
private static final int height = 30;// 定义图片的height
private static final int codeCount = 4;// 定义图片上显示验证码的个数
private static final int xx = 18;
private static final int fontHeight = 28;
private static final int codeY = 27;
private static final char[] codeSequence = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R','T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'a','b','c','d','e','f','g','h','j','k','m','n','p','q','r','s','t','u','v','w','x','y', '2', '3', '4','5', '6', '7', '8', '9' };
/**
* 生成一个map集合
* code为生成的验证码
* codePic为生成的验证码BufferedImage对象
*/
public static Map<String,Object> generateCodeAndPic(String ip, String sessionCode, int lx) {
// 定义图像buffer
BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// Graphics2D gd = buffImg.createGraphics();
// Graphics2D gd = (Graphics2D) buffImg.getGraphics();
Graphics gd = buffImg.getGraphics();
// 创建一个随机数生成器类
Random random = new Random();
// 将图像填充为白色
gd.setColor(Color.WHITE);
gd.fillRect(0, 0, width, height);
// 创建字体,字体的大小应该根据图片的高度来定。
Font font = new Font("Times New Roman", Font.BOLD, fontHeight);
// 设置字体。
gd.setFont(font);
// 画边框。
gd.setColor(Color.BLACK);
gd.drawRect(0, 0, width - 1, height - 1);
// 随机产生40条干扰线,使图象中的认证码不易被其它程序探测到。
gd.setColor(Color.BLACK);
for (int i = 0; i < 30; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
gd.drawLine(x, y, x + xl, y + yl);
}
StringBuffer randomCode = new StringBuffer();
Map<String,Object> map = new HashMap<>();
// randomCode用于保存随机产生的验证码,以便用户登录后进行验证。
int red, green, blue;
if (lx ==1){
// 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。
red = random.nextInt(255);
green = random.nextInt(255);
blue = random.nextInt(255);
// 用随机产生的颜色将验证码绘制到图像中。
gd.setColor(new Color(red, green, blue));
gd.drawString(sessionCode, 18, codeY);
randomCode.append(sessionCode);
}else {
// 随机产生codeCount数字的验证码。
for (int i = 0; i < codeCount; i++) {
// 得到随机产生的验证码数字。
String code = String.valueOf(codeSequence[random.nextInt(52)]);
// 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。
red = random.nextInt(255);
green = random.nextInt(255);
blue = random.nextInt(255);
// 用随机产生的颜色将验证码绘制到图像中。
gd.setColor(new Color(red, green, blue));
gd.drawString(code, (i + 1) * xx, codeY);
// 将产生的四个随机数组合在一起。
randomCode.append(code);
}
}
//存放验证码
map.put("code", randomCode);
//存放生成的验证码BufferedImage对象
map.put("codePic", buffImg);
return map;
}
}

56
server/src/main/java/cn/keking/utils/WebUtils.java

@ -9,6 +9,8 @@ import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.util.HtmlUtils;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
@ -27,7 +29,7 @@ import java.util.regex.Pattern;
public class WebUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(WebUtils.class);
private static final String BASE64_MSG = "base64";
private static final String BASE64_MSG = "base64";
/**
* 获取标准的URL
*
@ -205,13 +207,7 @@ public class WebUtils {
Matcher matcher = pattern.matcher(url);
return matcher.find();
}
public static boolean kuayu(String host, String wjl) { //查询域名是否相同
if (wjl.contains(host)) {
return true;
}else {
return false;
}
}
/**
* Base64 字符串解码再解码URL参数, 默认使用 UTF-8
* @param source 原始 Base64 字符串
@ -265,4 +261,48 @@ public class WebUtils {
}
return null;
}
/**
* 获取 session 中的 String 属性
* @param request 请求
* @return 属性值
*/
public static String getSessionAttr(HttpServletRequest request, String key) {
HttpSession session = request.getSession();
if (session == null) {
return null;
}
Object value = session.getAttribute(key);
if (value == null) {
return null;
}
return value.toString();
}
/**
* 获取 session 中的 long 属性
* @param request 请求
* @param key 属性名
* @return 属性值
*/
public static long getLongSessionAttr(HttpServletRequest request, String key) {
String value = getSessionAttr(request, key);
if (value == null) {
return 0;
}
return Long.parseLong(value);
}
/**
* 移除 session 中的属性
* @param request 请求
* @param key 属性名
*/
public static void removeSessionAttr(HttpServletRequest request, String key) {
HttpSession session = request.getSession();
if (session == null) {
return;
}
session.removeAttribute(key);
}
}

84
server/src/main/java/cn/keking/web/controller/FileController.java

@ -2,7 +2,9 @@ package cn.keking.web.controller;
import cn.keking.config.ConfigConstants;
import cn.keking.model.ReturnResponse;
import cn.keking.utils.DateUtils;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.CaptchaUtil;
import cn.keking.utils.RarUtils;
import cn.keking.utils.WebUtils;
import org.slf4j.Logger;
@ -11,11 +13,16 @@ import org.springframework.util.ObjectUtils;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.RenderedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@ -24,9 +31,11 @@ import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import static cn.keking.utils.CaptchaUtil.*;
/**
* @author yudian-it
* 2017/12/1
* 2017/12/1
*/
@RestController
public class FileController {
@ -35,6 +44,7 @@ public class FileController {
private final String fileDir = ConfigConstants.getFileDir();
private final String demoDir = "demo";
private final String demoPath = demoDir + File.separator;
public static final String BASE64_DECODE_ERROR_MSG = "Base64解码失败,请检查你的 %s 是否采用 Base64 + urlEncode 双重编码了!";
@ -61,28 +71,11 @@ public class FileController {
@GetMapping("/deleteFile")
public ReturnResponse<Object> deleteFile(HttpServletRequest request, String fileName, String password) {
ReturnResponse<Object> checkResult = this.deleteFileCheck(fileName);
ReturnResponse<Object> checkResult = this.deleteFileCheck(request, fileName, password);
if (checkResult.isFailure()) {
return checkResult;
}
fileName = checkResult.getContent().toString();
if(ConfigConstants.getDeleteCaptcha()){
String sessionCode;
try {
sessionCode = request.getSession().getAttribute("code").toString(); //获取已经保存的验证码
} catch (Exception e) {
sessionCode = "null";
}
if (!sessionCode.equalsIgnoreCase(password)){
logger.error("删除文件【{}】失败,密码错误!",fileName);
return ReturnResponse.failure("删除文件失败,密码错误!");
}
}else {
if(!ConfigConstants.getPassword().equalsIgnoreCase(password)) {
logger.error("删除文件【{}】失败,密码错误!",fileName);
return ReturnResponse.failure("删除文件失败,密码错误!");
}
}
fileName = checkResult.getContent().toString();
File file = new File(fileDir + demoPath + fileName);
logger.info("删除文件:{}", file.getAbsolutePath());
if (file.exists() && !file.delete()) {
@ -90,10 +83,45 @@ public class FileController {
logger.error(msg);
return ReturnResponse.failure(msg);
}
request.getSession().removeAttribute("code"); //删除缓存验证码
WebUtils.removeSessionAttr(request, captcha_code); //删除缓存验证码
return ReturnResponse.success();
}
/**
* 验证码方法
*/
@RequestMapping("/deleteFile/captcha")
public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception {
if (!ConfigConstants.getDeleteCaptcha()) {
return;
}
response.setContentType("image/jpeg");
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", -1);
String captchaCode = WebUtils.getSessionAttr(request, captcha_code);
long captchaGenerateTime = WebUtils.getLongSessionAttr(request, captcha_generate_time);
long timeDifference = DateUtils.calculateCurrentTimeDifference(captchaGenerateTime);
Map<String, Object> codeMap;
// 验证码为空,且生成验证码超过50秒,重新生成验证码
if (timeDifference > 50 && ObjectUtils.isEmpty(captchaCode)) {
codeMap = CaptchaUtil.generateCaptcha(null);
// 更新验证码
request.getSession().setAttribute(captcha_code, codeMap.get(captcha_code).toString());
request.getSession().setAttribute(captcha_generate_time, DateUtils.getCurrentSecond());
} else {
captchaCode = ObjectUtils.isEmpty(captchaCode) ? "wait" : captchaCode;
codeMap = CaptchaUtil.generateCaptcha(captchaCode);
}
ServletOutputStream sos = response.getOutputStream();
ImageIO.write((RenderedImage) codeMap.get(captcha_code_pic), "jpeg", sos);
sos.close();
}
@GetMapping("/listFiles")
public List<Map<String, String>> getFiles() {
List<Map<String, String>> list = new ArrayList<>();
@ -121,7 +149,7 @@ public class FileController {
return ReturnResponse.failure("文件传接口已禁用");
}
String fileName = WebUtils.getFileNameFromMultipartFile(file);
if(fileName.lastIndexOf(".")==-1){
if (fileName.lastIndexOf(".") == -1) {
return ReturnResponse.failure("不允许上传的类型");
}
if (!KkFileUtils.isAllowedUpload(fileName)) {
@ -144,7 +172,7 @@ public class FileController {
* @param fileName 文件名
* @return 校验结果
*/
private ReturnResponse<Object> deleteFileCheck(String fileName) {
private ReturnResponse<Object> deleteFileCheck(HttpServletRequest request, String fileName, String password) {
if (ObjectUtils.isEmpty(fileName)) {
return ReturnResponse.failure("文件名为空,删除失败!");
}
@ -161,6 +189,16 @@ public class FileController {
if (KkFileUtils.isIllegalFileName(fileName)) {
return ReturnResponse.failure("非法文件名,删除失败!");
}
if (ObjectUtils.isEmpty(password)) {
return ReturnResponse.failure("密码 or 验证码为空,删除失败!");
}
String expectedPassword = ConfigConstants.getDeleteCaptcha() ? WebUtils.getSessionAttr(request, captcha_code) : ConfigConstants.getPassword();
if (!password.equalsIgnoreCase(expectedPassword)) {
logger.error("删除文件【{}】失败,密码错误!", fileName);
return ReturnResponse.failure("删除文件失败,密码错误!");
}
return ReturnResponse.success(fileName);
}

88
server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java

@ -1,6 +1,5 @@
package cn.keking.web.controller;
import cn.keking.config.ConfigConstants;
import cn.keking.model.FileAttribute;
import cn.keking.service.FileHandlerService;
import cn.keking.service.FilePreview;
@ -8,7 +7,6 @@ import cn.keking.service.FilePreviewFactory;
import cn.keking.service.cache.CacheService;
import cn.keking.service.impl.OtherFilePreviewImpl;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.RandomValidateCodeUtil;
import cn.keking.utils.WebUtils;
import fr.opensagres.xdocreport.core.io.IOUtils;
import io.mola.galimatias.GalimatiasParseException;
@ -17,17 +15,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.RenderedImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
@ -35,11 +28,8 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import static cn.keking.service.FilePreview.PICTURE_FILE_PREVIEW_PAGE;
@ -66,7 +56,7 @@ public class OnlinePreviewController {
@GetMapping( "/onlinePreview")
public String onlinePreview(String url, Model model, HttpServletRequest req) {
String fileUrl;
try {
fileUrl = WebUtils.decodeUrl(url);
@ -199,82 +189,6 @@ public class OnlinePreviewController {
}
}
}
/**
* 验证码方法
*/
@RequestMapping("/captcha")
public void captcha(HttpServletRequest request, HttpServletResponse response) throws Exception {
if(!ConfigConstants.getDeleteCaptcha()){
return;
}
response.setContentType("image/gif");
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
Date date = new Date(); // 当前时间
SimpleDateFormat formater = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 设置时间格式
String sessionCode;
try {
sessionCode = request.getSession().getAttribute("code").toString(); //获取已经保存的验证码
} catch (Exception e) {
sessionCode= null;
}
Object time = request.getSession().getAttribute("time"); //获取已经保存的时间
if (ObjectUtils.isEmpty(time)){ //判断时间是否为空
request.getSession().setAttribute("time", formater.format(date)); //为空重新添加缓存时间
time = request.getSession().getAttribute("time");
}
Date joinTime = formater.parse(String.valueOf(time));
String dateStart = formater.format(joinTime);
Date d1=formater.parse(dateStart);
// 时间差:
long diff = date.getTime() - d1.getTime();
long diffSeconds = diff / 1000 % 60;
String ip=request.getRemoteAddr();
ServletOutputStream sos = null;
if (ObjectUtils.isEmpty(sessionCode) || diffSeconds > 50){ //判断验证码是否为空 为空重新生成 判断是否在有效时间内 默认50秒
Map<String, Object> codeMap = RandomValidateCodeUtil.generateCodeAndPic(ip,sessionCode,0);
// 验证码存入session
request.getSession().setAttribute("code", codeMap.get("code").toString());
// 时间存入session
request.getSession().setAttribute("time", formater.format(date));
// 禁止图像缓存。
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", -1);
response.setContentType("image/jpeg");
// 将图像输出到Servlet输出流中。
try {
sos = response.getOutputStream();
ImageIO.write((RenderedImage) codeMap.get("codePic"), "jpeg", sos);
} catch (IOException e) {
e.printStackTrace();
} finally {
assert sos != null;
sos.close();
}
}else {
// System.out.println("请输入你的姓名:");
Map<String, Object> codeMap = RandomValidateCodeUtil.generateCodeAndPic(ip,sessionCode,1);
// 禁止图像缓存。
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", -1);
response.setContentType("image/jpeg");
// 将图像输出到Servlet输出流中。
try {
sos = response.getOutputStream();
ImageIO.write((RenderedImage) codeMap.get("codePic"), "jpeg", sos);
} catch (IOException e) {
e.printStackTrace();
} finally {
assert sos != null;
sos.close();
}
}
}
/**
* 通过api接口入队

10
server/src/main/resources/web/main/index.ftl

@ -201,7 +201,7 @@
<script>
<#if deleteCaptcha >
function deleteFile(fileName) {
var codename =`<div class="code"><h4>请输入下面删除码!</h4><div><img id="verImg" width="130px" height="48px" src="/captcha"></div><form><input type="type" oninput="if(value.length>5)value=value.slice(0,5);" class="code-input" id="_code" placeholder="请输入验证码"><button id="deleteFile1" type="button" class="btn btn-success">提交</button></form><button id="close" type="button"class="btn btn-danger">关闭</button></div>`;
var codename =`<div class="code"><h4>请输入下面删除码!</h4><div><img id="verImg" width="130px" height="48px" src="/deleteFile/captcha"></div><form><input type="type" oninput="if(value.length>5)value=value.slice(0,5);" class="code-input" id="_code" placeholder="请输入验证码"><button id="deleteFile1" type="button" class="btn btn-success">提交</button></form><button id="close" type="button"class="btn btn-danger">关闭</button></div>`;
$('#codeContent').html(codename);
var code = document.querySelector('.code');
var closeBtn = document.getElementById("close");
@ -213,15 +213,14 @@
closedelete.addEventListener('click', deleteFile1);
function deleteFile1(){
var password = $("#_code").val();
//console.log(password);
$.ajax({
url: '${baseUrl}deleteFile?fileName=' + fileName +'&password='+password,
success: function (data) {
// console.log(data);
// 删除完成,刷新table
if ("删除文件失败,密码错误!" === data.msg) {
alert(data.msg);
} else {
//刷新验证码
document.getElementById('verImg').click();
$('#table').bootstrapTable('refresh', {});
code.style.display = 'none';
}
@ -241,7 +240,7 @@
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
//2、调用open
xhr.open("get", "/captcha", true);
xhr.open("get", "/deleteFile/captcha", true);
xhr.responseType = "blob";
//3、调用send
xhr.send();
@ -279,6 +278,7 @@
if ("删除文件失败,密码错误!" === data.msg) {
alert(data.msg);
} else {
$('#table').bootstrapTable('refresh', {});
}
},

Loading…
Cancel
Save