1. 程式人生 > >SpringSceurity(4)---簡訊驗證碼功能實現

SpringSceurity(4)---簡訊驗證碼功能實現

#
SpringSceurity(4)---簡訊驗證碼功能實現
有關SpringSceurity系列之前有寫文章 1、[SpringSecurity(1)---認證+授權程式碼實現](https://www.cnblogs.com/qdhxhz/p/12755627.html) 2、[SpringSecurity(2)---記住我功能實現](https://www.cnblogs.com/qdhxhz/p/12783471.html) 3、[SpringSceurity(3)---圖形驗證碼功能實現](https://www.cnblogs.com/qdhxhz/p/12791083.html) ## 一、思考 #### 1、設計思路 在獲取簡訊驗證碼功能和圖形驗證碼還是有很多相似的地方,所以這裡在設計獲取簡訊驗證的時候,將之前開發好的的圖形驗證碼進一步整合、抽象與重構。 在獲取驗證碼的時候,它們最大的不同在於: **圖形驗證碼是通過介面返回獲取給前端。而簡訊驗證碼而言是通過第三方API向我們手機推送**。 但是它們在登陸的時候就有很大的不同了,對於圖形驗證碼而言驗證通過之前就走 **UsernamePasswordAuthenticationFilter** 過濾器了開始校驗使用者名稱密碼了。 但對於簡訊登陸而言,確實也需要先現在簡訊驗證碼是否通過,但是一旦通過他是不走 **UsernamePasswordAuthenticationFilter**,而是通過其它方式查詢使用者資訊來校驗 認證已經通過了。 這篇部落格只寫獲取獲取簡訊驗證碼的功能,不寫通過簡訊驗證碼登陸的邏輯。 #### 2、重構設計 這裡才是最重要的,如何去設計和整合簡訊驗證碼和圖形驗證碼的程式碼,是我們最應該思考的。如何將相似部分抽離出來,然後去實現不相同的部分。 整理後發現不同點主要在於 ```html 1、獲取驗證碼。因為對於圖形驗證碼需要有個畫布,而簡訊驗證碼並不需要,所以它們可以實現同一個介面,來完成不同的邏輯。 2、傳送驗證碼。對於圖形驗證碼來講只要把驗證碼返給前端就可以,而簡訊驗證碼而言是通過第三方API將驗證碼發到我們的手機上。 所以這裡也可以通過實現統一介面來具體實現不同的方法。 ``` 相同部分我可以通過抽象類來完成實現,不同部分可以通過具體的實現類來實現。
**AbstractValidateCodeProcessorService** 抽象類是用來實現兩種驗證碼可以抽離的部分。**ImageCodeProcessorServiceImpl** 和 **SmsCodeProcessorServiceImpl**方法是來實現兩種驗證碼不同的傳送方式。 在簡單看下時序圖可能會更加明白點。 一個介面只有一個方法(processor)就是處理驗證碼,它其實需要做三件事。 ``` 1、獲取驗證碼。2、將驗證碼存入session。3、將驗證碼資訊通過簡訊或者圖形驗證碼傳送出去。 ``` 首先講生成獲取驗證碼,這裡有一個公共介面和兩個實現類
對於儲存驗證碼資訊而言,可以在直接在 AbstractValidateCodeProcessorService抽象類來完成,都不需要去實現。 對傳送驗證碼資訊而言,只需要實現AbstractValidateCodeProcessorService抽象類的send傳送驗證碼介面即可。 整個大致介面設計就是這樣,具體的可以通過程式碼來展示。
## 二、程式碼實現 #### 1、驗證碼屬性 簡訊驗證碼和圖形驗證後包含屬性有 `code` 和 `expireTime`,簡訊驗證碼只有這兩個屬性,而圖形驗證碼還多一個`BufferedImage`例項物件屬性,所以將共同屬性進行抽取 ,抽取為ValidateCode類,程式碼如下: `ValidateCode實體` ```java @Data @AllArgsConstructor @NoArgsConstructor public class ValidateCode { private String code; private LocalDateTime expireTime; public boolean isExpired() { return LocalDateTime.now().isAfter(expireTime); } } ``` 對於圖形驗證碼而言,除了需要code和過期時間還需要圖片的畫布,所以繼承ValidateCode之後再寫自己屬性 `ImageCode實體` ```java @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實體` ```java 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介面主要是完成 驗證碼的生成、儲存與傳送的完整流程,介面的主要設計如下所示: `ValidateCodeProcessorService介面` ```java public interface ValidateCodeProcessorService { /** * 因為現在有兩種驗證碼,所以存放到seesion的key不能一樣,所以字首+具體type */ String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_"; /** * 通過也是 type+CODE_PROCESSOR獲取對於的bean */ String CODE_PROCESSOR = "CodeProcessorService"; /** * 這個介面要做三件事 * 1、獲取驗證碼。 * 2、將驗證碼存入session * 3、將驗證碼資訊通過簡訊或者圖形驗證碼傳送出去。 * (將spring-boot-security-study-03接口裡的那三步進行裡封裝) * */ void processor(ServletWebRequest request) throws Exception; ``` 由於圖片驗證碼和簡訊驗證碼的 生成和儲存、傳送等流程是固定的。所以這裡寫一個抽象類來實現ValidateCodeProcessor介面,來實現相似部分。 `AbstractValidateCodeProcessorService抽象類` ```java @Component public abstract class AbstractValidateCodeProcessorService implements ValidateCodeProcessorService { private static final String SEPARATOR = "/code/"; /** * 操作session的工具集 */ private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); /** * 這是Spring的一個特性,就是在專案啟動的時候會自動收集系統中 {@link ValidateCodeGeneratorService} 介面的實現類物件 * * key為bean name */ @Autowired private Map 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 對應的就是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); } /** * 傳送驗證碼 (只有傳送驗證碼是需要自己去實現的。) */ 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 validateCodeGeneratorMap 可以把ValidateCodeGeneratorService所以的實現類都放 到這個map中,key為bean的名稱。 2、抽象類中實現了 **ValidateCodeProcessor介面的processor方法**,它主要是完成了驗證碼的建立、儲存和傳送的功能。 3、**generate** 方法根據傳入的不同泛型而生成了特定的驗證碼。 4、**save** 方法是將生成的驗證碼例項物件存入到session中,兩種驗證碼的儲存方式一致,只是有個key不一致,所以程式碼也是通用的。 5、**send** 方法一個抽象方法,分別由ImageCodeProcessorService和SmsCodeProcessorService來具體實現,也是根據泛型來判斷具體呼叫哪一個具體的實現類的send方法。 #### 3、驗證碼的生成介面 上面說過驗證的生成應該也是通過實現類 `ValidateCodeGeneratorService` ```java public interface ValidateCodeGeneratorService { /** * 這個常量也是用來 type+CodeGeneratorService獲取對於bean物件 */ String CODE_GENERATOR = "CodeGeneratorService"; /** * 生成驗證碼 * 具體是圖片驗證碼 還是簡訊驗證碼就需要對應的實現類 */ ValidateCode generate(ServletWebRequest request); } ``` 圖形驗證碼具體實現類 `mageCodeGeneratorServiceImpl` ```java @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); } // 生成數字驗證碼 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()); } /** * 生成隨機背景條紋 * * @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); } } ``` 簡訊驗證碼具體實現類 `SmsCodeGeneratorServiceImpl` ```java @Data @Component("smsCodeGeneratorService") public class SmsCodeGeneratorServiceImpl implements ValidateCodeGeneratorService { @Autowired private ValidateCodeProperties validateCodeProperties; @Override public SmsCode generate(ServletWebRequest request) { //生成隨機數 String code = RandomStringUtils.randomNumeric(validateCodeProperties.getSms().getLength()); return new SmsCode(code, validateCodeProperties.getSms().getExpireIn()); } } ``` #### 4、驗證碼的傳送邏輯類 獲取的實現類寫好了,我們在寫傳送具體的傳送實現類,傳送類的實現類是實現AbstractValidateCodeProcessorService抽象類的。 圖片傳送實現類 `ImageCodeProcessorServiceImpl` ```java @Component("imageCodeProcessorService") public class ImageCodeProcessorServiceImpl extends AbstractValidateCodeProcessorService { private static final String FORMAT_NAME = "JPEG"; /** * 傳送圖形驗證碼,將其寫到相應中 * * @param request ServletWebRequest例項物件 * @param imageCode 驗證碼 */ @Override protected void send(ServletWebRequest request, ImageCode imageCode) throws Exception { ImageIO.write(imageCode.getImage(), FORMAT_NAME, request.getResponse().getOutputStream()); } } ``` 簡訊傳送具體實現類。這裡只是後臺輸出就好了,實際中只要接入對於的SDK就可以了。 `SmsCodeProcessorServiceImpl` ```java @Component("smsCodeProcessorService") public class SmsCodeProcessorServiceImpl extends AbstractValidateCodeProcessorService { private static final String SMS_CODE_PARAM_NAME = "mobile"; @Override protected void send(ServletWebRequest request, SmsCode smsCode) throws Exception { //這裡有一個引數也是前端需要傳來的 就是使用者的手機號 String mobile = ServletRequestUtils.getRequiredStringParameter(request.getRequest(), SMS_CODE_PARAM_NAME); // 這裡僅僅寫個列印,具體邏輯一般都是呼叫第三方介面傳送簡訊 System.out.println("向手機號為:" + mobile + "的使用者傳送驗證碼:" + smsCode.getCode()); } ``` 整個大致就是這樣,我們再來測試一下。
## 三、測試 #### 1、ValidateCodeController介面 獲取驗證碼介面 ```java @RestController @Slf4j public class ValidateCodeController { @Autowired private Map 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; }; //根據type獲取具體的實現類 ValidateCodeProcessorService validateCodeProcessorService = validateCodeProcessorMap.get(type.concat(ValidateCodeProcessorService.CODE_PROCESSOR)); validateCodeProcessorService.processor(new ServletWebRequest(request, response)); } } ``` #### 2、獲得圖形驗證碼 獲取成功 #### 3、獲取簡訊驗證碼 獲取簡訊驗證碼需要多傳一個引數就是mobile 手機號碼 因為這裡傳送簡訊沒有接第三方SDK,而是直接在控制檯輸出

### 參考 1、Spring Security技術棧開發企業級認證與授權(JoJo) **這一套架構設計真的非常的好,程式碼可讀性和可用性都非常高,以後如果要接第三個驗證碼只要實現傳送和獲取的介面來自定義實a現就好了。很受啟發!**

```html 別人罵我胖,我會生氣,因為我心裡承認了我胖。別人說我矮,我就會覺得好笑,因為我心裡知道我不可能矮。這就是我們為什麼會對別人的攻擊生氣。 攻我盾者,乃我內心之矛(20) ```