springboot统一验证码组件设计(三)

简介: springboot统一验证码组件设计
import com.zx.silverfox.common.exception.GlobalException;
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor;
import com.zx.silverfox.common.validate.code.ValidateCodeRepository;
import com.zx.silverfox.common.validate.code.ValidateCodeType;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.request.ServletWebRequest;
import javax.imageio.ImageIO;
import java.io.IOException;
/** @author zouwei */
public class ImageValidateCodeProcessor extends AbstractValidateCodeProcessor<ImageValidateCode> {
    /** 生成的图片的格式 */
    private static final String JPEG_IMAGE_TYPE = "JPEG";
    public ImageValidateCodeProcessor(
            @Autowired ValidateCodeProperties validateCodeProperties,
            @Autowired ValidateCodeRepository repository) {
        super(
                new ImageValidateCodeGenerator(validateCodeProperties.getImage()),
                repository,
                validateCodeProperties.getImage());
    }
    @Override
    protected ValidateCodeType getValidateCodeType() {
        return ValidateCodeType.IMAGE;
    }
    @Override
    protected void send(ServletWebRequest request, ImageValidateCode validateCode)
            throws GlobalException {
        try {
            ImageIO.write(
                    validateCode.getImage(),
                    JPEG_IMAGE_TYPE,
                    request.getResponse().getOutputStream());
        } catch (IOException e) {
            throw GlobalException.newInstance("IMAGE_CODE_CREATE_FAIL", "图片验证码生成失败");
        }
    }
    @Override
    protected boolean validate(String code, ImageValidateCode validateCode) {
        return StringUtils.equalsIgnoreCase(code, validateCode.getCode());
    }
}
复制代码

滑块验证码:

