第十三章 Redis短信登录实战(基于Redis)

news2024/12/28 17:45:21

目录

一、概述

1.1. Session复制

1.2. 使用Redis

二、基于Redis实现共享Session登录 

2.1. 实现思路

2.2. 功能实现的主要代码

2.2.1. 用户业务接口

2.2.2. 用户业务接口实现类 

2.2.3. 用户控制层 

 2.2.4. 登录拦截器

2.2.5. 拦截器配置类 

2.3. 优化登录拦截器


 

完整的项目资源共享地址,当中包含了代码、资源文件以及Nginx(Windows版)和完整的配置:

链接:https://pan.quark.cn/s/5c28484d7882
提取码:cJxQ

一、概述

分布式集群的Session共享问题是多台Tomcat并不共享session存储空间,当请求切换到不同tomcat的服务时会导致数据丢失问题。

1.1. Session复制

在早期比较普遍的解决方案是通过Session复制,该方案的主要问题体现在以下几方面:

  1. 性能瓶颈:通过减少Session复制的频率来提高性能,例如使用基于时间的Session失效策略来复制,而不是在每次Session更新时复制。

  2. 数据一致性问题:使用更强一致性的机制来保证数据的一致性,例如使用分布式事务管理,或者通过合理的操作顺序来保证数据的一致性。

  3. 内存占用:通过使用更高效的序列化机制来减少Session数据占用的内存大小。

  4. 配置管理复杂度高:通过集中式Session存储来简化配置,例如使用分布式缓存(如Redis、Memcached)或者数据库来存储Session信息。

    具体实现代码取决于所使用的技术栈和架构。在Java环境中,可以使用Tomcat提供的Session管理器,或者集成Redis这样的第三方缓存服务来解决Session复制的问题。

1.2. 使用Redis

使用Redis作为Session共享,是比较成熟和理想的方案,能够满足Session数据的共享、且通过内存以及键值对的形式进行存储,更高效且便捷。

二、基于Redis实现共享Session登录 

2.1. 实现思路

1. 通过Redis实现共享Session登录的关键点是将手机号和验证码作为键值对存入Redis,并将登录的令牌token和用户信息作为键值对存入Redis:

 2. 存储用户信息更推荐使用Hash :

2.2. 功能实现的主要代码

2.2.1. 用户业务接口

package com.hmdp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.entity.User;
import javax.servlet.http.HttpSession;

public interface IUserService extends IService<User> {

    Result sendCode(String phone, HttpSession session);

    Result login(LoginFormDTO loginForm, HttpSession session);

}

2.2.2. 用户业务接口实现类 

package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.SystemConstants.USER_NICK_NAME_PREFIX;

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);

        // 4.保存验证码到 session
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

        // 5.发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);
        // 返回ok
        return Result.ok();
    }

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.从redis获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.equals(code)) {
            // 不一致,报错
            return Result.fail("验证码错误");
        }

        // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
        User user = query().eq("phone", phone).one();

        // 5.判断用户是否存在
        if (user == null) {
            // 6.不存在,创建新用户并保存
            user = createUserWithPhone(phone);
        }

        // 7.保存用户信息到 redis中
        // 7.1.随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 7.2.将User对象转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        // StringRedisTemplate的key value必须是String型,因此需要做自定义类型转换
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        // true 忽略空的值
                        .setIgnoreNullValue(true)
                        // 字段值修改器,对字段值进行修改
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        // 7.3.存储
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 7.4.设置token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.返回token
        return Result.ok(token);
    }

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

2.2.3. 用户控制层 

package com.hmdp.controller;


import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.service.IUserService;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpSession;

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;

    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // 发送短信验证码并保存验证码
        return userService.sendCode(phone, session);
    }

    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // 实现登录功能
        return userService.login(loginForm, session);
    }

    @GetMapping("/me")
    public Result me(){
        // 获取当前登录的用户并返回
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }
}

 2.2.4. 登录拦截器

通过拦截器实现登录用户每次访问都会触发刷新 token有效期的操作:

package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;

public class LoginInterceptor implements HandlerInterceptor {
    
    private StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            // 不存在 拦截 返回401状态码
            response.setStatus(401);
            return false;
        }
        // 2.基于TOKEN获取redis中的用户
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            // 不存在 拦截 返回401状态码
            response.setStatus(401);
            return false;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

2.2.5. 拦截器配置类 

package com.hmdp.config;

