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

项目整体介绍

项目功能介绍

在这里插入图片描述

项目结构

在这里插入图片描述
该项目前后端分离架构模式,后端部署在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.mfbz.cn/a/771957.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

实现各平台确定性的物理碰撞

1.使用FixedUpdate而不是Update 1.物理运算&#xff0c;比如刚体运动系统的运算是通过固定的时间来驱动的。 2.再moba帧同步游戏中&#xff0c;15帧的固定调用差不多是网络那边的极限了&#xff0c;采用其他手段如平滑显示来提高画面的平滑度。 FixedUpdate是以一个固定的帧率…

Linux Shell 脚本入门教程:开启你的自动化之旅

目录 一、什么是Shell&#xff1f; 二、 编写第一个Shell脚本 ​编辑 2.2 变量 2.3 功能语句 2.4 数组 一、什么是Shell&#xff1f; Shell是一种计算机程序&#xff0c;它充当了用户与操作系统之间的接口。在Linux系统中&#xff0c;Shell允许用户通过命令行界面&#x…

Windows下cmd中cd命令不起作用的原因和解决办法

1. 问题 即便是输入了cd指令&#xff0c;但是并没有跳转。 2. 原因 实际上cmd换目录跨磁盘的话需要先进行磁盘的转换。 3. 解决办法 先执行 D:

闲鱼商品搜索关键词优化攻略

一、闲鱼商品详情关键词搜索概述 闲鱼作为国内最大的二手交易平台之一&#xff0c;其商品搜索功能对于买家和卖家来说至关重要。商品详情页中的关键词搜索功能&#xff0c;可以帮助买家更快速地找到心仪的商品&#xff0c;也可以帮助卖家提高商品的曝光度&#xff0c;从而促进…

工厂自动化相关设备工业一体机起到什么作用?

在当今的制造业领域&#xff0c;工厂自动化已成为提高生产效率、保证产品质量和降低成本的关键。在这一进程中&#xff0c;工业一体机作为一种重要的设备&#xff0c;发挥着不可或缺的作用。 工业一体机是自动化生产线上的控制中心。它能够整合和处理来自各个传感器、执行器和其…

【3分钟准备前端面试】vue3

目录 Vue3比vue2有什么优势vue3升级了哪些重要功能生命周期变化Options APIComposition APIreftoRef和toRefstoReftoRefsHooks (代码复用)Vue3 script setupsetupdefineProps和defineEmitsdefineExposeVue3比vue2有什么优势 性能更好体积更小更好的TS支持更好的代码组织更好的逻…

【开发笔记】如何用正则匹配出百度云盘分享链接的提取码和链接?

用Wordpress做下载站&#xff0c;需要复制网盘链接到后台的文章发布自定义字段&#xff0c;然后我不想每次手动拆分链接和提取码分别到两个input&#xff0c;就想在后台粘帖时候实现拆分它。 $link 链接&#xff1a;https://pan.baidu.com/s/16y9Z5mTSE6gewStGDNndNQ 提取码…

论文解读——如何生成高分辨率图像PGGAN

论文&#xff1a;Progressive Growing of GANs for Improved Quality, Stability, and Variation&#xff08;2017.10&#xff09; 作者&#xff1a;Tero Karras, Timo Aila, Samuli Laine, Jaakko Lehtinen 链接&#xff1a;https://arxiv.org/abs/1710.10196 代码&#xff1a…

将多个SQL查询合并的两种方式

说明&#xff1a;单个简单查询是非常容易的&#xff0c;但是为了避免多次访问访问数据库&#xff0c;我们会尽可能通过表关联将业务所需要的字段值一次性查出来。而有时候不太清楚表之间的关联关系&#xff08;这取决于对业务的熟悉程度&#xff09;&#xff0c;或者实际情况就…

【SSL 1823】消灭怪物(非传统BFS)

题目大意 小b现在玩一个极其无聊的游戏&#xff0c;它控制角色从基地出发&#xff0c;一路狂奔夺走了对方的水晶&#xff0c;可是正准备回城时&#xff0c;发现地图上已经生成了 n n n 个怪。 现在假设地图是二维平面&#xff0c;所有的怪和角色都认为是在这个二维平面的点上…