import com.zx.silverfox.common.validate.code.ValidateCode;
import lombok.Data;
import lombok.EqualsAndHashCode;
/** @author zouwei */
@Data
@EqualsAndHashCode(callSuper = true)
public class SlideImageCode extends ValidateCode {
    private double heightYPercentage;
    private transient String srcImg;
    private transient String markImg;
    public SlideImageCode(
            double heightYPercentage,
            String srcImg,
            String markImg,
            String code,
            long expireInSeconds) {
        super(code, expireInSeconds);
        this.heightYPercentage = heightYPercentage;
        this.srcImg = srcImg;
        this.markImg = markImg;
    }
}
复制代码
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import com.zx.silverfox.common.validate.code.ValidateCode;
import com.zx.silverfox.common.validate.code.ValidateCodeGenerator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.context.request.ServletWebRequest;
import javax.imageio.ImageIO;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Random;
@Slf4j
public class SlideImageCodeGenerator implements ValidateCodeGenerator {
    private ValidateCodeProperties.SlideImageProperties slideImageProperties;
    public SlideImageCodeGenerator(
            ValidateCodeProperties.SlideImageProperties slideImageProperties) {
        this.slideImageProperties = slideImageProperties;
    }
    @Override
    public ValidateCode createValidateCode(ServletWebRequest request) {
        try (InputStream in = getOriginImage()) {
            SlideImageUtil.SlideImage slideImage = SlideImageUtil.getVerifyImage(ImageIO.read(in));
            int width = slideImage.getWidth();
            int x = slideImage.getX();
            int height = slideImage.getHeight();
            int y = slideImage.getY();
            double widthXPercentage = width / (x * 1.0);
            double heightYPercentage = height / (y * 1.0);
            String code = widthXPercentage + ":" + heightYPercentage;
            return new SlideImageCode(
                    heightYPercentage,
                    slideImage.getSrcImg(),
                    slideImage.getMarkImg(),
                    code,
                    slideImageProperties.getExpiredInSecond());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
    private InputStream getOriginImage() throws IOException {
        // 从resources下的slideimg文件夹中随机获取一张图片进行处理
        ClassPathResource classPathResource = new ClassPathResource("slideimg");
        File dirFile = classPathResource.getFile();
        File[] listFiles = dirFile.listFiles();
        int index = new Random().nextInt(listFiles.length);
        return new FileInputStream(listFiles[index]);
    }
}
复制代码
import com.zx.silverfox.common.exception.GlobalException;
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import com.zx.silverfox.common.util.CastUtil;
import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor;
import com.zx.silverfox.common.validate.code.ValidateCodeRepository;
import com.zx.silverfox.common.validate.code.ValidateCodeType;
import com.zx.silverfox.common.vo.CommonResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
 * 滑动验证码
 *
 * @author zouwei
 */
public class SlideImageCodeProcessor extends AbstractValidateCodeProcessor<SlideImageCode> {
    public SlideImageCodeProcessor(
            @Autowired ValidateCodeProperties validateCodeProperties,
            @Autowired ValidateCodeRepository validateCodeRepository) {
        super(
                new SlideImageCodeGenerator(validateCodeProperties.getSlide()),
                validateCodeRepository,
                validateCodeProperties.getSlide());
    }
    @Override
    protected ValidateCodeType getValidateCodeType() {
        return ValidateCodeType.SLIDE;
    }
    @Override
    protected void send(ServletWebRequest request, SlideImageCode validateCode)
            throws GlobalException {
        double heightY = validateCode.getHeightYPercentage();
        try {
            HttpServletResponse response = request.getResponse();
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getOutputStream()
                    .write(
                            CommonResponse.successInstance(
                                            new SlideValidateCodeImage(
                                                    heightY,
                                                    validateCode.getSrcImg(),
                                                    validateCode.getMarkImg()))
                                    .toJson()
                                    .getBytes());
        } catch (IOException e) {
            throw GlobalException.newInstance("", "图片验证码生成失败");
        }
    }
    /**
     * 滑动验证码验证
     *
     * @param code
     * @param validateCode
     * @return
     */
    @Override
    protected boolean validate(String code, SlideImageCode validateCode) {
        try {
            String[] location = StringUtils.splitByWholeSeparatorPreserveAllTokens(code, ":");
            double x1 = CastUtil.castDouble(location[0]);
            double y1 = CastUtil.castDouble(location[1]);
            String sessionCode = validateCode.getCode();
            String[] sessionLocation =
                    StringUtils.splitByWholeSeparatorPreserveAllTokens(sessionCode, ":");
            double x2 = CastUtil.castDouble(sessionLocation[0]);
            double y2 = CastUtil.castDouble(sessionLocation[1]);
            double distance = Math.sqrt(Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2));
            return distance < 0.06;
        } catch (Exception e) {
            return false;
        }
    }
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    private static class SlideValidateCodeImage {
        private double heightY;
        private String srcImg;
        private String markImg;
    }
}
复制代码

滑块处理工具类:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.util.Base64Utils;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
 * @author zouwei
 */
public final class SlideImageUtil {
    private static final String IMAGE_TYPE = "png";
    /** 源文件宽度 */
    private static int ORI_WIDTH = 300;
    /** 源文件高度 */
    private static int ORI_HEIGHT = 150;
    /** 模板图宽度 */
    private static int CUT_WIDTH = 50;
    /** 模板图高度 */
    private static int CUT_HEIGHT = 50;
    /** 抠图凸起圆心 */
    private static int circleR = 5;
    /** 抠图内部矩形填充大小 */
    private static int RECTANGLE_PADDING = 8;
    /** 抠图的边框宽度 */
    private static int SLIDER_IMG_OUT_PADDING = 1;
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class SlideImage {
        /** 底图 */
        private String srcImg;
        /** 标记图片 */
        private String markImg;
        /** x轴 */
        private int x;
        /** y轴 */
        private int y;
        /** 原图的宽度 */
        private int width;
        /** 原图的高度 */
        private int height;
    }
    /**
     * 根据传入的路径生成指定验证码图片
     *
     * @param originImage
     * @return
     * @throws IOException
     */
    public static SlideImage getVerifyImage(BufferedImage originImage) throws IOException {
        int width = originImage.getWidth();
        int height = originImage.getHeight();
        int locationX = CUT_WIDTH + new Random().nextInt(width - CUT_WIDTH * 3);
        int locationY = CUT_HEIGHT + new Random().nextInt(height - CUT_HEIGHT) / 2;
        BufferedImage markImage =
                new BufferedImage(CUT_WIDTH, CUT_HEIGHT, BufferedImage.TYPE_4BYTE_ABGR);
        int[][] data = getBlockData();
        cutImgByTemplate(originImage, markImage, data, locationX, locationY);
        return new SlideImage(
                getImageBASE64(originImage),
                getImageBASE64(markImage),
                locationX,
                locationY,
                width,
                height);
    }
    /**
     * 生成随机滑块形状
     *
     * <p>0 透明像素 1 滑块像素 2 阴影像素
     *
     * @return int[][]
     */
    private static int[][] getBlockData() {
        int[][] data = new int[CUT_WIDTH][CUT_HEIGHT];
        Random random = new Random();
        // (x-a)²+(y-b)²=r²
        // x中心位置左右5像素随机
        double x1 =
                RECTANGLE_PADDING
                        + (CUT_WIDTH - 2 * RECTANGLE_PADDING) / 2.0
                        - 5
                        + random.nextInt(10);
        // y 矩形上边界半径-1像素移动
        double y1_top = RECTANGLE_PADDING - random.nextInt(3);
        double y1_bottom = CUT_HEIGHT - RECTANGLE_PADDING + random.nextInt(3);
        double y1 = random.nextInt(2) == 1 ? y1_top : y1_bottom;
        double x2_right = CUT_WIDTH - RECTANGLE_PADDING - circleR + random.nextInt(2 * circleR - 4);
        double x2_left = RECTANGLE_PADDING + circleR - 2 - random.nextInt(2 * circleR - 4);
        double x2 = random.nextInt(2) == 1 ? x2_right : x2_left;
        double y2 =
                RECTANGLE_PADDING
                        + (CUT_HEIGHT - 2 * RECTANGLE_PADDING) / 2.0
                        - 4
                        + random.nextInt(10);
        double po = Math.pow(circleR, 2);
        for (int i = 0; i < CUT_WIDTH; i++) {
            for (int j = 0; j < CUT_HEIGHT; j++) {
                // 矩形区域
                boolean fill;
                if ((i >= RECTANGLE_PADDING && i < CUT_WIDTH - RECTANGLE_PADDING)
                        && (j >= RECTANGLE_PADDING && j < CUT_HEIGHT - RECTANGLE_PADDING)) {
                    data[i][j] = 1;
                    fill = true;
                } else {
                    data[i][j] = 0;
                    fill = false;
                }
                // 凸出区域
                double d3 = Math.pow(i - x1, 2) + Math.pow(j - y1, 2);
                if (d3 < po) {
                    data[i][j] = 1;
                } else {
                    if (!fill) {
                        data[i][j] = 0;
                    }
                }
                // 凹进区域
                double d4 = Math.pow(i - x2, 2) + Math.pow(j - y2, 2);
                if (d4 < po) {
                    data[i][j] = 0;
                }
            }
        }
        // 边界阴影
        for (int i = 0; i < CUT_WIDTH; i++) {
            for (int j = 0; j < CUT_HEIGHT; j++) {
                // 四个正方形边角处理
                for (int k = 1; k <= SLIDER_IMG_OUT_PADDING; k++) {
                    // 左上、右上
                    if (i >= RECTANGLE_PADDING - k
                            && i < RECTANGLE_PADDING
                            && ((j >= RECTANGLE_PADDING - k && j < RECTANGLE_PADDING)
                                    || (j >= CUT_HEIGHT - RECTANGLE_PADDING - k
                                            && j < CUT_HEIGHT - RECTANGLE_PADDING + 1))) {
                        data[i][j] = 2;
                    }
                    // 左下、右下
                    if (i >= CUT_WIDTH - RECTANGLE_PADDING + k - 1
                            && i < CUT_WIDTH - RECTANGLE_PADDING + 1) {
                        for (int n = 1; n <= SLIDER_IMG_OUT_PADDING; n++) {
                            if (((j >= RECTANGLE_PADDING - n && j < RECTANGLE_PADDING)
                                    || (j >= CUT_HEIGHT - RECTANGLE_PADDING - n
                                            && j <= CUT_HEIGHT - RECTANGLE_PADDING))) {
                                data[i][j] = 2;
                            }
                        }
                    }
                }
                if (data[i][j] == 1
                        && j - SLIDER_IMG_OUT_PADDING > 0
                        && data[i][j - SLIDER_IMG_OUT_PADDING] == 0) {
                    data[i][j - SLIDER_IMG_OUT_PADDING] = 2;
                }
                if (data[i][j] == 1
                        && j + SLIDER_IMG_OUT_PADDING > 0
                        && j + SLIDER_IMG_OUT_PADDING < CUT_HEIGHT
                        && data[i][j + SLIDER_IMG_OUT_PADDING] == 0) {
                    data[i][j + SLIDER_IMG_OUT_PADDING] = 2;
                }
                if (data[i][j] == 1
                        && i - SLIDER_IMG_OUT_PADDING > 0
                        && data[i - SLIDER_IMG_OUT_PADDING][j] == 0) {
                    data[i - SLIDER_IMG_OUT_PADDING][j] = 2;
                }
                if (data[i][j] == 1
                        && i + SLIDER_IMG_OUT_PADDING > 0
                        && i + SLIDER_IMG_OUT_PADDING < CUT_WIDTH
                        && data[i + SLIDER_IMG_OUT_PADDING][j] == 0) {
                    data[i + SLIDER_IMG_OUT_PADDING][j] = 2;
                }
            }
        }
        return data;
    }
    /**
     * 裁剪区块 根据生成的滑块形状,对原图和裁剪块进行变色处理
     *
     * @param oriImage 原图
     * @param targetImage 裁剪图
     * @param blockImage 滑块
     * @param x 裁剪点x
     * @param y 裁剪点y
     */
    private static void cutImgByTemplate(
            BufferedImage oriImage, BufferedImage targetImage, int[][] blockImage, int x, int y) {
        for (int i = 0; i < CUT_WIDTH; i++) {
            for (int j = 0; j < CUT_HEIGHT; j++) {
                int _x = x + i;
                int _y = y + j;
                int rgbFlg = blockImage[i][j];
                int rgb_ori = oriImage.getRGB(_x, _y);
                // 原图中对应位置变色处理
                if (rgbFlg == 1) {
                    // 抠图上复制对应颜色值
                    targetImage.setRGB(i, j, rgb_ori);
                    // 原图对应位置颜色变化
                    oriImage.setRGB(_x, _y, Color.LIGHT_GRAY.getRGB());
                } else if (rgbFlg == 2) {
                    targetImage.setRGB(i, j, Color.WHITE.getRGB());
                    oriImage.setRGB(_x, _y, Color.GRAY.getRGB());
                } else if (rgbFlg == 0) {
                    // int alpha = 0;
                    targetImage.setRGB(i, j, rgb_ori & 0x00ffffff);
                }
            }
        }
    }
    /**
     * 随机获取一张图片对象
     *
     * @param path
     * @return
     * @throws IOException
     */
    public static BufferedImage getRandomImage(String path) throws IOException {
        File files = new File(path);
        File[] fileList = files.listFiles();
        List<String> fileNameList = new ArrayList<>();
        if (fileList != null && fileList.length != 0) {
            for (File tempFile : fileList) {
                if (tempFile.isFile() && tempFile.getName().endsWith(".jpg")) {
                    fileNameList.add(tempFile.getAbsolutePath().trim());
                }
            }
        }
        Random random = new Random();
        File imageFile = new File(fileNameList.get(random.nextInt(fileNameList.size())));
        return ImageIO.read(imageFile);
    }
    /**
     * 将IMG输出为文件
     *
     * @param image
     * @param file
     * @throws Exception
     */
    public static void writeImg(BufferedImage image, String file) throws Exception {
        try (ByteArrayOutputStream bao = new ByteArrayOutputStream()) {
            ImageIO.write(image, IMAGE_TYPE, bao);
            FileOutputStream out = new FileOutputStream(new File(file));
            out.write(bao.toByteArray());
        }
    }
    /**
     * 将图片转换为BASE64
     *
     * @param image
     * @return
     * @throws IOException
     */
    public static String getImageBASE64(BufferedImage image) throws IOException {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            ImageIO.write(image, IMAGE_TYPE, out);
            // 生成BASE64编码
            return Base64Utils.encodeToString(out.toByteArray());
        }
    }
    /**
     * 将BASE64字符串转换为图片
     *
     * @param base64String
     * @return
     */
    public static BufferedImage base64StringToImage(String base64String) throws IOException {
        try (ByteArrayInputStream bais =
                new ByteArrayInputStream(Base64Utils.decodeFromString(base64String))) {
            return ImageIO.read(bais);
        }
    }
}
复制代码