import com.hmdp.utils.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
    }
}

2.3. 优化登录拦截器

上面的拦截器代码逻辑有个问题是,登录用户如果一直访问一些不需要登录的页面,token依然不会刷新。对此,我们需要做如下的代码优化:

package com.hmdp.utils;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户,则放行
        return true;
    }
}
package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2.基于TOKEN获取redis中的用户
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}
package com.hmdp.config;

import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

详细代码请参看文章开头共享的代码资源,当中包含了后续章节要讲解的有关Redis所有内容,我会持续更新…… 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2202618.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JVM系列(一) -浅谈虚拟机的成长史

一、摘要 众所周知&#xff0c;Java 经过多年的发展&#xff0c;已经从一门单纯的计算机编程语言&#xff0c;发展成了一套成熟的软件解决方案。从互联网到企业平台&#xff0c;Java 是目前使用最广泛的编程语言。 以下这段内容是来自 Java 的官方介绍&#xff01; 从笔记本电…

程序设计基础I-实验7 函数(函数题)

6-1 sdut-C语言实验-计算组合数 计算组合数。C(n,m),表示从n个数中选择m个的组合数。 计算公式如下&#xff1a; 若&#xff1a;m0&#xff0c;C(n,m)1 否则&#xff0c; 若 n1&#xff0c;C(n,m)1 否则&#xff0c;若mn&#xff0c;C(n,m)1 否则 C(n,m) C(n-1,m-1) C(n-1,m…

linux下新增加一块sata硬盘并使用

1&#xff09;确认新硬盘能被正确识别到 2&#xff09;对新硬盘进行分区 说明&#xff1a;fdisk指令中输入“m”&#xff0c;可以看到详细的指令含义。 3&#xff09;确认新创建的分区 5&#xff09;格式化新创建的分区 6&#xff09;挂载新分区并使用

网优学习干货:王者荣耀游戏用户体验洞察及质差识别(2)

王者荣耀卡顿特点 影响时延的因素 手游定界定位解决方案 基于“9段法”进行卡顿问题分解 通过数据关联->体验定标->优化提升&#xff0c;改善手游卡顿 无线侧通过“面”和“点”优化改善空口时延 参数及互操作策略优化提升业务感知 传输优化准确定位管道问题——无TWAM…

kafka快速入门系统学习示例

1.主要名词 Broker&#xff1a;消息中间件处理节点&#xff0c;⼀个Kafka节点就是⼀个broker&#xff0c;⼀个或者多个 Broker可以组成⼀个Kafka集群。 partition&#xff1a;通过partition将⼀个topic中的消息分区来存储。这样的好处有多个&#xff1a;分区存储&#xff0…

ArgoWorkflow教程(六)---无缝实现步骤间参数传递

之前我们分析了&#xff0c;Workflow、WorkflowTemplate 、template 3 者之间如何传递参数。 本文主要分析同一个 Workflow 中的不同 step 之间实现参数传递&#xff0c;比如将上一个步骤的输出作为下一个步骤的结果进行使用&#xff08;而非以文件方式传递&#xff09;。 1. …

引领零售未来:年销售额500亿的运动品牌如何实现多渠道会员管理?

通过技术创新和全渠道整合&#xff0c;提升运动品牌影响力、用户粘性和市场竞争力。 运动热情高涨持续释放消费潜力&#xff0c;全球知名运动品牌2024财年营收超500亿美元&#xff0c;保持七个季度连续增长。 而随着电商的崛起和消费习惯的变化&#xff0c;会员数量激增且分布…

Android实现ViewPager剧中放大效果

效果图 实现方式核心思想是自定义PageTransformer继承ViewPager.PageTransformer&#xff0c;精确控制每一个page的动效。 PageTransformer的transformPage方法并不会区分当前的page是哪一个&#xff0c;所以需要我们自己去识别&#xff0c;我的方法是每个page添加一个text显…

Nuxt.js 应用中的 page:transition:finish 钩子详解

title: Nuxt.js 应用中的 page:transition:finish 钩子详解 date: 2024/10/10 updated: 2024/10/10 author: cmdragon excerpt: page:transition:finish 是 Nuxt.js 中的一个事件钩子,专门用于处理页面过渡效果结束后的逻辑。这一钩子在页面过渡的 onAfterLeave 事件之后…

qt+opengl 实现纹理贴图,平移旋转,绘制三角形,方形

