Java SMS One-Click Login: Complete Implementation with Rate Limiting & Anti-Brute Force
This article provides a complete Java implementation of SMS one-click login using Spring Boot 3, Redis, and JWT, covering business flow, configuration, Redis key design, code for verification code generation, rate limiting, blacklist, and login validation, plus production security optimizations like Lua scripts, CAPTCHA, channel fallback, and compliance.
Business Flow
User submits phone number to request verification code.
Backend validates phone format, 60-second cooldown, and daily send limit (10).
On pass, generates a 6-digit code.
Stores code in Redis with key sms:code:phone and 5-minute TTL.
Calls SMS provider SDK (Aliyun/Tencent Cloud) to send code.
User submits code to login endpoint.
Backend retrieves code from Redis, compares, deletes on success, issues JWT token.
On failure, records attempts and may impose temporary block.
Core security principle: verification code must be single-use, time-limited, and sending frequency strictly controlled.
Technology Stack
Spring Boot 3 + JDK 17
Redis: stores verification codes, rate-limit counters, blacklist
JWT: issues login token on success
Rate limiting: Redis + Lua scripts (atomic under concurrency)
SMS providers: Aliyun SMS / Tencent Cloud SMS (abstracted for easy swap)
Configuration (YAML)
sms:
code-length: 6
code-expire: 300
send-interval: 60
day-max-count: 10
template-id: SMS_XXXXXXRedis Key Design
sms:code:13800138000 → verification code, 5 min TTL
sms:send:interval:13800138000 → cooldown marker, 60s TTL
sms:send:day:13800138000 → daily send counter, expires at midnight
sms:blacklist:13800138000 → malicious number blacklistCode Implementation
1. SMS Service Abstraction
public interface SmsService {
void sendCode(String phone, String code);
}
@Service
public class AliSmsServiceImpl implements SmsService {
@Override
public void sendCode(String phone, String code) {
// Aliyun SDK call logic
System.out.println("向 " + phone + " 发送验证码:" + code);
}
}2. Verification Code Service (Core Anti-Brute Logic)
@Service
public class SmsLoginService {
@Autowired private StringRedisTemplate redisTemplate;
@Autowired private SmsService smsService;
@Autowired private JwtTokenUtil jwtTokenUtil;
private static final String SMS_CODE_PREFIX = "sms:code:";
private static final String SMS_INTERVAL_PREFIX = "sms:send:interval:";
private static final String SMS_DAY_COUNT_PREFIX = "sms:send:day:";
private static final String SMS_BLACK_PREFIX = "sms:blacklist:";
private String generateCode() {
return String.format("%06d", new Random().nextInt(1000000));
}
public void sendVerifyCode(String phone) {
// 1. Blacklist check
if (Boolean.TRUE.equals(redisTemplate.hasKey(SMS_BLACK_PREFIX + phone))) {
throw new RuntimeException("该号码暂时禁止发送验证码");
}
// 2. Cooldown check (60s)
if (Boolean.TRUE.equals(redisTemplate.hasKey(SMS_INTERVAL_PREFIX + phone))) {
throw new RuntimeException("请求过于频繁,请稍后再试");
}
// 3. Daily limit (10)
String dayCountStr = redisTemplate.opsForValue().get(SMS_DAY_COUNT_PREFIX + phone);
int dayCount = dayCountStr == null ? 0 : Integer.parseInt(dayCountStr);
if (dayCount >= 10) {
throw new RuntimeException("今日验证码发送次数已达上限");
}
// 4. Generate code
String code = generateCode();
// 5. Store in Redis
redisTemplate.opsForValue().set(SMS_CODE_PREFIX + phone, code, 5, TimeUnit.MINUTES);
redisTemplate.opsForValue().set(SMS_INTERVAL_PREFIX + phone, "1", 60, TimeUnit.SECONDS);
redisTemplate.opsForValue().increment(SMS_DAY_COUNT_PREFIX + phone);
// Set daily counter expiry to next midnight
LocalDateTime tomorrow = LocalDate.now().plusDays(1).atStartOfDay();
long expireSeconds = Duration.between(LocalDateTime.now(), tomorrow).getSeconds();
redisTemplate.expire(SMS_DAY_COUNT_PREFIX + phone, expireSeconds, TimeUnit.SECONDS);
// 6. Send SMS
smsService.sendCode(phone, code);
}
public String loginByCode(String phone, String inputCode) {
String redisKey = SMS_CODE_PREFIX + phone;
String realCode = redisTemplate.opsForValue().get(redisKey);
if (realCode == null) {
throw new RuntimeException("验证码已过期,请重新获取");
}
if (!realCode.equals(inputCode)) {
throw new RuntimeException("验证码错误");
}
// Success: delete code (single-use)
redisTemplate.delete(redisKey);
// Find or auto-register user
User user = userRepository.findByPhone(phone)
.orElseGet(() -> registerNewUser(phone));
// Issue JWT
return jwtTokenUtil.createToken(user.getId(), user.getPhone());
}
private User registerNewUser(String phone) {
User user = new User();
user.setPhone(phone);
user.setCreateTime(LocalDateTime.now());
return userRepository.save(user);
}
}3. Controller Endpoints
@RestController
@RequestMapping("/api/sms")
public class SmsLoginController {
@Autowired private SmsLoginService smsLoginService;
@PostMapping("/sendCode")
public Result sendCode(@RequestBody SmsCodeDTO dto) {
smsLoginService.sendVerifyCode(dto.getPhone());
return Result.success();
}
@PostMapping("/login")
public Result login(@RequestBody SmsLoginDTO dto) {
String token = smsLoginService.loginByCode(dto.getPhone(), dto.getCode());
return Result.success(token);
}
}Production Security Optimizations & Pitfalls
Prevent SMS bombing : Basic interval + daily count; high concurrency requires Lua atomic scripts to avoid race conditions.
Graphical CAPTCHA front-gate : For high-risk scenarios, require slider/image CAPTCHA before SMS request.
Never log verification codes to prevent leakage.
Channel fallback : Switch to backup SMS provider if primary fails.
Risk control : Block IPs making many requests for different numbers in short time.
Token expiry : Treat SMS login like normal login; JWT must expire.
Compliance : China SMS requires unsubscribe channel, template registration, no unsolicited marketing.
Extension Directions
One-tap login (carrier-based number verification, no SMS code).
Verification code login + account binding.
Anomaly detection: geo/device change triggers second factor.
Summary
SMS verification login is a classic Java backend scenario. While seemingly simple, security and anti-abuse are the real challenges . Many incidents stem not from code bugs but from malicious SMS flooding causing huge bills. This solution encapsulates rate limiting, TTL, single-use enforcement, and blacklisting, ready for admin panels, mini-programs, and apps.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
