黑马点评DAY4|整体项目介绍、短信登录模块

news2025/2/25 19:17:49

项目整体介绍

项目功能介绍

在这里插入图片描述

项目结构

在这里插入图片描述
该项目前后端分离架构模式,后端部署在Tomcat服务器,前端部署在Niginx服务器上,这也是现在企业开发的标准做法。PC端首先向Niginx发起请求,得到页面的静态资源,页面再通过ajax向服务端发起请求查询数据。这些数据可能来自Mysql或者Redis集群。再把查询到的数据返回给前端,前端完成渲染。
当然该项目也会考虑水平扩展能力,在单个Tomcat服务器无法承载时,水平扩展多个服务器形成可以负载均衡的集群,在多台Tomcat服务器上部署代码。

短信登录模块

导入黑马点评项目

  • 创建hmdp数据库,导入sql文件
    在这里插入图片描述
    表的介绍
    在这里插入图片描述
  • 修改application.yaml文件中redis和mysql数据库的配置
server:
  port: 8081
spring:
  application:
    name: hmdp
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/hmdp?useSSL=false&serverTimezone=UTC
    username: root
    password: 122045
  redis:
    host: 192.168.101.65
    port: 6379
    password: 123321
    lettuce:
      pool:
        max-active: 10
        max-idle: 10
        min-idle: 1
        time-between-eviction-runs: 10s
  jackson:
    default-property-inclusion: non_null # JSON处理时忽略非空字段
mybatis-plus:
  type-aliases-package: com.hmdp.entity # 别名扫描包
logging:
  level:
    com.hmdp: debug
  • 点击+号处,将项目配置为一个SpringBoot的项目,启动项目
    在这里插入图片描述
  • 如果配置正确,在浏览器中访问http://localhost:8081/shop-type/list会跳转到如下页面:在这里插入图片描述
  • 访问http://localhost:8080/会跳转到前端画面

在这里插入图片描述
此时,项目搭建完毕。

基于Session实现登录

业务梳理:

  • 左图:当用户请求验证码时需要对用户提交的手机号进行格式校验,如果不符合格式,则需要重新提交。如果符合格式,在本地生成验证码,并将验证码保存到session中,并将验证码发送给用户。
  • 中图:用户收到验证码,输入并提交手机号和验证码,首先校验验证码是否正确。如果不正确,用户重新提交。如果正确,再根据手机号查询用户,如果用户存在,将用户保存到session,如果用户不存在,创建新用户并将用户保存到数据库,再将用户保存到session。
  • 右图:校验用户是否为收到验证码的用户,cookie中有sessionid,根据这个sessionid找到session并从session中获取用户,判断用户是否存在,如果存在,证明该用户曾经登陆过。因为后续业务会用到该用户的信息,所以将用户缓存到线程的本地存储ThreadLocal中,这样后续的业务就可以从ThreadLocal中获取到用户信息,并放行;如果不存在,进行拦截。

注解:ThreadLocal就是一个线程域对象,每一个请求到达微服务,都是一个独立的线程,如果没有用ThreadLocal,而是直接将用户保存到本地变量,可能会出现多线程并发修改的安全问题,而ThreadLocal会把数据保存到每一个线程的内部,在线程内部创建一个Map去保存,这样每一个线程都有自己独立的存储空间,相互之间没有干扰,规避了多线程并发修改的问题。后续的所有业务都可以从ThreadLocal中取出自己的用户信息,这就是基于Session的登录状态的校验。
在这里插入图片描述

发送短信验证码

流程:

  • 校验手机格式
  • 手机格式错误,返回错误信息
  • 手机格式正确,生成验证码
  • 将验证码保存到session
  • 发送验证码给客户,一般公司都会有现成的服务直接调用,此处模拟即可
  • 返回ok
    先完善Controller接口,再写service。
package com.hmdp.service.impl;

import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
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.stereotype.Service;

import javax.servlet.http.HttpSession;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
        //2.如果错误,返回错误提示
        if(!phoneInvalid){
            return Result.fail("手机号不符合格式");
        }
        //3.如果正确,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4.将验证码保存到session中
        session.setAttribute("code", code);
        //5.向用户发送验证码
        log.debug("发送验证码成功,验证码为:{}",code);
        return Result.ok();
    }
}