短信验证码:

import com.zx.silverfox.common.validate.code.ValidateCode;
public class SmsValidateCode extends ValidateCode {
    public SmsValidateCode(String code, long expireInSeconds) {
        super(code, expireInSeconds);
    }
}
复制代码
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import com.zx.silverfox.common.validate.code.ValidateCodeGenerator;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.web.context.request.ServletWebRequest;
/** @author zouwei */
public class SmsValidateCodeGenerator implements ValidateCodeGenerator {
    private ValidateCodeProperties.SmsProperties smsProperties;
    public SmsValidateCodeGenerator(ValidateCodeProperties.SmsProperties smsProperties) {
        this.smsProperties = smsProperties;
    }
    @Override
    public SmsValidateCode createValidateCode(ServletWebRequest request) {
        String code = RandomStringUtils.randomNumeric(smsProperties.getLength());
        return new SmsValidateCode(code, smsProperties.getExpiredInSecond());
    }
}
复制代码
import com.zx.silverfox.common.exception.GlobalException;
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import com.zx.silverfox.common.util.CastUtil;
import com.zx.silverfox.common.util.SmsUtil;
import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor;
import com.zx.silverfox.common.validate.code.ValidateCodeRepository;
import com.zx.silverfox.common.validate.code.ValidateCodeType;
import com.zx.silverfox.common.vo.CommonResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/** @author zouwei */
@Slf4j
public class SmsValidateCodeProcessor extends AbstractValidateCodeProcessor<SmsValidateCode> {
    public SmsValidateCodeProcessor(
            @Autowired ValidateCodeProperties validateCodeProperties,
            @Autowired ValidateCodeRepository validateCodeRepository) {
        super(
                new SmsValidateCodeGenerator(validateCodeProperties.getSms()),
                validateCodeRepository,
                validateCodeProperties.getSms());
    }
    @Override
    protected ValidateCodeType getValidateCodeType() {
        return ValidateCodeType.SMS;
    }
    @Override
    protected void send(ServletWebRequest request, SmsValidateCode validateCode)
            throws GlobalException {
        // 手机号码
        String mobile = request.getParameter("mobile");
        String type = request.getParameter("type");
        if (StringUtils.isBlank(mobile) || StringUtils.isBlank(type)) {
            // 获取验证码参数没提供
            throw GlobalException.newInstance(
                    "SMS_VALIDATE_CODE_PARAM_ERROR", "没有给电话号码或者指明短信类型,无法发送短信");
        }
        long minute = validateCode.minute();
        SmsUtil.send(
                SmsUtil.SmsType.format(type),
                mobile,
                validateCode.getCode(),
                CastUtil.castString(minute));
        HttpServletResponse response = request.getResponse();
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        try {
            response.getOutputStream().write(CommonResponse.successInstance().toJson().getBytes());
        } catch (IOException e) {
            log.error("response.getOutputStream()出异常", e);
        }
    }
    @Override
    protected boolean validate(String code, SmsValidateCode validateCode) {
        return StringUtils.equalsIgnoreCase(code, validateCode.getCode());
    }
}
复制代码

