背景
在前两篇文章中给大家介绍了Spring Security的认证流程,包含对项目的定制化处理,可以明白security的设计还是比较强大的,通过一系列的过滤器注册的过滤器链,对流程进行链式的处理。
今天介绍一种以配置器的方式处理验证码生成、校验,流程可以参考Security中的FormLoginConfigurer表单登录配置器,在前两篇中提到的定制化UsernamePasswordAuthenticationFilter过滤器,就是表单登录配置器中的认证实现环节,而FormLoginConfigurer表单登录配置器的作用是定义了登录功能的入口、实现流程(从上文中可以看出我们所做的定制化是对于方法体,于配置器而言,我们的定制化是模板方法模式下的一种实现)。
配置器设计——模板方法模式
Security应用configurer配置器
Security的核心是HttpSecurity对象的应用,HttpSecurity配置完成后会通过build()方法生成SecurityFilterChain过滤器链应用到系统中。
SecurityFilterChain加入过滤器的两种方式:
- 通过HttpSecurity对象直接调用addFilterAt或者addFilterBefor方法,两种方法含义分别是:在某个过滤器的位置添加过滤器、在某个过滤器前面添加过滤器;
- 通过配置器加入过滤器,HttpSecurity在build时会把所有的配置器应用到过滤器中
下面展示下模型图
configurer配置器设计
下面是对验证码配置器的设计,大致的骨架如图,自定义的验证码配置器的三个功能:加入自定义的验证码过滤器、校验码生成器对象、校验码处理器对象。骨架是模板,子类在不改变整体结构的情况,实现流程中的某些步骤细节。定义校验码接口,是为了方便扩展多种验证码校验例如:图片、短信、语音等。
代码详解
ValidateCodeConfigurer配置器
配置器中添加了验证码处理的过滤器,声明了两个集合字段属性,key是验证码的接口地址,value是接口子类对象,分别是生成器接口子类和处理器接口子类,目的是兼容项目中多中验证方式。
public final class ValidateCodeConfigurer<H extends HttpSecurityBuilder<H>> extends AbstractHttpConfigurer<ValidateCodeConfigurer<H>, H> {
private AuthenticationFailureHandler failureHandler;
private final Map<String, ValidateCodeGenerator> validateCodeGenerators = new HashMap<>();
private final Map<String, ValidateCodeProcessor> validateCodeProcessors = new HashMap<>();
public ValidateCodeConfigurer() {
}
@Override
public void configure(H http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(this.validateCodeGenerators, this.validateCodeProcessors);
validateCodeFilter.setFailureHandler(this.failureHandler);
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
*
* @param generateUrl 应用接口地址
* @param validateCodeGenerator 生成器子类
* @return
*/
public ValidateCodeConfigurer<H> validateCodeGenerator(String generateUrl, ValidateCodeGenerator validateCodeGenerator) {
Assert.isNull(this.validateCodeGenerators.get(generateUrl), StrUtil.format("{} has already exist", generateUrl));
this.validateCodeGenerators.put(generateUrl, validateCodeGenerator);
return ValidateCodeConfigurer.this;
}
/**
*
* @param generateUrl 应用接口地址
* @param validateCodeProcessor 处理器子类
* @return
*/
public ValidateCodeConfigurer<H> validateCodeProcessor(String processingUrl, ValidateCodeProcessor validateCodeProcessor) {
Assert.isNull(this.validateCodeProcessors.get(processingUrl), StrUtil.format("{} has already exist", processingUrl));
this.validateCodeProcessors.put(processingUrl, validateCodeProcessor);
return ValidateCodeConfigurer.this;
}
public ValidateCodeConfigurer<H> failureHandler(AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.failureHandler = failureHandler;
return ValidateCodeConfigurer.this;
}
}
ValidateCodeFilter过滤器
验证码过滤器在doFilterInternal()方法中做了放行的校验,随即执行了验证码的生成或者处理。
放行的校验:生成和处理的子类是以Map的形式存在Filter中,key是具体的接口地址,所以如果不为获取验证码地址或登录接口地址,Filter会直接放行
@RequiredArgsConstructor
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private final Map<String, ValidateCodeGenerator> validateCodeGenerators;
private final Map<String, ValidateCodeProcessor> validateCodeProcessors;
private AuthenticationFailureHandler failureHandler;
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
// init
Assert.isNull(this.failureHandler, "failureHandler cannot be null");
// validateCodeGenerators 和 validateCodeProcessors 不能都为空
Assert.isTrue(CollUtil.isEmpty(validateCodeGenerators) && CollUtil.isEmpty(validateCodeProcessors), "validateCodeGenerators and validateCodeProcessors must be init at least one");
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
if (!requiresValidateMethod(request, response)) {
chain.doFilter(request, response);
return;
}
ValidateCodeGenerator validateCodeGenerator;
ValidateCodeProcessor validateCodeProcessor;
// generate
if (null != (validateCodeGenerator = getValidateGenerator(request, response))) {
try {
// 判断认证状态
if (validateCodeGenerator.needAuthenticated()
&& null == SecurityContextHolder.getContext().getAuthentication()) {
throw new BadCredentialsException("Failed to generate since no authentication found");
}
validateCodeGenerator.send(request, response, validateCodeGenerator.create(request));
} catch (AuthenticationException e) {
if (e instanceof ValidateCodeException) {
this.failureHandler.onAuthenticationFailure(request, response, e);
} else if (e instanceof BadCredentialsException) {// 透出未登录异常
this.failureHandler.onAuthenticationFailure(request, response, e);
} else {
logger.error("validate code send error", e);
this.failureHandler.onAuthenticationFailure(request, response, new ValidateCodeException("验证码发送出错"));
}
}
return;
}
// validate
if (null != (validateCodeProcessor = getValidateProcessor(request, response))) {
try {
validateCodeProcessor.validate(request);
} catch (AuthenticationException e) {
if (e instanceof ValidateCodeException) {
this.failureHandler.onAuthenticationFailure(request, response, e);
} else {
logger.error("validate code validate error", e);
this.failureHandler.onAuthenticationFailure(request, response, new ValidateCodeException("验证码校验出错"));
}
return;
}
}
chain.doFilter(request, response);
}
private ValidateCodeGenerator getValidateGenerator(HttpServletRequest request, HttpServletResponse response) {
return validateCodeGenerators.get(request.getRequestURI());// 简单直接获取,避免循环 match
}
private ValidateCodeProcessor getValidateProcessor(HttpServletRequest request, HttpServletResponse response) {
return validateCodeProcessors.get(request.getRequestURI());// 简单直接获取,避免循环 match
}
private boolean requiresValidateMethod(HttpServletRequest request, HttpServletResponse response) {
String method = request.getMethod();
return HttpMethod.GET.matches(method) || HttpMethod.POST.matches(method);
}
public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
Assert.notNull(failureHandler, "failureHandler cannot be null");
this.failureHandler = failureHandler;
}
}
ValidateCodeRepository验证码缓存操作接口
这个是将验证码的缓存拉出来做了设计,为了其它形式的缓存扩展。接口定义了保存、获取验证码方法。
public interface ValidateCodeRepository {
/**
* 保存验证码
*
* @param validateCode the code
* @param expireIn 失效时间,单位秒
*/
void save(ValidateCode validateCode, int expireIn);
/**
* 获取验证码
*
* @param uid 验证码唯一 ID
* @return validate code
*/
ValidateCode get(String uid);
/**
* 获取验证码
*
* @param uid 验证码唯一 ID
* @return validate code
*/
default ValidateCode getAndRemove(String uid) {
ValidateCode validateCode = get(uid);
remove(uid);
return validateCode;
}
/**
* 移除验证码
*
* @param uid 验证码唯一 ID
*/
void remove(String uid);
}
@Slf4j
@RequiredArgsConstructor
public class RedissonValidateCodeRepository implements ValidateCodeRepository {
private String keyPrefix = "VALIDATE:CODE";
private final RedissonClient redissonClient;
@Override
public void save(ValidateCode validateCode, int expireIn) {
RBucket<ValidateCode> bucket = getBucket(validateCode.getUid());
bucket.set(validateCode, expireIn, TimeUnit.SECONDS);
}
@Override
public ValidateCode get(String uid) {
return getBucket(uid).get();
}
@Override
public ValidateCode getAndRemove(String uid) {
return getBucket(uid).getAndDelete();
}
@Override
public void remove(String uid) {
getBucket(uid).delete();
}
private RBucket<ValidateCode> getBucket(String uid) {
if (StringUtils.isBlank(uid)) {
throw new ValidateCodeException("验证码的id不能为空");
}
String key = getKeyPrefix() + ":" + uid;
return redissonClient.getBucket(key);
}
public String getKeyPrefix() {
return keyPrefix;
}
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
}
ValidateCode验证码对象封装
对验证码的一些属性做一层封装,包含了uid、code、type、expireIn、expireTime等,其子类对自己相关特征再添加其余信息,比如图片验证码,将Base64的图片放在子类中。
@Getter
@Setter
public class ValidateCode implements Serializable {
/**
* 验证码类型
*/
private ValidateCodeType type;
/**
* 校验码唯一 ID
*/
private final String uid;
/**
* 校验码
*/
private String code;
/**
* 有效期,单位秒
*/
private int expireIn;
/**
* 失效时间
*/
private LocalDateTime expireTime;
/**
* @param type 类型
* @param uid 校验码唯一 ID
* @param code 校验码
* @param expireIn 有效期, 单位秒
*/
public ValidateCode(ValidateCodeType type, String uid, String code, int expireIn) {
this.type = type;
this.uid = uid;
this.code = code;
this.expireIn = expireIn;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
/**
* 设置校验码有效期
*
* @param expireIn 有效期,单位秒
*/
public void setExpireTime(int expireIn) {
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
/**
* 是否过期
*
* @return true/false
*/
public boolean isExpired() {
return LocalDateTime.now().isAfter(expireTime);
}
}
@Getter
@Setter
public class ImageCode extends ValidateCode {
private String base64Str;
public ImageCode(String uid, String code, int expireIn, String base64Str) {
super(ValidateCodeType.IMAGE, uid, code, expireIn);
this.base64Str = base64Str;
}
}
ValidateCodeGenerator校验码生成器接口
定义了验证码生成的模板方法,方便子类以扩展和实现。目前使用的rediss做缓存操作。
public interface ValidateCodeGenerator {
/**
* 生成校验码
*
* @param request the request
* @return validate code
*/
ValidateCode generate(HttpServletRequest request);
/**
* 生成并保存校验码
*
* @param request the request
* @return validate code
*/
ValidateCode create(HttpServletRequest request);
/**
* 发送校验码
*
* @param request the request
* @param response the response
* @param validateCode the validateCode
*/
void send(HttpServletRequest request, HttpServletResponse response, ValidateCode validateCode);
/**
* 是否需要认证
*
* @return boolean
*/
default boolean needAuthenticated() {
return false;
}
}
@Getter
@Setter
@Slf4j
@Accessors(chain = true)
public class ImageCodeGenerator implements ValidateCodeGenerator {
/**
* 基础字符,排除易混淆字符: o, O, i, I, l, L, p, P, q, Q
*/
public static final String BASE_CHAR_WITHOUT = "abcdefghjkmnrstuvwxyz";
public static final String BASE_CHAR = "abcdefghijklmnopqrstuvwxyz";
public static final String BASE_NUMBER = "1234567890";
private ValidateCodeRepository validateCodeRepository;
/**
* 验证码类型
*/
private ValidateCodeType type;
/**
* 有效期, 单位秒
*/
private int expireIn = 60;
/**
* 验证码位数,默认 4 位
*/
private int count = 4;
/**
* 是否仅数字,默认否
*/
private boolean numberOnly = false;
/**
* 是否需要认证
*/
private boolean needAuthenticated = false;
public ImageCodeGenerator(ValidateCodeRepository validateCodeRepository) {
this.validateCodeRepository = validateCodeRepository;
}
@Override
public ImageCode generate(HttpServletRequest request) {
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(75, 30, getCount(), 4);
lineCaptcha.setGenerator(new RandomGenerator(isNumberOnly() ? BASE_NUMBER : BASE_NUMBER + BASE_CHAR_WITHOUT, getCount()));
return new ImageCode(IdUtil.fastUUID(), lineCaptcha.getCode(), getExpireIn(), lineCaptcha.getImageBase64Data());
}
@Override
public ValidateCode create(HttpServletRequest request) {
ImageCode imageCode = generate(request);
getValidateCodeRepository().save(imageCode, imageCode.getExpireIn());
return imageCode;
}
@Override
public void send(HttpServletRequest request, HttpServletResponse response, ValidateCode validateCode) {
if (!(validateCode instanceof ImageCode)) {
throw new ValidateCodeException("发送验证码错误, 未生成图形");
}
ImageCode imageCode = (ImageCode) validateCode;
if (StrUtil.isBlank(imageCode.getBase64Str())) {
throw new ValidateCodeException("发送验证码错误, 未生成图形");
}
ImageCodeResponse responseData = new ImageCodeResponse().setUid(imageCode.getUid())
.setCaptcha(imageCode.getBase64Str());
log.info("发送图形验证码[{}]成功...", validateCode.getUid());
HttpContextUtil.write(request, response, Rest.success(responseData));
}
}
ValidateCodeProcessor校验码处理器
设计上和生成器类似,接口定义方法,不同类型去实现接口形成不同的子类。
public interface ValidateCodeProcessor {
/**
* 校验验证码(验证后删除)
*
* @param servletWebRequest the servlet web request
*/
void validate(HttpServletRequest servletWebRequest);
/**
* 校验验证码(验证后不删除)
*
* @param servletWebRequest the servlet web request
*/
void check(HttpServletRequest servletWebRequest);
}
@Slf4j
@RequiredArgsConstructor
public class DefaultValidateCodeProcessor implements ValidateCodeProcessor {
private String keyParameter = "uid";
private String valueParameter = "captcha";
private final ValidateCodeRepository validateCodeRepository;
protected String obtainKey(HttpServletRequest request) {
String key = request.getParameter(keyParameter);
if (StringUtils.isBlank(key)) {
throw new ValidateCodeException("验证码的id不能为空");
}
return key;
}
protected String obtainValue(HttpServletRequest request) {
String value = request.getParameter(valueParameter);
if (StringUtils.isBlank(value)) {
throw new ValidateCodeException("验证码的id不能为空");
}
return value;
}
/**
* 验证后删除
*
* @param request the request
*/
@Override
public void validate(HttpServletRequest request) {
String key = obtainKey(request);
String value = obtainValue(request);
ValidateCode validateCode = validateCodeRepository.getAndRemove(key);
if (validateCode == null || validateCode.isExpired()) {
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equalsIgnoreCase(value, validateCode.getCode())) {
throw new ValidateCodeException("验证码不匹配");
}
log.info("验证码[{}]校验通过...", key);
}
/**
* 验证
*
* @param request the request
*/
@Override
public void check(HttpServletRequest request) {
String key = obtainKey(request);
String value = obtainValue(request);
ValidateCode validateCode = validateCodeRepository.get(key);
if (validateCode == null || validateCode.isExpired()) {
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equalsIgnoreCase(value, validateCode.getCode())) {
throw new ValidateCodeException("验证码不匹配");
}
}
public String getKeyParameter() {
return keyParameter;
}
public ValidateCodeProcessor setKeyParameter(String keyParameter) {
this.keyParameter = keyParameter;
return DefaultValidateCodeProcessor.this;
}
public String getValueParameter() {
return valueParameter;
}
public ValidateCodeProcessor setValueParameter(String valueParameter) {
this.valueParameter = valueParameter;
return DefaultValidateCodeProcessor.this;
}
}