Redis实战(黑马点评)——涉及session、redis存储验证码,双拦截器处理请求

项目整体介绍

数据库表介绍

基于session的短信验证码登录与注册

controller层

java 复制代码
     // 获取验证码
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        return userService.sendCode(phone, session);
    }


    // 获取验证码之后登录页面
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // TODO 实现登录功能
        return userService.login(loginForm, session);
    }

service层

java 复制代码
@Override
public Result sendCode(String phone, HttpSession session) {
    // 1. 校验手机号格式是否正确
    if (RegexUtils.isPhoneInvalid(phone)) {
        return Result.fail("手机号格式不正确"); // 如果手机号格式不正确,返回失败结果
    }

    // 2. 生成6位随机数字验证码
    String code = RandomUtil.randomNumbers(6);
    
    // 3. 将验证码存储到HttpSession中
    session.setAttribute("code", code);

    // 4. 模拟发送验证码(实际开发中可以替换为短信发送逻辑)
    log.debug("发送验证码成功,验证码为:" + code);

    // 5. 返回成功结果
    return Result.ok();
}

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1. 校验手机号格式是否正确(防止用户在发送验证码后修改手机号)
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        return Result.fail("手机号格式不正确"); // 如果手机号格式不正确,返回失败结果
    }

    // 2. 从HttpSession中获取存储的验证码
    Object Cachecode = session.getAttribute("code");

    // 3. 校验用户输入的验证码是否正确
    if (!loginForm.getCode().equals(Cachecode.toString()) || Cachecode == null) {
        return Result.fail("验证码错误"); // 如果验证码不匹配或为空,返回失败结果
    }

    // 4. 判断数据库中是否存在该手机号对应的用户
    User user = lambdaQuery().eq(User::getPhone, phone).one(); // 查询用户

    // 5. 如果用户不存在,则创建新用户并保存到数据库
    if (user == null) {
        user = new User();
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)) // 设置随机昵称
                .setPhone(phone); // 设置手机号
        save(user); // 保存新用户到数据库
    }

    // 6. 将用户信息存储到HttpSession中
    session.setAttribute("user", user);

    // 7. 返回登录成功结果
    return Result.ok();
}

基于session登录的拦截器相关配置

创建拦截器

java 复制代码
@Slf4j // 使用Lombok自动生成日志对象
@Component // 标识这是一个Spring组件
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) {
            // 4. 如果用户不存在,拦截请求,返回401状态码(未授权)
            response.setStatus(401);
            return false; // 返回false表示请求被拦截,不再继续执行后续的处理器
        }
        // 5. 如果用户存在,将用户信息保存到ThreadLocal中,以便在其他地方使用
        BaseContext.setCurrent((User) user);  
        // 6. 放行请求,继续执行后续的处理器
        return true;
    }
    // 视图渲染完毕后运行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 记录日志信息
        log.info("afterCompletion ....");        
        // 通常移除线程池中的用户信息,防止内存泄漏
        BaseContext.removeCurrent();
    }
}

注册拦截器拦截对象

java 复制代码
@Configuration // 标识这是一个Spring配置类
public class MvcConfig implements WebMvcConfigurer {
    @Autowired // 自动注入LoginInterceptor的实例
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 向Spring MVC注册拦截器
        registry.addInterceptor(loginInterceptor) // 添加拦截器
                .addPathPatterns("/**") // 拦截所有请求路径
                .excludePathPatterns( // 排除不需要拦截的路径
                        "/shop/**", // 排除/shop/下的请求
                        "/voucher/**", // 排除/voucher/下的请求
                        "/shop-type/**", // 排除/shop-type/下的请求
                        "/upload/**", // 排除/upload/下的请求
                        "/blog/hot", // 排除/blog/hot请求
                        "/user/code", // 排除/user/code请求
                        "/user/login" // 排除/user/login请求
                );
    }
}

基于redis实现token登录与拦截器刷新token的过期时间

拦截器内获取请求头token同时检验与刷新

java 复制代码
@Override  
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  

    // 从请求头中获取 "authorization" 的值  
    String token = request.getHeader("authorization");  
    
    // 检查 token 是否为空或者空串  
    if(StrUtil.isBlank(token)){   
        // 如果 token 为空,设置响应状态为 401(未授权)  
        response.setStatus(401);  
        return false; // 返回 false,表示请求未被处理  
    }  

    // 从 Redis 中获取与 token 关联的用户信息  
    Map<Object, Object> userMap = redisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);  
    
    // 检查用户信息是否为空  
    if(userMap.isEmpty()){  
        // 如果用户信息为空,设置响应状态为 401(未授权)  
        response.setStatus(401);  
        return false; // 返回 false,表示请求未被处理  
    }  
    
    // 将用户信息填充到 UserDTO 对象中  
    UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);  
    
    // 将当前用户信息设置到上下文中  
    BaseContext.setCurrent(userDTO);  
    
    // 刷新 token 的过期时间
    String key = LOGIN_USER_KEY + token;  
    redisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);  

    // 返回 true,表示请求可以继续处理  
    return true;  
}

