短信验证码接口开发
- 短信验证码生成接口
- 短信验证码发送接口
- 短信生成策略模板模式重构
1.短信验证码接口开发
1.1短信验证码生成接口
发送短信验证码controller
package com.rui.tiger.auth.core.captcha;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 验证码控制器
*
* @author CaiRui
* @date 2018-12-10 12:13
*/
@RestController
@Slf4j
public class CaptchaController {
public static final String CAPTCHA_SESSION_KEY = "captcha_session_key";
private static final String FORMAT_NAME = "JPEG";
@Autowired
private CaptchaGenerate imageCaptchaGenerate;
@Autowired
private CaptchaGenerate smsCaptchaGenerate;
@Autowired
private SmsCaptchaSend smsCaptchaSend;
//spring session 工具类
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* 获取图片验证码
*
* @param request
* @param response
* @throws IOException
*/
@GetMapping("/captcha/image")
public void createKaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
//1.接口生成验证码
ImageCaptchaVo imageCaptcha = (ImageCaptchaVo) imageCaptchaGenerate.generate();
//2.保存到session中
sessionStrategy.setAttribute(new ServletWebRequest(request), CAPTCHA_SESSION_KEY, imageCaptcha);
//3.写到响应流中
response.setHeader("Cache-Control", "no-store, no-cache");// 没有缓存
response.setContentType("image/jpeg");
ImageIO.write(imageCaptcha.getImage(), FORMAT_NAME, response.getOutputStream());
}
/**
* 获取图片验证码
*
* @param request
* @param response
* @throws IOException
*/
@GetMapping("/captcha/sms")
public void createSms(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {
log.info("获取短信验证码");
//1.获取短信验证码
CaptchaVo captchaVo = smsCaptchaGenerate.generate();
//2.保存到session中
sessionStrategy.setAttribute(new ServletWebRequest(request), CAPTCHA_SESSION_KEY + "sms", captchaVo);
//3.发送
String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
smsCaptchaSend.sendSms(mobile, captchaVo.getCode());
}
}
由于短信验证码和图片验证码基本结构都差不多,只是没有图形,故对实体进行调整
package com.rui.tiger.auth.core.captcha;
import lombok.Data;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
/**
* 验证码
*
* @author CaiRui
* @Date 2018/12/15 9:11
*/
@Data
public class CaptchaVo {
/**
* 验证码
*/
private String code;
/**
* 失效时间 这个通常放在缓存中或维护在数据库中
*/
private LocalDateTime expireTime;
public CaptchaVo(String code, int expireAfterSeconds) {
this.code = code;
//多少秒后
this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds);
}
public CaptchaVo(String code, LocalDateTime expireTime) {
this.code = code;
this.expireTime = expireTime;
}
/**
* 是否失效
*
* @return
*/
public boolean isExpried() {
return LocalDateTime.now().isAfter(expireTime);
}
}
package com.rui.tiger.auth.core.captcha;
import lombok.Data;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
/**
* 图片验证码信息对象
*
* @author CaiRui
* @Date 2018/12/9 18:03
*/
@Data
public class ImageCaptchaVo extends CaptchaVo {
/**
* 图片验证码
*/
private BufferedImage image;
public ImageCaptchaVo(BufferedImage image, String code, int expireAfterSeconds) {
super(code, expireAfterSeconds);
this.image = image;
}
public ImageCaptchaVo(BufferedImage image, String code, LocalDateTime expireTime) {
super(code, expireTime);
this.image = image;
}
}
相关配置类调整修改
package com.rui.tiger.auth.core.properties;
/**
* 短信验证码配置类
* @author CaiRui
* @Date 2018/12/15 9:30
*/
public class SmsCaptchaProperties {
/**
* 长度
*/
private int length=6;
/**
* 过期秒数 默认3分钟
*/
private int expireSeconds=180;
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getExpireSeconds() {
return expireSeconds;
}
public void setExpireSeconds(int expireSeconds) {
this.expireSeconds = expireSeconds;
}
}
package com.rui.tiger.auth.core.properties;
/**
* 验证码配置类
* @author CaiRui
* @Date 2018/12/15 9:43
*/
public class CaptchaProperties {
/**
*图片验证码配置
*/
private ImageCaptchaProperties image=new ImageCaptchaProperties();
/**
* 短信验证码配置
*/
private SmsCaptchaProperties sms=new SmsCaptchaProperties();
public ImageCaptchaProperties getImage() {
return image;
}
public void setImage(ImageCaptchaProperties image) {
this.image = image;
}
public SmsCaptchaProperties getSms() {
return sms;
}
public void setSms(SmsCaptchaProperties sms) {
this.sms = sms;
}
}
package com.rui.tiger.auth.core.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 权限配置文件父类(注意这里不用lombok 会读取不到)
* 这里会有很多权限配置子模块
*
* @author CaiRui
* @date 2018-12-6 8:41
*/
@ConfigurationProperties(value = "tiger.auth", ignoreInvalidFields = true)
public class SecurityProperties {
/**
* 浏览器配置类
*/
private BrowserProperties browser = new BrowserProperties();
/**
* 验证码配置类
*/
private CaptchaProperties captcha = new CaptchaProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
public CaptchaProperties getCaptcha() {
return captcha;
}
public void setCaptcha(CaptchaProperties captcha) {
this.captcha = captcha;
}
}
短信验证码生成接口及实现类
package com.rui.tiger.auth.core.captcha;
/**
* 验证码生成接口
*
* @author CaiRui
* @date 2018-12-10 12:03
*/
public interface CaptchaGenerate {
/**
* 生成验证码
*
* @return
*/
CaptchaVo generate();
}
package com.rui.tiger.auth.core.captcha;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.apache.commons.lang.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 短信验证码生成器
*
* @author CaiRui
* @Date 2018/12/15 9:10
*/
@Component("smsCaptchaGenerate")
public class SmsCaptchaGenerate implements CaptchaGenerate {
@Autowired
private SecurityProperties securityProperties;
/**
* 生成短信验证码
*
* @return
*/
@Override
public CaptchaVo generate() {
String code = RandomStringUtils.randomNumeric(securityProperties.getCaptcha().getSms().getLength());
return new CaptchaVo(code, securityProperties.getCaptcha().getSms().getExpireSeconds());
}
}
1.2短信登陆码发送接口
短信验证码发送接口及其实现类
package com.rui.tiger.auth.core.captcha;
/**
* 短信验证码发送接口
* @author CaiRui
* @Date 2018/12/15 10:03
*/
public interface SmsCaptchaSend {
/**
* 发送短信验证码
* @param mobile
* @param code
* @return
*/
boolean sendSms(String mobile,String code);
}
package com.rui.tiger.auth.core.captcha;
import lombok.extern.slf4j.Slf4j;
/**
* @author CaiRui
* @Date 2018/12/15 10:05
*/
@Slf4j
public class DefaultSmsCaptchaSender implements SmsCaptchaSend {
//实际生产环境中,调用渠道供应商发送短信
@Override
public boolean sendSms(String mobile, String code) {
log.info("模拟向手机{}发送短信验证码{}",mobile,code);
log.info("短信渠道发送中...发送成功");
return true;
}
}
验证码配置类
package com.rui.tiger.auth.core.config;
import com.google.code.kaptcha.Producer;
import com.rui.tiger.auth.core.captcha.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 验证码Bean生成配置类
*
* @author CaiRui
* @date 2018-12-12 8:41
*/
@Configuration
public class CaptchaBeanConfig {
//图片验证码生成
@Bean
// spring 容器中如果存在imageCaptchaGenerate的bean就不会再初始化该bean了
//可参见:https://www.cnblogs.com/yixianyixian/p/7346894.html 这篇博文
@ConditionalOnMissingBean(name = "imageCaptchaGenerate")
public CaptchaGenerate imageCaptchaGenerate() {
ImageCaptchaGenerate imageCaptchaGenerate = new ImageCaptchaGenerate();
return imageCaptchaGenerate;
}
//短信验证码生成
@Bean
@ConditionalOnMissingBean(name = "smsCaptchaGenerate")
public CaptchaGenerate smsCaptchaGenerate() {
SmsCaptchaGenerate smsCaptchaGenerate = new SmsCaptchaGenerate();
return smsCaptchaGenerate;
}
@Bean
@ConditionalOnMissingBean(DefaultSmsCaptchaSender.class)
public SmsCaptchaSend defaultSmsCaptchaSender() {
DefaultSmsCaptchaSender defaultSmsCaptchaSender=new DefaultSmsCaptchaSender();
return defaultSmsCaptchaSender;
}
}
在权限路径中放行 .antMatchers(securityProperties.getBrowser().getLoginPage(), "/authentication/require", "/captcha/*")
package com.rui.tiger.auth.browser.config;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationFailureHandler;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationSuccessHandler;
import com.rui.tiger.auth.core.captcha.CaptchaFilter;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
/**
* 浏览器security配置类
*
* @author CaiRui
* @date 2018-12-4 8:41
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private TigerAuthenticationFailureHandler tigerAuthenticationFailureHandler;
@Autowired
private TigerAuthenticationSuccessHandler tigerAuthenticationSuccessHandler;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
/**
* 密码加密解密
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 记住我持久化数据源
* JdbcTokenRepositoryImpl CREATE_TABLE_SQL 建表语句可以先在数据库中执行
*
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//第一次会执行CREATE_TABLE_SQL建表语句 后续会报错 可以关掉
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//加入图片验证码过滤器
CaptchaFilter captchaFilter = new CaptchaFilter();
captchaFilter.setFailureHandler(tigerAuthenticationFailureHandler);
captchaFilter.setSecurityProperties(securityProperties);
captchaFilter.afterPropertiesSet();
//图片验证码放在认证之前
http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")//自定义登录请求
.loginProcessingUrl("/authentication/form")//自定义登录表单请求
.successHandler(tigerAuthenticationSuccessHandler)
.failureHandler(tigerAuthenticationFailureHandler)
.and()
//记住我相关配置
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRemberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
.antMatchers(securityProperties.getBrowser().getLoginPage(),
"/authentication/require", "/captcha/*")//此路径放行 否则会陷入死循环
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable()//跨域关闭
;
}
}
postman测试下
看控制台日志,可以看见我们的短信发送接口可以用
1.3 验证码生成策略模板方式重构
经过分析图片验证码和手机验证码的创建流程基本都是一样,主要经过三步。
- 生成验证码
- 保存到session中
- 发送(图片响应流写回,短信调用渠道直接发送)
下面结合UML图和代码看看是怎么实现的吧。
CaptchaController调用CaptchaCreateService,CaptchaCreateService策略调用CaptchaProcessor
CaptchaController 经过调整后如下,将原来的分别生成图片和短信的合并调整
package com.rui.tiger.auth.core.captcha;
import com.rui.tiger.auth.core.captcha.sms.SmsCaptchaSend;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 验证码控制器
*
* @author CaiRui
* @date 2018-12-10 12:13
*/
@RestController
@Slf4j
public class CaptchaController {
@Autowired
private CaptchaCreateService captchaCreateService;
/**
* 获取验证码
*
* @param request
* @param response
* @throws IOException
*/
@GetMapping("/captcha/{type}")
public void createCaptcha(HttpServletRequest request, HttpServletResponse response, @PathVariable String type) throws Exception {
log.info("获取验证码开始");
captchaCreateService.createCaptcha(new ServletWebRequest(request, response), type);
log.info("获取验证码结束");
}
}
验证码生成接口
package com.rui.tiger.auth.core.captcha;
import org.springframework.web.context.request.ServletWebRequest;
/**
* @author CaiRui
* @Date 2018/12/15 21:27
*/
public interface CaptchaCreateService {
/**
* 生成验证码
* @param request
* @param type
*/
void createCaptcha(ServletWebRequest request, String type);
}
package com.rui.tiger.auth.core.captcha;
import com.rui.tiger.auth.core.support.strategy.StrategyContainerImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.ServletWebRequest;
/**
* @author CaiRui
* @Date 2018/12/15 21:27
*/
@Service
@Slf4j
public class CaptchaCreateServiceImpl implements CaptchaCreateService {
/**
* 生成验证码
*
* @param request
* @param type
*/
@Override
public void createCaptcha(ServletWebRequest request, String type) {
CaptchaTypeEnum captchaType=CaptchaTypeEnum.forCode(type);
if (type==null){
throw new CaptchaException("验证码类型不支持");
}
try {
StrategyContainerImpl.getStrategy(CaptchaProcessor.class,captchaType)
.create(request);
} catch (Exception e) {
log.info("");
}
}
}
验证码类型枚举类
package com.rui.tiger.auth.core.captcha;
import java.util.HashMap;
import java.util.Map;
/**
* 验证码类型枚举类
* @author CaiRui
* @Date 2018/12/15 17:58
*/
public enum CaptchaTypeEnum {
SMS("sms","短信"),
IMAGE("image","图形验证码");
CaptchaTypeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
private static Map<String,CaptchaTypeEnum> codeLookup = new HashMap<String,CaptchaTypeEnum>();
private String code;
private String desc;
static {
for (CaptchaTypeEnum type : CaptchaTypeEnum.values()) {
codeLookup.put(type.code, type);
}
}
/**
* 根据类型获取枚举类
* @param code
* @return
*/
public static CaptchaTypeEnum forCode(String code) {
return codeLookup.get(code);
}
public String getCode() {
return code;
}
}
验证码生成策略接口,
package com.rui.tiger.auth.core.captcha;
import com.rui.tiger.auth.core.support.strategy.IStrategy;
import org.springframework.web.context.request.ServletWebRequest;
/**
* 验证码处理器接口
* @author CaiRui
* @Date 2018/12/15 17:53
*/
public interface CaptchaProcessor extends IStrategy<CaptchaTypeEnum> {
/**
* 验证码
*/
String CAPTCHA_SESSION_KEY="captcha_session_key_";
/**
* 创建验证码
* @param request 封装请求和响应
* @throws Exception
*/
void create(ServletWebRequest request) throws Exception;
}
抽象实现父类
package com.rui.tiger.auth.core.captcha;
import org.apache.commons.lang.StringUtils;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.context.request.ServletWebRequest;
import java.io.IOException;
/**
* 验证码处理器抽象父类
* @author CaiRui
* @Date 2018/12/15 18:21
*/
public abstract class AbstractCaptchaProcessor<C extends CaptchaVo> implements CaptchaProcessor {
private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
/**
* 创建验证码
*
* @param request 封装请求和响应
* @throws Exception
*/
@Override
public void create(ServletWebRequest request) throws Exception {
//生成
C captcha=generateCaptcha(request);
//保存
save(request,captcha);
//发送
send(request,captcha);
}
protected abstract C generateCaptcha(ServletWebRequest request);
protected abstract void send(ServletWebRequest request, C captcha) throws IOException, ServletRequestBindingException;
private void save(ServletWebRequest request, C captcha) {
sessionStrategy.setAttribute(request, CAPTCHA_SESSION_KEY+getCaptchaTypeFromUrl(request), captcha);
}
/**
* 根据请求的url获取校验码的类型
* @param request
* @return
*/
private String getCaptchaTypeFromUrl(ServletWebRequest request) {
return StringUtils.substringAfter(request.getRequest().getRequestURI(), "/captcha/");
}
}
图片验证码生成实现
package com.rui.tiger.auth.core.captcha.image;
import com.rui.tiger.auth.core.captcha.AbstractCaptchaProcessor;
import com.rui.tiger.auth.core.captcha.CaptchaGenerate;
import com.rui.tiger.auth.core.captcha.CaptchaTypeEnum;
import com.rui.tiger.auth.core.captcha.ImageCaptchaVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.ServletWebRequest;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author CaiRui
* @Date 2018/12/15 18:31
*/
@Service
@Slf4j
public class ImageCaptchaProcessor extends AbstractCaptchaProcessor<ImageCaptchaVo> {
private static final String FORMAT_NAME = "JPEG";
@Autowired
private CaptchaGenerate imageCaptchaGenerate;
/**
* 获得策略条件
*
* @return 用来注册的策略处理条件
*/
@Override
public CaptchaTypeEnum getCondition() {
return CaptchaTypeEnum.IMAGE;
}
@Override
protected ImageCaptchaVo generateCaptcha(ServletWebRequest request) {
return (ImageCaptchaVo) imageCaptchaGenerate.generate();
}
@Override
protected void send(ServletWebRequest request, ImageCaptchaVo captcha) throws IOException {
HttpServletResponse response=request.getResponse();
response.setHeader("Cache-Control", "no-store, no-cache");// 没有缓存
response.setContentType("image/jpeg");
ImageIO.write(captcha.getImage(), FORMAT_NAME, response.getOutputStream());
}
}
短信验证码生成实现
package com.rui.tiger.auth.core.captcha.sms;
import com.rui.tiger.auth.core.captcha.AbstractCaptchaProcessor;
import com.rui.tiger.auth.core.captcha.CaptchaGenerate;
import com.rui.tiger.auth.core.captcha.CaptchaTypeEnum;
import com.rui.tiger.auth.core.captcha.CaptchaVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
/**
* @author CaiRui
* @Date 2018/12/15 18:29
*/
@Service
@Slf4j
public class SmsCaptchaProcessor extends AbstractCaptchaProcessor<CaptchaVo> {
@Autowired
private CaptchaGenerate smsCaptchaGenerate;
@Autowired
private SmsCaptchaSend captchaSend;
/**
* 获得策略条件
*
* @return 用来注册的策略处理条件
*/
@Override
public CaptchaTypeEnum getCondition() {
return CaptchaTypeEnum.SMS;
}
@Override
protected CaptchaVo generateCaptcha(ServletWebRequest request) {
return smsCaptchaGenerate.generate();
}
@Override
protected void send(ServletWebRequest request, CaptchaVo captcha) throws ServletRequestBindingException {
String mobile= ServletRequestUtils.getRequiredStringParameter(request.getRequest(),"mobile");
captchaSend.sendSms(mobile, captcha.getCode());
}
}
ok 重构完成 下面我们自定义短信登陆开发