跳转至

后端

数据库设计

user表

  1. id int 主键
  2. phone varchar(20)
  3. username varchar(20)
  4. createdDate 创建时间
  5. isDeleted int 删除位
create table user
(
    id          int auto_increment
        primary key,
    username    varchar(20)                         null,
    phone       varchar(20)                         not null,
    isDeleted   int       default 0                 not null,
    createdTime timestamp default CURRENT_TIMESTAMP null
);

创建项目

使用IDEA创建项目

添加依赖项

image-20240113194952959

数据库配置

配置MySQL数据源

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/RedisSessionDemo?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root

导入MybatisPlus坐标(⭐️:注意需要导boot-starter的版本)

<!-- Mybatis Plus -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.2</version>
</dependency>

使用MybatisX生成代码

给UserMapper加@Mapper注解(⭐️:包一定要选对,不能选父文件夹)

@SpringBootApplication
@MapperScan("cn.wmhwiki.redissessiondemo.mapper")
public class RedisSessionDemoApplication {...}

配置逻辑删除

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: isDeleted # 全局逻辑删除的实体字段名
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

关闭驼峰转换下划线

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: false

配置Redis

spring:
  data:
    redis:
      host: localhost
      password: 123.com
      port: 12345
      database: 0
      timeout: 100000
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          max-wait: 10
          min-idle: 0

通用返回对象

定义通用返回对象

@Data
public class BaseResponse<T> implements Serializable {
    private int code;
    private T data;
    private String msg;
}

通用返回对象工具类

public class ResponseUtils {
    public static <T> BaseResponse<T> ok(T data) {
        BaseResponse<T> response = new BaseResponse<>();
        response.setCode(200);
        response.setData(data);
        response.setMsg("OK");
        return response;
    }

    public static <T> BaseResponse<T> error(String msg) {
        BaseResponse<T> response = new BaseResponse<>();
        response.setCode(500);
        response.setMsg(msg);
        return response;
    }
}

全局异常处理类

定义业务异常类

@Data
@AllArgsConstructor
public class BusinessException extends RuntimeException {
    private int code;
    private String description;
}

编写全局异常处理器

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public BaseResponse handleBusinessException(BusinessException e) {
        log.error("BusinessException: {}", e.getDescription());
        return ResponseUtils.error(e.getDescription());
    }

    @ExceptionHandler(RuntimeException.class)
    public BaseResponse handleRuntimeException(RuntimeException e) {
        log.error("RuntimeException: {}", e.getMessage());
        return ResponseUtils.error(e.getMessage());
    }
}

获取验证码接口 /code

思路:

graph LR
    1([begin])
    2([end])
    a[提交手机号]
    b{手机号合法校验}
    e[保存到Redis]
    f[发送验证码]

    1 --> a
    a --> b
    b -->|"合法"| e --> f --> 2
    b -->|"不合法"| 1

使用 Hutools工具类 生成随机验证码

String code = RandomUtil.randomNumbers(6);

将验证码保存到Redis

redisTemplate.opsForValue().set(
    REDIS_CODE_PRE + phone, code, 2, TimeUnit.MINUTES
);

登录接口 /login

思路

graph
    1([begin])
    2([end])
    a[提交手机号和验证码]
    b{手机号合法校验}
    e[在Redis上通过手机号获取验证码]
    f{验证码校验}
    h[生成Token]
    c{判断用户是否存在}
    i[删除Redis上的验证码]
    g[将用户信息保存到Redis]
    k[创建新用户]
    d[返回Token]

    1 --> a
    a --> b
    b -->|"合法"| e
    b -->|"不合法"| 1
    e --> f
    f -->|"一致"| i
    f -->|"不一致"| 1
    i --> c
    c -->|存在| h
    c -->|不存在| k
    k --> 保存到数据库 --> h
    h --> g
    g --> d --> 2

配置拦截器

思路

graph LR
    1([请求])
    2([Controller])

    1 --> Token刷新拦截器 --> 登录拦截器 --> 2

拦截器配置类

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private StringRedisTemplate template;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TokenInterceptor(template)).addPathPatterns(
            "/**"
        ).order(0);
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns(
            "/user"
        ).order(2);
    }
}

Token刷新拦截器

拦截所有请求

用于刷新Token有效期,并将用户信息保存到ThreadLocal。之后使用ThreadLocal获取用户信息。

graph LR
    1([请求])
    2([Controller])

    a[提交Token]
    b[在Redis中查询用户]

    1 --> a --> b
    b -->|不存在| 2
    b -->|存在| 查询用户信息 --> 保存到ThreadLocal --> Token续期 --> 2

在请求头上获取Token

String token = request.getHeader("authorization"));

使用ThreadLocal保存用户信息

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<UserDTO>();

    public static void saveUser(UserDTO user) {
        tl.set(user);
    }

    public static UserDTO getUser() {
        return tl.get();
    }

    public static void removeUser() {
        tl.remove();
    }
}

登录拦截器

拦截需要登录才能访问的接口(/user 个人信息)

用于判断是否登录,通过ThreadLocal判空操作获取登录信息。

graph LR
    1([请求])
    2([Controller])
    3(["❎ 401"])
    a[从ThreadLoacl中获取用户信息]
    1 -->a -->|存在| 2
    a -->|不存在| 3
@Data
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (UserHolder.getUser() == null) {
            response.setStatus(401);
            return false;
        }
        return true;
    }
}

配置允许跨域

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600)
                .exposedHeaders("Authorization");
    }

    @Bean
    public FilterRegistrationBean<CorsFilter> corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOriginPattern("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);
        source.registerCorsConfiguration("/**", config);
        FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}