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);
相关推荐
程序员徐师兄1 小时前
Java 基于 SpringBoot 的校园外卖点餐平台微信小程序(附源码,部署,文档)
java·spring boot·微信小程序·校园外卖点餐·外卖点餐小程序·校园外卖点餐小程序
chengpei1471 小时前
chrome游览器JSON Formatter插件无效问题排查,FastJsonHttpMessageConverter导致Content-Type返回不正确
java·前端·chrome·spring boot·json
Q_27437851092 小时前
springboot基于微信小程序的周边游小程序
spring boot·微信小程序·小程序
计算机学姐2 小时前
基于微信小程序的民宿预订管理系统
java·vue.js·spring boot·后端·mysql·微信小程序·小程序
Fly不安全3 小时前
Web安全:缓存欺骗攻击;基于缓存、CDN的新型Web漏洞
nginx·web安全·缓存·web·cdn·缓存欺骗攻击
阿猿收手吧!3 小时前
【Redis】Redis入门以及什么是分布式系统{Redis引入+分布式系统介绍}
数据库·redis·缓存
奈葵3 小时前
Spring Boot/MVC
java·数据库·spring boot
Sunny_lxm4 小时前
<keep-alive> <component ></component> </keep-alive>缓存的组件实现组件,实现组件切换时每次都执行指定方法
前端·缓存·component·active
liuyunshengsir4 小时前
Spring Boot 使用 Micrometer 集成 Prometheus 监控 Java 应用性能
java·spring boot·prometheus