service层业务逻辑

java 复制代码
@Autowired  
private RedisTemplate redisTemplate;  

// 发送验证码的方法  
@Override  
public Result sendCode(String phone, HttpSession session) {  
    // 1、校验手机号格式  
    if(RegexUtils.isPhoneInvalid(phone)){  
        return Result.fail("手机号格式不正确"); // 返回失败结果,提示手机号格式不正确  
    }  
    
    // 生成一个随机的6位验证码  
    String code = RandomUtil.randomNumbers(6);  
    
    // 将验证码存储到 Redis 中,设置过期时间  
    redisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);  
    
    // 模拟发送验证码(此处仅为日志记录,实际应用中应调用短信发送服务)  
    log.debug("发送验证码成功,验证码为:" + code);  
    
    return Result.ok(); // 返回成功结果  
}  

// 登录的方法  
@Override  
public Result login(LoginFormDTO loginForm, HttpSession session) {  
    // 校验手机号,可能在收到验证码后修改了手机号  
    String phone = loginForm.getPhone();  
    if(RegexUtils.isPhoneInvalid(phone)){  
        return Result.fail("手机号格式不正确"); // 返回失败结果,提示手机号格式不正确  
    }  
    
    // 从 Redis 中获取与手机号相关的验证码  
    String code = (String) redisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);  
    
    // 校验输入的验证码是否与 Redis 中的验证码匹配,且验证码不为空  
    if(!loginForm.getCode().equals(code) || code == null){  
        return Result.fail("验证码错误"); // 返回失败结果,提示验证码错误  
    }  
    
    // 判断数据库中是否存在此电话号码的用户,如果没有就插入数据库  
    User user = lambdaQuery().eq(User::getPhone, phone).one();  
    if(user == null) {  
        // 如果用户不存在,创建新用户  
        user = new User();  
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10)) // 设置用户昵称  
                .setPhone(phone); // 设置用户手机号  
        save(user); // 保存新用户到数据库  
    }  
    
    // 生成一个新的 token  
    String token = UUID.randomUUID().toString();  
    
    // 将用户信息复制到 UserDTO 对象中  
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);  
    
    // 将 UserDTO 转换为 Map 以便存储到 Redis  
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);  
    
    // 生成 Redis 中的 token 键  
    String tokenkey = LOGIN_USER_KEY + token;  
    
    // 将用户信息存储到 Redis 中  
    redisTemplate.opsForHash().putAll(tokenkey, userMap);  
    
    // 设置 token 的过期时间  
    redisTemplate.expire(tokenkey, LOGIN_USER_TTL, TimeUnit.MINUTES);  

    // 返回成功结果,携带生成的 token  
    return Result.ok(token);  
}

双拦截器实现登录与未登录功能差别

第一层拦截器

java 复制代码
@Autowired
    private RedisTemplate redisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String token = request.getHeader("authorization");
        if(StrUtil.isBlank(token)){ // 检查是否为空或者空串
            return true;
        }

        Map<Object, Object> userMap = redisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        if(userMap.isEmpty()){
            return true;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        BaseContext.setCurrent(userDTO);
        String key = LOGIN_USER_KEY + token;
        redisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);

        return true;
    }

    //视图渲染完毕后运行
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion ....RefreshTokenInterceptor");
        BaseContext.removeCurrent(); // 通常移除线程池
    }

第二层拦截器

java 复制代码
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(BaseContext.getCurrent() == null){
            response.setStatus(401);
            return false;
        }

        return true;
    }

注册双拦截器

.order();方法用于指定拦截器的优先级,里面的值越小,那么优先级越高

java 复制代码
registry.addInterceptor(loginInterceptor)
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);

        // 拦截所有请求
        registry.addInterceptor(refreshTokenInterceptor).addPathPatterns("/**").order(0);
相关推荐
IT毕设实战小研3 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
一只爱撸猫的程序猿4 小时前
使用Spring AI配合MCP(Model Context Protocol)构建一个"智能代码审查助手"
spring boot·aigc·ai编程
甄超锋4 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
鼠鼠我捏,要死了捏5 小时前
生产环境Redis缓存穿透与雪崩防护性能优化实战指南
redis·cache
武昌库里写JAVA7 小时前
JAVA面试汇总(四)JVM(一)
java·vue.js·spring boot·sql·学习
Pitayafruit8 小时前
Spring AI 进阶之路03:集成RAG构建高效知识库
spring boot·后端·llm
zru_96028 小时前
Spring Boot 单元测试:@SpyBean 使用教程
spring boot·单元测试·log4j
甄超锋9 小时前
Java Maven更换国内源
java·开发语言·spring boot·spring·spring cloud·tomcat·maven
曾经的三心草9 小时前
微服务的编程测评系统11-jmeter-redis-竞赛列表
redis·jmeter·微服务
还是鼠鼠9 小时前
tlias智能学习辅助系统--Maven 高级-私服介绍与资源上传下载
java·spring boot·后端·spring·maven