Skip to content
Java SMS One-Click Login: Complete Production-Ready Implementation with Rate Limiting & Anti-Brute Force

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.

Java Tech Enthusiast
Java Tech Enthusiast
Java Tech Enthusiast

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 ID

Redis 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 blacklist

Code 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaRedisSpring BootRate LimitingJWTSMS VerificationProduction SecurityAnti-Brute Force
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.