短信验证码登录和校验

流程:

  • 手机格式验证,错误则返回错误信息
  • 校验码验证错误,返回错误信息
  • 校验验证码正确,根据手机号查询用户(这里MyBatisPlus帮我们完成了sql查询)
  • 如果用户不存在,创建用户并保存到数据库
  • 如果用户存在,直接保存到数据库
  • 将用户保存到session
  • 返回成功提示

代码如下

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //手机格式验证,错误则返回错误信息
        if(!RegexUtils.isPhoneInvalid(loginForm.getPhone())){
            return Result.fail("手机号不符合格式");
        }
        //校验码验证错误,返回错误信息
        String code = loginForm.getCode();
        Object cacheCode = session.getAttribute("code");//取出之前保存的验证码
        if(cacheCode == null || !cacheCode.toString().equals(loginForm.getCode())){
            return Result.fail("验证码错误");
        }
        //校验验证码正确,根据手机号查询用户(这里MyBatisPlus帮我们完成了sql查询)
        String phone = loginForm.getPhone();
        User user = query().eq("phone", phone).one();//这里查一个就是.one(),查多个就是list()
        //如果用户不存在,创建用户并保存到数据库
        if(user == null){
            user = createUserWithPhone(phone);
        }
        //将用户保存到session
        session.setAttribute("user", user);
        //返回成功提示
        return Result.ok();
    }
    private User createUserWithPhone(String phone){
        User user = new User();
        user.setPhone(phone);
        //向user中插入一个随机的用户昵称
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
        //如果用户存在,直接保存到数据库
        save(user);
        return user;
    }

完成之后,输入后端生成的验证码,发现页面瞬间跳转有退出。因为我们还只是完成了发送验证码、登录以及注册的功能,还没有完成校验功能。

登录校验

存在问题:在项目中有很多的Controller,前端向UserController发送请求完成业务,但是越来越多的业务需要去校验用户的登录,这样太麻烦。那么有什么方便的办法嘛?答案就是拦截器?有了拦截器,用户的请求不会直接到Controller,而是会先到拦截器,先由拦截器判断是否应该放行让其到达Controller,我们可以把所有用户校验的流程放到拦截器里面去完成,这样所有Controller就不用再去写登录校验,而全部由拦截器完成。但是光是拦截也不够,还需要将拦截到的信息传递到Controller里面去,传递的过程中还要注意线程的安全问题吗,可以用ThreadLocal来解决
ThreadLocal是一个线程域对象,每一个进入到Tomcat服务器的请求都是一个独立的线程,ThreadLocal会开辟一个内存空间保存对应用户,每个用户都有自己的独立线程,到了Controller之后再从ThreadLocal中取出用户就OK了。
在这里插入图片描述
因此,我们可以在拦截器中实现右图的功能,在SpringBoot中创建拦截器可以通过实现HandlerInterceptor接口定义,在HandlerInterceptor中有三个可以实现的方法分别是:

  • preHandler:前置拦截,用于用户校验,也就是流程图中的内容,在写完拦截的逻辑之后,还需要一个拦截器的配置类用于配置哪些需要拦截,哪些不需要拦截。这个配置类要实现WebMvcConfigurer接口
    • 获取用户session
    • 获取session中的用户
    • 判断用户是否存在
    • 不存在,拦截
    • 存在,将用户信息保存到ThreadLocal
    • 放行
  • postHandler:在controller执行之后
  • afterCompletion:视图渲染之后,返回用户之前,用于销毁用户信息,避免内存泄露

首先写loginInterceptor的业务逻辑:

package com.hmdp.utils;

import com.hmdp.dto.UserDTO;
import org.springframework.web.servlet.HandlerInterceptor;

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

/**
 * @author Zonda
 * @version 1.0
 * @description TODO
 * @2024/7/1 23:07
 */
