SpringSceurity實現(xiàn)短信驗證碼功能的示例代碼
一、思考
1、設(shè)計思路
在獲取短信驗證碼功能和圖形驗證碼還是有很多相似的地方,所以這里在設(shè)計獲取短信驗證的時候,將之前開發(fā)好的的圖形驗證碼進(jìn)一步整合、抽象與重構(gòu)。
在獲取驗證碼的時候,它們最大的不同在于: 圖形驗證碼是通過接口返回獲取給前端。而短信驗證碼而言是通過第三方API向我們手機(jī)推送。
但是它們在登陸的時候就有很大的不同了,對于圖形驗證碼而言驗證通過之前就走UsernamePasswordAuthenticationFilter 過濾器了開始校驗用戶名密碼了。
但對于短信登陸而言,確實也需要先現(xiàn)在短信驗證碼是否通過,但是一旦通過他是不走 UsernamePasswordAuthenticationFilter,而是通過其它方式查詢用戶信息來校驗
認(rèn)證已經(jīng)通過了。
這篇博客只寫獲取獲取短信驗證碼的功能,不寫通過短信驗證碼登陸的邏輯。
2、重構(gòu)設(shè)計
這里才是最重要的,如何去設(shè)計和整合短信驗證碼和圖形驗證碼的代碼,是我們最應(yīng)該思考的。如何將相似部分抽離出來,然后去實現(xiàn)不相同的部分。
整理后發(fā)現(xiàn)不同點主要在于
1、獲取驗證碼。因為對于圖形驗證碼需要有個畫布,而短信驗證碼并不需要,所以它們可以實現(xiàn)同一個接口,來完成不同的邏輯。
2、發(fā)送驗證碼。對于圖形驗證碼來講只要把驗證碼返給前端就可以,而短信驗證碼而言是通過第三方API將驗證碼發(fā)到我們的手機(jī)上。
所以這里也可以通過實現(xiàn)統(tǒng)一接口來具體實現(xiàn)不同的方法。
相同部分我可以通過抽象類來完成實現(xiàn),不同部分可以通過具體的實現(xiàn)類來實現(xiàn)。

AbstractValidateCodeProcessorService 抽象類是用來實現(xiàn)兩種驗證碼可以抽離的部分。ImageCodeProcessorServiceImpl 和
SmsCodeProcessorServiceImpl方法是來實現(xiàn)兩種驗證碼不同的發(fā)送方式。
在簡單看下時序圖可能會更加明白點。

一個接口只有一個方法(processor)就是處理驗證碼,它其實需要做三件事。
1、獲取驗證碼。2、將驗證碼存入session。3、將驗證碼信息通過短信或者圖形驗證碼發(fā)送出去。
首先講生成獲取驗證碼,這里有一個公共接口和兩個實現(xiàn)類

