一、图片验证码
我们知道在登录界面往往要添加图形验证码来进行人机验证,这里我们就来在之前的认证流程中加入图形验证码校验功能。
1、开发生成图型验证码接口
流程其实很简单,首先根据随机数生成图片,然后将随机数存到session中,最后将生成的图片写入响应中,没了。这里生成图片什么的就不说了。网上一大堆,记录下这里的一些代码思路。首先是图片验证码信息类
/**
* 图形验证码
*/
public class ImageCode {
private BufferedImage image;
private String code;
private LocalDateTime expireTime; // 过期时间
/**
* @param image
* @param code
* @param expireIn 过期时间,单位秒
*/
public ImageCode(BufferedImage image, String code, int expireIn) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
// 是否过期
public boolean isExpried() {
return this.expireTime.isBefore(LocalDateTime.now());
}
然后验证码服务
@RestController
public class ValidateCodeController {
private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
// 这里又使用了spring的工具类来操作session
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = createImageCode(request);
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
response.setContentType("image/jpeg");
//禁止图像缓存。
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
private ImageCode createImageCode(HttpServletRequest request) throws IOException {
String code = RandomStringUtils.randomAlphanumeric(4);
BufferedImage image = createImageCode(80, 40, code);
return new ImageCode(image, code, 60);
}
当然,不要忘了该服务需要配置放行。
2、在认证流程中加入图像验证码校验
之前已经看过Spring Security源码了,我们知道如果想在Security中加入功能,只要把过滤器添加到spring Security现有的过滤器链上就可以了。现在开始编写验证码过滤器
/**
* 图片验证码验证过滤器
* OncePerRequestFilter spring提供的,保证在一个请求中只会被调用一次
*/
public class ValidateCodeFilter extends OncePerRequestFilter {
// 在初始化本类的地方进行注入
// 一般在配置security http的地方进行添加过滤器
private AuthenticationFailureHandler failureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 为登录请求,并且为post请求【即登录请求时,先验证图片验证码】
if (StringUtils.equals("/authentication/form", request.getRequestURI())
&& StringUtils.equalsAnyIgnoreCase(request.getMethod(), "post")) {
try {
validate(request);//验证
} catch (ValidateCodeException e) {
failureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}
private void validate(HttpServletRequest request) throws ServletRequestBindingException {
// 拿到之前存储的imageCode信息
ServletWebRequest swr = new ServletWebRequest(request);
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(swr, ValidateCodeController.SESSION_KEY);
// 又是一个spring中的工具类,
// 试问一下,如果不看源码怎么可能知道有这些工具类可用?
String codeInRequest = ServletRequestUtils.getStringParameter(request, "imageCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在");
}
if (codeInSession.isExpried()) {
sessionStrategy.removeAttribute(swr, ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不匹配");
}
//移除验证码
sessionStrategy.removeAttribute(swr, ValidateCodeController.SESSION_KEY);
}
public AuthenticationFailureHandler getFailureHandler() {
return failureHandler;
}
public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
this.failureHandler = failureHandler;
}
}
把过滤器添加到现有认证流程中,我们要放在UsernamePasswordAuthenticationFilter过滤器之前
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setFailureHandler(myAuthenticationFailureHandler);
http
// 由源码得知,在最前面的是UsernamePasswordAuthenticationFilter
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
// 定义表单登录 - 身份认证的方式
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
需要注意的一个地方就是myAuthenticationFailureHandler,因为失败会调用这个处理器。
3、图片验证码重构
我们的目的是写一个可重用的安全模块,但是上面~~~很乱,不过代码还不多,现在重新梳理一下。那我们就从项目开始,首先我们创建如下几个项目【实际项目使用时,按需依赖下面具体的项目模块即可】:
自此项目搭建完成。现在开始进行代码重构。
1.配置信息抽取
我们如果想写一个可重用的项目模块,就需要把一些配置(比如自定义登录请求等)让使用方来进行配置。思路是使用配置文件,比如在application.yml中:
tin:
security:
browser:
loginPage: /tin-signIn.html
由于这些配置类是 token 和 browser 项目公用的,写在core里面
@ConfigurationProperties(prefix = "tin.security")
@Data
public class SecurityProperties {
//浏览器端配置, tin.security.browser 路径下的配置会被映射到该配置类中
private BrowserProperties browser = new BrowserProperties();
//验证码配置
private ValidateCodeProperties code = new ValidateCodeProperties();
//当请求需要身份认证时,默认跳转的url
private String authenticationUrl = SecurityConstant.DEFAULT_UNAUTHENTICATION_URL;
//默认的用户请求登录处理url
private String loginProcessingUrl = SecurityConstant.DEFAULT_LOGIN_PROCESSING_URL_FORM;
}
然后我们在看看 BrowserProperties 的内部实现
@Data
public class BrowserProperties {
//session相关配置,类推
private SessionProperties session = new SessionProperties();
//默认的注册页面
private String signUpUrl = "/tin-signUp.html";
//默认的登录页面
private String loginPage = "/tin-signIn.html";
//默认的退出登录时跳转的url。如果配置了,则跳到指定的url,如果没配置,则返回json数据。
private String signOutUrl;
//默认的登录响应方式
private LoginResponseType loginType = LoginResponseType.JSON;
}
为了职责分离,单独写一个入口被扫描开启的配置类(也在core中)
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}
然后我们就可以使用配置的方式,对这些信息进行配置了。之前写死 登录页面 相关代码处都需要更改从配置类中获取(browser),使用 Autowired 注解可以获得SecurityProperties配置类实例
@Autowired
private SecurityProperties securityProperties;
// 等等。。。
.antMatchers(//"/authentication/require",
securityProperties.getBrowser().getLoginPage()).permitAll()
2.图片验证码重构
其他部分的代码重构后面再说,现在先进行图片验证码的代码重构。我们重构的目的是:
- 验证码基本参数可配置
- 验证码校验拦截的接口可配置
- 验证码的生成逻辑可配
1.验证码基本参数配置
在参数配置中上面的会覆盖下级的配置,请求级配置(配置值在调用接口的时候传递)会覆盖应用级配置(配置写在portal项目中);应用级配置会覆盖默认配置(配置值写在core项目中)。首先创建图形验证码配置类
@Data
public class ImageCodeProperties {
//图片验证码长度
private int length = 4;
//图片验证码过期时间
private int expireIn = 60;
//图片验证码宽
private int width = 67;
//图片验证码高
private int height = 23;
}
然后用验证码配置类封装图片验证码配置类
@Data
public class ValidateCodeProperties {
//图片验证码配置
private ImageCodeProperties image = new ImageCodeProperties();
//后面还会新增短信验证码等的配置
}
最后加入总配置类中
@ConfigurationProperties(prefix = "tin.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
private ValidateCodeProperties code = new ValidateCodeProperties();
然后修改处理逻辑处中的代码即可,将之前写死的改成配置项。
2.验证码校验拦截的接口可配置
我们提供url拦截地址配置属性,然后在过滤器中获取配置的属性,并且循环匹配。首先增加url配置属性
@Data
public class ImageCodeProperties {
//图片验证码长度
private int length = 4;
//图片验证码过期时间
private int expireIn = 60;
//需要拦截的路径
private String[] interceptUrls = {};
//图片验证码宽
private int width = 67;
//图片验证码高
private int height = 23;
}
然后在过滤器中对目标url进行匹配逻辑
@Slf4j
@Component("validateCodeFilter")
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
@Autowired
private AuthenticationFailureHandler failureHandler;
@Autowired
private SecurityProperties securityProperties;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
// 存储所有需要拦截的url
private Set<String> urls;
////验证请求url与配置的url是否匹配的工具类
private AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* org.springframework.beans.factory.InitializingBean 保证在其他属性都设置完成后,有beanFactory调用
* 但是在这里目前还是需要初始化处调用该方法
*/
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrl = securityProperties.getCode().getImage().getInterceptUrls();
urls = Stream.of(configUrl).collect(Collectors.toSet());
urls.add("/authentication/form"); // 登录请求
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 为登录请求,并且为post请求
boolean action = false;
for (String url : urls) {
// org.springframework.util.AntPathMatcher 能匹配spring中的url模式
// 支持通配符路径那种
if (pathMatcher.match(url, request.getRequestURI())) {
action = true;
}
}
if (action) {
try {
validate(request);
} catch (ValidateCodeException e) {
failureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}
然后在原配置中心进行属性注入
// 有三个configure的方法,这里使用http参数的
@Override
protected void configure(HttpSecurity http) throws Exception {
// 最简单的修改默认配置的方法
// 在v5+中,该配置(表单登录)应该是默认配置了
// basic登录(也就是弹框登录的)应该是v5-的版本默认
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setFailureHandler(myAuthenticationFailureHandler);
validateCodeFilter.setSecurityProperties(securityProperties); // 注入配置属性类
validateCodeFilter.afterPropertiesSet(); // 初始化url配置
测试:tin-portal/application.yml
tin:
security:
browser:
loginType: JSON
code:
image:
width: 100
height: 50
interceptUrls[0]: /order
interceptUrls[1]: /user/*
3.验证码的生成逻辑可配
逻辑可配,就是抽象成接口,然后由客户端提供。这里提供一个生成图片信息的接口
public interface ValidateCodeGenerate {
ImageCode generate(HttpServletRequest request) throws IOException;
}
然后实现默认图片生成接口类
public class ImageCodeGenerate implements ValidateCodeGenerate {
private SecurityProperties securityProperties;
//就是返回生成的图片验证码对象即可
@Override
public ImageCode generate(HttpServletRequest request) throws IOException {
return createImageCode(request);
}
public ImageCode createImageCode(HttpServletRequest request) throws IOException {
ImageCodeProperties imageProperties = securityProperties.getCode().getImage()
int width = ServletRequestUtils.getIntParameter(request, "width", imageProperties.getWidth());
int height = ServletRequestUtils.getIntParameter(request, "height", imageProperties.getHeight());
int length = ServletRequestUtils.getIntParameter(request, "length", imageProperties.getLength());
int expireIn = ServletRequestUtils.getIntParameter(request, "expireIn", imageProperties.getExpireIn());
String code = RandomStringUtils.randomNumeric(length);
BufferedImage image = createImageCode(width, height, code);
return new ImageCode(image, code, expireIn);
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
//...后面的就是具体的工具类代码 不贴了
增加配置类,初始化图片生成器实例。这个是重点!!!
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
// 条件注解,spring 容器中如果存在imageCodeGenerate的bean就不会再初始化该bean了
@ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
public ValidateCodeGenerate imageValidateCodeGenerator() {
ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
}
之前调用处修改成调用接口【1-1】
public class ValidateCodeController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired // 使用生成接口处理信息
private ValidateCodeGenerate validateCodeGenerate;
二、短信验证码
短信验证码登录是目前惯用的登录方式之一,这里我们就先来实现短信验证码的功能。这里的套路与之前图形验证码的套路类似(后面会实现短信验证码登录)。
1、开发短信验证码接口
我们在之前的验证码API上加上短信验证码的服务。
@RestController
public class ValidateCodeController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ValidateCodeGenerate smsCodeGenerate;
@Autowired
private SmsCodeSender smsCodeSender;
@GetMapping("/code/sms")
public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {
ValidateCode validateCode = smsCodeGenerate.generate(request);
String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, validateCode.getCode());
smsCodeSender.send(mobile, validateCode.getCode());
}
然后配置类上
@Configuration
public class ValidateCodeBeanConfig{
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
public ValidateCodeGenerate imageValidateCodeGenerator() {
//...
}
// 这里由于产生了多个ValidateCodeGenerate的实现类,所以需要使用name来区分
@Bean
@ConditionalOnMissingBean(name = "smsValidateCodeGenerator")
public ValidateCodeGenerator smsValidateCodeGenerator() {
SmsCodeGenerator codeGenerator = new SmsCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
}
短信验证码生成类
public class SmsCodeGenerate implements ValidateCodeGenerate {
private SmsCodeProperties smsCodeProperties;
public SmsCodeGenerate(SmsCodeProperties smsCodeProperties) {
this.smsCodeProperties = smsCodeProperties;
}
@Override
public ValidateCode generate(HttpServletRequest request) {
int count = smsCodeProperties.getLength();
int expireIn = smsCodeProperties.getExpireIn();
String smsCode = RandomStringUtils.randomNumeric(count);
//ValidateCode——验证码,把之前的ImageCode给抽取了一下,ImageCode继承ValidateCode【code,expireTime;】
return new ValidateCode(smsCode, expireIn);
}
这里目前没有什么特别的,都是伪代码,提供一种思路。 还有就是贴出来的代码与之前图形验证码的部分代码重合了,就重构了。
2、重构验证码逻辑
由于发现有好多逻辑都是重复的,所以在这里我们进行深度重构抽象。验证码处理器结构如下:
上图逻辑清晰,看着么多类,实际上是把变化的部分抽象成接口了,公共的逻辑使用模版方法模式封装起来了。以后可以应对不同的变化,比如:
- 图形验证码或则短信验证码的 生成逻辑变了,提供ValidateCodeGenerator实现类即可
- 图形或则短信验证码的响应/发送逻辑变了,提供AbstractValidateCodeProcessor的子类实现abstract void send发送方法
经过上面的思路分析,就是把变化的流程单独拿出来了。下面先把主要的重构代码粘一下吧,看一下大体流程。首先我们来看一下ValidateCodeController实现
@RestController
@RequestMapping(SecurityConstant.DEFAULT_VALIDATE_CODE_URL_PREFIX)
public class ValidateCodeController {
@Autowired
private ValidateCodeProcessorHolder validateCodeProcessorHolder;
/*
* 功能描述:根据不同类型的参数获取验证码
* 创建时间:2018/11/29 11:39
* 入参:[request, response, type 验证码类型【image:图片验证码;sms:短信验证码;email:邮箱验证码】]
* 返回值:void
*/
@GetMapping("/{type}")
public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type)
throws Exception {
validateCodeProcessorHolder.findValidateCodeProcessor(type).create(new ServletWebRequest(request, response));
}
}
很简单,不过这里把创建手机、图片验证码等通过url传参合并了,我们跟进一下findValidateCodeProcessor
@Autowired
private Map<String, ValidateCodeProcessor> validateCodeProcessors;//这里我们后面会讲
public ValidateCodeProcessor findValidateCodeProcessor(String type) {
String name = type.toLowerCase() + ValidateCodeProcessor.class.getSimpleName();
ValidateCodeProcessor processor = validateCodeProcessors.get(name);
if (processor == null) {
throw new ValidateCodeException("验证码处理器" + name + "不存在");
}
return processor;
}
这里主要干的事就是查找是否存在我们的验证码校验器(根据type),如果不存在则抛异常。如果存在,我们回去看上一块内容,即调用ValidateCodeProcessor.create方法。其实ValidateCodeProcessor接口中内容很简单,创建和校验
public interface ValidateCodeProcessor {
//验证码放入session时的前缀
String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";
//创建校验码
void create(ServletWebRequest request) throws Exception;
//校验验证码
void validate(ServletWebRequest request);
}
AbstractValidateCodeProcessor的create实现如下:
public void create(ServletWebRequest request) throws Exception {
C validateCode = generate(request);//调用验证码生成器,生成验证码
save(request, validateCode);//保存验证码到Session
send(request, validateCode);//发送验证码
}
其中send方法为抽象方法,需要子类来实现。好了,抽取完成!还有一个遗留问题,
@Autowired
private Map<String, ValidateCodeGenerate> validateCodeGenerates;
此功能为 收集系统中所有 {@link ValidateCodeGenerate} 接口的实现,spring会查找所有ValidateCodeGenerate的实现,beanName做为key,实现作为value注入这里。在 ValidateCodeProcessorHolder、AbstractValidateCodeProcessor 中声明的该参数,再来看下在其他地方是怎么初始化的
@Bean
@ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
public ValidateCodeGenerator imageValidateCodeGenerator() {
ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
@Bean
@ConditionalOnMissingBean(name = "smsValidateCodeGenerator")
public ValidateCodeGenerator smsValidateCodeGenerator() {
SmsCodeGenerator codeGenerator = new SmsCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
上面使用了 条件排除,可以看出来这里有一个限制,就是短信和图形验证码的生成接口都使用的同一个。那么这里在排除的时候就只能写上beanName来限制了,对于上面的依赖查找技巧不会产生任何问题,但是对于使用处想替换该实现的时候。对于bean的name只能是覆盖这里的同名name,否则就会出现配置不成功的问题。
三、邮箱验证码
邮箱验证码的实现就很简单了,和上面没什么区别。我们在上面的实现流程上补上邮箱验证
1、开发邮箱验证码接口
上面的代码开发完成后,邮箱验证码接口就很简单了,基本上和短信验证码一致,这里就不详说了(EmailCodeProcessor,EmailCodeGenerator,EmailCodeSender)。我们就在上面的基础上再改写一下这几个验证码对拦截路径的处理。首先,邮箱验证码同样有配置信息
@Data
public class EmailCodeProperties {
//邮箱验证码长度
private int length = 6;
//邮箱验证码过期时间
private int expireIn = 600;
//需要拦截的路径
private String[] interceptUrls = {};
}
把其加入到验证码配置中
@Data
public class ValidateCodeProperties {
//图片验证码配置
private ImageCodeProperties image = new ImageCodeProperties();
//手机验证码配置
private SmsCodeProperties sms = new SmsCodeProperties();
//邮箱验证码配置
private EmailCodeProperties email = new EmailCodeProperties();
}
然后我们再回到图片验证码验证过滤器ValidateCodeFilter中,还记得之前的代码吗?好吧,这里重粘一下
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
// 存储所有需要拦截的url
private Set<String> urls;
////验证请求url与配置的url是否匹配的工具类
private AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrl = securityProperties.getCode().getImage().getInterceptUrls();
urls = Stream.of(configUrl).collect(Collectors.toSet());
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
for (String url : urls) {
if (pathMatcher.match(url, request.getRequestURI())) {
validate(request);
}
}
filterChain.doFilter(request, response);
}
private void validate(HttpServletRequest request) throws ServletRequestBindingException {
// 拿到之前存储的imageCode信息
ServletWebRequest swr = new ServletWebRequest(request);
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(swr, ValidateCodeController.SESSION_KEY);
String codeInRequest = ServletRequestUtils.getStringParameter(request, "imageCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}
//。。。各种条件判断:验证码不存在,验证码已过期,验证码不匹配
}
}
首先通过afterPropertiesSet方法获取所有的图片验证码需要拦截的路径集合(Set),然后在做url过滤时循环匹配,如果匹配成功调用validate方法进行验证码校验,当然这里只支持图片验证码。如果我们想支持所有的验证码验证类型,这里就不能用Set了,改成Map
//存放所有需要校验验证码的url,key是url value是type
private Map<String, ValidateCodeType> urlMap = new HashMap<>();
这里用到了ValidateCodeType枚举,我把代码粘上
/*
* 功能描述:验证码类型
*/
public enum ValidateCodeType {
/*
* 功能描述:验证短信验证码时,http请求中默认的携带短信验证码信息的参数的名称
*/
SMS {
@Override
public String getParamNameOnValidate() {
return "smsCode";
}
},
/*
* 功能描述:验证图片验证码时,http请求中默认的携带图片验证码信息的参数的名称
*/
IMAGE {
@Override
public String getParamNameOnValidate() {
return "imageCode";
}
},
/*
* 功能描述:验证邮箱验证码时,http请求中默认的携带邮箱验证码信息的参数的名称
*/
EMAIL{
@Override
public String getParamNameOnValidate() {
return "emailCode";
}
};
/*
* 功能描述:校验时从请求中获取的参数的名字
*/
public abstract String getParamNameOnValidate();
}
然后我们在afterPropertiesSet中分别加载
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
//手机验证码需要拦截的路径
for (String url : securityProperties.getCode().getSms().getInterceptUrls()) {
addUrlToMap(url, ValidateCodeType.SMS);
}
//图片验证码需要拦截的路径
for (String url : securityProperties.getCode().getImage().getInterceptUrls()){
addUrlToMap(url, ValidateCodeType.IMAGE);
}
//邮箱验证码需要拦截的路径
for (String url : securityProperties.getCode().getEmail().getInterceptUrls()) {
addUrlToMap(url, ValidateCodeType.EMAIL);
}
}
在doFilterInternal方法中我们做的主要有两件事:匹配url获取要拦截的请求;验证。下面我们先做第一件事
private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
ValidateCodeType result = null;
//这里限制不能是get请求方式
if (!StringUtils.equalsIgnoreCase(request.getMethod(), "get")) {
Set<String> urls = urlMap.keySet();
for (String url : urls) {
//支持通配符
if (pathMatcher.match(url, request.getRequestURI())) {
result = urlMap.get(url);
break;
}
}
}
return result;
}
这样就可以匹配到需要验证的请求并知道他的验证类型了。然后后面再根据其验证码类型进行相应校验即可。我们上面已经封装了验证码校验器ValidateCodeProcessor,所有这里直接使用即可
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
ValidateCodeType type = getValidateCodeType(request);
if (type != null) {
log.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type);
try {
validateCodeProcessorHolder.findValidateCodeProcessor(type)
.validate(new ServletWebRequest(request, response));
log.info("验证码校验通过");
} catch (ValidateCodeException exception) {
authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
return;
}
}
chain.doFilter(request, response);
}
2、默认的邮箱验证码发送实现
我们知道邮箱发送是要根据具体业务调取相应的邮箱服务商接口的,这里肯定不适合放到core下面,但是我们为了功能的完整性可以先实现一个简单的默认实现,后面其他模块使用时直接替换掉即可(当然短信发送吗默认实现也一样)。下面是一个简单实现
public class DefaultEmailCodeSender implements EmailCodeSender{
@Override
public void send(String email, String code, ValidateCodeTemplate template) {
System.out.println("向邮箱"+email+"发送验证码"+code);
}
}
然后在ValidateCodeBeanConfig中配置条件注入即可
//短信验证码发送方式
@Bean
@ConditionalOnMissingBean(SmsCodeSender.class)
public SmsCodeSender smsCodeSender() {
return new DefaultSmsCodeSender();
}
//邮箱验证码发送方式
@Bean
@ConditionalOnMissingBean(EmailCodeSender.class)
public EmailCodeSender emailCodeSender() {
return new DefaultEmailCodeSender();
}
3、验证码模板
当我们使用图片验证码时,直接生成一种验证码图片返回给页面,没有什么格式上的区别。但是对于邮箱/短信验证码是有格式区别的,比如注册和登录时给用户发送的验证码模板是不一样的。下面我们就来实现不同情况下发送不同模板的验证码,首先我们来定义一个枚举对象
/**
* 功能描述 :验证码模板
*/
public enum ValidateCodeTemplate {
//默认
NONE,
//登录
LONGIN,
//注册
REGISTER,
//修改密码
CHANGEPASSWORD;
public static ValidateCodeTemplate get(String code) {
ValidateCodeTemplate template = ValidateCodeTemplate.NONE;
if (code.equals("0")){
template = ValidateCodeTemplate.NONE;
}else if (code.equals("1")){
template = ValidateCodeTemplate.LONGIN;
}else if (code.equals("2")){
template = ValidateCodeTemplate.REGISTER;
}else if (code.equals("3")){
template = ValidateCodeTemplate.CHANGEPASSWORD;
}
return template;
}
}
同时,我们在ValidateCode中也加一个模板变量
public class ValidateCode implements Serializable {
private String code;////验证码
private ValidateCodeTemplate template = ValidateCodeTemplate.NONE;//模板,默认没有模板
private LocalDateTime expireTime;//保留时间
至于在发送验证码时对不同情况的区分,我们通过request中的参数来判断,下面我们改一下EmailCodeProcessor#send方法
protected void send(ServletWebRequest request, ValidateCode validateCode) throws Exception {
String emailParam = SecurityConstant.DEFAULT_EMAIL_PARAMETER;
String templateParam = SecurityConstant.DEFAULT_TEMPLATE_PARAMETER;
String email = ServletRequestUtils.getRequiredStringParameter(request.getRequest(), emailParam);
String template = "0";
try {//可以为空
template = ServletRequestUtils.getRequiredStringParameter(request.getRequest(), templateParam);
}catch (Exception e){
}
emailCodeSender.send(email, validateCode.getCode(), ValidateCodeTemplate.get(template));
}