public class loginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取用户session
        HttpSession session = request.getSession();
        //获取session中的用户
        Object user = session.getAttribute("name");
        //判断用户是否存在
        if(user == null){
            //不存在,拦截
            response.setStatus(401);
            return false;
        }
        //存在,将用户信息保存到ThreadLocal
        UserHolder.saveUser((UserDTO) user);
        //放行
        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 org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author Zonda
 * @version 1.0
 * @description TODO
 * @2024/7/1 23:18
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //registry是拦截器的注册器,在这里面配置拦截器
        registry.addInterceptor(new loginInterceptor())
                .excludePathPatterns(
                        //不应该被拦截的一些功能
                        //"/shop/**"指所有shop有关的请求都不用拦截
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );
    }
}

完善/user/me接口,在拦截器中我们已经用UserHolder保存了用户,所以只需要从UserHolder中取并返回即可。

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

关键问题解决

为什么使用拦截器对用户进行校验,并且如果用户已经在Session中,则放行并将用户信息存储到ThreadLocal中,而不是直接使用session中的用户呢?
在这个场景中,使用拦截器对用户进行校验,并且如果用户已经在Session中,则放行并将用户信息存储到ThreadLocal中,这样做的目的可能包括以下几点:

  1. 性能优化:访问ThreadLocal比访问Session要快,因为ThreadLocal存储在当前线程的栈上,而Session可能需要从服务器的内存或分布式缓存中检索。这样可以减少I/O操作,提高系统响应速度。

  2. 减少Session的使用:如果用户信息频繁地被访问,每次访问都从Session中读取会增加不必要的I/O操作。通过将用户信息暂存到ThreadLocal,可以在请求处理过程中避免多次访问Session。

  3. 数据隔离:使用ThreadLocal可以在当前请求的生命周期内保持数据的隔离性,确保不同请求之间不会相互干扰。即使在多线程环境中,每个线程的ThreadLocal是独立的,不会共享数据。

  4. 简化请求处理:在请求处理过程中,如果需要多次使用用户信息,将用户信息存储在ThreadLocal中可以简化代码逻辑,避免在每次需要时都去Session中查询。

  5. 临时数据存储:ThreadLocal适合存储请求过程中的临时数据,这些数据在请求结束后就不再需要了。这样可以避免在Session中存储过多临时数据,保持Session的简洁性。

  6. 安全性:在某些情况下,为了安全考虑,可能不希望用户信息在整个会话期间一直存储在Session中。使用ThreadLocal可以在请求结束后立即清除敏感信息,降低安全风险。

  7. 控制Session的生命周期:通过将用户信息存储在ThreadLocal,可以在请求结束后立即清除,而不需要依赖Session的超时机制或手动清理,这样可以更精确地控制数据的生命周期。

  8. 适应性:在某些复杂的应用场景中,可能需要根据请求的不同阶段来动态地存储或更新用户信息。使用ThreadLocal可以更灵活地控制数据的存储和更新。

集群的Session共享问题

session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务器是导致数据丢失。session的替代方案应该满足:

  • 数据共享
  • 内存存储(内存读写性能高)
  • key-value结构
    那么就可以用Redis替代session!!!
    在这里插入图片描述

基于Redis实现共享session登录

因为用户访问客户端,每次都会获取唯一的session,因此,可以将验证码通过key = “code”,value = 具体验证码字符串的形式存储。但是如果用redis的话,不同用户都访问同一个redis的内存空间,如果再用key = “code”,value = 具体验证码字符串的形式去存的话就会导致不同用户用相同的字符串,会有对应不上的问题。所以我们可以用手机号作为key,实现了唯一性,也利于后期获取验证码验证登录。
用户的保存也可以用redis保存,但是value需要用JSON或者Hash表保存。我们选择用Hash表保存,因为内存占用小,而且修改方便

在这里插入图片描述
在之前的实现中,用于区分用户的是session,每个用户向后端发送请求会携带一个cookie,cookie中的sessionid找到对应的唯一的session,我们从session中可以获取到自己的用户信息。但是如果用redis,没有sessionid这个用户登录凭证了,可以用随机token为key存储用户数据。那么token是保存在后端的,用户在访问的时候也需要携带token去取出value,因此这个token需要被返回给客户端(浏览器)。这样在校验的时候用户携带token访问就可以获取用户信息了。
在这里插入图片描述
修改发送短信验证码的逻辑修改,另外要注意验证码要设置有效时间为两分钟,LOGIN_CODE_KEY是业务前缀,用于区分不同业务:

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

