【Redis|实战篇1】黑马点评|短信登录功能实现

本文记录在学习Redis过程中的关键技术实践与踩坑思考,包含个人在实际学习中的具体过程遇到的问题 以及知识点总结
希望可以给一起学习的大家带来帮助ヽ( ̄▽ ̄)ノ

文章目录

黑马点评

项目简介

1.导入基础代码

该项目需要JDK的版本必须为1.8,没装JDK1.8的可以直接在IDEA里找到Project Structure选择SDK版本时点击Download下载1.8版本

2.修改application.yaml文件,把相关信息改成自己的

3.启动项目,在浏览器地址栏输入localhost:8081/shop-type/list

证明代码导入成功

1.短信登录

1.1基于session实现短信登录的流程
1.2发送短信验证码

快速找到接口的实现类:选中接口点击ctrl+alt+B

UserController

实现接口的开发

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

IUserService

java 复制代码
public interface IUserService extends IService<User> {

    Result sendCode(String phone, HttpSession session);
}

UserServiceImpl

java 复制代码
/**
     * 发送手机验证码
     * @param phone
     * @param session
     * @return
     */
    @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();
    }
1.3短信验证码登录和注册

此项目使用了Mybatisplus

@TableName 注解的核心作用就是告诉 MyBatis-Plus (MP):这个 Java 类对应数据库里的哪张表

由于项目继承了Mybatisplus提供的ServiceImpl,所以可以直接实现简单的单表查询:

User user = query().eq("phone", phone).one();等同于select * from tb_user where phone = ?

完整代码

UserController

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

UserServiceImpl

java 复制代码
**
     * 登录 + 注册
     * @param loginForm
     * @param session
     * @return
     */
    @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)) {
            //不一致,报错
            return Result.fail("验证码错误");
        }
        //3.根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //4.判断用户是否存在
        if(user == null) {
            //5.不存在则创建新用户并保存
            user = cresteUserWithPhone(phone);
        }

        //6.保存用户信息到session
        session.setAttribute("user",user);

        return Result.ok();
    }

    private User cresteUserWithPhone(String phone) {
        //1.创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName("user_" + RandomUtil.randomString(10));
        //2.保存用户到数据库
        save(user);//Mybatisplus
        return user;
    }
1.4登录校验拦截器

在Util包下新建LoginInterceptor作为登录校验时的拦截器

Ctrl + i实现重写方法

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {
    @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){
            //不存在,拦截
            response.setStatus(401);
            return false;
        }
        //4.保存用户信息到ThreadLocal
        UserHolder.saveUser((User)user);
        //5.放行
        return true;
    }

}

配置拦截器

java 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    
    //添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/login",
                        "/user/code",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                );
    }
}

获取当前登录用户在UserController中增加

java 复制代码
@GetMapping("/me")
    public Result me(){
        //获取当前登录的用户并返回
        User user = UserHolder.getUser();
        return Result.ok(user);
    }
1.5隐藏用户敏感信息

修改一下UserServiceImpl里的登录注册

java 复制代码
//6.保存用户信息到session
        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

只保存

java 复制代码
@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}
1.6集群的session共享问题

session的共享问题:多态Tomcat并不共享session存储空间,当请求切换到不同的Tomcat服务时会导致数据丢失的问题

解决方法

session替代方案,需满足:

  • 数据共享
  • 内存存储
  • key、value结构

使用Redis代替session

1.7基于Redis实现共享session登录

先注入Redis的API

java 复制代码
@Autowired
private StringRedisTemplate stringRedisTemplate;

修改发送验证码,把第四步改为把验证码存入Redis,存入时设置有效期2minutes

java 复制代码
/**
     * 发送手机验证码
     * @param phone
     * @param session
     * @return
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        if(RegexUtils.isPhoneInvalid(phone)) {
            //2.若不符合,返回错误信息
            return Result.fail("手机号格式错误");
        }
        //3.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4.保存验证码到Redis
        stringRedisTemplate.opsForValue().set("login:code:" + phone, code,2, TimeUnit.MINUTES);
        //5.发送验证码
        log.debug("发送短信验证码成功,验证码:{}",code);
        //返回ok
        return Result.ok();
    }

修改登录注册

java 复制代码
/**
     * 登录 + 注册
     * @param loginForm
     * @param session
     * @return
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)) {
            //若不符合,返回错误信息
            return Result.fail("手机号格式错误");
        }
        //2.校验验证码 从Redis里取
        String cacheCode = stringRedisTemplate.opsForValue().get("login:code:" + phone);
        String code = loginForm.getCode();
        if(cacheCode == null || !cacheCode.equals(code)) {
            //不一致,报错
            return Result.fail("验证码错误");
        }
        //3.根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //4.判断用户是否存在
        if(user == null) {
            //5.不存在则创建新用户并保存
            user = cresteUserWithPhone(phone);
        }

        //6.保存用户信息到redis
        //6.1随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString();
        ///6.2将UserDTO对象转为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()));
        stringRedisTemplate.opsForHash().putAll("login:token:" + token, userMap);
        //设置有效期
        stringRedisTemplate.expire("login:token:" + token, 30, TimeUnit.MINUTES);


        return Result.ok();
    }

修改登录拦截器

java 复制代码
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)){
            //不存在,拦截
            response.setStatus(401);
            return false;
        }
        //2.基于token获取Redis中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
        //3.判断用户是否存在
        if(userMap.isEmpty()){
            //不存在,拦截
            response.setStatus(401);
            return false;
        }
        //将查询到的Hash数据转化为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //4.保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        //5.刷新token有效期
        stringRedisTemplate.expire("login:token:" + token, 30, TimeUnit.MINUTES);
        //6.放行
        return true;
    }

}
1.8登录拦截器的优化

解决状态登录刷新的问题:因为当前拦截器只拦截部分请求,进行不被拦截的请求时就不刷新token的有效期

此时只需再增添一个拦截器拦截所有路径

增加 RefreshToken拦截器

java 复制代码
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中的用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
        //3.判断用户是否存在
        if(userMap.isEmpty()){
            //不存在,拦截
            response.setStatus(401);
            return false;
        }
        //将查询到的Hash数据转化为UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //4.保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        //5.刷新token有效期
        stringRedisTemplate.expire("login:token:" + token, 30, TimeUnit.MINUTES);
        //6.放行
        return true;
    }

//    @Override
//    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
//    }
}

修改Login拦截器,只需要判断用户是否存在

java 复制代码
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;
    }

}

配置拦截器

把RefreshToken拦截器加进去

java 复制代码
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);

配置拦截器顺序

在Login拦截器后加.order(1);

在RefreshToken拦截器后加.order(0);

相关推荐
AI_56781 小时前
RabbitMQ消息队列:高可用集群搭建与消息幂等处理
开发语言·后端·ruby
古城小栈1 小时前
Rust 1.94.0 闪亮登台
开发语言·后端·rust
弹简特1 小时前
【JavaEE15-后端部分】SpringBoot配置文件的介绍
java·spring boot·后端
云存储小天使1 小时前
提效 77%:GooseFS 写缓存及其在自动驾驶数据处理中的应用
人工智能·缓存·自动驾驶
东离与糖宝1 小时前
OpenClaw + SpringCloud 微服务集成:AI 能力全局复用
java·人工智能
丈剑走天涯1 小时前
kubernetes Jenkins 二进制安装指南
java·kubernetes·jenkins
wuxinyan1232 小时前
Java面试题040:一文深入了解分布式锁
java·面试·分布式锁
弹简特2 小时前
【JavaEE16-后端部分】SpringBoot日志的介绍
java·spring boot·后端