對于保存驗證碼信息而言,可以在直接在 AbstractValidateCodeProcessorService抽象類來完成,都不需要去實現(xiàn)。
對發(fā)送驗證碼信息而言,只需要實現(xiàn)AbstractValidateCodeProcessorService抽象類的send發(fā)送驗證碼接口即可。
整個大致接口設(shè)計就是這樣,具體的可以通過代碼來展示。
二、代碼實現(xiàn)
1、驗證碼屬性
短信驗證碼和圖形驗證后包含屬性有 code 和 expireTime,短信驗證碼只有這兩個屬性,而圖形驗證碼還多一個BufferedImage實例對象屬性,所以將共同屬性進(jìn)行抽取
,抽取為ValidateCode類,代碼如下:
ValidateCode實體
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ValidateCode {
private String code;
private LocalDateTime expireTime;
public boolean isExpired() {
return LocalDateTime.now().isAfter(expireTime);
}
}
對于圖形驗證碼而言,除了需要code和過期時間還需要圖片的畫布,所以繼承ValidateCode之后再寫自己屬性
ImageCode實體
@Data
public class ImageCode extends ValidateCode {
private BufferedImage image;
public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
super(code, expireTime);
this.image = image;
}
public ImageCode(BufferedImage image, String code, int expireIn) {
super(code, LocalDateTime.now().plusSeconds(expireIn));
this.image = image;
}
}
對于短信驗證碼而言,暫時不需要添加自己的屬性字段了。
SmsCode實體
public class SmsCode extends ValidateCode {
public SmsCode(String code, LocalDateTime expireTime) {
super(code, expireTime);
}
public SmsCode(String code, int expireIn) {
super(code, LocalDateTime.now().plusSeconds(expireIn));
}
}
2、ValidateCodeProcessor接口
ValidateCodeProcessor接口主要是完成 驗證碼的生成、保存與發(fā)送的完整流程,接口的主要設(shè)計如下所示:
ValidateCodeProcessorService接口
public interface ValidateCodeProcessorService {
/**
* 因為現(xiàn)在有兩種驗證碼,所以存放到seesion的key不能一樣,所以前綴+具體type
*/
String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";
/**
* 通過也是 type+CODE_PROCESSOR獲取對于的bean
*/
String CODE_PROCESSOR = "CodeProcessorService";
/**
* 這個接口要做三件事
* 1、獲取驗證碼。
* 2、將驗證碼存入session
* 3、將驗證碼信息通過短信或者圖形驗證碼發(fā)送出去。
* (將spring-boot-security-study-03接口里的那三步進(jìn)行里封裝)
*
*/
void processor(ServletWebRequest request) throws Exception;
由于圖片驗證碼和短信驗證碼的 生成和保存、發(fā)送等流程是固定的。所以這里寫一個抽象類來實現(xiàn)ValidateCodeProcessor接口,來實現(xiàn)相似部分。
AbstractValidateCodeProcessorService抽象類
@Component
public abstract class AbstractValidateCodeProcessorService<C> implements ValidateCodeProcessorService {
private static final String SEPARATOR = "/code/";
/**
* 操作session的工具集
*/
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* 這是Spring的一個特性,就是在項目啟動的時候會自動收集系統(tǒng)中 {@link ValidateCodeGeneratorService} 接口的實現(xiàn)類對象
*
* key為bean name
*/
@Autowired
private Map<String, ValidateCodeGeneratorService> validateCodeGeneratorMap;
@Override
public void processor(ServletWebRequest request) throws Exception {
//第一件事
C validateCode = generate(request);
//第二件事
save(request, validateCode);
//第三件事
send(request, validateCode);
}
/**
* 生成驗證碼
*
*/
private C generate(ServletWebRequest request) {
String type = getProcessorType(request);
//這里 image+CodeGenerator = imgCodeGenerator 對應(yīng)的就是ImageCodeGeneratorServiceService
ValidateCodeGeneratorService validateCodeGenerator = validateCodeGeneratorMap.get(type.concat(ValidateCodeGeneratorService.CODE_GENERATOR));
return (C) validateCodeGenerator.generate(request);
}
/**
* 保存驗證碼到session中
*/
private void save(ServletWebRequest request, C validateCode) {
//這里也是封裝了一下
sessionStrategy.setAttribute(request, SESSION_KEY_PREFIX.concat(getProcessorType(request).toUpperCase()), validateCode);
}
/**
* 發(fā)送驗證碼 (只有發(fā)送驗證碼是需要自己去實現(xiàn)的。)
*/
protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;
/**
* 獲取請求URL中具體請求的驗證碼類型
*
*/
private String getProcessorType(ServletWebRequest request) {
// 獲取URI分割后的第二個片段 (/code/image 通過/code/ 切割后就只剩下 image
return StringUtils.substringAfter(request.getRequest().getRequestURI(), SEPARATOR);
}
}
簡單說明:
1、這里用到了Spring一個特性就是Map<String, ValidateCodeGeneratorService> validateCodeGeneratorMap 可以把ValidateCodeGeneratorService所以的實現(xiàn)類都放
到這個map中,key為bean的名稱。
2、抽象類中實現(xiàn)了 ValidateCodeProcessor接口的processor方法,它主要是完成了驗證碼的創(chuàng)建、保存和發(fā)送的功能。
3、generate 方法根據(jù)傳入的不同泛型而生成了特定的驗證碼。
4、save 方法是將生成的驗證碼實例對象存入到session中,兩種驗證碼的存儲方式一致,只是有個key不一致,所以代碼也是通用的。
5、send 方法一個抽象方法,分別由ImageCodeProcessorService和SmsCodeProcessorService來具體實現(xiàn),也是根據(jù)泛型來判斷具體調(diào)用哪一個具體的實現(xiàn)類的send方法。
3、驗證碼的生成接口
上面說過驗證的生成應(yīng)該也是通過實現(xiàn)類
ValidateCodeGeneratorService
public interface ValidateCodeGeneratorService {
/**
* 這個常量也是用來 type+CodeGeneratorService獲取對于bean對象
*/
String CODE_GENERATOR = "CodeGeneratorService";
/**
* 生成驗證碼
* 具體是圖片驗證碼 還是短信驗證碼就需要對應(yīng)的實現(xiàn)類
*/
ValidateCode generate(ServletWebRequest request);
}
圖形驗證碼具體實現(xiàn)類
mageCodeGeneratorServiceImpl
@Data
@Component("imageCodeGeneratorService")
public class ImageCodeGeneratorServiceImpl implements ValidateCodeGeneratorService {
private static final String IMAGE_WIDTH_NAME = "width";
private static final String IMAGE_HEIGHT_NAME = "height";
private static final Integer MAX_COLOR_VALUE = 255;
@Autowired
private ValidateCodeProperties validateCodeProperties;
@Override
public ImageCode generate(ServletWebRequest request) {
int width = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_WIDTH_NAME, validateCodeProperties.getImage().getWidth());
int height = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_HEIGHT_NAME,validateCodeProperties.getImage().getHeight());
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);
}
// 生成數(shù)字驗證碼
StringBuilder sRand = new StringBuilder();
for (int i = 0; i < validateCodeProperties.getImage().getLength(); i++) {
String rand = String.valueOf(random.nextInt(10));
sRand.append(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 ImageCode(image, sRand.toString(), validateCodeProperties.getImage().getExpireIn());
}
/**
* 生成隨機(jī)背景條紋
*
* @param fc 前景色
* @param bc 背景色
* @return RGB顏色
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > MAX_COLOR_VALUE) {
fc = MAX_COLOR_VALUE;
}
if (bc > MAX_COLOR_VALUE) {
bc = MAX_COLOR_VALUE;
}
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);
}
}
短信驗證碼具體實現(xiàn)類
SmsCodeGeneratorServiceImpl
@Data
@Component("smsCodeGeneratorService")
public class SmsCodeGeneratorServiceImpl implements ValidateCodeGeneratorService {
@Autowired
private ValidateCodeProperties validateCodeProperties;
@Override
public SmsCode generate(ServletWebRequest request) {
//生成隨機(jī)數(shù)
String code = RandomStringUtils.randomNumeric(validateCodeProperties.getSms().getLength());
return new SmsCode(code, validateCodeProperties.getSms().getExpireIn());
}
}
4、驗證碼的發(fā)送邏輯類
獲取的實現(xiàn)類寫好了,我們在寫發(fā)送具體的發(fā)送實現(xiàn)類,發(fā)送類的實現(xiàn)類是實現(xiàn)AbstractValidateCodeProcessorService抽象類的。
圖片發(fā)送實現(xiàn)類
ImageCodeProcessorServiceImpl
@Component("imageCodeProcessorService")
public class ImageCodeProcessorServiceImpl extends AbstractValidateCodeProcessorService<ImageCode> {
private static final String FORMAT_NAME = "JPEG";
/**
* 發(fā)送圖形驗證碼,將其寫到相應(yīng)中
*
* @param request ServletWebRequest實例對象
* @param imageCode 驗證碼
*/
@Override
protected void send(ServletWebRequest request, ImageCode imageCode) throws Exception {
ImageIO.write(imageCode.getImage(), FORMAT_NAME, request.getResponse().getOutputStream());
}
}
短信發(fā)送具體實現(xiàn)類。這里只是后臺輸出就好了,實際中只要接入對于的SDK就可以了。
SmsCodeProcessorServiceImpl
@Component("smsCodeProcessorService")
public class SmsCodeProcessorServiceImpl extends AbstractValidateCodeProcessorService<SmsCode> {
private static final String SMS_CODE_PARAM_NAME = "mobile";
@Override
protected void send(ServletWebRequest request, SmsCode smsCode) throws Exception {
//這里有一個參數(shù)也是前端需要傳來的 就是用戶的手機(jī)號
String mobile = ServletRequestUtils.getRequiredStringParameter(request.getRequest(), SMS_CODE_PARAM_NAME);
// 這里僅僅寫個打印,具體邏輯一般都是調(diào)用第三方接口發(fā)送短信
System.out.println("向手機(jī)號為:" + mobile + "的用戶發(fā)送驗證碼:" + smsCode.getCode());
}
整個大致就是這樣,我們再來測試一下。
三、測試
1、ValidateCodeController接口
獲取驗證碼接口
@RestController
@Slf4j
public class ValidateCodeController {
@Autowired
private Map<String, ValidateCodeProcessorService> validateCodeProcessorMap;
@RequestMapping("/code/{type}")
public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type) throws Exception {
if(!StringUtils.equalsAny(type, "image", "sms")){
log.info("type類型錯誤 type={}",type);
return;
};
//根據(jù)type獲取具體的實現(xiàn)類
ValidateCodeProcessorService validateCodeProcessorService = validateCodeProcessorMap.get(type.concat(ValidateCodeProcessorService.CODE_PROCESSOR));
validateCodeProcessorService.processor(new ServletWebRequest(request, response));
}
}
2、獲得圖形驗證碼