下面要修改user存储的业务,在login方法中,不再是将用户存储到session中,而是以map的形式存储到redis中。key为UUID工具类随机生成的token,value为转为Map形式的UserDTO的对象,这里我们采用BeanUtil.beanToMap()方法进行bean->map的转换。但是!!我们的stringRedisTemplate类要求key和value都是String结构,UserDTO对象中却有一个字段为Long类型,最终我们是以Hash表的形式将对象存入到value中而hash表中的filed和value都不允许是非String,所以会报错:无法将Long类型转化为String类型。所以需要对BeanUtil.beanToMap()中的CopyOptions中的setFieldValueEdito进行重写,具体如下:

Map<String, Object> beanMap = BeanUtil.beanToMap(userDTO,new HashMap<>(), CopyOptions.create().ignoreNullValue()
                .setFieldValueEditor((fieldName,fieldValue)-> fieldValue.toString()));

一定要注意,在Service层中要返回token给前端:return Result.ok(token);!不然访问后端请求被拦截时,没有token会被拦截!

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //手机格式验证,错误则返回错误信息
        if(!RegexUtils.isPhoneInvalid(loginForm.getPhone())){
            return Result.fail("手机号不符合格式");
        }
        //校验码验证
        String code = loginForm.getCode();
        //从redis中获取cacheCode
        Object cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+loginForm.getPhone());
        if(cacheCode == null || !cacheCode.toString().equals(code)){
            return Result.fail("验证码错误");
        }
        //校验验证码正确,根据手机号查询用户(这里MyBatisPlus帮我们完成了sql查询)
        String phone = loginForm.getPhone();
        User user = query().eq("phone", phone).one();//这里查一个就是.one(),查多个就是list()
        //如果用户不存在,创建用户并保存到数据库
        if(user == null){
            user = createUserWithPhone(phone);
        }
        //将用户保存到redis中
        //1.随机生成token作为登录令牌
        String token = UUID.randomUUID().toString();
        //2.将user对象转成map格式
        UserDTO userDTO = new UserDTO();
        BeanUtil.copyProperties(user, userDTO);
        Map<String, Object> beanMap = BeanUtil.beanToMap(userDTO,new HashMap<>(), CopyOptions.create().ignoreNullValue()
                .setFieldValueEditor((fieldName,fieldValue)-> fieldValue.toString()));
        //3.将map存入redis中
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,beanMap);
        //4.设置token的有效期
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        //返回token
        return Result.ok(token);
    }

另外还有一个问题就是,我们token在redis中的有效时长不应该只在第一次登录之后计时30分钟,而是应该在每一次访问被拦截的时候都要增加重新设定为30分钟,因此我们的拦截器中preHandle方法中应该加入对于token有效时长的刷新,这里一定一定要记得,在request中获取token的方法应该是:==String token = request.getHeader(“authorization”);==通过token从redis中取出对应的hash表,再通过:UserDTO userdto = BeanUtil.fillBeanWithMap(map, new UserDTO(),方法将其转化为UserDTO的对象,存入ThreadLocal中,最终刷新token的有效时间!stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
具体代码如下:

 @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求头中的token
        String token = request.getHeader("authorization");
        //如果不存在token,返回401状态码
        if(StringUtils.isBlank(token)){
            //不存在,拦截
            response.setStatus(401);
            return false;
        }
        String key = LOGIN_USER_KEY + token;
        //通过token获取用户信息
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(key);
        //判断用户是否存在
        if(map.isEmpty()){
            //不存在,拦截
            response.setStatus(401);
            return false;
        }
        //将hashmap转为UserDto
        UserDTO userdto = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
       //存在,将userdto存入ThreadLocal
        UserHolder.saveUser(userdto);
        //刷新token的有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
        //放行
        return true;
    }

