Redis实现短信登录的企业实战

 更新时间:2022年07月20日 15:00:01   作者:Super_Leng  
本文主要介绍了Redis实现短信登录的企业实战,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
(福利推荐:【腾讯云】服务器最新限时优惠活动,云服务器1核2G仅99元/年、2核4G仅768元/3年,立即抢购>>>:9i0i.cn/qcloud

(福利推荐:你还在原价购买阿里云服务器?现在阿里云0.8折限时抢购活动来啦!4核8G企业云服务器仅2998元/3年,立即抢购>>>:9i0i.cn/aliyun

一、导入黑马点评项目

黑马点评项目主要包括以下功能:

这一章主要介绍短信登录功能,短信登录功能是基于Redis的共享session实现的

1. 导入SQL

需要项目资料的私信我

其中的表有:

  • tb_user:用户表
  • tb_user_info:用户详情表
  • tb_shop:商户信息表
  • tb_shop_type:商户类型表
  • tb_blog:用户日记表(达人探店日记)
  • tb_follow:用户关注表
  • tb_voucher:优惠券表
  • tb_voucher_order:优惠券的订单表

注意:Mysql的版本采用5.7及以上版本

2. 前后端分离

3. 导入后端项目

3.1 将后端项目导入到 Idea 中

3.2 注意:修改application.yaml文件中的mysql、redis地址信息 将mysql、redis地址信息修改为自己的信息

3.3 启动项目 启动项目后,在浏览器访问:http://localhost:8081/shop-type/list ,如果可以看到数据则证明运行没有问题

4. 导入前端项目

4.1 导入nginx文件夹 将nginx文件夹复制到任意目录,要确保该目录不包含中文、特殊字符和空格,例如:

4.2 运行前端项目 在nginx所在目录下打开一个CMD窗口,输入命令启动nginx:

start nginx.exe

打开chrome浏览器,在空白页面点击鼠标右键,选择检查,即可打开开发者工具:

然后访问: http://127.0.0.1:8080 ,即可看到页面:

二、基于Session实现登录流程

  • 后端将生成的验证码和用户信息保存到session中,并将sessionId返回给前端保存到cookie中
  • 用户登录时,会携带cookie向后端发起请求,后端进行校验时,从cookie中获取sessionId,通过sessionId可以从session中获取用户信息并保存到ThreadLocal中
  • 后续每个线程都有一份ThreadLocal中的用户副本信息,不同线程拿到用户信息后可以实现不同的操作,从而起到线程隔离作用

1. 发送短信验证码

主要代码:

@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);
    }
}    
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @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
        session.setAttribute("code",code);

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

2. 短信验证码登录、注册

主要代码:

UserController

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

UserServiceImpl

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }

    // 2.校验验证码
    Object cacheCode = session.getAttribute("code");
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.toString().equals(code)) {
        // 3.验证码不一致,则报错
        return Result.fail("验证码错误");
    }

    // 4.验证码一致,根据手机号查询用户
    User user = query().eq("phone", phone).one();

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

    // 7.保存用户信息到session中,UserDTO只包含简单的用户信息,
    // 而不是完整的User,这样可以隐藏用户的敏感信息(例如:密码等),还能减少内存使用
    session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

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

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

3. 登录验证功能

用户请求登录时,会携带cookie,cookie中包含JSEESIONID

为了避免用户请求每个controller时,每次都去校验用户信息,所以可以加拦截器

拦截器只需在用户请求访问时,校验一次后将用户信息保存到ThreadLocal中,供后续线程使用

主要代码:

在工具类中编写ThreadLocal

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();
    }
}

在工具类中编写登录拦截器

