1. 项目背景与核心需求
黑马点评作为一个典型的互联网点评类应用,用户登录是业务闭环的起点。短信验证码登录相比传统账号密码方式,在用户体验和安全性上具有明显优势:
- 免去用户记忆密码的负担
- 通过运营商号码实名制间接验证用户身份
- 动态验证码有效防止暴力破解
- 符合当前移动互联网用户操作习惯
但实现过程中需要解决几个关键问题:
- 验证码生成与校验的时效性控制
- 登录状态维持与会话管理
- 高并发场景下的系统稳定性
- 安全防护(防刷、防重放攻击等)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计
2.1 整体流程设计
mermaid复制graph TD
A[用户请求发送验证码] --> B[生成6位随机数]
B --> C[存入Redis并设置TTL]
C --> D[通过短信平台发送]
D --> E[用户提交验证码]
E --> F[Redis校验]
F --> G[生成登录token]
G --> H[用户信息存入Redis]
2.2 核心组件选型
-
Redis选择:
- 采用String结构存储验证码(key:手机号,value:验证码)
- 使用Hash结构存储用户信息(避免序列化开销)
- TTL设置为5分钟(兼顾安全与用户体验)
-
会话管理:
- 采用Token+Redis方案替代传统Session
- Token生成规则:UUID + 时间戳 + 手机号哈希
- 通过ThreadLocal实现请求上下文用户信息传递
-
安全防护:
- 接口限流(Guava RateLimiter)
- IP黑名单机制
- 验证码使用后立即失效
3. 关键代码实现
3.1 验证码服务
java复制public Result sendCode(String phone) {
// 1. 校验手机号格式
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
// 2. 生成6位验证码
String code = RandomUtil.randomNumbers(6);
// 3. 存入Redis,有效期5分钟
stringRedisTemplate.opsForValue().set(
LOGIN_CODE_KEY + phone,
code,
LOGIN_CODE_TTL,
TimeUnit.MINUTES);
// 4. 发送短信(模拟实现)
log.debug("发送短信验证码:{},手机号:{}", code, phone);
return Result.ok();
}
3.2 登录校验逻辑
java复制public Result login(LoginFormDTO loginForm) {
// 1. 校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误");
}
// 2. 校验验证码
String cacheCode = stringRedisTemplate.opsForValue()
.get(LOGIN_CODE_KEY + phone);
String inputCode = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(inputCode)) {
return Result.fail("验证码错误");
}
// 3. 查询用户(不存在则自动注册)
User user = query().eq("phone", phone).one();
if (user == null) {
user = createUserWithPhone(phone);
}
// 4. 生成token作为登录令牌
String token = UUID.randomUUID().toString(true);
// 5. 用户信息转为Hash存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create()
.setIgnoreNullValue(true)
.setFieldValueEditor((fieldName, fieldValue) ->
fieldValue.toString()));
// 6. 存入Redis
String tokenKey = LOGIN_USER_KEY + token;
stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
// 设置token有效期(30分钟)
stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 7. 删除已使用的验证码
stringRedisTemplate.delete(LOGIN_CODE_KEY + phone);
return Result.ok(token);
}
4. 会话管理优化
4.1 ThreadLocal封装
java复制public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
4.2 拦截器实现
java复制public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 1. 获取token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
response.setStatus(401);
return false;
}
// 2. 查询Redis用户
String tokenKey = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
.entries(tokenKey);
if (userMap.isEmpty()) {
response.setStatus(401);
return false;
}
// 3. 转换为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,
new UserDTO(), false);
// 4. 保存到ThreadLocal
UserHolder.saveUser(userDTO);
// 5. 刷新token有效期
stringRedisTemplate.expire(tokenKey,
LOGIN_USER_TTL, TimeUnit.MINUTES);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
// 移除用户,避免内存泄漏
UserHolder.removeUser();
}
}
5. 性能优化实践
5.1 Redis缓存设计
| 键类型 | 命名规范 | 数据结构 | TTL设置 | 注意事项 |
|---|---|---|---|---|
| 验证码 | login:code: | String | 5分钟 | 使用后立即删除 |
| 用户信息 | login:user:{token} | Hash | 30分钟 | 每次访问刷新TTL |
5.2 高并发优化
-
缓存击穿防护:
- 采用互斥锁解决缓存重建冲突
- 逻辑过期方案避免大量线程阻塞
-
库存超卖问题:
java复制// Redis原子性递减 Long count = stringRedisTemplate.opsForValue() .decrement("seckill:stock:" + voucherId); if (count < 0) { // 库存不足回滚 stringRedisTemplate.opsForValue() .increment("seckill:stock:" + voucherId); return Result.fail("库存不足"); }
6. 安全防护方案
6.1 接口限流配置
java复制@Configuration
public class RateLimitConfig implements WebMvcConfigurer {
private static final RateLimiter limiter =
RateLimiter.create(100); // 每秒100个令牌
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (!limiter.tryAcquire()) {
response.setStatus(429);
return false;
}
return true;
}
}).addPathPatterns("/user/code");
}
}
6.2 常见攻击防护
-
验证码爆破:
- 单个IP限流(1分钟5次)
- 验证码复杂度控制(6位数字+字母)
-
重放攻击:
- 使用一次性验证码
- 请求时间戳校验(5分钟内有效)
-
Token泄露:
- HTTPS传输
- 设置合理有效期
- 关键操作二次验证
7. 生产环境问题排查
7.1 典型问题记录
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 验证码发送超时 | 短信平台限流 | 接入多通道容灾 |
| Redis连接池耗尽 | 未正确释放连接 | 使用try-with-resources |
| Token无故失效 | Redis内存不足 | 监控内存使用率 |
7.2 监控指标配置
-
Redis监控:
- 内存使用率
- 连接数
- 慢查询日志
-
业务监控:
- 验证码发送成功率
- 登录接口RT
- 异常登录行为报警
关键提示:所有Redis操作必须设置合理的超时时间,避免网络问题导致线程阻塞。建议使用Spring RedisTemplate的execute方法配合RedisCallback实现原子操作。
8. 扩展优化方向
-
分布式会话方案:
- 采用JWT替换原生Token
- 结合Redis实现无状态认证
-
风险控制升级:
- 设备指纹识别
- 行为异常检测
-
用户体验优化:
- 滑动验证码
- 本机号码一键登录
实际开发中发现,当用户量突破10万时,原始方案会出现Redis内存压力。我们最终采用的优化方案是:
- 将用户信息中的不常用字段移出Hash结构
- 对手机号进行哈希存储(避免原始号码暴露)
- 采用Lua脚本保证操作原子性
lua复制-- 验证码校验Lua脚本
local code = redis.call('get', KEYS[1])
if not code or code ~= ARGV[1] then
return 0
end
redis.call('del', KEYS[1])
return 1
这个脚本在项目中使得验证码校验性能提升了40%,特别是在促销活动期间效果显著。建议在类似场景中都采用Lua脚本来减少网络往返开销。