那么既然在拦截器中要从redis中获取到UserDTO对象,就必须要用到StringRedisTemplate类,但是这个loginInterceptor并没有加任何注解,也就是说不在Spring的管辖范围之内,无法通过自动注入的方式定义一个StringRedisTemplate对象,那么就可以通过构造函数的方式传入参数赋值。具体代码如下:

    private StringRedisTemplate stringRedisTemplate;

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

在MvcConfig配置类中需要加入loginInterceptor拦截器对象,此时我们可以在配置类中自动注入一个StringRedisTemplate的对象,然后传到loginInterceptor中,这样就解决了loginInterceptor类自身无法自动注入的问题。相当于配置类在创建拦截器的时候给拦截器传入了一个redis操作对象stringRedisTemplate,具体代码如下:

package com.hmdp.config;

import com.hmdp.utils.loginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * @author Zonda
 * @version 1.0
 * @description TODO
 * @2024/7/1 23:18
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //registry是拦截器的注册器,在这里面配置拦截器
        registry.addInterceptor(new loginInterceptor(stringRedisTemplate))
                .excludePathPatterns(
                        //不应该被拦截的一些功能
                        //"/shop/**"指所有shop有关的请求都不用拦截
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                );

    }
}

最后一个小缺陷的解决

注:在拦截器中放行就是return true;拦截就是return false;
在我们的 loginInterceptor拦截器中配置类一系列不被拦截校验的请求,这些请求不会执行preHandle 方法,因此也不会更新token,如果用户一直访问的是这些请求的内容,那么token就不会更新,可能一段时间之后,token就失效了。
解决这个问题,我们可以在原有请求的基础上再加一个拦截器。因为 loginInterceptor有一些请求不被拦截,那我们新加的这个拦截器就拦截所有请求。我们在新加的这个拦截器中作刷新token有效期的工作。这样所有的请求都会刷新token有效时长
注意,在第一个拦截器中,对于token和token对应value为空的情况都不要拦截,直接放行,交给下一层去处理拦截的逻辑。而loginInterceptor只用处理是否拦截的逻辑就OK。
在这里插入图片描述新增的拦截器命名为RefreshTokenInterceptor,具体代码如下:

package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
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;

/**
 * @author Zonda
 * @version 1.0
 * @description TODO
 * @2024/7/1 23:07
 */
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 {
        //获取请求头中的token
        String token = request.getHeader("authorization");
        if(StringUtils.isBlank(token)){
            //如果为空,放行
            return true;
        }
        String key = LOGIN_USER_KEY + token;
        //通过token获取用户信息
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(key);
        //判断用户是否存在
        if(map.isEmpty()){
            //为空直接放行
            return true;
        }
        //将hashmap转为UserDto
        UserDTO userdto = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
        //存在,将userdto存入ThreadLocal
        UserHolder.saveUser(userdto);
        //刷新token的有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
        //放行
        return true;
    }



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

另一个拦截器就可以简化为只要ThreadLocal中用户为空,就拦截:

package com.hmdp.utils;

import org.springframework.web.servlet.HandlerInterceptor;

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

/**
 * @author Zonda
 * @version 1.0
 * @description TODO
 * @2024/7/1 23:07
 */
public class loginInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断ThreadLocal中是否有用户,没有就拦截
        if(UserHolder.getUser() == null){
            response.setStatus(401);
            return false;
        }
        //有用户则放行
        return true;
    }



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

但是拦截器是有执行的先后顺序的,应该先是RefreshTokenInterceptor,再是loginInterceptor。
两种方式:

  • 默认按照添加顺序执行
  • 在 registry.addInterceptor().order(1);方法后面加order(1),order中的值越小优先级越高,越大优先级越小。
    代码如下:
package com.hmdp.config;

import com.hmdp.utils.RefreshTokenInterceptor;
import com.hmdp.utils.loginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * @author Zonda
 * @version 1.0
 * @description TODO
 * @2024/7/1 23:18
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //registry是拦截器的注册器,在这里面配置拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
        //registry是拦截器的注册器,在这里面配置拦截器
        registry.addInterceptor(new loginInterceptor())
                .excludePathPatterns(
                        //不应该被拦截的一些请求
                        //"/shop/**"指所有shop有关的请求都不用拦截
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
    }
}

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

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

