亦或是這種
還有就是這種:
好吧蓄喇,這種也算:
所有的驗(yàn)證碼发侵,無論是圖片驗(yàn)證碼,還是滑塊驗(yàn)證碼妆偏,亦或是短信驗(yàn)證碼刃鳄、語音驗(yàn)證碼,它們的作用都是為了防止應(yīng)用接口被惡意的非人為操作不斷調(diào)用钱骂。
以第一張圖或第二張圖為例叔锐,不針對這個發(fā)短信的接口做一個圖片驗(yàn)證碼的話,那么就很可能被惡意程序調(diào)用见秽,導(dǎo)致后臺程序不斷地發(fā)送短信驗(yàn)證碼給指定手機(jī)號碼的人愉烙,這樣不僅會造成公司的損失,也會給接收短信的人造成不必要的困擾解取。有了圖片驗(yàn)證碼后步责,調(diào)用接口的時候需要帶上被識別的驗(yàn)證碼,惡意程序就相對有難度才能調(diào)用你的這個被保護(hù)的接口了,大大降低了這方面的困擾蔓肯。
注意點(diǎn):很多同學(xué)在這個驗(yàn)證碼的時候遂鹊,僅僅是簡單地通過前端調(diào)用獲取驗(yàn)證碼的接口,然后再把用戶提交的驗(yàn)證碼交給后臺驗(yàn)證省核,驗(yàn)證通過后再發(fā)起業(yè)務(wù)請求稿辙。這種做法只是做到了表面上有驗(yàn)證碼的驗(yàn)證過程,實(shí)際上還是沒有做到對業(yè)務(wù)接口的保護(hù)气忠。交互過程如下圖:
這樣的交互邏輯是有很明顯的漏洞的,它把驗(yàn)證的權(quán)限交給了客戶端赋咽,前端說通過就通過旧噪,那么對于任何一個了解并且會使用一定手段或工具的人來說,這樣的驗(yàn)證碼就是形同虛設(shè)脓匿。使用api工具就可以直接跳到第三步直接調(diào)用業(yè)務(wù)接口淘钟。 真正的驗(yàn)證碼應(yīng)該做到和業(yè)務(wù)接口綁定,如下圖的交互邏輯:
按照以上交互邏輯陪毡,無論如何米母,客戶端必須帶上驗(yàn)證碼才能真正地調(diào)用后臺服務(wù)處理業(yè)務(wù)請求,否則就無法達(dá)到目的毡琉。
后面就以java web為例來實(shí)現(xiàn)上述的交互邏輯:
為了可以將驗(yàn)證碼邏輯和具體業(yè)務(wù)邏輯解藕铁瞒,利用了servlet的Filter作為過濾器來判斷當(dāng)前請求的接口是否需要通過驗(yàn)證碼驗(yàn)證后才能被調(diào)用
import com.zx.silverfox.common.exception.GlobalException;
import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor;
import com.zx.silverfox.common.vo.CommonResponse;
import lombok.SneakyThrows;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.filter.OrderedFilter;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
/** @author zouwei */
public class ValidateCodeFilter implements OrderedFilter {
//利用spring特性獲取所有的驗(yàn)證碼處理器
@Autowired private List<AbstractValidateCodeProcessor> validateCodeProcessorList;
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
AntPathMatcher matcher = new AntPathMatcher();
// 判斷當(dāng)前這個請求是否需要驗(yàn)證,并且驗(yàn)證請求中攜帶的驗(yàn)證碼
if (!validateCode(req, res, matcher)) {
return;
}
// 生成驗(yàn)證碼
if (generatorCode(req, res, matcher)) {
return;
}
chain.doFilter(request, response);
}
@Override
public void destroy() {}
/**
* 驗(yàn)證操作
* @param request
* @param response
* @param matcher
* @return
*/
private boolean validateCode(
HttpServletRequest request, HttpServletResponse response, AntPathMatcher matcher) {
String url = request.getRequestURI();
//循環(huán)調(diào)用驗(yàn)證碼處理器進(jìn)行驗(yàn)證
for (AbstractValidateCodeProcessor processor : validateCodeProcessorList) {
String[] filterUrls = processor.filterUrls();
if (ArrayUtils.isEmpty(filterUrls)) {
continue;
}
for (String filterUrl : filterUrls) {
// 先判斷當(dāng)前接口是否需要攔截桅滋,如果匹配成功慧耍,就開始進(jìn)行驗(yàn)證
if (matcher.match(filterUrl, url)) {
return validate(request, response, processor);
}
}
}
return true;
}
@SneakyThrows
private boolean validate(
HttpServletRequest request,
HttpServletResponse response,
AbstractValidateCodeProcessor processor) {
if (Objects.isNull(processor)) {
return false;
}
try {
// 執(zhí)行驗(yàn)證
processor.validate(new ServletWebRequest(request, response));
} catch (GlobalException e) {
// 驗(yàn)證失敗的話,捕獲異常丐谋,并處理響應(yīng)
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream()
.write(CommonResponse.exceptionInstance(e).toJson().getBytes());
return false;
}
return true;
}
/**
* 生成驗(yàn)證碼
* @param request
* @param response
* @param matcher
* @return
*/
@SneakyThrows
private boolean generatorCode(
HttpServletRequest request, HttpServletResponse response, AntPathMatcher matcher) {
// 獲取驗(yàn)證碼只能通過GET請求
if (!StringUtils.equalsIgnoreCase(request.getMethod(), HttpMethod.GET.name())) {
return false;
}
String url = request.getRequestURI();
// 依然還是通過驗(yàn)證碼處理器去做生成驗(yàn)證碼的操作
for (AbstractValidateCodeProcessor processor : validateCodeProcessorList) {
// 檢查當(dāng)前請求是要生成哪種類型的驗(yàn)證碼
if (matcher.match(processor.generatorUrl(), url)) {
try {
// 生成驗(yàn)證碼
processor.create(new ServletWebRequest(request, response));
} catch (GlobalException e) {
//失敗后捕獲異常芍碧,并處理響應(yīng)
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getOutputStream()
.write(CommonResponse.exceptionInstance(e).toJson().getBytes());
}
return true;
}
}
return false;
}
// 設(shè)置當(dāng)前過濾器的優(yōu)先級
@Override
public int getOrder() {
return REQUEST_WRAPPER_FILTER_MAX_ORDER - 104;
}
}
上述ValidateCodeFilter是一個驗(yàn)證碼邏輯入口類,也是整個邏輯的黏合劑号俐,真正實(shí)現(xiàn)還是要靠AbstractValidateCodeProcessor這個處理器
import com.zx.silverfox.common.exception.GlobalException;
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Objects;
/** @author zouwei */
public abstract class AbstractValidateCodeProcessor<C extends ValidateCode>
implements ValidateCodeProcessor, ApplicationContextAware {
/** 標(biāo)記驗(yàn)證碼的唯一key */
protected static final String CODE_KEY = "code_key";
/** 發(fā)送驗(yàn)證碼前需要調(diào)用的操作 */
@Autowired(required = false)
private List<ValidateCodeHandler> handlerList;
/** 實(shí)現(xiàn)ApplicationContextAware泌豆,獲取ApplicationContext */
private static ApplicationContext APPLICATION_CONTEXT;
/** 用作生成驗(yàn)證碼 */
private ValidateCodeGenerator validateCodeGenerator;
/** 用作存取驗(yàn)證碼 */
private ValidateCodeRepository validateCodeRepository;
/** 用作獲取驗(yàn)證碼相關(guān)系統(tǒng)配置 */
private ValidateCodeProperties.CodeProperties codeProperties;
/** 構(gòu)造函數(shù) */
public AbstractValidateCodeProcessor(
ValidateCodeGenerator validateCodeGenerator,
ValidateCodeRepository validateCodeRepository,
ValidateCodeProperties.CodeProperties codeProperties) {
this.validateCodeGenerator = validateCodeGenerator;
this.validateCodeRepository = validateCodeRepository;
this.codeProperties = codeProperties;
}
protected static ApplicationContext getApplicationContext() {
return APPLICATION_CONTEXT;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
APPLICATION_CONTEXT = applicationContext;
}
/** 生成驗(yàn)證碼邏輯 */
@Override
public void create(ServletWebRequest request) throws GlobalException {
// 生成指定驗(yàn)證碼
C validateCode = generate(request);
ValidateCodeType codeType = getValidateCodeType();
// 檢查是否需要在發(fā)送該驗(yàn)證碼之前執(zhí)行一些指定的操作;比如注冊的時候驗(yàn)證一下手機(jī)號碼是否已經(jīng)被注冊吏饿;
if (!CollectionUtils.isEmpty(handlerList)) {
for (ValidateCodeHandler handler : handlerList) {
if (handler.support(request, codeType)) {
handler.beforeSend(request, codeType, validateCode);
}
}
}
HttpServletResponse response = request.getResponse();
// 用作保存驗(yàn)證碼的key踪危,方便后續(xù)的驗(yàn)證操作
String codeKeyValue = request.getSessionId();
response.setHeader(CODE_KEY, codeKeyValue);
// 保存驗(yàn)證碼數(shù)據(jù)
save(request, validateCode, codeKeyValue);
// 發(fā)送驗(yàn)證碼
send(request, validateCode);
}
/**
* 保存驗(yàn)證碼
*
* @param request
* @param validateCode
*/
private void save(ServletWebRequest request, C validateCode, String codeKeyValue) {
validateCodeRepository.save(request, validateCode, getValidateCodeType(), codeKeyValue);
}
/**
* 獲取ValidateCodeType
*
* @return
*/
protected abstract ValidateCodeType getValidateCodeType();
/**
* 驗(yàn)證碼發(fā)送
*
* @param request
* @param validateCode
* @throws Exception
*/
protected abstract void send(ServletWebRequest request, C validateCode) throws GlobalException;
/**
* 創(chuàng)建驗(yàn)證碼
*
* @param request
* @return
*/
private C generate(ServletWebRequest request) {
return (C) validateCodeGenerator.createValidateCode(request);
}
private String getCodeKeyValue(ServletWebRequest servletWebRequest)
throws ServletRequestBindingException {
HttpServletRequest request = servletWebRequest.getRequest();
// 從請求頭或者參數(shù)中獲取用戶輸入的驗(yàn)證碼
String codeKeyValue = request.getHeader(CODE_KEY);
codeKeyValue =
StringUtils.isBlank(codeKeyValue)
? ServletRequestUtils.getStringParameter(request, CODE_KEY)
: codeKeyValue;
return codeKeyValue;
}
/**
* 校驗(yàn)驗(yàn)證碼
*
* @param servletWebRequest
* @return
* @throws GlobalException
*/
@Override
public boolean validate(ServletWebRequest servletWebRequest) throws GlobalException {
// 獲取驗(yàn)證碼類型
ValidateCodeType codeType = getValidateCodeType();
C codeInSession;
String codeKeyValue;
String codeInRequest;
try {
codeKeyValue = getCodeKeyValue(servletWebRequest);
// 使用codeKeyValue取出保存在后臺驗(yàn)證碼數(shù)據(jù)
codeInSession =
(C) validateCodeRepository.get(servletWebRequest, codeType, codeKeyValue);
// 獲取請求中用戶輸入的驗(yàn)證碼
codeInRequest =
ServletRequestUtils.getStringParameter(
servletWebRequest.getRequest(), codeType.getParamNameOnValidate());
} catch (Exception e) {
throw GlobalException.newInstance(
"VALIDATE_CODE_OBTAIN_ERROR", "獲取驗(yàn)證碼失敗,應(yīng)該是前端請求中沒有提交驗(yàn)證碼");
}
if (StringUtils.isBlank(codeInRequest)) {
throw GlobalException.newInstance("VALIDATE_CODE_EMPTY_ERROR", "驗(yàn)證碼為空找岖,用戶沒有填寫驗(yàn)證碼");
}
if (Objects.isNull(codeInSession) || Objects.isNull(codeInSession.getCode())) {
throw GlobalException.newInstance(
"VALIDATE_CODE_VALIDATE_ERROR", "存儲的驗(yàn)證碼沒有找到陨倡,應(yīng)該是驗(yàn)證碼失效了");
}
if (codeInSession.isExpired()) {
validateCodeRepository.remove(servletWebRequest, codeType, codeKeyValue);
throw GlobalException.newInstance("VALIDATE_CODE_VALIDATE_ERROR", "驗(yàn)證碼已過期,請重新獲取");
}
if (!validate(codeInRequest, codeInSession)) {
throw GlobalException.newInstance("VALIDATE_CODE_VALIDATE_ERROR", "驗(yàn)證碼匹配錯誤");
}
// 驗(yàn)證成功后移除保存的數(shù)據(jù)
validateCodeRepository.remove(servletWebRequest, codeType, codeKeyValue);
return true;
}
/**
* 驗(yàn)證
*
* @param code
* @return
*/
protected abstract boolean validate(String code, C validateCode);
/**
* 生成驗(yàn)證碼的url
*
* @return
*/
public String generatorUrl() {
return this.codeProperties.getGeneratorUrl();
}
/**
* 需要攔截的url
*
* @return
*/
public String[] filterUrls() {
return this.codeProperties.getFilterUrls();
}
}
所有的驗(yàn)證碼處理器必須實(shí)現(xiàn)的接口,創(chuàng)建和驗(yàn)證
import com.zx.silverfox.common.exception.GlobalException;
import org.springframework.web.context.request.ServletWebRequest;
/**
* @author zouwei
*/
public interface ValidateCodeProcessor {
/**
* 創(chuàng)建驗(yàn)證碼
*
* @param request
* @throws Exception
*/
void create(ServletWebRequest request) throws GlobalException;
/**
* 校驗(yàn)驗(yàn)證碼
*
* @param servletWebRequest
*/
boolean validate(ServletWebRequest servletWebRequest) throws GlobalException;
}
創(chuàng)建驗(yàn)證碼接口:
import org.springframework.web.context.request.ServletWebRequest;
public interface ValidateCodeGenerator {
/**
* 生成驗(yàn)證碼
*
* @param request
* @return
*/
ValidateCode createValidateCode(ServletWebRequest request);
}
存取驗(yàn)證碼接口:
import org.springframework.web.context.request.ServletWebRequest;
/** @author zouwei */
public interface ValidateCodeRepository {
/**
* 保存驗(yàn)證碼
*
* @param request
* @param code
* @param validateCodeType
*/
void save(
ServletWebRequest request,
ValidateCode code,
ValidateCodeType validateCodeType,
String codeKeyValue);
/**
* 獲取驗(yàn)證碼
*
* @param request
* @param validateCodeType
* @return
*/
ValidateCode get(
ServletWebRequest request, ValidateCodeType validateCodeType, String codeKeyValue);
/**
* 移除驗(yàn)證碼
*
* @param request
* @param codeType
*/
void remove(ServletWebRequest request, ValidateCodeType codeType, String codeKeyValue);
}
存取驗(yàn)證碼的具體實(shí)現(xiàn)许布,我就使用了redis來做兴革,其他的小伙伴也可以使用其他存儲方案來做:
import com.zx.silverfox.common.validate.code.ValidateCode;
import com.zx.silverfox.common.validate.code.ValidateCodeRepository;
import com.zx.silverfox.common.validate.code.ValidateCodeType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.context.request.ServletWebRequest;
import java.util.concurrent.TimeUnit;
/** @author zouwei */
public class RedisValidateCodeRepository implements ValidateCodeRepository {
@Autowired private RedisTemplate<Object, Object> redisTemplate;
@Override
public void save(
ServletWebRequest request,
ValidateCode code,
ValidateCodeType type,
String codeKeyValue) {
redisTemplate.opsForValue().set(buildKey(type, codeKeyValue), code, 30, TimeUnit.MINUTES);
}
@Override
public ValidateCode get(ServletWebRequest request, ValidateCodeType type, String codeKeyValue) {
Object value = redisTemplate.opsForValue().get(buildKey(type, codeKeyValue));
if (value == null) {
return null;
}
return (ValidateCode) value;
}
@Override
public void remove(ServletWebRequest request, ValidateCodeType type, String codeKeyValue) {
redisTemplate.delete(buildKey(type, codeKeyValue));
}
/**
* @param type
* @param key
* @return
*/
private String buildKey(ValidateCodeType type, String key) {
return "code:" + type.toString().toLowerCase() + ":" + key;
}
}
發(fā)送驗(yàn)證碼前需要處理的接口:
import com.zx.silverfox.common.exception.GlobalException;
import org.springframework.web.context.request.ServletWebRequest;
/**
* @author zouwei
*/
public interface ValidateCodeHandler<C extends ValidateCode> {
/**
* 是否匹配成功
* @param request
* @param validateCodeType
* @return
*/
boolean support(ServletWebRequest request, ValidateCodeType validateCodeType);
/**
* 開始處理發(fā)送驗(yàn)證碼前的邏輯
* @param request
* @param validateCodeType
* @param validateCode
* @throws GlobalException
*/
void beforeSend(ServletWebRequest request, ValidateCodeType validateCodeType, C validateCode) throws GlobalException;
}
驗(yàn)證碼實(shí)體類:
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/** @author zouwei */
@Data
public class ValidateCode implements Serializable {
private static final long serialVersionUID = -7827043337909063779L;
private String code;
private long expireInSeconds;
private LocalDateTime expireTime;
public ValidateCode(String code, LocalDateTime expireTime) {
this.code = code;
this.expireTime = expireTime;
}
public ValidateCode(String code, long expireInSeconds) {
this.code = code;
this.expireInSeconds = expireInSeconds;
this.expireTime = LocalDateTime.now().plusSeconds(expireInSeconds);
}
/**
* 判斷是否過期
*
* @return
*/
public boolean isExpired() {
return LocalDateTime.now().isAfter(expireTime);
}
/**
* 轉(zhuǎn)換成分鐘
*
* @return
*/
public long minute() {
return this.expireInSeconds / 60;
}
}
各種驗(yàn)證碼類型,可無限擴(kuò)展:
public enum ValidateCodeType {
/** 短信驗(yàn)證碼 */
SMS {
@Override
public String getParamNameOnValidate() {
return "smsCode";
}
},
/** 圖片驗(yàn)證碼 */
IMAGE {
@Override
public String getParamNameOnValidate() {
return "imageCode";
}
},
/** 滑動圖片驗(yàn)證碼 */
SLIDE {
@Override
public String getParamNameOnValidate() {
return "slideCode";
}
};
public abstract String getParamNameOnValidate();
}
還有相關(guān)配置類:
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/** @author zouwei */
@Data
@Configuration
@ConfigurationProperties(prefix = "validate.code")
public class ValidateCodeProperties {
/** 圖像驗(yàn)證碼 */
private ImageProperties image = new ImageProperties();
/** 短信驗(yàn)證碼 */
private SmsProperties sms = new SmsProperties();
/** 滑動驗(yàn)證碼 */
private SlideImageProperties slide = new SlideImageProperties();
@Data
@EqualsAndHashCode(callSuper = true)
public static class SlideImageProperties extends CodeProperties {
private String generatorUrl = "/code/slide";
}
@Data
@EqualsAndHashCode(callSuper = true)
public static class ImageProperties extends CodeProperties {
private int length = 6;
private int height = 23;
private int width = 67;
private String generatorUrl = "/code/image";
}
@Data
@EqualsAndHashCode(callSuper = true)
public static class SmsProperties extends CodeProperties {
private int length = 6;
private String generatorUrl = "/code/sms";
}
@Data
public abstract static class CodeProperties {
private long expiredInSecond = 300;
private String[] filterUrls;
private String generatorUrl;
}
}
為了開發(fā)者使用方便,我也模仿spring boot的方式使用注解自動化配置:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** @author zouwei */
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import(ValidateCodeConfigSelector.class)
public @interface EnableValidateCode {
/**
* 驗(yàn)證碼實(shí)現(xiàn)類
*
* @return
*/
Class<? extends AbstractValidateCodeProcessor>[] value() default {
ImageValidateCodeProcessor.class
};
/**
* 驗(yàn)證碼存儲方式
*
* @return
*/
Class<? extends ValidateCodeRepository> repository() default RedisValidateCodeRepository.class;
}
import com.zx.silverfox.common.filter.ValidateCodeFilter;
import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor;
import com.zx.silverfox.common.validate.code.ValidateCodeRepository;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import java.util.Map;
public class ValidateCodeConfigSelector implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
Map<String, Object> attributeMap =
importingClassMetadata.getAnnotationAttributes(
EnableValidateCode.class.getName());
AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap);
Class<? extends ValidateCodeRepository> repositoryClass = attributes.getClass("repository");
Class<? extends AbstractValidateCodeProcessor>[] imageProcessorClass =
(Class<? extends AbstractValidateCodeProcessor>[])
attributes.getClassArray("value");
if (!registry.containsBeanDefinition("validateCodeRepository")) {
registry.registerBeanDefinition(
"validateCodeRepository", new RootBeanDefinition(repositoryClass));
}
if (ArrayUtils.isNotEmpty(imageProcessorClass)) {
for (Class<? extends AbstractValidateCodeProcessor> clazz : imageProcessorClass) {
registry.registerBeanDefinition(
clazz.getSimpleName(), new RootBeanDefinition(clazz));
}
}
if (!registry.containsBeanDefinition("validateCodeFilter")) {
registry.registerBeanDefinition(
"validateCodeFilter", new RootBeanDefinition(ValidateCodeFilter.class));
}
}
}
上述代碼基本框架已經(jīng)完成杂曲,后續(xù)代碼就是真正地實(shí)現(xiàn)圖片驗(yàn)證碼及短信驗(yàn)證碼:
簡單圖片驗(yàn)證碼:
import com.zx.silverfox.common.validate.code.ValidateCode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.awt.image.BufferedImage;
/** @author zouwei */
@Data
@EqualsAndHashCode(callSuper = true)
public class ImageValidateCode extends ValidateCode {
private transient BufferedImage image;
public ImageValidateCode(BufferedImage image, String code, long expireInSeconds) {
super(code, expireInSeconds);
this.image = image;
}
}
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import com.zx.silverfox.common.validate.code.ValidateCode;
import com.zx.silverfox.common.validate.code.ValidateCodeGenerator;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/** @author zouwei */
public class ImageValidateCodeGenerator implements ValidateCodeGenerator {
private ValidateCodeProperties.ImageProperties imageProperties;
public ImageValidateCodeGenerator(ValidateCodeProperties.ImageProperties imageProperties) {
this.imageProperties = imageProperties;
}
@Override
public ValidateCode createValidateCode(ServletWebRequest request) {
int height =
ServletRequestUtils.getIntParameter(
request.getRequest(), "height", imageProperties.getHeight());
int width =
ServletRequestUtils.getIntParameter(
request.getRequest(), "width", imageProperties.getWidth());
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
String sRand = "";
for (int i = 0; i < imageProperties.getLength(); i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(
new Color(
20 + random.nextInt(110),
20 + random.nextInt(110),
20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageValidateCode(image, sRand, imageProperties.getExpiredInSecond());
}
/**
* 生成隨機(jī)背景條紋
*
* @param fc
* @param bc
* @return
*/
private 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);
}
}
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", "圖片驗(yàn)證碼生成失敗");
}
}
@Override
protected boolean validate(String code, ImageValidateCode validateCode) {
return StringUtils.equalsIgnoreCase(code, validateCode.getCode());
}
}
滑塊驗(yàn)證碼:
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文件夾中隨機(jī)獲取一張圖片進(jìn)行處理
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;
/**
* 滑動驗(yàn)證碼
*
* @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("", "圖片驗(yàn)證碼生成失敗");
}
}
/**
* 滑動驗(yàn)證碼驗(yàn)證
*
* @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;
/** 摳圖內(nèi)部矩形填充大小 */
private static int RECTANGLE_PADDING = 8;
/** 摳圖的邊框?qū)挾?*/
private static int SLIDER_IMG_OUT_PADDING = 1;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class SlideImage {
/** 底圖 */
private String srcImg;
/** 標(biāo)記圖片 */
private String markImg;
/** x軸 */
private int x;
/** y軸 */
private int y;
/** 原圖的寬度 */
private int width;
/** 原圖的高度 */
private int height;
}
/**
* 根據(jù)傳入的路徑生成指定驗(yàn)證碼圖片
*
* @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);
}
/**
* 生成隨機(jī)滑塊形狀
*
* <p>0 透明像素 1 滑塊像素 2 陰影像素
*
* @return int[][]
*/
private static int[][] getBlockData() {
int[][] data = new int[CUT_WIDTH][CUT_HEIGHT];
Random random = new Random();
// (x-a)2+(y-b)2=r2
// x中心位置左右5像素隨機(jī)
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++) {
// 矩形區(qū)域
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;
}
// 凸出區(qū)域
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;
}
}
// 凹進(jìn)區(qū)域
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;
}
/**
* 裁剪區(qū)塊 根據(jù)生成的滑塊形狀擎勘,對原圖和裁剪塊進(jìn)行變色處理
*
* @param oriImage 原圖
* @param targetImage 裁剪圖
* @param blockImage 滑塊
* @param x 裁剪點(diǎn)x
* @param y 裁剪點(diǎn)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);
// 原圖中對應(yīng)位置變色處理
if (rgbFlg == 1) {
// 摳圖上復(fù)制對應(yīng)顏色值
targetImage.setRGB(i, j, rgb_ori);
// 原圖對應(yīng)位置顏色變化
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);
}
}
}
}
/**
* 隨機(jī)獲取一張圖片對象
*
* @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());
}
}
/**
* 將圖片轉(zhuǎn)換為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字符串轉(zhuǎn)換為圖片
*
* @param base64String
* @return
*/
public static BufferedImage base64StringToImage(String base64String) throws IOException {
try (ByteArrayInputStream bais =
new ByteArrayInputStream(Base64Utils.decodeFromString(base64String))) {
return ImageIO.read(bais);
}
}
}
短信驗(yàn)證碼:
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 {
// 手機(jī)號碼
String mobile = request.getParameter("mobile");
String type = request.getParameter("type");
if (StringUtils.isBlank(mobile) || StringUtils.isBlank(type)) {
// 獲取驗(yàn)證碼參數(shù)沒提供
throw GlobalException.newInstance(
"SMS_VALIDATE_CODE_PARAM_ERROR", "沒有給電話號碼或者指明短信類型咱揍,無法發(fā)送短信");
}
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());
}
}
注意事項(xiàng):想要使用滑塊驗(yàn)證碼,需要在resources文件夾里面創(chuàng)建一個slideimg文件夾棚饵,并且把需要的圖片放進(jìn)去:GlobalException是我自己設(shè)計(jì)的異常類煤裙,建議需要的小伙伴換成自己應(yīng)用的異常類。
ok噪漾,整個驗(yàn)證碼組件設(shè)計(jì)加上具體實(shí)現(xiàn)都已經(jīng)完畢硼砰,下面就是如何使用:
首先,把自定義注解放在springboot項(xiàng)目啟動類上
建議按需配置欣硼,如果不需要圖片驗(yàn)證碼或者滑塊驗(yàn)證碼题翰,可以不加載進(jìn)來
然后就是配置文件:比如你需要在發(fā)生短信驗(yàn)證碼之前先觸發(fā)滑塊驗(yàn)證碼,那么可以把"/code/sms"這個url放進(jìn)validate.slide.filter-urls配置中诈胜。
好吧豹障,怎么使用已經(jīng)講解完畢,配置文件中的其他配置參數(shù)包括圖片的大小和驗(yàn)證碼的位數(shù)等等焦匈,小伙伴可以根據(jù)自身需要去配置血公。