簡單聊聊Java中驗證碼功能的實現(xiàn)
前言
大家好,我是 god23bin,今天說說驗證碼功能的實現(xiàn),相信大家都經(jīng)常接觸到驗證碼的,畢竟平時上網(wǎng)也能遇到各種驗證碼,需要我們輸入驗證碼進行驗證我們是人類,而不是機器人。
驗證碼有多種類型,比如圖片驗證碼、短信驗證碼和郵件驗證碼等等,雖說多種類型,圖片也好,短信也好,郵件也好,都是承載驗證碼的載體,最主要的核心就是一個驗證碼的生成、存儲和校驗。
本篇文章就從這幾個方面出發(fā)說說驗證碼,廢話不多說,下面開始正文。
實現(xiàn)思路
驗證碼驗證的功能,其實現(xiàn)思路還是挺簡單的,不論是圖片驗證碼、短信驗證碼還是郵件驗證碼,無非就以下幾點:
- 驗證碼本質(zhì)就是一堆字符的組合(數(shù)字也好,英文字母也好),后端生成驗證碼,并存儲到某個位置(比如存儲到 Redis,并設(shè)置驗證碼的過期時間)。
- 返回驗證碼給前端頁面、發(fā)送短信驗證碼給用戶或者發(fā)送郵件驗證碼給用戶。驗證碼可以是以文字顯示或者圖片顯示。
- 用戶輸入看到的驗證碼,并提交驗證(驗證也可以忽略大小寫,當(dāng)然具體看需求)。
- 后端將用戶輸入的驗證碼拿過來進行校驗,對比用戶輸入的驗證碼是否和后端生成的一致,一致就驗證成功,否則驗證失敗。
驗證碼的生成
首先,需要知道的就是驗證碼的生成,這就涉及到生成驗證碼的算法,可以自己純手寫,也可以使用人家提供的工具,這里我就介紹下面 4 種生成驗證碼的方式。
1. 純原生手寫生成文本驗證碼
需求:隨機產(chǎn)生一個 n 位的驗證碼,每位可能是數(shù)字、大寫字母、小寫字母。
實現(xiàn):本質(zhì)就是隨機生成字符串,字符串可包含數(shù)字、大寫字母、小寫字母。
準備一個包含數(shù)字、大寫字母、小寫字母的字符串,借助 Random 類,循環(huán) n 次隨機獲取字符串的下標,就能拼接出一個隨機字符組成的字符串了。
package cn.god23bin.demo.util; import java.util.Random; public class MyCaptchaUtil { /** * 生成 n 位驗證碼 * @param n 位數(shù) * @return n 位驗證碼 **/ public static String generateCode(int n) { String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; StringBuilder sb = new StringBuilder(); Random random = new Random(); for (int i = 0; i < n; i++) { int index = random.nextInt(chars.length()); sb.append(chars.charAt(index)); } return sb.toString(); } }
2. 純原生手寫生成圖片驗證碼
實現(xiàn):使用 Java 的 awt 和 swing 庫來生成圖片驗證碼。下面使用 BufferedImage 類創(chuàng)建一個指定大小的圖片,然后隨機生成 n 個字符,將其畫在圖片上,將生成的字符和圖片驗證碼放到哈希表返回。后續(xù)我們就可以拿到驗證碼的文本值,并且可以將圖片驗證碼輸出到指定的輸出流中。
package cn.god23bin.demo.util; import java.awt.*; import java.awt.image.BufferedImage; import java.util.HashMap; import java.util.Map; public class MyCaptchaUtil { /** * 生成 n 位的圖片驗證碼 * @param n 位數(shù) * @return 哈希表,code 獲取文本驗證碼,img 獲取 BufferedImage 圖片對象 **/ public static Map<String, Object> generateCodeImage(int n) { int width = 100, height = 50; BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g = image.createGraphics(); g.setColor(Color.LIGHT_GRAY); g.fillRect(0, 0, width, height); String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; Random random = new Random(); StringBuilder sb = new StringBuilder(); for (int i = 0; i < n; i++) { int index = random.nextInt(chars.length()); char c = chars.charAt(index); sb.append(c); g.setColor(new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255))); g.setFont(new Font("Arial", Font.BOLD, 25)); g.drawString(Character.toString(c), 20 + i * 15, 25); } Map<String, Object> res = new HashMap<>(); res.put("code", sb.toString()); res.put("img", image); return res; } }
我們可以寫一個獲取驗證碼的接口,以二進制流輸出返回給前端,前端可以直接使用 img
標簽來顯示我們返回的圖片,只需在 src
屬性賦值我們的獲取驗證碼接口。
@RequestMapping("/captcha") @RestController public class CaptchaController { @GetMapping("/code/custom") public void getCode(HttpServletResponse response) { Map<String, Object> map = MyCaptchaUtil.generateCodeImage(5); System.out.println(map.get("code")); BufferedImage img = (BufferedImage) map.get("img"); // 設(shè)置響應(yīng)頭,防止緩存 response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/png"); try { ImageIO.write(img, "png", response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } } }
3. 使用 Hutool 工具生成圖形驗證碼
引入依賴:可以單獨引入驗證碼模塊或者全部模塊都引入
<!-- 驗證碼模塊 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-captcha</artifactId> <version>5.8.15</version> </dependency> <!-- 全部模塊都引入 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.15</version> </dependency>
生成線段干擾的驗證碼:
// 設(shè)置圖形驗證碼的寬和高,同時生成了驗證碼,可以通過 lineCaptcha.getCode() 獲取文本驗證碼 LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
生成圓圈干擾的驗證碼:
// 設(shè)置圖形驗證碼的寬、高、驗證碼字符數(shù)、干擾元素個數(shù) CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20);
生成扭曲干擾的驗證碼:
// 定義圖形驗證碼的寬、高、驗證碼字符數(shù)、干擾線寬度 ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(200, 100, 4, 4);
獲取驗證碼接口:
@RequestMapping("/captcha") @RestController public class CaptchaController { @GetMapping("/code/hutool") public void getCodeByHutool(HttpServletResponse response) { LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100); System.out.println("線段干擾的驗證碼:" + lineCaptcha.getCode()); // 設(shè)置響應(yīng)頭,防止緩存 response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/png"); try { lineCaptcha.write(response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } } }
4. 使用 Kaptcha 生成驗證碼
Kaptcha 是谷歌的一個生成驗證碼工具包,我們簡單配置其屬性就可以實現(xiàn)驗證碼的驗證功能。
引入依賴項:它只有一個版本:2.3.2
<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency>
簡單看看 kaptcha 屬性:
屬性 | 描述 | 默認值 |
---|---|---|
kaptcha.border | 圖片邊框,合法值:yes , no | yes |
kaptcha.border.color | 邊框顏色,合法值: r,g,b (and optional alpha) 或者 white,black,blue. | black |
kaptcha.border.thickness | 邊框厚度,合法值:>0 | 1 |
kaptcha.image.width | 圖片寬 | 200 |
kaptcha.image.height | 圖片高 | 50 |
kaptcha.producer.impl | 圖片實現(xiàn)類 | com.google.code.kaptcha.impl.DefaultKaptcha |
kaptcha.textproducer.impl | 文本實現(xiàn)類 | com.google.code.kaptcha.text.impl.DefaultTextCreator |
kaptcha.textproducer.char.string | 文本集合,驗證碼值從此集合中獲取 | abcde2345678gfynmnpwx |
kaptcha.textproducer.char.length | 驗證碼長度 | 5 |
kaptcha.textproducer.font.names | 字體 | Arial, Courier |
kaptcha.textproducer.font.size | 字體大小 | 40px |
kaptcha.textproducer.font.color | 字體顏色,合法值: r,g,b 或者 white,black,blue. | black |
kaptcha.textproducer.char.space | 文字間隔 | 2 |
kaptcha.noise.impl | 干擾實現(xiàn)類 | com.google.code.kaptcha.impl.DefaultNoise |
kaptcha.noise.color | 干擾顏色,合法值: r,g,b 或者 white,black,blue. | black |
kaptcha.obscurificator.impl | 圖片樣式: 水紋com.google.code.kaptcha.impl.WaterRipple 魚眼com.google.code.kaptcha.impl.FishEyeGimpy 陰影com.google.code.kaptcha.impl.ShadowGimpy | com.google.code.kaptcha.impl.WaterRipple |
kaptcha.background.impl | 背景實現(xiàn)類 | com.google.code.kaptcha.impl.DefaultBackground |
kaptcha.background.clear.from | 背景顏色漸變,開始顏色 | light grey |
kaptcha.background.clear.to | 背景顏色漸變,結(jié)束顏色 | white |
kaptcha.word.impl | 文字渲染器 | com.google.code.kaptcha.text.impl.DefaultWordRenderer |
kaptcha.session.key | session key | KAPTCHA_SESSION_KEY |
kaptcha.session.date | session date | KAPTCHA_SESSION_DATE |
簡單配置下 Kaptcha:
package cn.god23bin.demo.config; import com.google.code.kaptcha.impl.DefaultKaptcha; import com.google.code.kaptcha.util.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Properties; @Configuration public class KaptchaConfig { /** * 配置生成圖片驗證碼的bean * @return */ @Bean(name = "kaptchaProducer") public DefaultKaptcha getKaptchaBean() { DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); Properties properties = new Properties(); properties.setProperty("kaptcha.border", "no"); properties.setProperty("kaptcha.textproducer.font.color", "black"); properties.setProperty("kaptcha.textproducer.char.space", "4"); properties.setProperty("kaptcha.textproducer.char.length", "4"); properties.setProperty("kaptcha.textproducer.char.string", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); Config config = new Config(properties); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
也是和 Hutool 一樣,很簡單就能生成驗證碼了。如下:
// 生成文字驗證碼 String text = kaptchaProducer.createText(); // 生成圖片驗證碼 BufferedImage image = kaptchaProducer.createImage(text);
獲取驗證碼接口:
@RequestMapping("/captcha") @RestController public class CaptchaController { @Autowired private Producer kaptchaProducer; @GetMapping("/code/kaptcha") public void getCodeByKaptcha(HttpServletResponse response) { // 生成文字驗證碼 String text = kaptchaProducer.createText(); System.out.println("文字驗證碼:" + text); // 生成圖片驗證碼 BufferedImage image = kaptchaProducer.createImage(text); // 設(shè)置響應(yīng)頭,防止緩存 response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/jpeg"); try { ImageIO.write(image, "jpg", response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } } }
驗證碼的存儲與校驗
上面的驗證碼的生成,就僅僅是生成驗證碼,并沒有將驗證碼存儲在后端,所以現(xiàn)在我們需要做的是:將驗證碼存儲起來,便于后續(xù)的校驗對比。
那么存儲到什么地方呢?如果你沒接觸過 Redis,那么第一次的想法可能就是存儲到關(guān)系型數(shù)據(jù)庫中,比如 MySQL。想當(dāng)年,我最開始的想法就是這樣哈哈哈。
不過,目前用得最多的就是將驗證碼存儲到 Redis 中,好處就是減少了數(shù)據(jù)庫的壓力,加快了驗證碼的讀取效率,還能輕松設(shè)置驗證碼的過期時間。
簡單配置 Redis
引入 Redis 依賴項:
我們使用 Spring Data Redis,它提供了 RedisTemplate
和 StringRedisTemplate
模板類,簡化了我們使用 Java 進行 Redis 的操作。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
簡單配置下 Redis:
spring: redis: host: localhost port: 6379 database: 1 timeout: 5000
@Configuration public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { // 大多數(shù)情況,都是選用<String, Object> RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); // 使用JSON的序列化對象,對數(shù)據(jù) key 和 value 進行序列化轉(zhuǎn)換 Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); // ObjectMapper 是 Jackson 的一個工作類,作用是將 JSON 轉(zhuǎn)成 Java 對象,即反序列化?;?qū)?Java 對象轉(zhuǎn)成 JSON,即序列化 ObjectMapper mapper = new ObjectMapper(); // 設(shè)置序列化時的可見性,第一個參數(shù)是選擇序列化哪些屬性,比如時序列化 setter? 還是 filed? 第二個參數(shù)是選擇哪些修飾符權(quán)限的屬性來序列化,比如 private 或者 public,這里的 any 是指對所有權(quán)限修飾的屬性都可見(可序列化) mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); jackson2JsonRedisSerializer.setObjectMapper(mapper); // 設(shè)置 RedisTemplate 模板的序列化方式為 jacksonSeial template.setDefaultSerializer(jackson2JsonRedisSerializer); return template; } }
將驗證碼存儲到 Redis
將驗證碼存儲到 Redis 設(shè)置 5 分鐘的過期時間,Redis 是 Key Value 這種形式存儲的,所以需要約定好 Key 的命名規(guī)則。
命名的時候,為了區(qū)分為每個用戶生成的驗證碼,所以需要一個標識,剛好可以通過當(dāng)前請求的 HttpSession 中的 SessionID 作為唯一標識,拼接到 Key 的名稱中。
當(dāng)然,也不一定使用 SessionID 作為唯一標識,如果能知道其他的,也可以用其他的作為標識,比如拼接用戶的手機號。
實現(xiàn):
@RequestMapping("/captcha") @RestController public class CaptchaController { @Autowired private Producer kaptchaProducer; @Autowired private RedisTemplate<String, Object> redisTemplate; @GetMapping("/code") public void getCode(HttpServletRequest request, HttpServletResponse response) { // 生成文字驗證碼 String text = kaptchaProducer.createText(); System.out.println("文字驗證碼:" + text); // 生成圖片驗證碼 BufferedImage image = kaptchaProducer.createImage(text); // 存儲到 Redis 設(shè)置 5 分鐘的過期時間 // 約定好存儲的 Key 的命名規(guī)則,這里使用 code_sessionId_type_1 表示圖形驗證碼 // Code_sessionId_Type_1:分為 3 部分,code 表明是驗證碼,sessionId 表明是給哪個用戶的驗證碼,type_n 表明驗證碼類型,n 為 1 表示圖形驗證碼,2 表示短信驗證碼,3 表示郵件驗證碼 String key = "code_" + request.getSession().getId() + "_type_1"; redisTemplate.opsForValue().set(key, text, 5, TimeUnit.SECONDS); response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/jpeg"); try { ImageIO.write(image, "jpg", response.getOutputStream()); } catch (IOException e) { e.printStackTrace(); } } }
上面代碼中有一個額外的設(shè)計就是,由于發(fā)送的驗證碼有多種類型(圖形驗證碼、短信驗證碼、郵件驗證碼),所以加多了一個 type_n
來標識當(dāng)前存儲的驗證碼是什么類型的,方便以后出現(xiàn)問題快速定位。
實際上,這里的命名規(guī)則,可以根據(jù)你的具體需求來定制,又比如說,登錄的時候需要驗證碼、注冊的時候也需要驗證碼、修改用戶密碼的時候也需要驗證碼,為了便于出現(xiàn)問題進行定位,也可以繼續(xù)加多一個標識 when_n
,n 為 1 表示注冊、n 為 2 表示登錄,以此類推。
校驗
我們模擬登錄的時候進行驗證碼的校驗,使用一個 LoginDTO 對象來接收前端的登錄相關(guān)的參數(shù)。
package cn.god23bin.demo.model.domain.dto; import lombok.Data; @Data public class LoginDTO { private String username; private String password; /** * 驗證碼 */ private String code; }
寫一個登錄接口,登錄的過程中,校驗用戶輸入的驗證碼。
@RequestMapping("/user") @RestController public class UserController { @Autowired private RedisTemplate<String, Object> redisTemplate; @PostMapping("/login") public Result<String> login(@RequestBody LoginDTO loginDTO, HttpServletRequest request) { if (!"root".equals(loginDTO.getUsername()) || !"123456".equals(loginDTO.getPassword())) { return Result.fail("登錄失敗!賬號或密碼不正確!"); } // 校驗用戶輸入的驗證碼 String code = loginDTO.getCode(); String codeInRedis = (String) redisTemplate.opsForValue().get("code_" + request.getSession().getId() + "_type_1"); if (!code.equals(codeInRedis)) { return Result.fail("驗證碼不正確!"); } return Result.ok("登錄成功!"); } }
至此,便完成了驗證碼功能的實現(xiàn)。
獲取驗證碼的安全設(shè)計
驗證碼功能的實現(xiàn)現(xiàn)在是OK的,但還有一點需要注意,那就是防止驗證碼被隨意調(diào)用獲取,或者被大量調(diào)用。如果不做限制,那么誰都能調(diào)用,就非常大的可能會被攻擊了。
我們上面實現(xiàn)的驗證碼功能是圖形驗證碼,是校驗用戶從圖形驗證碼中看到后輸入的數(shù)字字母組合跟后端生成的組合是否是一致的。對于圖形驗證碼,到這里就可以了,不用限制(當(dāng)然想限制也可以)。但是對于短信驗證碼,就還不可以。我們需要額外考慮一些防刷機制,以保障系統(tǒng)的安全性和可靠性(因為發(fā)短信是要錢的?。。?。
對于短信來說,一種常見的攻擊方式是「短信轟炸」,攻擊者通過自動批量提交手機號碼、模擬IP等手段,對系統(tǒng)進行大規(guī)模的短信請求,從而消耗資源或干擾正常業(yè)務(wù)。為了應(yīng)對這種情況,我們需要設(shè)計一些防刷機制。
防刷機制
目前我了解到的防刷機制有下面幾種,如果你有別的方法,歡迎評論說出來噢!
- 圖形驗證碼或者滑動驗證:發(fā)送短信前先使用圖形驗證碼或者滑動進行驗證,驗證成功才能調(diào)用發(fā)送短信驗證碼的接口。
- 時間限制:從用戶點擊發(fā)送短信驗證碼開始,前端進行一個 60 秒的倒數(shù),在這 60 秒之內(nèi),用戶無法提交發(fā)送信息的請求的,這樣就限制了發(fā)送短信驗證碼的接口的調(diào)用次數(shù)。不過這種方式,如果被攻擊者知道了發(fā)送短信的接口,那也是會被刷的。
- 手機號限制:對使用同一個手機號碼進行注冊或者其他發(fā)送短信驗證碼的操作的時候,系統(tǒng)可以對這個手機號碼進行限制,例如,一天只能發(fā)送 5 條短信驗證碼,超出限制則做出提示(如:系統(tǒng)繁忙,請稍后再試)。然而,這也只能夠避免人工手動刷短信而已,對于批量使用不同手機號碼來刷短信的機器,同樣是會被刷。
- IP地址限制:記錄請求的IP地址,并對同一 IP 地址的請求進行限制,比如限制某個 IP 地址在一定時間內(nèi)只能發(fā)送特定數(shù)量的驗證碼。同樣,也是可以被轟炸的。
至于這些機制的實現(xiàn),有機會再寫寫,你感興趣的話可以自己去操作試試!
總結(jié)
本篇文字就說了驗證碼功能的實現(xiàn)思路和實現(xiàn),包括驗證碼的生成、存儲、展示和校驗。
- 生成驗證碼可以手寫也可以借助工具。
- 存儲一般是存儲在 Redis 中的,當(dāng)然你想存儲在 MySQL 中也不是不可以,就是需要自己去實現(xiàn)諸如過期時間的功能。
- 展示可以通過文本展示或者圖片展示,我們可以返回一個二進制流給前端,前端通過
img
標簽的src
屬性去請求我們的接口。 - 校驗就拿到用戶輸入的驗證碼,和后端生成的驗證碼進行比對,相同就驗證成功,否則失敗。
最后我們也說了驗證碼的防刷機制,這是需要考慮的,這里的防刷機制對于使用大量不同手機號、不同 IP 地址是沒效果的,依舊可以暴刷。所以這部分內(nèi)容還是有待研究的。
以上就是簡單聊聊Java中驗證碼功能的實現(xiàn)的詳細內(nèi)容,更多關(guān)于Java驗證碼的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談Spring與SpringMVC父子容器的關(guān)系與初始化
這篇文章主要介紹了淺談Spring與SpringMVC父子容器的關(guān)系與初始化,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08java實現(xiàn)基于UDP協(xié)議的聊天小程序操作
UDP是與TCP相對應(yīng)的協(xié)議,UDP適用于一次只傳送少量數(shù)據(jù)、對可靠性要求不高的應(yīng)用環(huán)境。正因為UDP協(xié)議沒有連接的過程,所以它的通信效率高;但也正因為如此,它的可靠性不如TCP協(xié)議高,本文給大家介紹java實現(xiàn)基于UDP協(xié)議的聊天小程序操作,感興趣的朋友一起看看吧2021-10-10SpringBoot-Mail工具實現(xiàn)郵箱驗證碼登錄注冊功能
現(xiàn)在許多pc程序都有著使用郵箱驗證碼實現(xiàn)登錄注冊的功能,那么我們應(yīng)該如何完成郵箱驗證碼功能呢,我們可以使用springboot內(nèi)置的springboot-mail再結(jié)合redis來完成這個功能,感興趣的朋友跟隨小編一起看看吧2024-07-07