相关文章

【UE5.3】笔记7 控制Pawn移动

使用A、D键控制角色左右移动 打开我们的BP_Player蓝图类&#xff0c;选择事件图表&#xff0c;添加我们的控制事件 右键&#xff0c;搜索A keyboard&#xff0c;选择A,如下图&#xff0c;D也是 添加扭矩力 首先我们要把我们的player上的模拟物理选项打开&#xff0c;这样我们…

Windows系统安装MySQL8.0.38

MySQL 8.0 相对于 MySQL 5.7 来说有几个显著的区别和改进&#xff0c;以下是一些主要的区别&#xff1a; JSON 数据类型和函数改进&#xff1a; MySQL 8.0 引入了更多的 JSON 支持&#xff0c;包括 JSON 数据类型、JSON 函数和操作符。这使得存储和查询 JSON 数据更加方便和高效…

sql语句练习注意点

1、时间可以进行排序&#xff0c;也可以用聚合函数对时间求最大值max&#xff08;时间&#xff09; 例如下面的例子&#xff1a;取最晚入职的人&#xff0c;那就是将入职时间倒序排序&#xff0c;然后limit 1 表&#xff1a; 场景&#xff1a;查找最晚入职员工的所有信息 se…

MySQL-作业1

一、数据库 1、登陆数据库 2、创建数据库zoo 3、修改数据库zoo字符集为gbk 4、选择当前数据库为zoo 5、查看创建数据库zoo信息 6、删除数据库zoo 二、创建表 1、创建一个名称为db_system的数据库 2、在该数据库下创建两张表&#xff0c;具体要求如下&#xff1a; 员工表 user …

贵的智能猫砂盆值得买吗?包含百元、千元的高性价比品牌推荐!

对于养猫的上班族来说&#xff0c;智能猫砂盆真的是越早买越好&#xff0c;普通猫砂盆用这么久下来能把我们这些上班的都累死&#xff0c;每次一回到家就能闻到猫屎的臭味&#xff0c;一看就收获猫砂盆里满满当当的猫屎&#xff0c;在外面要上班&#xff0c;在家里也要给猫上班…

C语言数据类型和变量(三)

目录 1.赋值操作符 1&#xff09;连续赋值 2&#xff09;复合赋值符 2.单目操作符&#xff1a;、- -、&#xff08;正号&#xff09;、- &#xff08;负号&#xff09; 1&#xff09;和-- 2&#xff09; 和 - 3.强制类型转换 4.printf&#xff08;&#xff09;函数 1…

用for语句实现九九乘法表

① #define _CRT_SECURE_NO_WARNINGS #include <stdio.h>int main() {for (int i 1; i < 9; i){for (int j 1; j < i; j){printf("%d*%d%d\t", j, i, i * j);}printf("\n");}return 0; } ② #define _CRT_SECURE_NO_WARNINGS #include &…

美特CRM anotherValue FastJson反序列化RCE漏洞复现

0x01 产品简介 MetaCRM是一款智能平台化CRM软件,通过提升企业管理和协同办公,全面提高企业管理水平和运营效率,帮助企业实现卓越管理。美特软件开创性地在CRM领域中引入用户级产品平台MetaCRM V5/V6,多年来一直在持续地为客户创造价值,大幅提升了用户需求满足度与使用的满意…

前端学习(三)CSS介绍及选择符

##最近在忙期末考试&#xff0c;因此前端笔记的梳理并未及时更新。在学习语言过程中&#xff0c;笔记的梳理对于知识的加深very vital.因此坚持在明天学习新知识前将笔记梳理完整。 主要内容&#xff1a;CSS介绍及选择符 最后更新时间&#xff1a;2024/7/4 目录 内容&#x…

震惊!运气竟能如此放大!运气的惊人作用,你了解吗?

芒格&#xff1a;得到你想要的东西&#xff0c;最保险的办法&#xff0c;就是让自己配得上你想要的那个东西。今天仔细想了想这句话&#xff0c;他其实说的是无数成功人士的心声 —— “我配得上&#xff01;” 美剧《绝命毒师》有个导演叫文斯吉里根&#xff08;Vince Gilliga…

