Java SMS One-Click Login: Complete Production-Ready 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, core service code with rate limiting, blacklist, daily limits, one-time verification codes, login verification with JWT issuance, controller endpoints, production security optimizations including Lua atomic scripts, graphical captcha, channel fallback, risk control, and compliance, plus extension directions like carrier authentication and anomaly detection.
SMS verification code login is a high-frequency business scenario: the user submits a phone number, receives a code, and logs in without a password. The article begins by listing common pitfalls that beginners encounter: no rate limiting leading to huge SMS bills, storing codes in plaintext or without expiry, allowing code reuse, lacking a blacklist for malicious numbers, and having no fallback when the SMS provider fails.
Overall Business Flow
Frontend submits phone number to the sendCode endpoint.
Backend validates: phone format, 60-second cooldown, daily send limit (10 per day).
On pass, generate a 4/6-digit numeric code.
Store in Redis: key sms:code:phone, value = code, TTL 5 minutes.
Call SMS provider SDK to send the code.
User enters code and calls login endpoint.
Backend retrieves code from Redis, compares; on success, delete the Redis key, generate JWT token, return.
On failure, return error, record failure count, temporarily restrict after multiple failures.
Core security principle: verification codes must be single-use, time-limited, and sending frequency strictly controlled.
Technology Stack
Spring Boot 3 + JDK 17
Redis: stores verification codes, sending frequency counters, blacklist
JWT: issues login token on success
Rate limiting: Redis + Lua scripts (to prevent concurrent over-sending)
SMS providers: Alibaba Cloud SMS / Tencent Cloud SMS (abstracted for easy switching)
Core Configuration (YAML)
sms:
code-length: 6
code-expire: 300 # 5 minutes, seconds
send-interval: 60 # minimum interval between sends, seconds
day-max-count: 10 # daily max sends per phone
template-id: SMS_XXXXXX # SMS template IDRedis Key Design
sms:code:13800138000 → verification code, expires in 5 minutes
sms:send:interval:13800138000 → cooldown marker, expires in 60 seconds
sms:send:day:13800138000 → daily send counter, expires at midnight
sms:blacklist:13800138000 → malicious number blacklistCode Implementation
1. SMS Service Abstraction (Provider-Agnostic)
public interface SmsService {
void sendCode(String phone, String code);
}
@Service
public class AliSmsServiceImpl implements SmsService {
@Override
public void sendCode(String phone, String code) {
// Alibaba Cloud SMS SDK call logic
// SendSmsRequest request = new SendSmsRequest();
// request.setPhoneNumbers(phone);
// request.setTemplateParam("{\"code\":\""+code+"\"}");
// Catch provider exceptions, alert
System.out.println("Sending code to " + phone + ": " + code);
}
}2. Verification Code Sending Service (Core Anti-Brush 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
Boolean isBlack = redisTemplate.hasKey(SMS_BLACK_PREFIX + phone);
if (Boolean.TRUE.equals(isBlack)) {
throw new RuntimeException("This number is temporarily blocked from receiving codes");
}
// 2. Cooldown check: 60s
Boolean intervalExist = redisTemplate.hasKey(SMS_INTERVAL_PREFIX + phone);
if (Boolean.TRUE.equals(intervalExist)) {
throw new RuntimeException("Requests too frequent, please try again later");
}
// 3. Daily limit check
String dayCountStr = redisTemplate.opsForValue().get(SMS_DAY_COUNT_PREFIX + phone);
int dayCount = dayCountStr == null ? 0 : Integer.parseInt(dayCountStr);
if (dayCount >= 10) {
throw new RuntimeException("Daily verification code send limit reached");
}
// 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("Verification code expired, please request again");
}
if (!realCode.equals(inputCode)) {
throw new RuntimeException("Verification code incorrect");
}
// Success: delete code immediately to enforce one-time use
redisTemplate.delete(redisKey);
// Find or auto-register user
User user = userRepository.findByPhone(phone)
.orElseGet(() -> registerNewUser(phone));
// Generate JWT token
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 & Pitfall Checklist
Prevent SMS bombing : Basic interval + daily count is a start; high-concurrency scenarios should use Lua atomic scripts to avoid race conditions.
Graphical captcha prefix : In high-risk scenarios, require slider/image captcha before sending SMS to block scripts.
Never log verification codes to prevent leakage.
Channel fallback : Switch to backup SMS provider if primary fails.
Risk control : Block IPs that request codes for many different numbers in a short time.
Don't rely on verification codes long-term : After login, treat token like normal login with expiry.
Compliance : Domestic SMS requires unsubscribe channel, template filing; no arbitrary marketing messages.
Extension Directions
One-click login (carrier-based phone number verification, no SMS code).
Verification code login + account binding.
Login anomaly detection: geo-location change, new device → trigger second factor.
Summary
SMS verification code login is a classic Java backend implementation. It looks simple, but security and anti-brush are the core difficulties . Many production incidents are not code errors but massive bills from malicious SMS flooding. This solution encapsulates frequency control, expiry, one-time use, and blacklist, ready for direct use in admin systems, 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.