甲骨文首次将LLMs引入数据库,集成Llama 3和Mistral,和数据库高效对话

信息时代&#xff0c;数据为王。数据库作为数据存储&管理的一种方式&#xff0c;正在以势不可挡的趋势与AI结合。 前有OpenAI 收购了数据库初创公司 Rockset&#xff0c;引发广泛关注&#xff1b;Oracle公司&#xff08;甲骨文&#xff09;作为全球最大的信息管理软件及服…

基于 Windows Server 2019 部署域控服务器

文章目录 前言1. 域控服务器设计规划2. 安装部署域控服务器2.1. 添加 Active Directory 域服务2.2. 将服务器提升为域控制器2.3. 检查域控服务器配置信息 3. 管理域账号3.1. 新建域管理员账号3.2. 新建普通域账号 4. 服务器加域和退域4.1. 服务器加域操作4.2. 服务器退域操作 总…

谷歌地图 | 路线优化 API 助力企业解锁物流新潜能

在当今竞争激烈的市场环境中&#xff0c;企业面临着越来越大的压力&#xff0c;需要提高运营效率、降低成本并满足不断增长的客户期望。对于依赖车队进行交付或服务的企业来说&#xff0c;这些挑战尤为艰巨。 近日&#xff0c; Google 地图平台路线优化 API 已经正式上线。路线…

推荐 2个功能强大的黑科技工具,真的会让你直呼卧槽

Waifu2X Waifu2x 是一个基于深度学习的开源项目&#xff0c;主要用于处理二次元动漫风格的图像。它使用卷积神经网络&#xff08;CNN&#xff09;进行超分辨率处理和降噪&#xff0c;能够将图像放大2倍或更多&#xff0c;同时显著提高清晰度和减少噪声。Waifu2x 特别针对日系漫…

React 中如何使用 Monaco

Monaco 是微软开源的一个编辑器&#xff0c;VSCode 也是基于 Monaco 进行开发的。如果在 React 中如何使用 Monaco&#xff0c;本文将介绍如何在 React 中引入 Monaco。 安装 React 依赖 yarn add react-app-rewired --dev yarn add monaco-editor-webpack-plugin --dev yarn…

海外短剧CPS推广分佣系统平台讲解,他和短剧播放平台有啥区别?

首先来讲讲什么是海外短剧系统&#xff1f;什么是海外短剧cps系统&#xff1f;这俩有何区别&#xff1f; 海外短剧系统 顾名思义&#xff1a;就是做一套海外短剧系统&#xff0c;把剧放在自己的系统内&#xff0c;让用户来充值&#xff0c;充值的钱全部都是我自己的&#xff…

广州自闭症机构哪家好?

在广州&#xff0c;众多的自闭症康复机构中&#xff0c;星贝育园自闭症儿童康复学校以其独特的优势脱颖而出。 一、专业的师资团队 我们拥有一支经验丰富、专业素养极高的师资队伍。每位老师都经过严格的专业培训&#xff0c;深入了解自闭症儿童的特点和需求。他们不仅具…

数字化工厂EasyCVR视频监控智能解决方案:引领工业4.0时代新趋势

随着工业4.0的深入发展和数字化转型的浪潮&#xff0c;数字化工厂视频监控智能解决方案成为了现代工业生产中不可或缺的一部分。这一解决方案集成了先进的视频监控技术、人工智能&#xff08;AI&#xff09;和大数据分析&#xff0c;为工厂提供了更高效、更安全、更智能的监控和…

css持续学习

一、样式层叠 当一个css样式发生冲突时&#xff0c;比如多处给一个字体设置了不同的颜色&#xff0c;这个时候就需要样式层叠了&#xff0c;它会进行三种比较 比较重要性 重要性从高到低&#xff1a; 1.带有 important 的作者样式&#xff08;作者样式就是开发者写的样式&…

内网穿透--利用everything实现目录映射

免责声明:本文仅做技术交流与学习... 目录 来源文章 frp下载网址 为了隐藏: 演示: 1-靶机的everything开启http服务 2-Linux服务器: 3-靶机windows: 4-最后访问: 来源文章 渗透测试技巧|Everything的利用 frp下载网址 Release v0.58.1 fatedier/frp GitHub 为了隐…