SpringBoot中登錄驗(yàn)證碼的4種實(shí)現(xiàn)方案
在當(dāng)今互聯(lián)網(wǎng)安全形勢日益嚴(yán)峻的環(huán)境下,驗(yàn)證碼已成為保護(hù)用戶賬戶安全、防止破解和自動(dòng)化攻擊的重要手段。尤其在登錄系統(tǒng)中,合理使用驗(yàn)證碼不僅能有效阻止機(jī)器人批量嘗試賬號(hào)密碼,還能降低賬戶被盜風(fēng)險(xiǎn),提升系統(tǒng)安全性。
本文將詳細(xì)介紹在SpringBoot應(yīng)用中實(shí)現(xiàn)四種登錄驗(yàn)證碼的技術(shù)方案,包括圖形驗(yàn)證碼、短信驗(yàn)證碼、郵箱驗(yàn)證碼和滑動(dòng)拼圖驗(yàn)證碼。
方案一:基于Kaptcha的圖形驗(yàn)證碼
原理介紹
圖形驗(yàn)證碼是最傳統(tǒng)且應(yīng)用最廣泛的驗(yàn)證碼類型,原理是在服務(wù)端生成隨機(jī)字符串并渲染成圖片,用戶需要識(shí)別圖片中的字符并輸入。圖形驗(yàn)證碼實(shí)現(xiàn)簡單,對用戶體驗(yàn)影響較小,是中小型應(yīng)用的理想選擇。
實(shí)現(xiàn)步驟
1. 添加Kaptcha依賴
<dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency>
2. 配置Kaptcha生成器
@Configuration public class KaptchaConfig { @Bean public Producer kaptchaProducer() { Properties properties = new Properties(); // 圖片寬度 properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "150"); // 圖片高度 properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "50"); // 字體大小 properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "32"); // 字體顏色 properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "black"); // 文本集合,驗(yàn)證碼值從此集合中獲取 properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"); // 驗(yàn)證碼長度 properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); // 干擾線顏色 properties.setProperty(Constants.KAPTCHA_NOISE_COLOR, "blue"); // 去除背景漸變 properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); Config config = new Config(properties); return config.getProducerImpl(); } }
3. 創(chuàng)建驗(yàn)證碼存儲(chǔ)服務(wù)
@Service public class CaptchaService { @Autowired private RedisTemplate<String, String> redisTemplate; // 驗(yàn)證碼有效期(秒) private static final long CAPTCHA_EXPIRATION = 300; // 存儲(chǔ)驗(yàn)證碼 public void storeCaptcha(String sessionId, String captchaCode) { redisTemplate.opsForValue().set( "CAPTCHA:" + sessionId, captchaCode, CAPTCHA_EXPIRATION, TimeUnit.SECONDS); } // 驗(yàn)證驗(yàn)證碼 public boolean validateCaptcha(String sessionId, String userInputCaptcha) { String key = "CAPTCHA:" + sessionId; String storedCaptcha = redisTemplate.opsForValue().get(key); if (storedCaptcha != null && storedCaptcha.equalsIgnoreCase(userInputCaptcha)) { // 驗(yàn)證成功后立即刪除,防止重復(fù)使用 redisTemplate.delete(key); return true; } return false; } }
4. 實(shí)現(xiàn)驗(yàn)證碼生成控制器
@RestController @RequestMapping("/captcha") public class CaptchaController { @Autowired private Producer kaptchaProducer; @Autowired private CaptchaService captchaService; @GetMapping("/image") public void getImageCaptcha(HttpServletResponse response, HttpServletRequest request) throws IOException { // 清除瀏覽器緩存 response.setDateHeader("Expires", 0); response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); response.addHeader("Cache-Control", "post-check=0, pre-check=0"); response.setHeader("Pragma", "no-cache"); response.setContentType("image/jpeg"); // 創(chuàng)建驗(yàn)證碼文本 String capText = kaptchaProducer.createText(); // 獲取會(huì)話ID String sessionId = request.getSession().getId(); // 存儲(chǔ)驗(yàn)證碼 captchaService.storeCaptcha(sessionId, capText); // 創(chuàng)建驗(yàn)證碼圖片 BufferedImage image = kaptchaProducer.createImage(capText); ServletOutputStream out = response.getOutputStream(); // 輸出圖片 ImageIO.write(image, "jpg", out); out.flush(); out.close(); } }
5. 登錄控制器集成驗(yàn)證碼校驗(yàn)
@RestController @RequestMapping("/auth") public class AuthController { @Autowired private CaptchaService captchaService; @Autowired private UserService userService; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest, HttpServletRequest request) { // 獲取會(huì)話ID String sessionId = request.getSession().getId(); // 驗(yàn)證驗(yàn)證碼 if (!captchaService.validateCaptcha(sessionId, loginRequest.getCaptcha())) { return ResponseEntity.badRequest().body(new ApiResponse(false, "驗(yàn)證碼錯(cuò)誤或已過期")); } // 用戶名密碼驗(yàn)證 boolean authenticated = userService.authenticate( loginRequest.getUsername(), loginRequest.getPassword()); if (authenticated) { // 生成Token或設(shè)置會(huì)話 String token = jwtTokenProvider.generateToken(loginRequest.getUsername()); return ResponseEntity.ok(new JwtAuthResponse(token)); } else { return ResponseEntity.badRequest().body(new ApiResponse(false, "用戶名或密碼錯(cuò)誤")); } } }
6. 前端集成示例
<div class="login-form"> <form id="loginForm"> <div class="form-group"> <label for="username">用戶名</label> <input type="text" class="form-control" id="username" name="username" required> </div> <div class="form-group"> <label for="password">密碼</label> <input type="password" class="form-control" id="password" name="password" required> </div> <div class="form-group"> <label for="captcha">驗(yàn)證碼</label> <div class="captcha-container"> <input type="text" class="form-control" id="captcha" name="captcha" required> <img id="captchaImg" src="/captcha/image" alt="驗(yàn)證碼" onclick="refreshCaptcha()"> </div> </div> <button type="submit" class="btn btn-primary">登錄</button> </form> </div> <script> function refreshCaptcha() { document.getElementById('captchaImg').src = '/captcha/image?t=' + new Date().getTime(); } document.getElementById('loginForm').addEventListener('submit', function(e) { e.preventDefault(); const data = { username: document.getElementById('username').value, password: document.getElementById('password').value, captcha: document.getElementById('captcha').value }; fetch('/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => { if (data.token) { localStorage.setItem('token', data.token); window.location.href = '/dashboard'; } else { alert(data.message); refreshCaptcha(); } }) .catch(error => { console.error('Error:', error); refreshCaptcha(); }); }); </script>
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 實(shí)現(xiàn)簡單,開發(fā)成本低
- 對服務(wù)器資源占用少
- 用戶體驗(yàn)相對友好
- 無需第三方服務(wù),降低依賴
缺點(diǎn)
- 安全性較低,容易被OCR識(shí)別破解
- 對視障用戶不友好
- 在移動(dòng)設(shè)備上體驗(yàn)不佳
- 隨著AI發(fā)展,圖形驗(yàn)證碼的安全性逐漸降低
方案二:基于短信驗(yàn)證碼
原理介紹
短信驗(yàn)證碼通過向用戶手機(jī)發(fā)送一次性驗(yàn)證碼實(shí)現(xiàn)身份驗(yàn)證。用戶需要輸入收到的驗(yàn)證碼完成登錄過程。這種方式不僅驗(yàn)證了賬號(hào)密碼的正確性,還確認(rèn)了用戶對手機(jī)號(hào)的控制權(quán),大幅提高了安全性。
實(shí)現(xiàn)步驟
1. 添加阿里云SDK依賴
<dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId> <version>4.5.3</version> </dependency>
2. 配置短信服務(wù)參數(shù)
@Configuration @ConfigurationProperties(prefix = "aliyun.sms") @Data public class SmsProperties { private String accessKeyId; private String accessKeySecret; private String signName; private String templateCode; private String endpoint = "dysmsapi.aliyuncs.com"; }
# application.properties aliyun.sms.access-key-id=YOUR_ACCESS_KEY_ID aliyun.sms.access-key-secret=YOUR_ACCESS_KEY_SECRET aliyun.sms.sign-name=YOUR_SMS_SIGN_NAME aliyun.sms.template-code=SMS_TEMPLATE_CODE
3. 實(shí)現(xiàn)短信服務(wù)
@Service @Slf4j public class SmsService { @Autowired private SmsProperties smsProperties; @Autowired private RedisTemplate<String, String> redisTemplate; private static final long SMS_EXPIRATION = 300; // 5分鐘過期 private static final String SMS_PREFIX = "SMS_CAPTCHA:"; /** * 發(fā)送短信驗(yàn)證碼 * @param phoneNumber 手機(jī)號(hào) * @return 是否發(fā)送成功 */ public boolean sendSmsCaptcha(String phoneNumber) { try { // 生成6位隨機(jī)驗(yàn)證碼 String captcha = generateCaptcha(6); // 存儲(chǔ)驗(yàn)證碼 redisTemplate.opsForValue().set( SMS_PREFIX + phoneNumber, captcha, SMS_EXPIRATION, TimeUnit.SECONDS); // 構(gòu)建短信客戶端 DefaultProfile profile = DefaultProfile.getProfile( "cn-hangzhou", smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret()); IAcsClient client = new DefaultAcsClient(profile); // 構(gòu)建短信請求 CommonRequest request = new CommonRequest(); request.setSysMethod(MethodType.POST); request.setSysDomain(smsProperties.getEndpoint()); request.setSysVersion("2017-05-25"); request.setSysAction("SendSms"); request.putQueryParameter("RegionId", "cn-hangzhou"); request.putQueryParameter("PhoneNumbers", phoneNumber); request.putQueryParameter("SignName", smsProperties.getSignName()); request.putQueryParameter("TemplateCode", smsProperties.getTemplateCode()); // 設(shè)置模板參數(shù),將驗(yàn)證碼作為參數(shù)傳入 Map<String, String> templateParam = Map.of("code", captcha); request.putQueryParameter("TemplateParam", new ObjectMapper().writeValueAsString(templateParam)); // 發(fā)送短信 CommonResponse response = client.getCommonResponse(request); log.info("短信發(fā)送結(jié)果: {}", response.getData()); // 解析響應(yīng) JsonNode responseJson = new ObjectMapper().readTree(response.getData()); return "OK".equals(responseJson.get("Code").asText()); } catch (Exception e) { log.error("發(fā)送短信驗(yàn)證碼失敗", e); return false; } } /** * 驗(yàn)證短信驗(yàn)證碼 * @param phoneNumber 手機(jī)號(hào) * @param captcha 用戶輸入的驗(yàn)證碼 * @return 是否驗(yàn)證成功 */ public boolean validateSmsCaptcha(String phoneNumber, String captcha) { String key = SMS_PREFIX + phoneNumber; String storedCaptcha = redisTemplate.opsForValue().get(key); if (storedCaptcha != null && storedCaptcha.equals(captcha)) { // 驗(yàn)證成功后刪除驗(yàn)證碼,防止重復(fù)使用 redisTemplate.delete(key); return true; } return false; } /** * 生成指定長度的隨機(jī)數(shù)字驗(yàn)證碼 */ private String generateCaptcha(int length) { StringBuilder captcha = new StringBuilder(); Random random = new Random(); for (int i = 0; i < length; i++) { captcha.append(random.nextInt(10)); } return captcha.toString(); } }
4. 實(shí)現(xiàn)短信驗(yàn)證碼控制器
@RestController @RequestMapping("/captcha") public class SmsCaptchaController { @Autowired private SmsService smsService; @PostMapping("/sms/send") public ResponseEntity<?> sendSmsCaptcha(@RequestBody PhoneNumberRequest request) { String phoneNumber = request.getPhoneNumber(); // 驗(yàn)證手機(jī)號(hào)格式 if (!isValidPhoneNumber(phoneNumber)) { return ResponseEntity.badRequest().body(new ApiResponse(false, "手機(jī)號(hào)格式不正確")); } // 發(fā)送短信驗(yàn)證碼 boolean sent = smsService.sendSmsCaptcha(phoneNumber); if (sent) { return ResponseEntity.ok(new ApiResponse(true, "驗(yàn)證碼已發(fā)送,請注意查收")); } else { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ApiResponse(false, "驗(yàn)證碼發(fā)送失敗,請稍后再試")); } } // 驗(yàn)證手機(jī)號(hào)格式(簡化版) private boolean isValidPhoneNumber(String phoneNumber) { return phoneNumber != null && phoneNumber.matches("^1[3-9]\d{9}$"); } }
5. 登錄控制器集成短信驗(yàn)證碼
@RestController @RequestMapping("/auth") public class SmsLoginController { @Autowired private SmsService smsService; @Autowired private UserService userService; @Autowired private JwtTokenProvider jwtTokenProvider; @PostMapping("/sms-login") public ResponseEntity<?> smsLogin(@RequestBody SmsLoginRequest request) { String phoneNumber = request.getPhoneNumber(); String captcha = request.getCaptcha(); // 驗(yàn)證短信驗(yàn)證碼 if (!smsService.validateSmsCaptcha(phoneNumber, captcha)) { return ResponseEntity.badRequest().body(new ApiResponse(false, "驗(yàn)證碼錯(cuò)誤或已過期")); } // 查找或創(chuàng)建用戶 User user = userService.findOrCreateByPhone(phoneNumber); if (user != null) { // 生成JWT令牌 String token = jwtTokenProvider.generateToken(user.getUsername()); return ResponseEntity.ok(new JwtAuthResponse(token)); } else { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ApiResponse(false, "登錄失敗,請稍后再試")); } } }
6. 前端集成示例
<div class="sms-login-form"> <form id="smsLoginForm"> <div class="form-group"> <label for="phoneNumber">手機(jī)號(hào)</label> <input type="text" class="form-control" id="phoneNumber" name="phoneNumber" required> </div> <div class="form-group"> <label for="smsCaptcha">驗(yàn)證碼</label> <div class="captcha-container"> <input type="text" class="form-control" id="smsCaptcha" name="smsCaptcha" required> <button type="button" class="btn btn-outline-primary" id="sendSmsBtn">獲取驗(yàn)證碼</button> </div> </div> <button type="submit" class="btn btn-primary">登錄</button> </form> </div> <script> let countdown = 60; document.getElementById('sendSmsBtn').addEventListener('click', function() { const phoneNumber = document.getElementById('phoneNumber').value; if (!phoneNumber || !/^1[3-9]\d{9}$/.test(phoneNumber)) { alert('請輸入正確的手機(jī)號(hào)'); return; } // 發(fā)送短信驗(yàn)證碼請求 fetch('/captcha/sms/send', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phoneNumber: phoneNumber }) }) .then(response => response.json()) .then(data => { alert(data.message); if (data.success) { startCountdown(); } }) .catch(error => { console.error('Error:', error); alert('發(fā)送驗(yàn)證碼失敗,請稍后再試'); }); }); function startCountdown() { const btn = document.getElementById('sendSmsBtn'); btn.disabled = true; let timer = setInterval(() => { btn.textContent = `${countdown}秒后重新獲取`; countdown--; if (countdown < 0) { clearInterval(timer); btn.disabled = false; btn.textContent = '獲取驗(yàn)證碼'; countdown = 60; } }, 1000); } document.getElementById('smsLoginForm').addEventListener('submit', function(e) { e.preventDefault(); const data = { phoneNumber: document.getElementById('phoneNumber').value, captcha: document.getElementById('smsCaptcha').value }; fetch('/auth/sms-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => { if (data.token) { localStorage.setItem('token', data.token); window.location.href = '/dashboard'; } else { alert(data.message); } }) .catch(error => { console.error('Error:', error); alert('登錄失敗,請稍后再試'); }); }); </script>
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 安全性高,驗(yàn)證了用戶對手機(jī)的控制權(quán)
- 可以作為無密碼登錄的方式,簡化用戶操作
- 適合移動(dòng)應(yīng)用和需要高安全性的場景
- 可與其他驗(yàn)證方式結(jié)合,實(shí)現(xiàn)多因素認(rèn)證
缺點(diǎn)
- 依賴第三方短信服務(wù),存在成本
- 可能受網(wǎng)絡(luò)和運(yùn)營商影響,導(dǎo)致延遲
- 對國際用戶不友好
- 用戶頻繁登錄時(shí),體驗(yàn)不佳
方案三:基于Spring Mail的郵箱驗(yàn)證碼
原理介紹
郵箱驗(yàn)證碼通過向用戶注冊的電子郵箱發(fā)送一次性驗(yàn)證碼實(shí)現(xiàn)身份驗(yàn)證。這種方式與短信驗(yàn)證碼類似,但使用電子郵件作為傳遞媒介,適合對成本敏感或不需要實(shí)時(shí)驗(yàn)證的場景。
實(shí)現(xiàn)步驟
1. 添加Spring Mail依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
2. 配置郵件服務(wù)
# application.properties spring.mail.host=smtp.gmail.com spring.mail.port=587 spring.mail.username=your-email@gmail.com spring.mail.password=your-app-password spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true # 自定義配置 app.email.from=noreply@yourapp.com app.email.personal=Your App Name
3. 創(chuàng)建郵件驗(yàn)證碼服務(wù)
@Service @Slf4j public class EmailCaptchaService { @Autowired private JavaMailSender mailSender; @Autowired private RedisTemplate<String, String> redisTemplate; @Autowired private TemplateEngine templateEngine; @Value("${app.email.from}") private String fromEmail; @Value("${app.email.personal}") private String emailPersonal; private static final long EMAIL_CAPTCHA_EXPIRATION = 600; // 10分鐘過期 private static final String EMAIL_CAPTCHA_PREFIX = "EMAIL_CAPTCHA:"; /** * 發(fā)送郵箱驗(yàn)證碼 * @param email 電子郵箱 * @return 是否發(fā)送成功 */ public boolean sendEmailCaptcha(String email) { try { // 生成6位隨機(jī)驗(yàn)證碼 String captcha = generateCaptcha(6); // 存儲(chǔ)驗(yàn)證碼 redisTemplate.opsForValue().set( EMAIL_CAPTCHA_PREFIX + email, captcha, EMAIL_CAPTCHA_EXPIRATION, TimeUnit.SECONDS); // 準(zhǔn)備郵件內(nèi)容 Context context = new Context(); context.setVariable("captcha", captcha); context.setVariable("expirationMinutes", EMAIL_CAPTCHA_EXPIRATION / 60); String emailContent = templateEngine.process("email/captcha-template", context); // 創(chuàng)建MIME郵件 MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); // 設(shè)置發(fā)件人、收件人、主題和內(nèi)容 helper.setFrom(new InternetAddress(fromEmail, emailPersonal)); helper.setTo(email); helper.setSubject("登錄驗(yàn)證碼"); helper.setText(emailContent, true); // 發(fā)送郵件 mailSender.send(message); log.info("郵箱驗(yàn)證碼已發(fā)送至: {}", email); return true; } catch (Exception e) { log.error("發(fā)送郵箱驗(yàn)證碼失敗", e); return false; } } /** * 驗(yàn)證郵箱驗(yàn)證碼 * @param email 電子郵箱 * @param captcha 用戶輸入的驗(yàn)證碼 * @return 是否驗(yàn)證成功 */ public boolean validateEmailCaptcha(String email, String captcha) { String key = EMAIL_CAPTCHA_PREFIX + email; String storedCaptcha = redisTemplate.opsForValue().get(key); if (storedCaptcha != null && storedCaptcha.equals(captcha)) { // 驗(yàn)證成功后刪除驗(yàn)證碼,防止重復(fù)使用 redisTemplate.delete(key); return true; } return false; } /** * 生成指定長度的隨機(jī)數(shù)字驗(yàn)證碼 */ private String generateCaptcha(int length) { StringBuilder captcha = new StringBuilder(); Random random = new Random(); for (int i = 0; i < length; i++) { captcha.append(random.nextInt(10)); } return captcha.toString(); } }
4. 創(chuàng)建郵件模板
<!-- src/main/resources/templates/email/captcha-template.html --> <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>登錄驗(yàn)證碼</title> <style> body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; } .container { max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px; } .header { text-align: center; padding-bottom: 10px; border-bottom: 1px solid #eee; } .content { padding: 20px 0; } .code { font-size: 24px; font-weight: bold; text-align: center; padding: 10px; margin: 20px 0; background-color: #f5f5f5; border-radius: 5px; letter-spacing: 5px; } .footer { font-size: 12px; color: #777; text-align: center; padding-top: 10px; border-top: 1px solid #eee; } </style> </head> <body> <div class="container"> <div class="header"> <h2>登錄驗(yàn)證碼</h2> </div> <div class="content"> <p>您好,</p> <p>您正在進(jìn)行登錄操作,請使用以下驗(yàn)證碼完成驗(yàn)證:</p> <div class="code" th:text="${captcha}">123456</div> <p>驗(yàn)證碼有效期為 <span th:text="${expirationMinutes}">10</span> 分鐘,請及時(shí)使用。</p> <p>如果這不是您的操作,請忽略此郵件。</p> </div> <div class="footer"> <p>此郵件由系統(tǒng)自動(dòng)發(fā)送,請勿回復(fù)。</p> </div> </div> </body> </html>
5. 實(shí)現(xiàn)郵箱驗(yàn)證碼控制器
@RestController @RequestMapping("/captcha") public class EmailCaptchaController { @Autowired private EmailCaptchaService emailCaptchaService; @PostMapping("/email/send") public ResponseEntity<?> sendEmailCaptcha(@RequestBody EmailRequest request) { String email = request.getEmail(); // 驗(yàn)證郵箱格式 if (!isValidEmail(email)) { return ResponseEntity.badRequest().body(new ApiResponse(false, "郵箱格式不正確")); } // 發(fā)送郵箱驗(yàn)證碼 boolean sent = emailCaptchaService.sendEmailCaptcha(email); if (sent) { return ResponseEntity.ok(new ApiResponse(true, "驗(yàn)證碼已發(fā)送,請查收郵件")); } else { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ApiResponse(false, "驗(yàn)證碼發(fā)送失敗,請稍后再試")); } } // 驗(yàn)證郵箱格式 private boolean isValidEmail(String email) { String regex = "^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"; return email != null && email.matches(regex); } }
6. 登錄控制器集成郵箱驗(yàn)證碼
@RestController @RequestMapping("/auth") public class EmailLoginController { @Autowired private EmailCaptchaService emailCaptchaService; @Autowired private UserService userService; @Autowired private JwtTokenProvider jwtTokenProvider; @PostMapping("/email-login") public ResponseEntity<?> emailLogin(@RequestBody EmailLoginRequest request) { String email = request.getEmail(); String captcha = request.getCaptcha(); // 驗(yàn)證郵箱驗(yàn)證碼 if (!emailCaptchaService.validateEmailCaptcha(email, captcha)) { return ResponseEntity.badRequest().body(new ApiResponse(false, "驗(yàn)證碼錯(cuò)誤或已過期")); } // 查找或創(chuàng)建用戶 User user = userService.findOrCreateByEmail(email); if (user != null) { // 生成JWT令牌 String token = jwtTokenProvider.generateToken(user.getUsername()); return ResponseEntity.ok(new JwtAuthResponse(token)); } else { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(new ApiResponse(false, "登錄失敗,請稍后再試")); } } }
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 成本低廉,適合預(yù)算有限的項(xiàng)目
- 無需依賴第三方服務(wù),可自行部署
- 可發(fā)送富文本內(nèi)容,支持品牌定制
- 適合注冊和找回密碼等非實(shí)時(shí)場景
缺點(diǎn)
- 郵件可能被歸類為垃圾郵件或延遲送達(dá)
- 用戶體驗(yàn)不如短信驗(yàn)證碼直接
- 郵件服務(wù)器配置相對復(fù)雜
- 不適合對實(shí)時(shí)性要求高的場景
方案四:基于AJ-Captcha的滑動(dòng)拼圖驗(yàn)證碼
原理介紹
滑動(dòng)拼圖驗(yàn)證碼是一種更現(xiàn)代的驗(yàn)證方式,通過讓用戶拖動(dòng)滑塊完成拼圖來驗(yàn)證人機(jī)交互。這種驗(yàn)證碼在用戶體驗(yàn)和安全性之間取得了很好的平衡,既能有效防止自動(dòng)化攻擊,又不會(huì)像傳統(tǒng)圖形驗(yàn)證碼那樣影響用戶體驗(yàn)。
實(shí)現(xiàn)步驟
1. 添加AJ-Captcha依賴
<dependency> <groupId>com.anji-plus</groupId> <artifactId>captcha-spring-boot-starter</artifactId> <version>1.4.0</version> </dependency>
2. 配置滑動(dòng)驗(yàn)證碼參數(shù)
aj: captcha: cache-type: local expires-in: 300 req-frequency-limit-count: 50 cache-number: 1000 jigsaw: classpath:images/jigsaw pic-click: classpath:images/pic-click
3. 自定義滑動(dòng)驗(yàn)證碼控制器
@RestController @RequestMapping("/captcha") public class JigsawCaptchaController { @Autowired private CaptchaService captchaService; @RequestMapping(value = "/get", method = {RequestMethod.GET, RequestMethod.POST}) public ResponseModel get(@RequestParam(value = "type", defaultValue = "slide") String captchaType) { CaptchaVO captchaVO = new CaptchaVO(); captchaVO.setCaptchaType(captchaType); return captchaService.get(captchaVO); } @PostMapping("/check") public ResponseModel check(@RequestBody CaptchaVO captchaVO) { return captchaService.check(captchaVO); } }
4. 登錄控制器集成滑動(dòng)驗(yàn)證碼
@RestController @RequestMapping("/auth") public class JigsawLoginController { @Autowired private CaptchaService captchaService; @PostMapping("/jigsaw-login") public ResponseEntity<?> jigsawLogin(@RequestBody JigsawLoginRequest request) { // 驗(yàn)證滑動(dòng)驗(yàn)證碼 CaptchaVO captchaVO = new CaptchaVO(); captchaVO.setCaptchaVerification(request.getCaptchaVerification()); ResponseModel response = captchaService.verification(captchaVO); if (!response.isSuccess()) { return ResponseEntity.badRequest().body(new ApiResponse(false, "驗(yàn)證碼校驗(yàn)失敗")); } // TODO 模擬驗(yàn)證用戶名密碼 boolean authenticated = request.getPassword().equals("admin"); if (authenticated) { // TODO 模擬生成令牌 String token = IdUtil.simpleUUID(); return ResponseEntity.ok(new JwtAuthResponse(token)); } else { return ResponseEntity.badRequest().body(new ApiResponse(false, "用戶名或密碼錯(cuò)誤")); } } }
@SpringBootApplication public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); } @Bean public CaptchaService captchaService(){ BlockPuzzleCaptchaServiceImpl clickWordCaptchaService = new BlockPuzzleCaptchaServiceImpl(); Properties properties = new Properties(); properties.setProperty(Const.CAPTCHA_FONT_TYPE,"WenQuanZhengHei.ttf"); clickWordCaptchaService.init(properties); return clickWordCaptchaService; } }
5. 前端集成示例
注:依賴的本地js文件可從 https://github.com/anji-plus/captcha/tree/master/view/html 獲取
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>滑動(dòng)驗(yàn)證碼登錄</title> <link rel="stylesheet" > <link rel="stylesheet" > <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"/> <link rel="stylesheet" type="text/css" href="css/verify.css"> <style> </style> </head> <body> <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-md-6"> <div class="card"> <div class="card-header">用戶登錄</div> <div class="card-body"> <form id="loginForm"> <div class="form-group"> <label for="username">用戶名</label> <input type="text" class="form-control" id="username" name="username" required> </div> <div class="form-group"> <label for="password">密碼</label> <input type="password" class="form-control" id="password" name="password" required> </div> <div class="form-group"> <div id="captcha"></div> <input type="hidden" id="captchaVerification" name="captchaVerification"> </div> <div id="slidePanel" ></div> <button type="submit" class="btn btn-primary btn-block">登錄</button> </form> </div> </div> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js"></script> <script> (function () { if (!window.Promise) { document.writeln('<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.min.js"><' + '/' + 'script>'); } })(); </script> <script src="./js/crypto-js.js"></script> <script src="./js/ase.js"></script> <script src="./js/verify.js" ></script> <script> let captchaVerification; // 初始化驗(yàn)證碼 嵌入式 $('#slidePanel').slideVerify({ baseUrl:'http://localhost:8080', // 服務(wù)器請求地址; mode:'fixed', imgSize : { //圖片的大小對象 width: '400px', height: '200px', }, barSize:{ width: '400px', height: '40px', }, ready : function() { //加載完畢的回調(diào) }, success : function(params) { //成功的回調(diào) // 返回的二次驗(yàn)證參數(shù) 合并到驗(yàn)證通過之后的邏輯 參數(shù)中回傳服務(wù)器 captchaVerification = params.captchaVerification; }, error : function() { //失敗的回調(diào) } }); // 表單提交處理 document.getElementById('loginForm').addEventListener('submit', function(e) { e.preventDefault(); if (!captchaVerification) { alert('請先完成滑動(dòng)驗(yàn)證'); return; } var data = { username: document.getElementById('username').value, password: document.getElementById('password').value, captchaVerification: captchaVerification }; fetch('/auth/jigsaw-login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => response.json()) .then(data => { if (data.token) { localStorage.setItem('token', data.token); alert('login success'); } else { alert(data.message); } }) .catch(error => { console.error('Error:', error); }); }); </script> </body> </html>
優(yōu)缺點(diǎn)分析
優(yōu)點(diǎn)
- 用戶體驗(yàn)良好,交互更加自然
- 安全性較高,難以被自動(dòng)化工具破解
- 支持移動(dòng)端和桌面端
- 可定制化程度高,支持品牌化設(shè)計(jì)
缺點(diǎn)
- 實(shí)現(xiàn)相對復(fù)雜,需要前后端配合
- 依賴JavaScript,不支持純靜態(tài)環(huán)境
四種驗(yàn)證碼方案對比
特性/方案 | 圖形驗(yàn)證碼 | 短信驗(yàn)證碼 | 郵箱驗(yàn)證碼 | 滑動(dòng)拼圖驗(yàn)證碼 |
---|---|---|---|---|
安全性 | 中 | 高 | 中高 | 高 |
實(shí)現(xiàn)復(fù)雜度 | 低 | 中 | 中 | 高 |
適用場景 | 簡單應(yīng)用 | 高安全性需求 | 注冊、找回密碼 | 現(xiàn)代Web應(yīng)用 |
防機(jī)器人效果 | 一般 | 優(yōu)秀 | 良好 | 優(yōu)秀 |
是否需要第三方 | 否 | 是 | 否 | 否 |
總結(jié)
在實(shí)際項(xiàng)目中,可以根據(jù)應(yīng)用特點(diǎn)、用戶需求和安全要求選擇合適的驗(yàn)證碼方案,甚至可以組合多種方案,在不同場景下使用不同的驗(yàn)證方式,既保障系統(tǒng)安全,又提供良好的用戶體驗(yàn)。
隨著AI技術(shù)的發(fā)展,驗(yàn)證碼技術(shù)也在不斷演進(jìn)。對于重要系統(tǒng),建議定期評估和更新驗(yàn)證碼方案,以應(yīng)對新型的自動(dòng)化攻擊手段。
到此這篇關(guān)于SpringBoot中登錄驗(yàn)證碼的4種實(shí)現(xiàn)方案的文章就介紹到這了,更多相關(guān)springboot登錄驗(yàn)證碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Netty進(jìn)階之EventExecutorGroup源碼詳解
這篇文章主要介紹了Netty進(jìn)階之EventExecutorGroup源碼詳解,EventExecutorGroup繼承了JDK的ScheduledExecutroService,那么它就擁有了執(zhí)行定時(shí)任務(wù),執(zhí)行提交的普通任務(wù),需要的朋友可以參考下2023-11-11java中File與MultipartFile互轉(zhuǎn)代碼示例
在Java開發(fā)中,當(dāng)需要將本地File對象轉(zhuǎn)換為MultipartFile對象以處理文件上傳時(shí),可以通過實(shí)現(xiàn)MultipartFile接口或使用CommonsMultipartFile類來實(shí)現(xiàn),本文提供了詳細(xì)的轉(zhuǎn)換方法和代碼示例,需要的朋友可以參考下2024-10-10Java打印星號(hào)圖案和數(shù)字圖案的示例代碼
在 Java 中打印圖案是一項(xiàng)常見的編程任務(wù),尤其在初學(xué)階段,通過以特定方式排列符號(hào)或數(shù)字,可以形成各種設(shè)計(jì)或形狀,這些圖案不僅有助于解決問題,還能培養(yǎng)算法思維能力,本文將討論如何在 Java 中打印圖案,并探索一些最常見的圖案類型,需要的朋友可以參考下2024-11-11java統(tǒng)計(jì)漢字字?jǐn)?shù)的方法示例
這篇文章主要介紹了java統(tǒng)計(jì)漢字字?jǐn)?shù)的方法,結(jié)合實(shí)例形式分析了java正則判定、字符串遍歷及統(tǒng)計(jì)相關(guān)操作技巧,需要的朋友可以參考下2017-05-05SpringBoot應(yīng)用剛啟動(dòng)時(shí)服務(wù)報(bào)大量超時(shí)的問題及解決
在Java項(xiàng)目上線過程中,經(jīng)常遇到的超時(shí)問題主要是由于JVM的JIT編譯導(dǎo)致,JIT(Just-In-Time)編譯是Java虛擬機(jī)的一項(xiàng)技術(shù),用于提高Java應(yīng)用的性能,它通過將熱點(diǎn)代碼(頻繁執(zhí)行的部分)轉(zhuǎn)換成本地機(jī)器碼來優(yōu)化執(zhí)行效率2024-11-11Java使用RandomAccessFile類對文件進(jìn)行讀寫
本篇文章主要介紹了Java使用RandomAccessFile類對文件進(jìn)行讀寫,詳細(xì)的介紹了RandomAccessFile類的使用技巧和實(shí)例應(yīng)用,有興趣的可以了解一下2017-04-04