m0_74961286 2025-11-27 16:26 采纳率: 0%
浏览 4

redis实现手机号短信登录,优化成JWT 登录认证(用户登录时生成jwttoken) + Redis 自动续期,还能怎么优化呢? 进一步优化:使用双token嘛?再加上rabbitmq?

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
// 1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(loginForm.getPhone()))
return Result.fail("手机号格式错误");
// 2.校验验证码,从session中获取
//2.1 改进 从redis中获取验证码并校验
String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
String code = loginForm.getCode();
if (cacheCode == null || !cacheCode.equals(code)) {
// 不一致,报错
return Result.fail("验证码错误");
}
//4一致,根据手机号查询用户, 并且不抛出异常 false
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getPhone, loginForm.getPhone());
User user = this.getOne(wrapper, false);
//5.判断用户是否存在,手机号注册
if (user == null) {
// 6没有查询到用户,则注册新用户
user = createUserWithPhone(loginForm.getPhone());
}
// TODO 新增 6.生成JWT
Map<String, Object> claims = new HashMap<>();
// 用户ID
claims.put(JwtClaimsConstant.USER_ID, user.getId());
// 确保密钥是 byte[] 类型
String secretKey = jwtProperties.getUserSecretKey();
byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8); // 推荐指定编码
// 生成 Token
String jwttoken = JWTUtil.createToken(claims, keyBytes);

    // 7.2.将User对象转为HashMap存储
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),   //beanToMap方法执行了对象到Map的转换
            CopyOptions.create()
                    .setIgnoreNullValue(true)   //BeanUtil在转换过程中忽略所有null值的属性
                    .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));  //对于每个字段值,它简单地调用toString()方法,将字段值转换为字符串。
    // 7.3.存储
    String tokenKey = LOGIN_USER_KEY + userDTO.getId();
    // 7.4.将jwttoken存入userMap中
    userMap.put("jwttoken",jwttoken);
    stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
    // 7.4.设置token有效期
    stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
    // 8.返回token
    return Result.ok(jwttoken);
}

private User createUserWithPhone(String phone) {
    // 1.创建用户对象
    User user = new User();
    user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    user.setPhone(phone);
    //2.保存用户
    save(user);
    return user;
}

  • 写回答