JAVA:文件防重设计指南

1、简述 在现代应用程序中&#xff0c;处理文件上传是一个常见的需求。为了保证文件存储的高效性和一致性&#xff0c;避免重复存储相同的文件是一个重要的优化点。本文将介绍一种基于哈希值的文件防重设计&#xff0c;并详细列出实现步骤。 2、设计原理 文件防重的基本思路…

「Java开发指南」如何用MyEclipse完成Spring Web Flow 2.0搭建?

本教程将引导您完成Spring Web Flow的软件组件生成&#xff0c;这是Spring的一个项目&#xff0c;用于简化Web应用程序的开发。虽然Spring Web Flow与Spring MVC兼容&#xff0c;但Spring Web Flow使用流而不是控制器来实现应用程序的Web层。在本教程中&#xff0c;您将学习如何…

跑腿平台小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;基础数据管理&#xff0c;管理员管理&#xff0c;接单详情管理&#xff0c;跑腿员管理&#xff0c;跑腿任务管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;跑腿任务&#xff0c;接单员&…

前端播放RTSP视频流,使用FLV请求RTSP视频流播放(Vue项目,在Vue中使用插件flv.js请求RTSP视频流播放)

简述&#xff1a;在浏览器中请求 RTSP 视频流并进行播放时&#xff0c;直接使用原生的浏览器 API 是行不通的&#xff0c;因为它们不支持 RTSP 协议。为了解决这个问题&#xff0c;开发者通常会选择使用像 flv.js 这样的库&#xff0c;它专为在浏览器中播放 FLV 和其他流媒体格…

使用SSE实现echarts数据实时更新

区别 SSE 和 WebSocket 原理和实现方式的区别 SSE( Server-Sent Events) SSE 是基于传统的 HTTP 协议实现的&#xff0c;采用了长轮询&#xff08;long-polling&#xff09;机制。客户端通过向服务器发送一个 HTTP 请求&#xff0c;服务器保持连接打开并周期性地向客户端发送…

使用React复刻ThreeJS官网示例——keyframes动画

最近在看three.js相关的东西&#xff0c;想着学习一下threejs给的examples。源码是用html结合js写的&#xff0c;恰好最近也在学习react&#xff0c;就用react框架学习一下。 本文参考的是threeJs给的第一个示例 three.js examples (threejs.org) 一、下载threeJS源码 通常我们…

【SpringCloud】Hystrix源码解析

hystrix是一个微服务容错组件&#xff0c;提供了资源隔离、服务降级、服务熔断的功能。这一章重点分析hystrix的实现原理 1、服务降级 当服务实例所在服务器承受的压力过大或者受到网络因素影响没法及时响应请求时&#xff0c;请求会阻塞堆积&#xff0c;情况严重的话整个系统…

PyCharm中如何将某个文件设置为默认运行文件

之前在使用JetBrain公司的另一款软件IDEA的时候&#xff0c;如果在选中static main函数后按键altenter可以默认以后运行Main类的main函数。最近在使用PyCharm学习Python&#xff0c;既然同为一家公司的产品而且二者的风格如此之像&#xff0c;所以我怀疑PyCharm中肯定也有类似的…

【Windows】Bootstrap Studio(网页设计)软件介绍及安装步骤

软件介绍 Bootstrap Studio 是一款专为前端开发者设计的强大工具&#xff0c;主要用于快速创建现代化的响应式网页和网站。以下是它的主要特点和功能&#xff1a; 直观的界面设计 Bootstrap Studio 提供了直观的用户界面&#xff0c;使用户能够轻松拖放元素来构建网页。界面…

2024年前端面试题及答案

7、 nginx代理跨域 8、 nodejs中间件代理跨域 9、 WebSocket协议跨域 前端数据加密问题 1 一般如何处理用户敏感信息&#xff1f; 前端一般使用md5、base64加密、sha1加密&#xff0c;想要了解详情请自行百度。 前端http相关问题 1 HTTP常用状态码及其含义&#xff1f; …