前言
大家好,我是 god23bin,今天說說驗證碼功能的實作,相信大家都經常接觸到驗證碼的,畢竟平時上網也能遇到各種驗證碼,需要我們輸入驗證碼進行驗證我們是人類,而不是機器人,
驗證碼有多種型別,比如圖片驗證碼、短信驗證碼和郵件驗證碼等等,雖說多種型別,圖片也好,短信也好,郵件也好,都是承載驗證碼的載體,最主要的核心就是一個驗證碼的生成、存盤和校驗,
本篇文章就從這幾個方面出發說說驗證碼,廢話不多說,下面開始正文,
實作思路
驗證碼驗證的功能,其實作思路還是挺簡單的,不論是圖片驗證碼、短信驗證碼還是郵件驗證碼,無非就以下幾點:
- 驗證碼本質就是一堆字符的組合(數字也好,英文字母也好),后端生成驗證碼,并存盤到某個位置(比如存盤到 Redis,并設定驗證碼的過期時間),
- 回傳驗證碼給前端頁面、發送短信驗證碼給用戶或者發送郵件驗證碼給用戶,驗證碼可以是以文字顯示或者圖片顯示,
- 用戶輸入看到的驗證碼,并提交驗證(驗證也可以忽略大小寫,當然具體看需求),
- 后端將用戶輸入的驗證碼拿過來進行校驗,對比用戶輸入的驗證碼是否和后端生成的一致,一致就驗證成功,否則驗證失敗,
驗證碼的生成
首先,需要知道的就是驗證碼的生成,這就涉及到生成驗證碼的演算法,可以自己純手寫,也可以使用人家提供的工具,這里我就介紹下面 4 種生成驗證碼的方式,
1. 純原生手寫生成文本驗證碼
需求:隨機產生一個 n 位的驗證碼,每位可能是數字、大寫字母、小寫字母,
實作:本質就是隨機生成字串,字串可包含數字、大寫字母、小寫字母,
準備一個包含數字、大寫字母、小寫字母的字串,借助 Random 類,回圈 n 次隨機獲取字串的下標,就能拼接出一個隨機字符組成的字串了,
package cn.god23bin.demo.util;
import java.util.Random;
public class MyCaptchaUtil {
/**
* 生成 n 位驗證碼
* @param n 位數
* @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. 純原生手寫生成圖片驗證碼
實作:使用 Java 的 awt 和 swing 庫來生成圖片驗證碼,下面使用 BufferedImage 類創建一個指定大小的圖片,然后隨機生成 n 個字符,將其畫在圖片上,將生成的字符和圖片驗證碼放到哈希表回傳,后續我們就可以拿到驗證碼的文本值,并且可以將圖片驗證碼輸出到指定的輸出流中,
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 位數
* @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");
// 設定回應頭,防止快取
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>
- 生成線段干擾的驗證碼:
// 設定圖形驗證碼的寬和高,同時生成了驗證碼,可以通過 lineCaptcha.getCode() 獲取文本驗證碼
LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100);
- 生成圓圈干擾的驗證碼:
// 設定圖形驗證碼的寬、高、驗證碼字符數、干擾元素個數
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(200, 100, 4, 20);
- 生成扭曲干擾的驗證碼:
// 定義圖形驗證碼的寬、高、驗證碼字符數、干擾線寬度
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());
// 設定回應頭,防止快取
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 是谷歌的一個生成驗證碼工具包,我們簡單配置其屬性就可以實作驗證碼的驗證功能,
引入依賴項:它只有一個版本: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 | 圖片實作類 | com.google.code.kaptcha.impl.DefaultKaptcha |
kaptcha.textproducer.impl | 文本實作類 | 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 | 干擾實作類 | 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 | 背景實作類 | com.google.code.kaptcha.impl.DefaultBackground |
kaptcha.background.clear.from | 背景顏色漸變,開始顏色 | light grey |
kaptcha.background.clear.to | 背景顏色漸變,結束顏色 | 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);
// 設定回應頭,防止快取
response.setHeader("Cache-Control", "no-store, no-cache");
response.setContentType("image/jpeg");
try {
ImageIO.write(image, "jpg", response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
}
}
}
驗證碼的存盤與校驗
上面的驗證碼的生成,就僅僅是生成驗證碼,并沒有將驗證碼存盤在后端,所以現在我們需要做的是:將驗證碼存盤起來,便于后續的校驗對比,
那么存盤到什么地方呢?如果你沒接觸過 Redis,那么第一次的想法可能就是存盤到關系型資料庫中,比如 MySQL,想當年,我最開始的想法就是這樣哈哈哈,
不過,目前用得最多的就是將驗證碼存盤到 Redis 中,好處就是減少了資料庫的壓力,加快了驗證碼的讀取效率,還能輕松設定驗證碼的過期時間,
簡單配置 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) {
// 大多數情況,都是選用<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 使用JSON的序列化物件,對資料 key 和 value 進行序列化轉換
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
// ObjectMapper 是 Jackson 的一個作業類,作用是將 JSON 轉成 Java 物件,即反序列化,或將 Java 物件轉成 JSON,即序列化
ObjectMapper mapper = new ObjectMapper();
// 設定序列化時的可見性,第一個引數是選擇序列化哪些屬性,比如時序列化 setter? 還是 filed? 第二個引數是選擇哪些修飾符權限的屬性來序列化,比如 private 或者 public,這里的 any 是指對所有權限修飾的屬性都可見(可序列化)
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
// 設定 RedisTemplate 模板的序列化方式為 jacksonSeial
template.setDefaultSerializer(jackson2JsonRedisSerializer);
return template;
}
}
將驗證碼存盤到 Redis
將驗證碼存盤到 Redis 設定 5 分鐘的過期時間,Redis 是 Key Value 這種形式存盤的,所以需要約定好 Key 的命名規則,
命名的時候,為了區分為每個用戶生成的驗證碼,所以需要一個標識,剛好可以通過當前請求的 HttpSession 中的 SessionID 作為唯一標識,拼接到 Key 的名稱中,
當然,也不一定使用 SessionID 作為唯一標識,如果能知道其他的,也可以用其他的作為標識,比如拼接用戶的手機號,
實作:
@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 設定 5 分鐘的過期時間
// 約定好存盤的 Key 的命名規則,這里使用 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();
}
}
}
上面代碼中有一個額外的設計就是,由于發送的驗證碼有多種型別(圖形驗證碼、短信驗證碼、郵件驗證碼),所以加多了一個 type_n
來標識當前存盤的驗證碼是什么型別的,方便以后出現問題快速定位,
實際上,這里的命名規則,可以根據你的具體需求來定制,又比如說,登錄的時候需要驗證碼、注冊的時候也需要驗證碼、修改用戶密碼的時候也需要驗證碼,為了便于出現問題進行定位,也可以繼續加多一個標識 when_n
,n 為 1 表示注冊、n 為 2 表示登錄,以此類推,
校驗
我們模擬登錄的時候進行驗證碼的校驗,使用一個 LoginDTO 物件來接收前端的登錄相關的引數,
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("登錄成功!");
}
}
至此,便完成了驗證碼功能的實作,
獲取驗證碼的安全設計
驗證碼功能的實作現在是OK的,但還有一點需要注意,那就是防止驗證碼被隨意呼叫獲取,或者被大量呼叫,如果不做限制,那么誰都能呼叫,就非常大的可能會被攻擊了,
我們上面實作的驗證碼功能是圖形驗證碼,是校驗用戶從圖形驗證碼中看到后輸入的數字字母組合跟后端生成的組合是否是一致的,對于圖形驗證碼,到這里就可以了,不用限制(當然想限制也可以),但是對于短信驗證碼,就還不可以,我們需要額外考慮一些防刷機制,以保障系統的安全性和可靠性(因為發短信是要錢的啊!),
對于短信來說,一種常見的攻擊方式是「短信轟炸」,攻擊者通過自動批量提交手機號碼、模擬IP等手段,對系統進行大規模的短信請求,從而消耗資源或干擾正常業務,為了應對這種情況,我們需要設計一些防刷機制,
防刷機制
目前我了解到的防刷機制有下面幾種,如果你有別的方法,歡迎評論說出來噢!
- 圖形驗證碼或者滑動驗證:發送短信前先使用圖形驗證碼或者滑動進行驗證,驗證成功才能呼叫發送短信驗證碼的介面,
- 時間限制:從用戶點擊發送短信驗證碼開始,前端進行一個 60 秒的倒數,在這 60 秒之內,用戶無法提交發送資訊的請求的,這樣就限制了發送短信驗證碼的介面的呼叫次數,不過這種方式,如果被攻擊者知道了發送短信的介面,那也是會被刷的,
- 手機號限制:對使用同一個手機號碼進行注冊或者其他發送短信驗證碼的操作的時候,系統可以對這個手機號碼進行限制,例如,一天只能發送 5 條短信驗證碼,超出限制則做出提示(如:系統繁忙,請稍后再試),然而,這也只能夠避免人工手動刷短信而已,對于批量使用不同手機號碼來刷短信的機器,同樣是會被刷,
- IP地址限制:記錄請求的IP地址,并對同一 IP 地址的請求進行限制,比如限制某個 IP 地址在一定時間內只能發送特定數量的驗證碼,同樣,也是可以被轟炸的,
至于這些機制的實作,有機會再寫寫,你感興趣的話可以自己去操作試試!
總結
本篇文字就說了驗證碼功能的實作思路和實作,包括驗證碼的生成、存盤、展示和校驗,
-
生成驗證碼可以手寫也可以借助工具,
-
存盤一般是存盤在 Redis 中的,當然你想存盤在 MySQL 中也不是不可以,就是需要自己去實作諸如過期時間的功能,
-
展示可以通過文本展示或者圖片展示,我們可以回傳一個二進制流給前端,前端通過
img
標簽的src
屬性去請求我們的介面, -
校驗就拿到用戶輸入的驗證碼,和后端生成的驗證碼進行比對,相同就驗證成功,否則失敗,
最后我們也說了驗證碼的防刷機制,這是需要考慮的,這里的防刷機制對于使用大量不同手機號、不同 IP 地址是沒效果的,依舊可以暴刷,所以這部分內容還是有待研究的,也歡迎大家在評論區說出你的看法!
最后的最后
希望各位螢屏前的靚仔靚女們
給個三連!你輕輕地點了個贊,那將在我的心里世界增添一顆明亮而耀眼的星!
咱們下期再見!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/554488.html
標籤:其他
上一篇:演算法 in Golang:Breadth-first search(BFS、廣度優先搜索)
下一篇:返回列表