基于SpringBoot實(shí)現(xiàn)驗(yàn)證碼功能的代碼及思路
現(xiàn)在的登錄都需要輸入驗(yàn)證碼用來檢測(cè)是否是真人登錄,所以驗(yàn)證碼功能在現(xiàn)在是非常普遍的,那么接下來我們就基于springboot來實(shí)現(xiàn)驗(yàn)證碼功能。
一 實(shí)現(xiàn)思路
今天我們介紹的是兩種主流的驗(yàn)證碼,一種就是上圖所示的需要進(jìn)行計(jì)算的驗(yàn)證碼,另外一種就是不需要計(jì)算,直接輸入的驗(yàn)證碼。
1.如果驗(yàn)證碼的類型是一個(gè)計(jì)算類型驗(yàn)證碼
* 那么我們就需要分析一下:
* 比如一個(gè)計(jì)算類型的驗(yàn)證碼:1+2=?
* 那么我們到時(shí)候應(yīng)該填入的驗(yàn)證碼的答案是:3
* 所以這個(gè)驗(yàn)證碼它包含了兩個(gè)部分,一個(gè)是"1+2=?" 另外一個(gè)是"3"
* 其實(shí)生成這個(gè)計(jì)算問題是我們自己來實(shí)現(xiàn)的,我們的工具類已經(jīng)封裝好了計(jì)算提,比如:“1+2=3”
* 所以我們現(xiàn)在要做的主要有三步:
* 1.得到這個(gè)算式后,將算是“1+2=3”分割成兩個(gè)部分,“1+2=?” 和 “3”
* 2.然后我們需要把它們存儲(chǔ)到緩存中去,前面可以當(dāng)作key,后面可以當(dāng)作value,然后進(jìn)行驗(yàn)證答案的正確性
* 3.最后,我們需要把“1+2=?”制作成一個(gè)圖片返回給前端,用于展示給用戶,
2.如果驗(yàn)證碼的類型是一個(gè)普通的圖形驗(yàn)證碼
那么我們不要分為表達(dá)式和答案兩個(gè)部分,我們只需要把生成的這個(gè)圖形直接存入緩存中去就可以了。
但是因?yàn)槲覀冞@兩種類型是在同一個(gè)類中進(jìn)行判斷的,所以最好還是用兩個(gè)變量來接收。
上面這兩中類型的驗(yàn)證碼判斷完成之后,不管是那種類型,最后都需要把數(shù)據(jù)存到緩存中,并且都會(huì)生成一個(gè)Base64編碼的一個(gè)圖片,我們只需要返回這個(gè)圖片即可,還需要一個(gè)uuid,因?yàn)檫@個(gè)是用來作為key的唯一標(biāo)識(shí)。
二 代碼實(shí)現(xiàn)
開始代碼之前,先介紹一下我注入的幾個(gè)bean:
@Autowired private ISysConfigService configService ; //判斷是否開啟驗(yàn)證碼 @Autowired private FeiSiConfig feiSiConfig ; //配置信息 @Autowired private KaptchaTextCreator kaptchaTextCreator ; //隨機(jī)生成驗(yàn)證碼表達(dá)式 @Autowired private RedisCache redisCache ; //存儲(chǔ)數(shù)據(jù)到緩存 @Resource(name = "captchaProducer") private Producer captchaProducer; //生成普通類型驗(yàn)證碼圖片 @Resource(name="captchaProducerMath") private Producer captchaProducerMath; //生成計(jì)算類型驗(yàn)證碼圖片
① 先判斷有沒有開啟驗(yàn)證碼功能。
我們有一個(gè)網(wǎng)頁功能的數(shù)據(jù)庫表,里面可以選擇是否開啟驗(yàn)證碼功能,并且在類加載的時(shí)候,我們就已經(jīng)使用 @PostConstruct(標(biāo)記在方法上, 當(dāng)這個(gè)bean創(chuàng)建完成,自動(dòng)執(zhí)行這個(gè)注解標(biāo)記方法,要求這個(gè)方法無參 類似 init-method配置) 注解將數(shù)據(jù)庫的數(shù)據(jù)緩存在了redis里面。
所以我們可以判斷先判斷有沒有開啟驗(yàn)證碼功能:
// 先要判斷一下是否開啟了驗(yàn)證碼功能,如果沒有開啟,則不需要進(jìn)行生成驗(yàn)證碼的操作了 boolean captchaEnabled = configService.selectCaptchaEnabled(); if (!captchaEnabled){ return ajaxResult.put("captchaOnOff",captchaEnabled) ; //返回沒有開啟驗(yàn)證碼信息給前端 }
configService這個(gè)類就是我們用來初始化緩存數(shù)據(jù)的,這個(gè)類代碼如下:
package com.fs.system.service.ipml; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fs.common.constant.CacheConstants; import com.fs.common.core.pojo.SysConfig; import com.fs.common.util.RedisCache; import com.fs.common.util.StringUtils; import com.fs.system.mapper.SysConfigMapper; import com.fs.system.service.ISysConfigService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.util.List; import java.util.Objects; @Service public class SysConfigServiceImpl implements ISysConfigService { @Autowired private SysConfigMapper sysConfigMapper; @Autowired private RedisCache redisCache; @PostConstruct //標(biāo)記在方法上, 當(dāng)這個(gè)bean創(chuàng)建完成,自動(dòng)執(zhí)行這個(gè)注解標(biāo)記方法,要求這個(gè)方法無參 類似 init-method配置 public void init(){ loadingConfigCache(); } // 初始化數(shù)據(jù),當(dāng)系統(tǒng)加載的時(shí)候,把數(shù)據(jù)存入到緩存中去 @Override public void loadingConfigCache() { System.out.println("初始化數(shù)據(jù)..."); //查詢數(shù)據(jù)庫 List<SysConfig> sysConfigs = sysConfigMapper.selectList(null); //保存到redis緩存中 sysConfigs.forEach((sysConfig)->{ redisCache.setCacheObject(getCacheKey(sysConfig.getConfigKey()),sysConfig.getConfigValue()); }); } /** * 構(gòu)建參數(shù)配置表的redis的key * @param configKey * @return */ // 獲取redis緩存中的key private String getCacheKey(String configKey){ return CacheConstants.SYS_CONFIG_KEY+configKey; } // 判斷賬號(hào)的驗(yàn)證碼是否以及開啟 @Override public boolean selectCaptchaEnabled() { String cacheObject = redisCache.getCacheObject("sys.account.captchaOnOff"); if (StrUtil.isBlank(cacheObject)){ // 如果查詢出來的是空,則說明該賬號(hào)是第一次登錄,則默認(rèn)開啟驗(yàn)證碼 return true ; } // 如果不是空,那么查詢到的數(shù)據(jù)不是true就是false,但是存到數(shù)據(jù)庫的并不是Boolean類型而是String類型, // 所以我們借用工具直接把字符串轉(zhuǎn)成對(duì)應(yīng)的Boolean類型 return Convert.toBool(cacheObject) ; } // 根據(jù)賬號(hào)的key獲取緩存中的value @Override public String selectConfigByKey(String configKey) { // 1. 如果從redis中得到了數(shù)據(jù),那么直接返回 String cacheObject = redisCache.getCacheObject(getCacheKey(configKey)); // 我們需要返回的是一個(gè)String類型,所以需要用工具包轉(zhuǎn)為String類型 String toStr = Convert.toStr(cacheObject); if (StrUtil.isNotBlank(toStr)){ return toStr ; } // 2.如果沒有得到數(shù)據(jù),那么我們需要從sys_config數(shù)據(jù)庫表中查詢數(shù)據(jù),同時(shí)把查詢到的數(shù)據(jù)存入緩存中 LambdaQueryWrapper<SysConfig> queryWrapper=new LambdaQueryWrapper<>(); queryWrapper.eq(SysConfig::getConfigKey,configKey); SysConfig sysConfig = sysConfigMapper.selectOne(queryWrapper); // 如果查到有數(shù)據(jù),就存入redis并且返回 if(Objects.nonNull(sysConfig)){ //這里判空的方法不能繼續(xù)和上面一樣,因?yàn)閟ysConfig并不是字符串類型,而是一個(gè)對(duì)象 // 獲取到值 String configValue = sysConfig.getConfigValue(); redisCache.setCacheObject(getCacheKey(configKey),configValue); return configValue; } // 否則沒有查到數(shù)據(jù),則說明沒有信息 return null ; } }
在進(jìn)行正式寫邏輯代碼之前,我們需要引入幾個(gè)變量。
按照我們上面分析的,如果是一個(gè)計(jì)算類型的驗(yàn)證碼,那么我們一個(gè)需要四個(gè)變量:
一個(gè)變量是表達(dá)式的前半部分,一個(gè)變量是表達(dá)式的答案,
一個(gè)變量是用于存儲(chǔ)生成的驗(yàn)證碼圖片的Base64編碼,
最后一個(gè)就是驗(yàn)證碼數(shù)據(jù)存儲(chǔ)在redis作為唯一標(biāo)識(shí)key的uuid
BufferedImage image = null ;//圖片 String expression ; //表達(dá)式部分 String answer ; //答案部分 String uuid = UUID.randomUUID().toString();; //uuid
② 判斷開啟后,我們接下來需要判斷使用的是哪一種驗(yàn)證碼,具體使用哪一種是我們自己配置好的,我們規(guī)定math是計(jì)算類型的驗(yàn)證碼,char是普通驗(yàn)證碼。
/** * 至于這個(gè)captchaType的值是根據(jù)配置文件設(shè)置的,因?yàn)檫@個(gè)FeisiConfig是一個(gè)配置類 * 它加了‘@ConfigurationProperties(prefix = "fs")'這個(gè)注解, * 而配置文件規(guī)定math是計(jì)算類型驗(yàn)證碼,char是圖形類型驗(yàn)證碼 */ String captchaType = feiSiConfig.getCaptchaType();
③ 如果是計(jì)算類型的驗(yàn)證碼,那么我們就需要按照以下步驟走:
1.首先,得到生成的計(jì)算表達(dá)式(由我們封裝的工具類生成)
// 獲取一個(gè)隨機(jī)生成的表達(dá)式(隨機(jī)生成的工具封裝在KaptchaTextCreator類中) String text = kaptchaTextCreator.getText();
生成表達(dá)式的工具類代碼如下:
package com.fs.system.util; import com.google.code.kaptcha.text.impl.DefaultTextCreator; import org.springframework.stereotype.Component; import java.util.Random; @Component public class KaptchaTextCreator extends DefaultTextCreator { private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(","); @Override public String getText() { Integer result = 0; Random random = new Random(); int x = random.nextInt(10); int y = random.nextInt(10); StringBuilder suChinese = new StringBuilder(); int randomoperands = random.nextInt(3); if (randomoperands == 0) { result = x * y; suChinese.append(CNUMBERS[x]); suChinese.append("*"); suChinese.append(CNUMBERS[y]); } else if (randomoperands == 1) { if ((x != 0) && y % x == 0) { result = y / x; suChinese.append(CNUMBERS[y]); suChinese.append("/"); suChinese.append(CNUMBERS[x]); } else { result = x + y; suChinese.append(CNUMBERS[x]); suChinese.append("+"); suChinese.append(CNUMBERS[y]); } } else { if (x >= y) { result = x - y; suChinese.append(CNUMBERS[x]); suChinese.append("-"); suChinese.append(CNUMBERS[y]); } else { result = y - x; suChinese.append(CNUMBERS[y]); suChinese.append("-"); suChinese.append(CNUMBERS[x]); } } suChinese.append("=?@" + result); return suChinese.toString(); //5+6=?@11 } }
2.得到表達(dá)式之后,我們需要對(duì)這個(gè)表達(dá)式進(jìn)行分割,分成表達(dá)式和答案兩部分
// 這個(gè)表達(dá)式其實(shí)我們?cè)诠ぞ哳惿傻臅r(shí)候做了處理,text其實(shí)是“1+2=@3”,這樣就方便分離表達(dá)式和答案 // 分割表達(dá)式 expression = text.substring(0,text.lastIndexOf("@")) ; //"1+2=?" answer = text.substring(text.lastIndexOf("@")+1) ; //"@"
3.正常來說,接下來我們需要把數(shù)據(jù)存到緩存中去,但是因?yàn)槠胀愋偷尿?yàn)證碼也有這一步,所以這一部分重復(fù)邏輯就放在最后,我們現(xiàn)在需要根據(jù)前面的表達(dá)式部分來生成一張圖片。
// 最后,我們需要把“1+2=?”制作成一個(gè)圖片返回給前端,用于展示給用戶, // 制作成圖片也有專門的工具類,然后我們需要把這個(gè)圖片轉(zhuǎn)成64編碼返回給前端,這個(gè)功能代碼其實(shí)是固定的 //生成驗(yàn)證碼圖片(google.code.kaptcha提供的工具類Product) image = captchaProducerMath.createImage(expression);
captchaProducerMath是谷歌提供的工具類,我們只需要注入使用就行。
4.正常來說,現(xiàn)在應(yīng)該是將驗(yàn)證碼圖片轉(zhuǎn)成Base64編碼,然后返回給前端展示,但是和上面緩存數(shù)據(jù)一樣,兩種編碼類型都是獲取到驗(yàn)證碼數(shù)據(jù)緩存在redis并且把生成的驗(yàn)證碼圖片以Base64編碼格式返回給前端,所以我們接下來就是為普通驗(yàn)證碼類型獲取驗(yàn)證碼數(shù)據(jù)和生成圖片。
④ 如果是普通類型的驗(yàn)證碼
else { /** * 如果不是計(jì)算式類型的驗(yàn)證碼,那就是圖形類型的驗(yàn)證碼,那么就更簡(jiǎn)單了。 * 只需要把圖形驗(yàn)證碼存到緩存中,然后再把圖片返回給前端就好 */ // 圖形驗(yàn)證碼的話不能和上面一樣生成表達(dá)式了,而是隨機(jī)生成一個(gè)文本,然后把文本賦值給exception和answer,這樣方便存儲(chǔ) expression = answer= captchaProducer.createText(); // 再把這個(gè)文本轉(zhuǎn)成驗(yàn)證碼圖片 image = captchaProducer.createImage(expression) ; }
captchaProducer和captchaProducerMath其實(shí)是一個(gè)類,知識(shí)取了不一樣的名字方便區(qū)分。
⑤ 上面兩種類型的驗(yàn)證碼都已經(jīng)成功得到表達(dá)式(exception),答案(anwser)(普通類型這個(gè)都一樣),生成的圖片(image)。
因?yàn)樯厦媸悄囊环N類型就會(huì)生成哪一種驗(yàn)證碼的數(shù)據(jù),所以我們最后只需要生成一個(gè)uuid唯一標(biāo)識(shí)作為key,然后把a(bǔ)nswer作為value存儲(chǔ)在redis中。然后把image轉(zhuǎn)換成Base64編碼。
最后返回給前端image,uuid即可。
// 然后把答案存入到redis中,當(dāng)然了key不能直接用表達(dá)式,因?yàn)橛锌赡軙?huì)重復(fù) // 所以我們用uuid來作為key,然后把uuid給前端,前端在訪問的時(shí)候再把uuid傳來進(jìn)行驗(yàn)證 uuid = UUID.randomUUID().toString();; //uuid //Constants.CAPTCHA_EXPIRATION :表示2 , TimeUnit.MINUTES:表示分鐘,即這個(gè)驗(yàn)證碼數(shù)據(jù)只存儲(chǔ)2分鐘 redisCache.setCacheObject(CacheConstants.CAPTCHA_CODE_KEY+uuid,answer,Constants.CAPTCHA_EXPIRATION , TimeUnit.MINUTES); //最后再把生成的驗(yàn)證碼圖片轉(zhuǎn)成Base64編碼 //轉(zhuǎn)之前需要先把圖片轉(zhuǎn)成字節(jié)數(shù)組 FastByteArrayOutputStream os = new FastByteArrayOutputStream(); ImageIO.write(image,"jpeg",os);//把圖片通過jpeg類型寫進(jìn)字節(jié)數(shù)組 //再轉(zhuǎn)成Base64編碼 String base64Str = Base64.encode(os.toByteArray()); //得到的Base64編碼不能直接返回,還需要按照規(guī)定添加頭部 String base64Img = "data:image/jpeg;base64," +base64Str; //BASE64對(duì)密文進(jìn)行傳輸加密時(shí),可能出現(xiàn)\r\n //原因: RFC2045中有規(guī)定:即Base64一行不能超過76字符,超過則添加回車換行符。 //解決方案: BASE64加密后,對(duì)\r\n進(jìn)行去除 base64Img= base64Img.replaceAll("\r|\n", ""); // 最后把這個(gè)Base64編碼的表達(dá)式圖片驗(yàn)證碼和用于表示key的uuid返回給前端 ajaxResult.put("img",base64Img); ajaxResult.put("uuid",uuid) ; os.close(); return ajaxResult ;
三 代碼匯總
最后,我將完整的代碼展出,并且包括了需要用到的工具類的代碼
Controller層主要的邏輯部分代碼:
package com.fs.system.web.controller.common; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.concurrent.TimeUnit; import javax.annotation.Resource; import javax.imageio.ImageIO; import javax.servlet.http.HttpServletResponse; import cn.hutool.core.codec.Base64; import cn.hutool.core.lang.UUID; import com.fs.common.config.FeiSiConfig; import com.fs.common.constant.CacheConstants; import com.fs.common.constant.Constants; import com.fs.common.core.vo.AjaxResult; import com.fs.common.util.RedisCache; import com.fs.system.service.ISysConfigService; import com.fs.system.util.KaptchaTextCreator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.FastByteArrayOutputStream; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import com.google.code.kaptcha.Producer; /** * 驗(yàn)證碼操作處理 * */ @RestController public class CaptchaController{ @Autowired private ISysConfigService configService ; //判斷是否開啟驗(yàn)證碼 @Autowired private FeiSiConfig feiSiConfig ; //配置信息 @Autowired private KaptchaTextCreator kaptchaTextCreator ; //隨機(jī)生成驗(yàn)證碼表達(dá)式 @Autowired private RedisCache redisCache ; //存儲(chǔ)數(shù)據(jù)到緩存 @Resource(name = "captchaProducer") private Producer captchaProducer; //生成普通類型驗(yàn)證碼圖片 @Resource(name="captchaProducerMath") private Producer captchaProducerMath; //生成計(jì)算類型驗(yàn)證碼圖片 /** * 生成驗(yàn)證碼 */ @GetMapping("/captchaImage") public AjaxResult getCode(HttpServletResponse response) throws IOException { AjaxResult ajaxResult = AjaxResult.success() ; /** * 思路: * 我們目前驗(yàn)證分為兩種,一種是計(jì)算類型驗(yàn)證碼,一種是單純的圖形驗(yàn)證碼 * 所以,我們第一步就是判斷驗(yàn)證碼的類型,而驗(yàn)證碼的類型是我們?cè)谂渲梦募渲煤玫? */ // 先要判斷一下是否開啟了驗(yàn)證碼功能,如果沒有開啟,則不需要進(jìn)行生成驗(yàn)證碼的操作了 boolean captchaEnabled = configService.selectCaptchaEnabled(); if (!captchaEnabled){ return ajaxResult.put("captchaOnOff",captchaEnabled) ; //返回沒有開啟驗(yàn)證碼信息給前端 } BufferedImage image = null ;//圖片 String expression ; //表達(dá)式部分 String answer ; //答案部分 String uuid ;; //uuid // 否則說明開啟了驗(yàn)證碼,那么需要判斷使用的是什么類型的驗(yàn)證碼 /** * 至于這個(gè)captchaType的值是根據(jù)配置文件設(shè)置的,因?yàn)檫@個(gè)FeisiConfig是一個(gè)配置類 * 它加了‘@ConfigurationProperties(prefix = "fs")'這個(gè)注解, * 而配置文件規(guī)定math是計(jì)算類型驗(yàn)證碼,char是圖形類型驗(yàn)證碼 */ String captchaType = feiSiConfig.getCaptchaType(); if (captchaType.equals("math")){ // 如果驗(yàn)證碼的類型是一個(gè)計(jì)算類型驗(yàn)證碼 /** * 那么我們就需要分析一下: * 比如一個(gè)計(jì)算類型的驗(yàn)證碼:1+2=? * 那么我們到時(shí)候應(yīng)該填入的驗(yàn)證碼的答案是:3 * 所以這個(gè)驗(yàn)證碼它包含了兩個(gè)部分,一個(gè)是"1+2=?" 另外一個(gè)是"3" * 其實(shí)生成這個(gè)計(jì)算問題是我們自己來實(shí)現(xiàn)的,我們的工具類已經(jīng)封裝好了計(jì)算提,比如:“1+2=3” * 所以我們現(xiàn)在要做的主要有三步: * 1.得到這個(gè)算式后,將算是“1+2=3”分割成兩個(gè)部分,“1+2=?” 和 “3” * 2.然后我們需要把它們存儲(chǔ)到緩存中去,前面可以當(dāng)作key,后面可以當(dāng)作value,然后進(jìn)行驗(yàn)證答案的正確性 * 3.最后,我們需要把“1+2=?”制作成一個(gè)圖片返回給前端,用于展示給用戶, */ // 獲取一個(gè)隨機(jī)生成的表達(dá)式(隨機(jī)生成的工具封裝在KaptchaTextCreator類中) String text = kaptchaTextCreator.getText(); // 這個(gè)表達(dá)式其實(shí)我們?cè)诠ぞ哳惿傻臅r(shí)候做了處理,text其實(shí)是“1+2=@3”,這樣就方便分離表達(dá)式和答案 // 分割表達(dá)式 expression = text.substring(0,text.lastIndexOf("@")) ; //"1+2=?" answer = text.substring(text.lastIndexOf("@")+1) ; //"@" // 最后,我們需要把“1+2=?”制作成一個(gè)圖片返回給前端,用于展示給用戶, // 制作成圖片也有專門的工具類,然后我們需要把這個(gè)圖片轉(zhuǎn)成64編碼返回給前端,這個(gè)功能代碼其實(shí)是固定的 //生成驗(yàn)證碼圖片(google.code.kaptcha提供的工具類Product) image = captchaProducerMath.createImage(expression); }else { /** * 如果不是計(jì)算式類型的驗(yàn)證碼,那就是圖形類型的驗(yàn)證碼,那么就更簡(jiǎn)單了。 * 只需要把圖形驗(yàn)證碼存到緩存中,然后再把圖片返回給前端就好 */ // 圖形驗(yàn)證碼的話不能和上面一樣生成表達(dá)式了,而是隨機(jī)生成一個(gè)文本,然后把文本賦值給exception和answer,這樣方便存儲(chǔ) expression = answer= captchaProducer.createText(); // 再把這個(gè)文本轉(zhuǎn)成驗(yàn)證碼圖片 image = captchaProducer.createImage(expression) ; } System.out.println(expression+":"+answer); // 然后把答案存入到redis中,當(dāng)然了key不能直接用表達(dá)式,因?yàn)橛锌赡軙?huì)重復(fù) // 所以我們用uuid來作為key,然后把uuid給前端,前端在訪問的時(shí)候再把uuid傳來進(jìn)行驗(yàn)證 uuid = UUID.randomUUID().toString();; //uuid //Constants.CAPTCHA_EXPIRATION :表示2 , TimeUnit.MINUTES:表示分鐘,即這個(gè)驗(yàn)證碼數(shù)據(jù)只存儲(chǔ)2分鐘 redisCache.setCacheObject(CacheConstants.CAPTCHA_CODE_KEY+uuid,answer,Constants.CAPTCHA_EXPIRATION , TimeUnit.MINUTES); //最后再把生成的驗(yàn)證碼圖片轉(zhuǎn)成Base64編碼 //轉(zhuǎn)之前需要先把圖片轉(zhuǎn)成字節(jié)數(shù)組 FastByteArrayOutputStream os = new FastByteArrayOutputStream(); ImageIO.write(image,"jpeg",os);//把圖片通過jpeg類型寫進(jìn)字節(jié)數(shù)組 //再轉(zhuǎn)成Base64編碼 String base64Str = Base64.encode(os.toByteArray()); //得到的Base64編碼不能直接返回,還需要按照規(guī)定添加頭部 String base64Img = "data:image/jpeg;base64," +base64Str; //BASE64對(duì)密文進(jìn)行傳輸加密時(shí),可能出現(xiàn)\r\n //原因: RFC2045中有規(guī)定:即Base64一行不能超過76字符,超過則添加回車換行符。 //解決方案: BASE64加密后,對(duì)\r\n進(jìn)行去除 base64Img= base64Img.replaceAll("\r|\n", ""); // 最后把這個(gè)Base64編碼的表達(dá)式圖片驗(yàn)證碼和用于表示key的uuid返回給前端 ajaxResult.put("img",base64Img); ajaxResult.put("uuid",uuid) ; os.close(); return ajaxResult ; } }
用于判斷是否開啟驗(yàn)證碼的功能的configService類代碼:
package com.fs.system.service.ipml; import cn.hutool.core.convert.Convert; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fs.common.constant.CacheConstants; import com.fs.common.core.pojo.SysConfig; import com.fs.common.util.RedisCache; import com.fs.common.util.StringUtils; import com.fs.system.mapper.SysConfigMapper; import com.fs.system.service.ISysConfigService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.util.List; import java.util.Objects; @Service public class SysConfigServiceImpl implements ISysConfigService { @Autowired private SysConfigMapper sysConfigMapper; @Autowired private RedisCache redisCache; @PostConstruct //標(biāo)記在方法上, 當(dāng)這個(gè)bean創(chuàng)建完成,自動(dòng)執(zhí)行這個(gè)注解標(biāo)記方法,要求這個(gè)方法無參 類似 init-method配置 public void init(){ loadingConfigCache(); } // 初始化數(shù)據(jù),當(dāng)系統(tǒng)加載的時(shí)候,把數(shù)據(jù)存入到緩存中去 @Override public void loadingConfigCache() { System.out.println("初始化數(shù)據(jù)..."); //查詢數(shù)據(jù)庫 List<SysConfig> sysConfigs = sysConfigMapper.selectList(null); //保存到redis緩存中 sysConfigs.forEach((sysConfig)->{ redisCache.setCacheObject(getCacheKey(sysConfig.getConfigKey()),sysConfig.getConfigValue()); }); } /** * 構(gòu)建參數(shù)配置表的redis的key * @param configKey * @return */ // 獲取redis緩存中的key private String getCacheKey(String configKey){ return CacheConstants.SYS_CONFIG_KEY+configKey; } // 判斷賬號(hào)的驗(yàn)證碼是否以及開啟 @Override public boolean selectCaptchaEnabled() { String cacheObject = redisCache.getCacheObject("sys.account.captchaOnOff"); if (StrUtil.isBlank(cacheObject)){ // 如果查詢出來的是空,則說明該賬號(hào)是第一次登錄,則默認(rèn)開啟驗(yàn)證碼 return true ; } // 如果不是空,那么查詢到的數(shù)據(jù)不是true就是false,但是存到數(shù)據(jù)庫的并不是Boolean類型而是String類型, // 所以我們借用工具直接把字符串轉(zhuǎn)成對(duì)應(yīng)的Boolean類型 return Convert.toBool(cacheObject) ; } // 根據(jù)賬號(hào)的key獲取緩存中的value @Override public String selectConfigByKey(String configKey) { // 1. 如果從redis中得到了數(shù)據(jù),那么直接返回 String cacheObject = redisCache.getCacheObject(getCacheKey(configKey)); // 我們需要返回的是一個(gè)String類型,所以需要用工具包轉(zhuǎn)為String類型 String toStr = Convert.toStr(cacheObject); if (StrUtil.isNotBlank(toStr)){ return toStr ; } // 2.如果沒有得到數(shù)據(jù),那么我們需要從sys_config數(shù)據(jù)庫表中查詢數(shù)據(jù),同時(shí)把查詢到的數(shù)據(jù)存入緩存中 LambdaQueryWrapper<SysConfig> queryWrapper=new LambdaQueryWrapper<>(); queryWrapper.eq(SysConfig::getConfigKey,configKey); SysConfig sysConfig = sysConfigMapper.selectOne(queryWrapper); // 如果查到有數(shù)據(jù),就存入redis并且返回 if(Objects.nonNull(sysConfig)){ //這里判空的方法不能繼續(xù)和上面一樣,因?yàn)閟ysConfig并不是字符串類型,而是一個(gè)對(duì)象 // 獲取到值 String configValue = sysConfig.getConfigValue(); redisCache.setCacheObject(getCacheKey(configKey),configValue); return configValue; } // 否則沒有查到數(shù)據(jù),則說明沒有信息 return null ; } }
驗(yàn)證碼類型配置信息的配置類feiSiConfig , 以及配置文件yam的代碼
package com.fs.common.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * 讀取項(xiàng)目相關(guān)配置 * */ @Component @ConfigurationProperties(prefix = "fs") public class FeiSiConfig { /** 項(xiàng)目名稱 */ private String name; /** 版本 */ private String version; /** 版權(quán)年份 */ private String copyrightYear; /** 上傳路徑 */ private static String profile; /** 獲取地址開關(guān) */ private static boolean addressEnabled; /** 驗(yàn)證碼類型 */ private static String captchaType; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getVersion() { return version; } public void setVersion(String version) { this.version = version; } public String getCopyrightYear() { return copyrightYear; } public void setCopyrightYear(String copyrightYear) { this.copyrightYear = copyrightYear; } public static String getProfile() { return profile; } public void setProfile(String profile) { FeiSiConfig.profile = profile; } public static boolean isAddressEnabled() { return addressEnabled; } public void setAddressEnabled(boolean addressEnabled) { FeiSiConfig.addressEnabled = addressEnabled; } public static String getCaptchaType() { return captchaType; } public void setCaptchaType(String captchaType) { FeiSiConfig.captchaType = captchaType; } /** * 獲取導(dǎo)入上傳路徑 */ public static String getImportPath() { return getProfile() + "/import"; } /** * 獲取頭像上傳路徑 */ public static String getAvatarPath() { return getProfile() + "/avatar"; } /** * 獲取下載路徑 */ public static String getDownloadPath() { return getProfile() + "/download/"; } /** * 獲取上傳路徑 */ public static String getUploadPath() { return getProfile() + "/upload"; } }
yml配置文件:
# 項(xiàng)目相關(guān)配置 fs: # 名稱 name: FeiSi # 版本 version: 1.0.0 # 版權(quán)年份 copyrightYear: 2023 # 文件路徑 示例( Windows配置D:/feisi/uploadPath,Linux配置 /home/feisi/uploadPath) profile: D:/feisi/uploadPath # 獲取ip地址開關(guān) addressEnabled: false # 驗(yàn)證碼類型 math 數(shù)字計(jì)算 char 字符驗(yàn)證 captchaType: math
生成隨機(jī)計(jì)算類型表達(dá)式的 kaptchaTextCreator類代碼
package com.fs.system.util; import com.google.code.kaptcha.text.impl.DefaultTextCreator; import org.springframework.stereotype.Component; import java.util.Random; @Component public class KaptchaTextCreator extends DefaultTextCreator { private static final String[] CNUMBERS = "0,1,2,3,4,5,6,7,8,9,10".split(","); @Override public String getText() { Integer result = 0; Random random = new Random(); int x = random.nextInt(10); int y = random.nextInt(10); StringBuilder suChinese = new StringBuilder(); int randomoperands = random.nextInt(3); if (randomoperands == 0) { result = x * y; suChinese.append(CNUMBERS[x]); suChinese.append("*"); suChinese.append(CNUMBERS[y]); } else if (randomoperands == 1) { if ((x != 0) && y % x == 0) { result = y / x; suChinese.append(CNUMBERS[y]); suChinese.append("/"); suChinese.append(CNUMBERS[x]); } else { result = x + y; suChinese.append(CNUMBERS[x]); suChinese.append("+"); suChinese.append(CNUMBERS[y]); } } else { if (x >= y) { result = x - y; suChinese.append(CNUMBERS[x]); suChinese.append("-"); suChinese.append(CNUMBERS[y]); } else { result = y - x; suChinese.append(CNUMBERS[y]); suChinese.append("-"); suChinese.append(CNUMBERS[x]); } } suChinese.append("=?@" + result); return suChinese.toString(); //5+6=?@11 } }
封裝redis,把數(shù)據(jù)存儲(chǔ)在redis緩存的工具類的redisCache類的代碼:
package com.fs.common.util; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.BoundSetOperations; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Component; /** * spring redis 工具類 * **/ @SuppressWarnings(value = { "unchecked", "rawtypes" }) @Component public class RedisCache { @Autowired public RedisTemplate redisTemplate; /** * 緩存基本的對(duì)象,Integer、String、實(shí)體類等 * * @param key 緩存的鍵值 * @param value 緩存的值 */ public <T> void setCacheObject(final String key, final T value) { redisTemplate.opsForValue().set(key, value); } /** * 緩存基本的對(duì)象,Integer、String、實(shí)體類等 * * @param key 緩存的鍵值 * @param value 緩存的值 * @param timeout 時(shí)間 * @param timeUnit 時(shí)間顆粒度 */ public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) { redisTemplate.opsForValue().set(key, value, timeout, timeUnit); } /** * 設(shè)置有效時(shí)間 * * @param key Redis鍵 * @param timeout 超時(shí)時(shí)間 * @return true=設(shè)置成功;false=設(shè)置失敗 */ public boolean expire(final String key, final long timeout) { return expire(key, timeout, TimeUnit.SECONDS); } /** * 設(shè)置有效時(shí)間 * * @param key Redis鍵 * @param timeout 超時(shí)時(shí)間 * @param unit 時(shí)間單位 * @return true=設(shè)置成功;false=設(shè)置失敗 */ public boolean expire(final String key, final long timeout, final TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } /** * 獲取有效時(shí)間 * * @param key Redis鍵 * @return 有效時(shí)間 */ public long getExpire(final String key) { return redisTemplate.getExpire(key); } /** * 判斷 key是否存在 * * @param key 鍵 * @return true 存在 false不存在 */ public Boolean hasKey(String key) { return redisTemplate.hasKey(key); } /** * 獲得緩存的基本對(duì)象。 * * @param key 緩存鍵值 * @return 緩存鍵值對(duì)應(yīng)的數(shù)據(jù) */ public <T> T getCacheObject(final String key) { ValueOperations<String, T> operation = redisTemplate.opsForValue(); return operation.get(key); } /** * 刪除單個(gè)對(duì)象 * * @param key */ public boolean deleteObject(final String key) { return redisTemplate.delete(key); } /** * 刪除集合對(duì)象 * * @param collection 多個(gè)對(duì)象 * @return */ public boolean deleteObject(final Collection collection) { return redisTemplate.delete(collection) > 0; } /** * 緩存List數(shù)據(jù) * * @param key 緩存的鍵值 * @param dataList 待緩存的List數(shù)據(jù) * @return 緩存的對(duì)象 */ public <T> long setCacheList(final String key, final List<T> dataList) { Long count = redisTemplate.opsForList().rightPushAll(key, dataList); return count == null ? 0 : count; } /** * 獲得緩存的list對(duì)象 * * @param key 緩存的鍵值 * @return 緩存鍵值對(duì)應(yīng)的數(shù)據(jù) */ public <T> List<T> getCacheList(final String key) { return redisTemplate.opsForList().range(key, 0, -1); } /** * 緩存Set * * @param key 緩存鍵值 * @param dataSet 緩存的數(shù)據(jù) * @return 緩存數(shù)據(jù)的對(duì)象 */ public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) { BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key); Iterator<T> it = dataSet.iterator(); while (it.hasNext()) { setOperation.add(it.next()); } return setOperation; } /** * 獲得緩存的set * * @param key * @return */ public <T> Set<T> getCacheSet(final String key) { return redisTemplate.opsForSet().members(key); } /** * 緩存Map * * @param key * @param dataMap */ public <T> void setCacheMap(final String key, final Map<String, T> dataMap) { if (dataMap != null) { redisTemplate.opsForHash().putAll(key, dataMap); } } /** * 獲得緩存的Map * * @param key * @return */ public <T> Map<String, T> getCacheMap(final String key) { return redisTemplate.opsForHash().entries(key); } /** * 往Hash中存入數(shù)據(jù) * * @param key Redis鍵 * @param hKey Hash鍵 * @param value 值 */ public <T> void setCacheMapValue(final String key, final String hKey, final T value) { redisTemplate.opsForHash().put(key, hKey, value); } /** * 獲取Hash中的數(shù)據(jù) * * @param key Redis鍵 * @param hKey Hash鍵 * @return Hash中的對(duì)象 */ public <T> T getCacheMapValue(final String key, final String hKey) { HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash(); return opsForHash.get(key, hKey); } /** * 獲取多個(gè)Hash中的數(shù)據(jù) * * @param key Redis鍵 * @param hKeys Hash鍵集合 * @return Hash對(duì)象集合 */ public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) { return redisTemplate.opsForHash().multiGet(key, hKeys); } /** * 刪除Hash中的某條數(shù)據(jù) * * @param key Redis鍵 * @param hKey Hash鍵 * @return 是否成功 */ public boolean deleteCacheMapValue(final String key, final String hKey) { return redisTemplate.opsForHash().delete(key, hKey) > 0; } /** * 獲得緩存的基本對(duì)象列表 * * @param pattern 字符串前綴 * @return 對(duì)象列表 */ public Collection<String> keys(final String pattern) { return redisTemplate.keys(pattern); } }
以上就是實(shí)現(xiàn)驗(yàn)證碼功能的整個(gè)后端代碼。
總結(jié)
到此這篇關(guān)于基于SpringBoot實(shí)現(xiàn)驗(yàn)證碼功能的代碼及思路的文章就介紹到這了,更多相關(guān)SpringBoot實(shí)現(xiàn)驗(yàn)證碼內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot實(shí)現(xiàn)前端驗(yàn)證碼圖片生成和校驗(yàn)
- SpringBoot實(shí)現(xiàn)短信驗(yàn)證碼校驗(yàn)方法思路詳解
- SpringBoot登錄驗(yàn)證碼實(shí)現(xiàn)過程詳解
- Springboot實(shí)現(xiàn)驗(yàn)證碼登錄
- springboot實(shí)現(xiàn)郵箱驗(yàn)證碼功能
- springboot短信驗(yàn)證碼登錄功能的實(shí)現(xiàn)
- SpringBoot發(fā)送郵箱驗(yàn)證碼功能
- springboot整合shiro多驗(yàn)證登錄功能的實(shí)現(xiàn)(賬號(hào)密碼登錄和使用手機(jī)驗(yàn)證碼登錄)
相關(guān)文章
Spring占位符Placeholder的實(shí)現(xiàn)原理解析
這篇文章主要介紹了Spring占位符Placeholder的實(shí)現(xiàn)原理,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03Java如何將json字符串與實(shí)體類互相轉(zhuǎn)換
在我們調(diào)用三方平臺(tái)接口時(shí),經(jīng)常需要將我們封裝的實(shí)體類轉(zhuǎn)換為json作為傳參,下面這篇文章主要給大家介紹了關(guān)于Java如何將json字符串與實(shí)體類互相轉(zhuǎn)換的相關(guān)資料,需要的朋友可以參考下2023-11-11關(guān)于Maven parent.relativePath說明
Maven中的relativePath用于指定父項(xiàng)目pom.xml的相對(duì)路徑,默認(rèn)值為../pom.xml,這個(gè)配置幫助Maven在構(gòu)建時(shí)定位父模塊的位置,確保模塊間的依賴關(guān)系正確,relativePath可以指向本地或遠(yuǎn)程倉庫中的父項(xiàng)目,如果不需要尋找父項(xiàng)目,可以將其設(shè)置為空2024-09-09Java中使用@CrossOrigin和Proxy解決跨域問題詳解
這篇文章主要介紹了Java中使用@CrossOrigin和Proxy解決跨域問題詳解,在Web開發(fā)中,如果前端頁面和后端接口不在同一個(gè)域名下,就會(huì)發(fā)生跨域請(qǐng)求的問題,同源策略是瀏覽器的一種安全策略,它限制了來自不同源的客戶端腳本在瀏覽器中運(yùn)行時(shí)的交互,需要的朋友可以參考下2023-12-12