public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 前置拦截
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取session
        HttpSession session = request.getSession();
        // 2.获取session中的用户
        Object user = session.getAttribute("user");
        // 3.判断用户是否存在
        if(user == null){
            // 4.不存在,拦截,返回401状态码
            response.setStatus(401);
            return false;
        }
        // 5.存在,保存用户信息到ThreadLocal
        UserHolder.saveUser((User)user);
        // 6.放行
        return true;
    }

    /**
     * 后置拦截器
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
        Object handler, Exception ex) throws Exception {
        // 请求结束后移除用户,防止ThreadLocal造成内存泄漏
        UserHolder.removeUser();
    }
}

在配置类中添加拦截器配置类

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    /**
     * 添加拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
            // 排除不需要拦截的路径
            .excludePathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
            );
    }
}

UserController

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

三、集群的session共享问题

四、基于Redis实现共享session的登录功能

1. 选择合适的数据结构存入Redis

  • 手机号作为key,String类型的验证码作为value
  • 用户登录时正好会提交手机号,方便通过Redis进行校验验证码

token作为key,Hash类型的用户信息作为value

后端校验成功后,会返回token给前端,前端会将token保存到sessionStorage中(这是浏览器的存储方式),以后前端每次请求都会携带token,方便后端通过Redis校验用户信息

前端代码:将后端返回的token保存到sessionStorage中

前端每次请求时,都会通过拦截器将token设置到请求头中,赋值给变量authorization,后端通过authorization获取前端携带的token进行校验

2. 发送短信验证码

修改之前代码,将验证码存入Redis

@Service
@Slf4j
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
//        session.setAttribute("code",code);
        // 4.保存验证码到 redis
        // "login:code:"是业务前缀,以"login:code:" + 手机号为key,过期时间2分钟
        stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);

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

3. 短信验证码登录、注册

  • 修改之前代码,从Redis获取验证码并校验
  • 随机生成token,保存用户信息到redis中,返回token
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1.校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }

//        // 2.校验验证码
//        Object cacheCode = session.getAttribute("code");
//        String code = loginForm.getCode();
//        if (cacheCode == null || !cacheCode.toString().equals(code)) {
//            // 3.验证码不一致,则报错
//            return Result.fail("验证码错误");
//        }

    // 2.从Redis获取验证码并校验
    String cacheCode = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + phone);
    String code = loginForm.getCode();
    if (cacheCode == null || !cacheCode.equals(code)) {
        // 3.验证码不一致,则报错
        return Result.fail("验证码错误");
    }

    // 4.验证码一致,根据手机号查询用户
    User user = query().eq("phone", phone).one();

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

//        // 7.保存用户信息到session中,UserDTO只包含简单的用户信息,而不是完整的User,这样可以隐藏用户的敏感信息(例如:密码等),还能减少内存使用
//        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

    // 7.保存用户信息到redis中
    // 7.1随机生成token,作为登录令牌
    // 使用hutool工具中的UUID,true表示不带“-”符号的UUID
    String token = UUID.randomUUID().toString(true);
    
    // 7.2将User对象转为Hash类型进行存储
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);   
    // 由于使用的是stringRedisTemplate,所以存入的value中的值必须都是String类型的
    // 但是UserDTO中的id是Long类型的,所以进行对象属性拷贝时,需要自定义实现转换规则
     Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
                                                     
    // 7.3存入redis, "login:token:"是业务前缀,以 "login:token:" + token作为key
    stringRedisTemplate.opsForHash().putAll(RedisConstants.LOGIN_USER_KEY + token, userMap);
    // 7.4设置token有效期,有效期为30分钟
    stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.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_mrkuw05lok
    user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    // 2.保存用户
    save(user);
    return user;
}

4. 解决token刷新问题

  • token刷新问题是指,用户长时间不进行界面操作时,到了过期时间,token自动失效;但是,用户一旦进行操作,就需要给token续期,即更新token过期时间
  • 为了解决token刷新问题,需要加2个拦截器
  • 第一个拦截器可以拦截所有请求,只要用户有请求就刷新token,并保存用户信息到ThreadLocal中
  • 第二个拦截器只对登录请求进行拦截,从ThreadLocal中获取用户信息进行校验

刷新token的拦截器代码:

public class RefreshTokenInterceptor implements HandlerInterceptor {

    // 因为LoginInterceptor不是通过Spring进行管理的Bean,所以不能再LoginInterceptor中进行注入StringRedisTemplate
    // 可以通过构造方法传入StringRedisTemplate
    private StringRedisTemplate stringRedisTemplate;

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

    /**
     * 前置拦截
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        // 1.获取session
//        HttpSession session = request.getSession();
//        // 2.获取session中的用户
//        Object user = session.getAttribute("user");
//        // 3.判断用户是否存在
//        if(user == null){
//            // 4.不存在,拦截,返回401状态码
//            response.setStatus(401);
//            return false;
//        }
//        // 5.存在,保存用户信息到ThreadLocal
//        UserHolder.saveUser((UserDTO)user);
//        // 6.放行
//        return true;

        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            // 不存在,则拦截,返回401状态码
            response.setStatus(401);
            return false;
        }

        // 2.通过token获取redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
            .entries(RedisConstants.LOGIN_USER_KEY + token);

        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            // 4.用户不存在,则拦截,返回401状态码
            response.setStatus(401);
            return false;
        }

        // 5.将redis中Hash类型数据转换成UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);

        // 6.用户存在,保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);

        // 7.刷新token有效期
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY + token, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.放行
        return true;
    }

    /**
     * 后置拦截器
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
        Object handler, Exception ex) throws Exception {
        // 请求结束后移除用户,防止ThreadLocal造成内存泄漏
        UserHolder.removeUser();
    }
}

登录拦截器的代码:

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;
    }

}
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 添加拦截器
     * @param registry
     */
    @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刷新的拦截器,order越小,执行优先级越高,所以token刷新的拦截器先执行
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**")
            .excludePathPatterns(
            // RefreshTokenInterceptor拦截器也需要放行"/user/code","/user/login",不然token过期后再重新登录就会一直被拦截
                "/user/code",
                "/user/login")
            .order(0);
    }
}