2条回答 默认 最新

  • 檀越@新空间 2025-11-27 16:45
    关注

    下午好🌅🌅🌅
    本答案参考通义千问

    在当前的 Redis + JWT 登录认证系统中,已经实现了基本的登录、JWT 生成和 Redis 存储。为了进一步优化性能、安全性与可扩展性,可以考虑以下几种优化方案:


    ✅ 一、引入双 Token(Access Token + Refresh Token)

    1.1 为什么需要双 Token?

    • Access Token:用于每次请求的鉴权,通常有效期较短(如 5 分钟)。
    • Refresh Token:用于刷新 Access Token,有效期较长(如 7 天),并存储在安全的地方(如 HttpOnly Cookie)。

    1.2 优势

    • 安全性更高:即使 Access Token 泄露,攻击者也无法长期访问系统。
    • 用户体验更好:用户无需频繁重新登录。
    • 减少 Redis 压力:通过刷新机制避免频繁更新 Token。

    1.3 实现步骤

    1. 用户登录成功后,生成 Access TokenRefresh Token
    2. Refresh Token 存入 Redis,设置较长时间的过期时间(如 7天)。
    3. 在每次请求中验证 Access Token,如果失效,则使用 Refresh Token 刷新 Access Token。
    4. 如果 Refresh Token 也失效,要求用户重新登录。

    1.4 代码示例(简化版)

    // 生成双 Token
    public Map<String, String> generateTokens(User user) {
        // Access Token(短期)
        String accessToken = generateJwtToken(user, jwtProperties.getAccessTokenExpire(), jwtProperties.getAccessSecretKey());
        // Refresh Token(长期)
        String refreshToken = generateJwtToken(user, jwtProperties.getRefreshTokenExpire(), jwtProperties.getRefreshSecretKey());
    
        // 存入 Redis(可选)
        stringRedisTemplate.opsForValue().set("refresh_token:" + user.getId(), refreshToken, jwtProperties.getRefreshTokenExpire(), TimeUnit.MINUTES);
    
        return Map.of("access_token", accessToken, "refresh_token", refreshToken);
    }
    

    ✅ 二、引入 RabbitMQ 实现异步操作

    2.1 为什么要用 RabbitMQ?

    • 提高系统响应速度:将非关键业务逻辑(如发送短信、日志记录)异步处理。
    • 解耦服务:降低模块间的依赖,提升系统稳定性。
    • 消息队列保障可靠性:确保消息不会丢失。

    2.2 典型应用场景

    • 发送短信验证码 → 异步发送
    • 记录用户行为日志 → 异步写入数据库
    • 用户注册后通知其他服务 → 异步通知

    2.3 实现步骤

    1. 在发送验证码时,将任务推送到 RabbitMQ 队列。
    2. 启动一个消费者监听该队列,执行实际发送逻辑。
    3. 确保消息消费失败时进行重试或记录错误日志。

    2.4 代码示例(发送短信到 RabbitMQ)

    // 生产者端:发送验证码到队列
    public void sendVerificationCodeToQueue(String phone, String code) {
        String message = String.format("{\"phone\": \"%s\", \"code\": \"%s\"}", phone, code);
        rabbitTemplate.convertAndSend("sms_queue", message);
    }
    
    // 消费者端:监听队列并发送短信
    @RabbitListener(queues = "sms_queue")
    public void receiveMessage(String message) {
        try {
            JSONObject json = new JSONObject(message);
            String phone = json.getString("phone");
            String code = json.getString("code");
            // 调用短信服务发送验证码
            smsService.sendVerificationCode(phone, code);
        } catch (Exception e) {
            log.error("发送短信失败: {}", e.getMessage());
            // 可以加入重试机制或记录失败日志
        }
    }
    

    ✅ 三、Redis 自动续期优化

    3.1 问题

    目前是手动设置 expire,但若用户持续活跃,可能需要自动续期。

    3.2 解决方案

    • 使用 Redis 的 PEXPIRE 命令,定期更新 Key 的过期时间。
    • 或者使用 Lua 脚本实现原子性的续期操作。

    3.3 代码示例(自动续期)

    public void renewTokenExpiration(Long userId) {
        String tokenKey = LOGIN_USER_KEY + userId;
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
    }
    

    可以在每次请求中调用此方法来延长 Token 有效期。


    ✅ 四、JWT 令牌签名优化

    4.1 问题

    当前使用的是简单的字符串密钥,不够安全。

    4.2 优化建议

    • 使用 HMAC-SHA256RSA 等更安全的算法。
    • 密钥应存储在安全配置中,而不是硬编码。

    4.3 代码示例(使用 RSA 签名)

    // 加载私钥
    File privateKeyFile = new File("private_key.pem");
    String privateKey = Files.readString(privateKeyFile.toPath());
    
    // 生成 JWT
    RSAPrivateKey key = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
    String jwttoken = JWT.create()
        .withSubject(user.getId())
        .withExpiresAt(new Date(System.currentTimeMillis() + 3600000))
        .sign(Algorithm.RSA256(key));
    

    ✅ 五、使用 Redis 集群或哨兵模式

    5.1 为什么重要?

    • 高可用性:避免单点故障。
    • 水平扩展:支持更大规模的并发请求。
    • 数据持久化:防止数据丢失。

    5.2 配置建议

    • 使用 Redis Cluster 模式。
    • 设置主从复制 + 哨兵监控。

    ✅ 六、增加 Token 黑名单机制(可选)

    6.1 场景

    • 用户登出后,需要立即使 Token 失效。
    • 若不加黑名单,即使 Token 未过期,仍可被使用。

    6.2 实现方式

    • 在 Redis 中维护一个 Token 黑名单(Set 结构)。
    • 每次请求时检查 Token 是否在黑名单中。

    6.3 代码示例

    // 登出时添加到黑名单
    public void addTokenToBlacklist(String token) {
        stringRedisTemplate.opsForSet().add("token_blacklist", token);
    }
    
    // 验证 Token 是否有效
    public boolean isTokenValid(String token) {
        return !stringRedisTemplate.opsForSet().isMember("token_blacklist", token);
    }
    

    ✅ 七、前端 Token 存储优化(HttpOnly Cookie)

    7.1 好处

    • 防止 XSS 攻击窃取 Token。
    • 更安全地存储敏感信息。

    7.2 实现方式

    • 在响应头中设置 Set-Cookie,并标记为 HttpOnlySecure

    7.3 示例响应头

    Set-Cookie: access_token=your_jwt_token; Path=/; HttpOnly; Secure
    

    ✅ 八、分布式锁解决并发问题

    8.1 场景

    • 多个请求同时修改同一用户数据,可能导致数据不一致。

    8.2 解决方案

    • 使用 Redis 的 SETNXRedLock 实现分布式锁。

    8.3 代码示例(使用 Redisson)

    RLock lock = redisson.getLock("user_lock:" + userId);
    try {
        lock.lock();
        // 执行业务逻辑
    } finally {
        lock.unlock();
    }
    

    ✅ 总结:优化建议清单

    | 优化方向 | 说明 | |---------|------| | 双 Token | 提高安全性,减少频繁登录 | | RabbitMQ | 异步处理非关键业务,提升响应速度 | | Redis 自动续期 | 避免 Token 过期中断用户会话 | | JWT 签名优化 | 使用更安全的签名算法 | | Redis 集群/哨兵 | 提升可用性和扩展性 | | Token 黑名单 | 支持用户登出后立即失效 | | HttpOnly Cookie | 更安全地存储 Token | | 分布式锁 | 防止并发操作导致的数据不一致 |


    如需我为你定制具体实现方案或提供完整项目结构,请告诉我你使用的框架(如 Spring Boot、Spring Security 等),我可以进一步帮你封装代码。

    评论

报告相同问题?

问题事件

  • 创建了问题 11月27日