1 首先qt 已经封装了opengl&#xff0c;那么我们就可以直接用了&#xff0c;这里面有三个函数需要继承 virtual void initializeGL() override; virtual void resizeGL(int w,int h) override; virtual void paintGL() override; 这三个函数是实现opengl的重要函数。 2 我们…

【NLP自然语言处理】03 - 使用Anaconda创建新的环境/pycharm切换环境

NLP基础阶段&#xff1a;创建新的虚拟环境 第一步&#xff1a;查看有多少个虚拟环境 conda env list 第二步&#xff1a;创建一个新的虚拟环境&#xff0c;起个名字&#xff1a;nlpbase 打开anconda prompt终端&#xff0c;输入命令: conda create -n nlpbase python3.10 第三步…

基于xml配置文件的Spring事务

在项目中对事务属性通常传播属性&#xff0c;回滚属性&#xff0c;隔离级别&#xff0c;超时属性都取默认值&#xff0c;只有只读属性会如下的配置&#xff1a; 什么意思&#xff1a;Service层你的类里的方法&#xff0c;以get&#xff0c;find&#xff0c;select等开头的方法是…

如何进行数据中心负载测试的自动化?

数据中心负载测试的自动化是一种通过使用软件工具和脚本来模拟大量用户访问数据中心的过程&#xff0c;以评估其性能、稳定性和可扩展性的方法。以下是进行数据中心负载测试自动化的一些建议&#xff1a; 市场上有许多负载测试工具可供选择&#xff0c;如LoadRunner、JMeter、…

【图论】(二)图论基础与路径问题

图论基础与路径问题 图的构造邻接矩阵邻接表 所有可达路径邻接矩阵存储邻接表存储 字符串接龙有向图的完全可达性 图的构造 这里仅对图论路径问题中图的构造做整理总结归纳&#xff0c;具体详细相关概念请参考代码随想录上的整理总结&#xff1a; 图论理论基础深度优先搜索理…

C语言函数栈帧的创建与销毁(32)

文章目录 前言一、什么是函数栈帧&#xff1f;二、理解函数栈帧能解决什么问题&#xff1f;三、函数栈帧的创建和销毁解析什么是栈&#xff1f;认识相关寄存器和汇编指令 四、解析函数栈帧的创建和销毁预备知识函数的调用堆栈准备环境转到反汇编函数栈帧的创建函数栈帧的销毁 五…

采用反相正基准电压电路的反相运算放大器

1 简介 本设计使用采用反相正基准电压的反相放大器将 –5V 至 –1V 的输入信号转换为 3.3V 至 0.05V 的输出电压。该电路可用于将传感器负输出电压转换为可用的 ADC 输入电压范围。 2 设计目标 2.1 输入 2.2 输出 2.3 电源 3 电路设计 根据设计目标&#xff0c;最终设计的电…

2.1类和对象(上)

本篇博客来梳理类和对象的基础知识 一、类的定义 1&#xff0e;类定义格式 &#xff08;1&#xff09;关键字&#xff1a;class。类中的变量称为类的属性/成员变量&#xff0c;类中的函数称为类的方法/成员函数 &#xff08;2&#xff09;为区分成员变量&#xff0c;一般会加…

MES管理系统对中小企业有哪些帮助

MES管理系统解决方案对中小企业具有显著的帮助&#xff0c;主要体现在以下几个方面&#xff1a; 一、提升生产效率 MES管理系统能够实时监控生产过程&#xff0c;提供准确的生产数据和及时的反馈。这种实时监控与数据分析能力&#xff0c;使中小企业能够精准把握生产脉搏&…

如何应对动态图片大小变化?Python解决网页图片截图难题

背景介绍 随着互联网的发展&#xff0c;许多网站&#xff0c;尤其是电商平台&#xff0c;如京东&#xff08;JD.com&#xff09;&#xff0c;为了提升用户体验&#xff0c;采用了许多动态内容加载技术。当我们使用爬虫获取商品图片时&#xff0c;往往会遇到一些棘手问题&#…

中科星图GVE(案例)——AI提取指定采样区域的建筑物范围

目录 简介 函数 gve.Image.fromGeometry(geometry,source,options) gve.Services.AI.buildingExtraction(fromGridRes) 代码 结果 ​编辑 知识星球 机器学习 简介 要提取指定采样区域的建筑物范围&#xff0c;可以使用遥感图像处理和计算机视觉技术。以下是一种可能的…