注意事项:想要使用滑块验证码,需要在resources文件夹里面创建一个slideimg文件夹,并且把需要的图片放进去:

image.png

GlobalException是我自己设计的异常类,建议需要的小伙伴换成自己应用的异常类。

ok,整个验证码组件设计加上具体实现都已经完毕,下面就是如何使用:

首先,把自定义注解放在springboot项目启动类上:

image.png

建议按需配置,如果不需要图片验证码或者滑块验证码,可以不加载进来

然后就是配置文件:

image.png

比如你需要在发生短信验证码之前先触发滑块验证码,那么可以把"/code/sms"这个url放进validate.slide.filter-urls配置中。

好吧,怎么使用已经讲解完毕,配置文件中的其他配置参数包括图片的大小和验证码的位数等等,小伙伴可以根据自身需要去配置。


相关文章
|
4天前
|
XML Java 数据格式
Springboot中自定义组件
Springboot中自定义组件
|
4天前
|
Java
SpringBoot集成RestTemplate组件
SpringBoot集成RestTemplate组件
46 0
|
4天前
|
Dubbo Java 应用服务中间件
微服务框架(十六)Spring Boot及Dubbo zipkin 链路追踪组件埋点
此系列文章将会描述Java框架Spring Boot、服务治理框架Dubbo、应用容器引擎Docker,及使用Spring Boot集成Dubbo、Mybatis等开源框架,其中穿插着Spring Boot中日志切面等技术的实现,然后通过gitlab-CI以持续集成为Docker镜像。 本文第一部分为调用链、OpenTracing、Zipkin和Jeager的简述;第二部分为Spring Boot及Dubbo zipkin 链路追踪组件埋点
|
4天前
|
Java 应用服务中间件 容器
SpringBoot之Web原生组件注入
SpringBoot之Web原生组件注入
|
4天前
|
前端开发 安全 Java
SpringBoot 实现登录验证码(附集成SpringSecurity)
SpringBoot 实现登录验证码(附集成SpringSecurity)
|
4天前
|
Java 数据库 数据安全/隐私保护
【SpringBoot】Validator组件+自定义约束注解实现手机号码校验和密码格式限制
【SpringBoot】Validator组件+自定义约束注解实现手机号码校验和密码格式限制
155 1
|
4天前
|
XML 缓存 算法
SpringBoot2 | SpingBoot FilterRegistrationBean 注册组件 | FilterChain 责任链源码分析(九)
SpringBoot2 | SpingBoot FilterRegistrationBean 注册组件 | FilterChain 责任链源码分析(九)
28 0
|
4天前
|
缓存 Java 开发者
10个点介绍SpringBoot3工作流程与核心组件源码解析
Spring Boot 是Java开发中100%会使用到的框架,开发者不仅要熟练使用,对其中的核心源码也要了解,正所谓知其然知其所以然,V 哥建议小伙伴们在学习的过程中,一定要去研读一下源码,这有助于你在开发中游刃有余。欢迎一起交流学习心得,一起成长。
|
4天前
|
XML Java 应用服务中间件
Springboot中tomcat配置、三大组件配置、拦截器配置
Springboot中tomcat配置、三大组件配置、拦截器配置
|
4天前
|
前端开发 JavaScript Java
springboot 集成easy-captcha实现图像验证码显示和登录
springboot 集成easy-captcha实现图像验证码显示和登录
177 0