獲取成功
3、獲取短信驗證碼
獲取短信驗證碼需要多傳一個參數(shù)就是mobile 手機(jī)號碼

因為這里發(fā)送短信沒有接第三方SDK,而是直接在控制臺輸出

參考
1、Spring Security技術(shù)棧開發(fā)企業(yè)級認(rèn)證與授權(quán)(JoJo)
這一套架構(gòu)設(shè)計真的非常的好,代碼可讀性和可用性都非常高,以后如果要接第三個驗證碼只要實現(xiàn)發(fā)送和獲取的接口來自定義實a現(xiàn)就好了。很受啟發(fā)!
總結(jié)
到此這篇關(guān)于SpringSceurity實現(xiàn)短信驗證碼功能的示例代碼的文章就介紹到這了,更多相關(guān)SpringSceurity短信驗證碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
如何使用Java 8 中的 Stream 遍歷樹形結(jié)構(gòu)
這篇文章主要介紹了使用Java 8中的Stream遍歷樹形結(jié)構(gòu),我們可以使用Java8中的Stream流一次性把數(shù)據(jù)查出來,然后通過流式處理,我們一起來看看,代碼實現(xiàn)為了實現(xiàn)簡單,就模擬查看數(shù)據(jù)庫所有數(shù)據(jù)到List里面,需要的朋友可以參考下2023-08-08
SpringBoot集成RocketMQ實現(xiàn)消息發(fā)送的三種方式
RocketMQ?支持3?種消息發(fā)送方式:?同步?(sync)、異步(async)、單向(oneway),本文就將給大家介紹一下SpringBoot集成RocketMQ實現(xiàn)消息發(fā)送的三種方式文中有詳細(xì)的代碼示例,需要的朋友可以參考下2023-09-09
Java設(shè)計模式之解釋器模式_動力節(jié)點Java學(xué)院整理
解釋器模式是一個比較少用的模式,本人之前也沒有用過這個模式。下面我們就來一起看一下解釋器模式2017-08-08