到此这篇关于Redis实现短信登录的企业实战的文章就介绍到这了,更多相关Redis 短信登录 内容请搜索程序员之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持程序员之家!

相关文章

  • 用Redis实现微博关注关系

    用Redis实现微博关注关系

    在微博中,每一个用户都会有一个关注列表,一个粉丝列表。用户可以查看自己的关注,粉丝列表,也可以查看别人的关注,粉丝列表。并且,要展示列表里每个人与当前查看者的关注状态。
    2015-09-09
  • 使用Redis实现延时任务的解决方案

    使用Redis实现延时任务的解决方案

    这篇文章主要介绍了使用Redis实现延时任务的解决方案,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-08-08
  • Redis Sentinel的基本搭建

    Redis Sentinel的基本搭建

    这篇文章主要介绍了Redis Sentinel的使用,帮助大家更好的理解和学习使用Redis数据库,感兴趣的朋友可以了解下
    2021-03-03
  • redis常用命令、常见错误、配置技巧等分享

    redis常用命令、常见错误、配置技巧等分享

    这篇文章主要介绍了redis常用命令、常见错误、配置技巧等分享,本文分享了12条redis知识,需要的朋友可以参考下
    2015-02-02
  • Python交互Redis的实现

    Python交互Redis的实现

    本文主要介绍了Python交互Redis的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • redis连接被拒绝的解决方案

    redis连接被拒绝的解决方案

    这篇文章主要介绍了redis连接被拒绝的解决方案,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • SpringBoot集成redis错误问题及解决方法

    SpringBoot集成redis错误问题及解决方法

    这篇文章主要介绍了SpringBoot集成redis错误问题,本文给大家分享完美解决方法,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-02-02
  • Redis 执行性能测试

    Redis 执行性能测试

    这篇文章主要介绍了Redis 执行性能测试的方法,文中讲解非常细致,帮助大家更好的理解和学习redis,感兴趣的朋友可以了解下
    2020-08-08
  • Redis分布式锁Redlock的实现

    Redis分布式锁Redlock的实现

    本文主要介绍了Redis分布式锁Redlock的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • Redis的9种数据类型用法解读

    Redis的9种数据类型用法解读

    这篇文章主要介绍了Redis的9种数据类型用法及说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-02-02

最新评论

?


http://www.vxiaotou.com