spring-security-oauth2(六) 簡訊驗證碼介面開發
阿新 • • 發佈:2018-12-29
簡訊驗證碼介面開發
- 簡訊驗證碼生成介面
- 簡訊驗證碼傳送介面
- 簡訊生成策略模板模式重構
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 重構完成 下面我們自定義簡訊登陸開發