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

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.

LuTiao Programming
LuTiao Programming
LuTiao Programming

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_XXXXXX

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

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

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 LimitingJWTProduction SecurityAnti-Brute ForceSMS Login
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.