短链接-后管用户模块

SoruxGPT
发布于 2024-09-05 / 7 阅读
0

短链接-后管用户模块

用户模块

功能分析

  • 检查用户名是否存在

  • 注册用户

  • 修改用户

  • 根据用户名查询用户

  • 用户登陆

  • 检查用户是否登陆

  • 用户推出登陆

  • 注销用户

CREATE TABLE `t_user` (
    `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `username` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名',
    `password` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '密码',
    `real_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '真实姓名',
    `phone` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '手机号码',
    `mail` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '邮箱',
    `deletion_time` bigint(20) DEFAULT NULL COMMENT '删除时间戳',
    `create_time` datetime DEFAULT NULL COMMENT '创建时间',
    `update_time` datetime DEFAULT NULL COMMENT '修改时间',
    `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0: 未删除 1: 已删除',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

导入maven坐标

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
​
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
​
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>
​
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
​
    </dependencies>

持久层配置文件

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/link?characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      connection-test-query: select 1
      connection-timeout: 20000
      idle-timeout: 300000
      maximum-pool-size: 5
      minimum-idle: 5

构造器注入方式

package com.hayaizo.shortlink.admin.controller;
​
import com.hayaizo.shortlink.admin.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
​
/**
 * 用户管理控制层
 */
@RestController
@RequiredArgsConstructor
public class UserController {
​
    private final UserService userService;
​
    /**
     * 根据用户名查询用户信息
     */
    @GetMapping("/api/shortlink/v1/user/{username}")
    public String getUserByUsername(@PathVariable("username") String username) {
        return "hello "+username;
    }
  
    public UserController(UserService userService) {
      this.userService = userService;
    }
}

@RequiredArgsConstructor给所有final字段添加了一个构造函数,比如在UserController中,加上注解就会在UserController中自动创建一个UserServicve的构造函数。

等效于:

package com.hayaizo.shortlink.admin.controller;
​
import com.hayaizo.shortlink.admin.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
​
/**
 * 用户管理控制层
 */
@RestController
public class UserController {
​
    private final UserService userService;
  
    public UserController(UserService userService) {
      this.userService = userService;
    }
}

Spring 会自动将 UserService 类型的 Bean 注入到这个构造器中,进而赋值给 userService 字段。

构造器注入的优点

  • 不可变性:使用构造器注入时,依赖通常是 final 的,这意味着它们在对象创建后不能再被修改,从而保证了对象的不可变性。

  • 确保依赖完整性:通过构造器注入,依赖在对象创建时就已经完全注入,这意味着你总是拥有一个完全初始化的对象,不会出现依赖未被注入的情况。

  • 容易进行单元测试:构造器注入的类可以很容易地在测试中创建,并且在测试时传入所需的 mock 对象,便于测试隔离。

动态导包

全局返回对象Result

package com.hayaizo.shortlink.admin.common.convention;
​
import lombok.Data;
import lombok.experimental.Accessors;
​
import java.io.Serializable;
​
/**
 * 全局返回对象
 */
@Data
@Accessors(chain = true)
public class Result<T> implements Serializable {
    
    private static final long serialVersionUID = 5679018624309023727L;
​
    /**
     * 正确返回码
     */
    public static final String SUCCESS_CODE = "0";
​
    /**
     * 返回码
     */
    private String code;
​
    /**
     * 返回消息
     */
    private String message;
​
    /**
     * 响应数据
     */
    private T data;
​
    /**
     * 请求ID
     */
    private String requestId;
​
    public boolean isSuccess() {
        return SUCCESS_CODE.equals(code);
    }
}
import com.nageoffer.shortlink.admin.common.convention.errorcode.BaseErrorCode;
import com.nageoffer.shortlink.admin.common.convention.exception.AbstractException;
​
import java.util.Optional;
​
/**
 * 全局返回对象构造器
 */
public final class Results {
​
    /**
     * 构造成功响应
     */
    public static Result<Void> success() {
        return new Result<Void>()
                .setCode(Result.SUCCESS_CODE);
    }
​
    /**
     * 构造带返回数据的成功响应
     */
    public static <T> Result<T> success(T data) {
        return new Result<T>()
                .setCode(Result.SUCCESS_CODE)
                .setData(data);
    }
​
    /**
     * 构建服务端失败响应
     */
    public static Result<Void> failure() {
        return new Result<Void>()
                .setCode(BaseErrorCode.SERVICE_ERROR.code())
                .setMessage(BaseErrorCode.SERVICE_ERROR.message());
    }
​
    /**
     * 通过 {@link AbstractException} 构建失败响应
     */
    public static Result<Void> failure(AbstractException abstractException) {
        String errorCode = Optional.ofNullable(abstractException.getErrorCode())
                .orElse(BaseErrorCode.SERVICE_ERROR.code());
        String errorMessage = Optional.ofNullable(abstractException.getErrorMessage())
                .orElse(BaseErrorCode.SERVICE_ERROR.message());
        return new Result<Void>()
                .setCode(errorCode)
                .setMessage(errorMessage);
    }
​
    /**
     * 通过 errorCode、errorMessage 构建失败响应
     */
    public static Result<Void> failure(String errorCode, String errorMessage) {
        return new Result<Void>()
                .setCode(errorCode)
                .setMessage(errorMessage);
    }
}

异常码设计

IErrorCode

package com.hayaizo.shortlink.admin.common.convention.errorcode;
​
/**
 * 平台错误码
 */
public interface IErrorCode {
​
    /**
     * 错误码
     */
    String code();
​
    /**
     * 错误信息
     */
    String message();
}

BaseErrorCode

package com.hayaizo.shortlink.admin.common.convention.errorcode;
​
/**
 * 基础错误码定义
 */
public enum BaseErrorCode implements IErrorCode {
​
    // ========== 一级宏观错误码 客户端错误 ==========
    CLIENT_ERROR("A000001", "用户端错误"),
​
    // ========== 二级宏观错误码 用户注册错误 ==========
    USER_REGISTER_ERROR("A000100", "用户注册错误"),
    USER_NAME_VERIFY_ERROR("A000110", "用户名校验失败"),
    USER_NAME_EXIST_ERROR("A000111", "用户名已存在"),
    USER_NAME_SENSITIVE_ERROR("A000112", "用户名包含敏感词"),
    USER_NAME_SPECIAL_CHARACTER_ERROR("A000113", "用户名包含特殊字符"),
    PASSWORD_VERIFY_ERROR("A000120", "密码校验失败"),
    PASSWORD_SHORT_ERROR("A000121", "密码长度不够"),
    PHONE_VERIFY_ERROR("A000151", "手机格式校验失败"),
​
    // ========== 二级宏观错误码 系统请求缺少幂等Token ==========
    IDEMPOTENT_TOKEN_NULL_ERROR("A000200", "幂等Token为空"),
    IDEMPOTENT_TOKEN_DELETE_ERROR("A000201", "幂等Token已被使用或失效"),
​
    // ========== 一级宏观错误码 系统执行出错 ==========
    SERVICE_ERROR("B000001", "系统执行出错"),
    // ========== 二级宏观错误码 系统执行超时 ==========
    SERVICE_TIMEOUT_ERROR("B000100", "系统执行超时"),
​
    // ========== 一级宏观错误码 调用第三方服务出错 ==========
    REMOTE_ERROR("C000001", "调用第三方服务出错");
​
    private final String code;
​
    private final String message;
​
    BaseErrorCode(String code, String message) {
        this.code = code;
        this.message = message;
    }
​
    @Override
    public String code() {
        return code;
    }
​
    @Override
    public String message() {
        return message;
    }
}

UserErrorCodeEnum

package com.hayaizo.shortlink.admin.common.enums;
​
import com.hayaizo.shortlink.admin.common.convention.errorcode.IErrorCode;
​
public enum UserErrorCodeEnum implements IErrorCode {
​
    USER_NULL("B00200","用户记录不存在"),
    USER_EXIT("B00201","用户记录已存在");
​
    private final String code;
​
    private final String message;
​
    UserErrorCodeEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }
​
    @Override
    public String code() {
        return code;
    }
​
    @Override
    public String message() {
        return message;
    }
}

全局异常拦截器

AbstractException

package com.hayaizo.shortlink.admin.common.convention.exception;
​
import com.hayaizo.shortlink.admin.common.convention.errorcode.IErrorCode;
import lombok.Getter;
import org.springframework.util.StringUtils;
​
import java.util.Optional;
​
/**
 * 抽象项目中三类异常体系,客户端异常、服务端异常以及远程服务调用异常
 *
 * @see ClientException
 * @see ServiceException
 * @see RemoteException
 */
@Getter
public abstract class AbstractException extends RuntimeException {
​
    public final String errorCode;
​
    public final String errorMessage;
​
    public AbstractException(String message, Throwable throwable, IErrorCode errorCode) {
        super(message, throwable);
        this.errorCode = errorCode.code();
        this.errorMessage = Optional.ofNullable(StringUtils.hasLength(message) ? message : null).orElse(errorCode.message());
    }
}

ClientException

package com.hayaizo.shortlink.admin.common.convention.exception;
​
import com.hayaizo.shortlink.admin.common.convention.errorcode.BaseErrorCode;
import com.hayaizo.shortlink.admin.common.convention.errorcode.IErrorCode;
​
/**
 * 客户端异常
 */
public class ClientException extends AbstractException {
​
    public ClientException(IErrorCode errorCode) {
        this(null, null, errorCode);
    }
​
    public ClientException(String message) {
        this(message, null, BaseErrorCode.CLIENT_ERROR);
    }
​
    public ClientException(String message, IErrorCode errorCode) {
        this(message, null, errorCode);
    }
​
    public ClientException(String message, Throwable throwable, IErrorCode errorCode) {
        super(message, throwable, errorCode);
    }
​
    @Override
    public String toString() {
        return "ClientException{" +
                "code='" + errorCode + "'," +
                "message='" + errorMessage + "'" +
                '}';
    }
}

RemoteException

package com.hayaizo.shortlink.admin.common.convention.exception;
​
import com.hayaizo.shortlink.admin.common.convention.errorcode.BaseErrorCode;
import com.hayaizo.shortlink.admin.common.convention.errorcode.IErrorCode;
​
/**
 * 远程服务调用异常
 */
public class RemoteException extends AbstractException {
​
    public RemoteException(String message) {
        this(message, null, BaseErrorCode.REMOTE_ERROR);
    }
​
    public RemoteException(String message, IErrorCode errorCode) {
        this(message, null, errorCode);
    }
​
    public RemoteException(String message, Throwable throwable, IErrorCode errorCode) {
        super(message, throwable, errorCode);
    }
​
    @Override
    public String toString() {
        return "RemoteException{" +
                "code='" + errorCode + "'," +
                "message='" + errorMessage + "'" +
                '}';
    }
}

ServiceException

package com.hayaizo.shortlink.admin.common.convention.exception;
​
import com.hayaizo.shortlink.admin.common.convention.errorcode.BaseErrorCode;
import com.hayaizo.shortlink.admin.common.convention.errorcode.IErrorCode;
​
import java.util.Optional;
​
/**
 * 服务端异常
 */
public class ServiceException extends AbstractException {
​
    public ServiceException(String message) {
        this(message, null, BaseErrorCode.SERVICE_ERROR);
    }
​
    public ServiceException(IErrorCode errorCode) {
        this(null, errorCode);
    }
​
    public ServiceException(String message, IErrorCode errorCode) {
        this(message, null, errorCode);
    }
​
    public ServiceException(String message, Throwable throwable, IErrorCode errorCode) {
        super(Optional.ofNullable(message).orElse(errorCode.message()), throwable, errorCode);
    }
​
    @Override
    public String toString() {
        return "ServiceException{" +
                "code='" + errorCode + "'," +
                "message='" + errorMessage + "'" +
                '}';
    }
}

拦截器

package com.hayaizo.shortlink.admin.common.web;
​
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.hayaizo.shortlink.admin.common.convention.Result;
import com.hayaizo.shortlink.admin.common.convention.Results;
import com.hayaizo.shortlink.admin.common.convention.errorcode.BaseErrorCode;
import com.hayaizo.shortlink.admin.common.convention.exception.AbstractException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
​
import java.util.Optional;
​
/**
 * 全局异常处理器
 *
 */
@Component
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
​
    /**
     * 拦截参数验证异常
     */
    @SneakyThrows
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result validExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors());
        String exceptionStr = Optional.ofNullable(firstFieldError)
                .map(FieldError::getDefaultMessage)
                .orElse(StrUtil.EMPTY);
        log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionStr);
        return Results.failure(BaseErrorCode.CLIENT_ERROR.code(), exceptionStr);
    }
​
    /**
     * 拦截应用内抛出的异常
     */
    @ExceptionHandler(value = {AbstractException.class})
    public Result abstractException(HttpServletRequest request, AbstractException ex) {
        if (ex.getCause() != null) {
            log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex.toString(), ex.getCause());
            return Results.failure(ex);
        }
        log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex.toString());
        return Results.failure(ex);
    }
​
    /**
     * 拦截未捕获异常
     */
    @ExceptionHandler(value = Throwable.class)
    public Result defaultErrorHandler(HttpServletRequest request, Throwable throwable) {
        log.error("[{}] {} ", request.getMethod(), getUrl(request), throwable);
        return Results.failure();
    }
​
    private String getUrl(HttpServletRequest request) {
        if (StringUtils.isEmpty(request.getQueryString())) {
            return request.getRequestURL().toString();
        }
        return request.getRequestURL().toString() + "?" + request.getQueryString();
    }
}

用户敏感信息展示

利用Jackson序列化来做数据的脱敏

package com.hayaizo.shortlink.admin.common.serialize;
​
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
​
import java.io.IOException;
​
public class EmailDesensitizationSerializer  extends JsonSerializer<String> {
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        if (value != null && value.contains("@")) {
            String desensitized = value.replaceAll("(\\w{2})\\w*(\\w?@.*)", "$1****$2");
            gen.writeString(desensitized);
        } else {
            gen.writeString(value);
        }
    }
}
package com.hayaizo.shortlink.admin.common.serialize;
​
import cn.hutool.core.util.DesensitizedUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
​
import java.io.IOException;
​
/**
 * 手机号脱敏反序列化
 */
public class PhoneDesensitizationSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String phone, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        String phoneDesensitization = DesensitizedUtil.mobilePhone(phone);
        jsonGenerator.writeString(phoneDesensitization);
    }
}

在类的成员上加上注解表示需要脱敏。

package com.hayaizo.shortlink.admin.dto.resp;
​
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.hayaizo.shortlink.admin.common.serialize.EmailDesensitizationSerializer;
import com.hayaizo.shortlink.admin.common.serialize.PhoneDesensitizationSerializer;
import lombok.Data;
​
import java.util.Date;
​
/**
 * 用户返回参数响应
 */
@Data
public class UserRespDTO {
​
    /**
     * 主键id
     */
    private Long id;
​
    /**
     * 用户名
     */
    private String username;
​
    /**
     * 真实姓名
     */
    private String realName;
​
    /**
     * 手机号码
     */
    @JsonSerialize(using  = PhoneDesensitizationSerializer.class)
    private String phone;
​
    /**
     * 邮箱
     */
    @JsonSerialize(using = EmailDesensitizationSerializer.class)
    private String mail;
​
    /**
     * 删除时间戳
     */
    private Long deletionTime;
​
    /**
     * 创建时间
     */
    private Date createTime;
​
    /**
     * 修改时间
     */
    private Date updateTime;
​
    /**
     * 删除标识 0: 未删除 1: 已删除
     */
    private Integer delFlag;
}

检查用户名是否存在

  • 海量用户如果说查询的用户名存在或不存在,全部请求数据库,会将数据库直接打满。

1.用户名加缓存

redis缓存太大了。

2.使用布隆过滤器

  • 误差是可以接受的,如果当前用户名误判了,大不了用户重新换一个名字。

  • 容量越大,误判越低

Redisson依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
​
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
</dependency>

配置Redis参数

spring:
  data:
    redis:
      host: 127.0.0.1
      port: 6379
      password: 123456

创建布隆过滤器实例

package com.hayaizo.shortlink.admin.config;
​
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
/**
 * 布隆过滤器配置
 */
@Configuration
public class RBloomFilterConfiguration {
​
    /**
     * 防止用户注册查询数据库的布隆过滤器
     */
    @Bean
    public RBloomFilter<String> userRegisterCachePenetrationBloomFilter(RedissonClient redissonClient) {
        RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter("xxx");
        cachePenetrationBloomFilter.tryInit(10000000, 0.01);
        return cachePenetrationBloomFilter;
    }
}

ryInit 有两个核心参数:

  • expectedInsertions:预估布隆过滤器存储的元素长度。

  • falseProbability:运行的误判率。

错误率越低,位数组越长,布隆过滤器的内存占用越大。

错误率越低,散列 Hash 函数越多,计算耗时较长。

一个布隆过滤器占用大小的在线网站:Bloom Filter Calculator

使用布隆过滤器的两种场景:

  • 初始使用:注册用户时就向容器中新增数据,就不需要任务向容器存储数据了。

  • 使用过程中引入:读取数据源将目标数据刷到布隆过滤器。

自定义可拓展布隆过滤器

ScalableBloomFilter

package com.hayaizo.shortlink.admin.config;
​
import org.redisson.api.RAtomicLong;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
​
import java.util.ArrayList;
import java.util.List;
​
public class ScalableBloomFilter {
​
    // RedissonClient用于和Redis进行交互
    private final RedissonClient redissonClient;
​
    // 每个布隆过滤器期望插入的元素数量
    private final long expectedInsertions;
​
    // 布隆过滤器的误报率
    private final double falseProbability;
​
    // 负载因子,超过负载因子后需要创建新的布隆过滤器
    private final double loadFactorThreshold;
​
    // 存储布隆过滤器的列表
    private final List<RBloomFilter<String>> bloomFilters;
​
    // 当前存在的布隆过滤器数量
    private RAtomicLong currentFilterCount;
​
    /**
     * 构造函数,用于初始化可扩展的布隆过滤器。
     *
     * @param redissonClient      Redisson客户端,用于与Redis交互。
     * @param expectedInsertions  每个布隆过滤器预期插入的元素数量。
     * @param falseProbability    布隆过滤器的误报率。
     * @param loadFactorThreshold 负载因子阈值,超过该阈值时创建新的布隆过滤器。
     */
    public ScalableBloomFilter(RedissonClient redissonClient, long expectedInsertions, double falseProbability, double loadFactorThreshold) {
        this.redissonClient = redissonClient;
        this.expectedInsertions = expectedInsertions;
        this.falseProbability = falseProbability;
        this.loadFactorThreshold = loadFactorThreshold;
        this.bloomFilters = new ArrayList<>();
​
        // 初始化计数器,从Redis中获取已存在的过滤器数量
        this.currentFilterCount = redissonClient.getAtomicLong("currentFilterCount");
​
        // 批量加载已存在的布隆过滤器
        this.bloomFilters.addAll(batchLoadBloomFilter());
    }
​
    /**
     * 批量加载已有的布隆过滤器。
     * 该方法用于从Redis中加载现有的布隆过滤器,以支持系统重启后的恢复。
     *
     * @return 已加载的布隆过滤器列表
     */
    private List<RBloomFilter<String>> batchLoadBloomFilter() {
        List<RBloomFilter<String>> loadedFilters = new ArrayList<>();
        long filterCount = currentFilterCount.get(); // 从Redis中获取当前过滤器数量
​
        if(filterCount==0){
            loadedFilters.add(createNewBloomFilter());
        }
​
        for (int i = 0; i < filterCount; i++) {
            RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("bloomFilter-" + (i+1));
            if (bloomFilter.isExists()) {
                loadedFilters.add(bloomFilter); // 只加载已存在的布隆过滤器
            }
        }
        return loadedFilters;
    }
​
    /**
     * 创建一个新的布隆过滤器并初始化。
     * 每当现有布隆过滤器满载时,创建新的过滤器。
     *
     * @return 创建的布隆过滤器
     */
    private RBloomFilter<String> createNewBloomFilter() {
        // 为新的布隆过滤器生成唯一名称,基于过滤器数量
        long filterIndex = currentFilterCount.incrementAndGet();
        RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("bloomFilter-" + filterIndex);
        bloomFilter.tryInit(expectedInsertions, falseProbability); // 初始化布隆过滤器
​
        // 为新布隆过滤器创建单独插入计数器
        redissonClient.getAtomicLong("bloomFilterInsertions-" + filterIndex).set(0);
        return bloomFilter;
    }
​
    /**
     * 检查指定的布隆过滤器是否已达到负载因子上限。
     * 当负载因子达到设定值时,布隆过滤器将被认为满载,需要创建新的过滤器。
     *
     * @param bloomFilter 当前布隆过滤器
     * @param filterIndex 布隆过滤器的索引
     * @return 布隆过滤器是否满载
     */
    private boolean isBloomFilterFull(RBloomFilter<String> bloomFilter, long filterIndex) {
        // 获取当前过滤器的插入计数器
        RAtomicLong insertionCount = redissonClient.getAtomicLong("bloomFilterInsertions-" + filterIndex);
        double currentLoadFactor = (double) insertionCount.get() / expectedInsertions; // 计算负载因子
        return currentLoadFactor >= loadFactorThreshold; // 判断是否超过阈值
    }
​
    /**
     * 向布隆过滤器中添加元素。如果当前过滤器已满,则创建新的过滤器。
     *
     * @param value 要添加的元素
     */
    public synchronized void add(String value) {
        // 获取最后一个布隆过滤器
        long lastIndex = currentFilterCount.get();
        RBloomFilter<String> currentBloomFilter = bloomFilters.get(bloomFilters.size()-1);
​
        // 获取当前布隆过滤器的插入计数器
        RAtomicLong insertionCount = redissonClient.getAtomicLong("bloomFilterInsertions-" + lastIndex);
​
        // 检查当前布隆过滤器是否已满载
        if (isBloomFilterFull(currentBloomFilter, lastIndex)) {
            // 如果已满载,则创建新的布隆过滤器并添加到列表中
            currentBloomFilter = createNewBloomFilter();
            bloomFilters.add(currentBloomFilter);
        }
​
        // 如果元素不存在才添加到布隆过滤器中
        if (!currentBloomFilter.contains(value)) {
            currentBloomFilter.add(value);
            insertionCount.incrementAndGet(); // 增加插入计数
        }
    }
​
    /**
     * 检查布隆过滤器是否可能包含指定的元素。
     *
     * @param value 要检查的元素
     * @return 如果元素可能存在,则返回 true;否则返回 false。
     */
    public boolean mightContain(String value) {
        // 遍历所有布隆过滤器
        for (RBloomFilter<String> bloomFilter : bloomFilters) {
            if (bloomFilter.contains(value)) {
                return true; // 如果元素在某个布隆过滤器中,返回 true
            }
        }
        return false; // 如果所有过滤器都不包含,返回 false
    }
}

BloomFilterService

package com.hayaizo.shortlink.admin.config;
​
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
​
@Service
public class BloomFilterService {
    // 逻辑拓展的布隆过滤器
    private final ScalableBloomFilter scalableBloomFilter;
​
    @Autowired
    private RedissonClient redissonClient;
​
    @Autowired
    public BloomFilterService(RedissonClient redissonClient) {
        // 创建可扩展的布隆过滤器,设置期望插入数量、误报率和负载因子阈值
        this.scalableBloomFilter = new ScalableBloomFilter(redissonClient, 1000000L, 0.01, 0.75);
    }
​
    /**
     * 添加元素到布隆过滤器中。
     *
     * @param value 要添加的元素。
     */
    public void addValue(String value) {
        scalableBloomFilter.add(value);
    }
​
    /**
     * 检查元素是否可能存在于布隆过滤器中。
     *
     * @param value 要检查的元素。
     * @return 如果元素可能存在于布隆过滤器中,则返回true;否则返回false。
     */
    public boolean mightContainValue(String value) {
        return scalableBloomFilter.mightContain(value);
    }
}

RedissonClientConfig

package com.hayaizo.shortlink.admin.config;
​
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
@Configuration
public class RedissonClientConfig {
​
    // 从 application.yml 中读取配置
    @Value("${spring.data.redis.host}")
    private String redisHost;
​
    @Value("${spring.data.redis.port}")
    private int redisPort;
​
    @Value("${spring.data.redis.password:}")  // 如果没有设置密码,则使用默认空字符串
    private String redisPassword;
​
    /**
     * 创建并配置 RedissonClient 实例。
     *
     * @return 配置好的 RedissonClient 实例。
     */
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        String redisAddress = String.format("redis://%s:%d", redisHost, redisPort);
​
        config.useSingleServer()
                .setAddress(redisAddress);
​
        // 仅在密码不为空时设置密码
        if (redisPassword != null && !redisPassword.isEmpty()) {
            config.useSingleServer().setPassword(redisPassword);
        }
​
        return Redisson.create(config);
    }
}

用户注册

采用MyBatis-Plus来进行元数据的填充。

package com.hayaizo.shortlink.admin.config;
​
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
​
import java.util.Date;
​
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
​
    // 插入时填充策略
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", Date.class, new Date()); // 插入时填充创建时间
        this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());   // 插入时填充更新时间
        this.strictInsertFill(metaObject,"delFlag", Integer.class, 0);
    }
​
    // 更新时填充策略
    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime",Date.class, new Date());   // 更新时填充更新时间
    }
}

类型写错会导致数据填充失败

package com.hayaizo.shortlink.admin.dao.entity;
​
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
​
import java.io.Serializable;
import java.util.Date;
​
/**
 * @description 用户持久层
 * @author Hayaizo
 * @date 2024-09-02
 */
@Data
@TableName("t_user")
public class UserDO implements Serializable {
​
    private static final long serialVersionUID = 1L;
​
    @TableId(type = IdType.AUTO)
    /**
     * 主键id
     */
    private Long id;
​
    /**
     * 用户名
     */
    private String username;
​
    /**
     * 密码
     */
    private String password;
​
    /**
     * 真实姓名
     */
    private String realName;
​
​
    /**
     * 手机号
     */
    private String phone;
​
    /**
     * 邮箱
     */
    private String mail;
​
    /**
     * 删除时间戳
     */
    private Long deletionTime;
​
    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;
​
    /**
     * 修改时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;
​
    /**
     * 删除标识 0: 未删除 1: 已删除
     */
    @TableField(fill = FieldFill.INSERT)
    private Integer delFlag;
​
    public UserDO() {}
}

如何防止恶意注册

    @Override
    public void registry(UserRegisterReqDTO requestParam) {
        // 判断在布隆过滤器中是否存在
        if(!hasUsername(requestParam.getUsername())){
            throw new ClientException(UserErrorCodeEnum.USER_NAME_EXIT);
        }
​
        RLock lock = redissonClient.getLock(LOCK_USER_REGISTER_KEY + requestParam.getUsername());
        try{
            if(lock.tryLock()){
                int insert = baseMapper.insert(BeanUtil.toBean(requestParam, UserDO.class));
                if(insert < 1){
                    throw new ClientException(UserErrorCodeEnum.USER_SAVE_ERROR);
                }
                bloomFilterService.addValue(requestParam.getUsername());
                return;
            }
            throw new ClientException(UserErrorCodeEnum.USER_EXIT);
        }finally {
            lock.unlock();
        }
    }

如何分库分表

package MySQL;
​
public class User {
​
    public static String sqlTemplate = "CREATE TABLE `t_user_%d` (\n" +
            "  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',\n" +
            "  `username` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '用户名',\n" +
            "  `password` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '密码',\n" +
            "  `real_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '真实姓名',\n" +
            "  `phone` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '手机号码',\n" +
            "  `mail` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '邮箱',\n" +
            "  `deletion_time` bigint DEFAULT NULL COMMENT '删除时间戳',\n" +
            "  `create_time` datetime DEFAULT NULL COMMENT '创建时间',\n" +
            "  `update_time` datetime DEFAULT NULL COMMENT '修改时间',\n" +
            "  `del_flag` tinyint(1) DEFAULT NULL COMMENT '删除标识 0: 未删除 1: 已删除',\n" +
            "  PRIMARY KEY (`id`),\n" +
            "  UNIQUE KEY `unique_username` (`username`) USING BTREE\n" +
            ") ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;";
​
    public static void main(String[] args) {
        for (int i = 0; i < 16; i++) {
            String sql = String.format(sqlTemplate, i);  // 替换表名中的编号
            System.out.println(sql);  // 输出每个表的创建语句
        }
    }
}

分片键

用于将数据库(表)水平拆分的数据库字段。

分库分表中的分片键(Sharding Key)是一个关键决策,它直接影响了分库分表的性能和可扩展性。以下是一些选择分片键的关键因素:

  1. 访问频率:选择分片键应考虑数据的访问频率。将经常访问的数据放在同一个分片上,可以提高查询性能和降低跨分片查询的开销。

  2. 数据均匀性:分片键应该保证数据的均匀分布在各个分片上,避免出现热点数据集中在某个分片上的情况。

  3. 数据不可变:一旦选择了分片键,它应该是不可变的,不能随着业务的变化而频繁修改。

用户名和用户ID选哪个作为分片键?

  • 用户名,用户名可以登录。

引入依赖

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core</artifactId>
    <version>5.3.2</version>
</dependency> 

分片规则

spring:
  datasource:
    # ShardingSphere 对 Driver 自定义,实现分库分表等隐藏逻辑
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver
    # ShardingSphere 配置文件路径
    url: jdbc:shardingsphere:classpath:shardingsphere-config.yaml

shardingsphere-config.yaml

# 数据源集合
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root
​
rules:
  - !SHARDING
    tables:
      t_user:
        # 真实数据节点,比如数据库源以及数据库在数据库中真实存在的
        actualDataNodes: ds_0.t_user_${0..15}
        # 分表策略
        tableStrategy:
          # 用于单分片键的标准分片场景
          standard:
            # 分片键
            shardingColumn: username
            # 分片算法,对应 rules[0].shardingAlgorithms
            shardingAlgorithmName: user_table_hash_mod
    # 分片算法
    shardingAlgorithms:
      # 数据表分片算法
      user_table_hash_mod:
        # 根据分片键 Hash 分片
        type: HASH_MOD
        # 分片数量
        props:
          sharding-count: 16
# 展现逻辑 SQL & 真实 SQL
props:
  sql-show: true

用户敏感信息实现加密存储

加密配置

# 配置数据源,底层被 ShardingSphere 进行了代理
dataSources:
  ds_0:
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driverClassName: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://127.0.0.1:3306/link?useUnicode=true&characterEncoding=UTF-8&rewriteBatchedStatements=true&allowMultiQueries=true&serverTimezone=Asia/Shanghai
    username: root
    password: root
​
rules:
# 数据加密存储规则
  - !ENCRYPT
    # 需要加密的表集合
    tables:
      # 用户表
      t_user:
        # 用户表中哪些字段需要进行加密
        columns:
          # 手机号字段,逻辑字段,不一定是在数据库中真实存在
          phone:
            # 手机号字段存储的密文字段,这个是数据库中真实存在的字段
            cipherColumn: phone
            # 身份证字段加密算法
            encryptorName: common_encryptor
          mail:
            cipherColumn: mail
            encryptorName: common_encryptor
        # 是否按照密文字段查询
        queryWithCipherColumn: true
    # 加密算法
    encryptors:
      # 自定义加密算法名称
      common_encryptor:
        # 加密算法类型
        type: AES
        props:
          # AES 加密密钥
          aes-key-value: d6oadClrrb9A3GWo
props:
  sql-show: true

双Token无感刷新

当用户进行Login登陆的时候,服务器会先判断用户有没有AccessToken以及AccessToken是否已经过期了,如果存在AccessToken的话,就抛异常(用户已存在),如果不存在,就去生成AccessToken和RefreshToken,RefreshToken的有效时长会比AccessToken长的多,如果AccessToken过期了,那么就通过username去redis获取到对应的RefreshToken,如果说RefreshToken还没有过期的话,那么久可以利用RefreshToken来对token进行刷新,AccessToken和RefreshToken都需要刷新,最后把AccessToken传递给前端。

为什么需要双Token?为什么不直接设置一个很长的Token,每次登陆就刷新一次有效期呢?

一个Token用的越久,那么被盗用的风险就越大,如果使用AccessToken的话,就算被盗用了,也只能临时上线一会,马上就会被RefreshToken给刷新了

RefreshToken是否会被盗用?

不会,因为RefreshToken不会返回给用户,用户只能拿到AccessToken,RefreshToken被服务端管理。

代码:

    @PostMapping("/api/short-link/v1/user/login")
    public Result<UserLoginRespDTO> login(@RequestBody UserLoginReqDTO requestParam) {
        UserLoginRespDTO respDTO = userService.login(requestParam);
        return Results.success(respDTO);
    }
​
    @PostMapping("/api/short-link/v1/user/refresh")
    public Result<UserLoginRespDTO> refresh(HttpServletRequest request) {
        String accessToken = request.getHeader("token"); // 提取请求头中的AccessToken
        String username = request.getHeader("username"); // 提取请求头中的username
        UserLoginRespDTO respDTO = userService.refreshAccessToken(accessToken, username);
        return Results.success(respDTO);
    }
@Override
public UserLoginRespDTO login(UserLoginReqDTO requestParam) {
​
    LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
            .eq(UserDO::getUsername, requestParam.getUsername())
            .eq(UserDO::getPassword, requestParam.getPassword())
            .eq(UserDO::getDelFlag, 0);
    UserDO userDO = baseMapper.selectOne(queryWrapper);
    if(userDO==null){
        throw new ClientException("用户登陆失败");
    }
​
    // 用户验证逻辑 (例如查询数据库验证用户名和密码)
    String username = requestParam.getUsername();
    String storeAccessToken = redisTokenStore.getAccessToken(username);
    if(storeAccessToken!=null&&!jwtTokenUtil.isTokenExpired(storeAccessToken)){
        throw new ClientException("用户已登陆");
    }
​
    // 生成访问Token和刷新Token
    String accessToken = jwtTokenUtil.generateAccessToken(username);
    String refreshToken = jwtTokenUtil.generateRefreshToken(username);
​
    // 将Token存入Redis
    redisTokenStore.storeAccessToken(username, accessToken);
    redisTokenStore.storeRefreshToken(username, refreshToken);
​
    UserLoginRespDTO response = new UserLoginRespDTO();
    response.setToken(accessToken);
    return response;
}
​
​
@Override
public UserLoginRespDTO refreshAccessToken(String accessToken, String username) {
    String storedRefreshToken = redisTokenStore.getRefreshToken(username);
​
    String storeAccessToken = redisTokenStore.getAccessToken(username);
​
    // 首先判断传入的 AccessToken 是否过期
    if (jwtTokenUtil.isTokenExpired(accessToken)||storeAccessToken==null) {
        // AccessToken 已过期,检查 refreshToken 是否有效
        if (storedRefreshToken != null && !jwtTokenUtil.isTokenExpired(storedRefreshToken)) {
            // refreshToken 有效,刷新 AccessToken 和 RefreshToken
            String newAccessToken = jwtTokenUtil.generateAccessToken(username);
            String newRefreshToken = jwtTokenUtil.generateRefreshToken(username);
            redisTokenStore.storeAccessToken(username, newAccessToken); // 更新 Redis 中的 accessToken
            redisTokenStore.storeRefreshToken(username, newRefreshToken); // 更新 Redis 中的 refreshToken
            UserLoginRespDTO response = new UserLoginRespDTO();
            response.setToken(newAccessToken);
            return response;
        } else {
            // refreshToken 也无效或已过期,抛出异常
            throw new RuntimeException("Invalid or expired refresh token");
        }
    } else {
        // AccessToken 未过期,返回当前的 AccessToken
        UserLoginRespDTO response = new UserLoginRespDTO();
        response.setToken(accessToken);
        return response;
    }
}

实现类

@Override
public UserLoginRespDTO login(UserLoginReqDTO requestParam) {
​
    LambdaQueryWrapper<UserDO> queryWrapper = Wrappers.lambdaQuery(UserDO.class)
            .eq(UserDO::getUsername, requestParam.getUsername())
            .eq(UserDO::getPassword, requestParam.getPassword())
            .eq(UserDO::getDelFlag, 0);
    UserDO userDO = baseMapper.selectOne(queryWrapper);
    if(userDO==null){
        throw new ClientException("用户登陆失败");
    }
​
    // 用户验证逻辑 (例如查询数据库验证用户名和密码)
    String username = requestParam.getUsername();
    String storeAccessToken = redisTokenStore.getAccessToken(username);
    if(storeAccessToken!=null&&!jwtTokenUtil.isTokenExpired(storeAccessToken)){
        throw new ClientException("用户已登陆");
    }
​
    // 生成访问Token和刷新Token
    String accessToken = jwtTokenUtil.generateAccessToken(username);
    String refreshToken = jwtTokenUtil.generateRefreshToken(username);
​
    // 将Token存入Redis
    redisTokenStore.storeAccessToken(username, accessToken);
    redisTokenStore.storeRefreshToken(username, refreshToken);
​
    UserLoginRespDTO response = new UserLoginRespDTO();
    response.setToken(accessToken);
    return response;
}
​
​
@Override
public UserLoginRespDTO refreshAccessToken(String accessToken, String username) {
    String storedRefreshToken = redisTokenStore.getRefreshToken(username);
​
    String storeAccessToken = redisTokenStore.getAccessToken(username);
​
    // 首先判断传入的 AccessToken 是否过期
    if (jwtTokenUtil.isTokenExpired(accessToken)||storeAccessToken==null) {
        // AccessToken 已过期,检查 refreshToken 是否有效
        if (storedRefreshToken != null && !jwtTokenUtil.isTokenExpired(storedRefreshToken)) {
            // refreshToken 有效,刷新 AccessToken 和 RefreshToken
            String newAccessToken = jwtTokenUtil.generateAccessToken(username);
            String newRefreshToken = jwtTokenUtil.generateRefreshToken(username);
            redisTokenStore.storeAccessToken(username, newAccessToken); // 更新 Redis 中的 accessToken
            redisTokenStore.storeRefreshToken(username, newRefreshToken); // 更新 Redis 中的 refreshToken
            UserLoginRespDTO response = new UserLoginRespDTO();
            response.setToken(newAccessToken);
            return response;
        } else {
            // refreshToken 也无效或已过期,抛出异常
            throw new RuntimeException("Invalid or expired refresh token");
        }
    } else {
        // AccessToken 未过期,返回当前的 AccessToken
        UserLoginRespDTO response = new UserLoginRespDTO();
        response.setToken(accessToken);
        return response;
    }
}

JWTUtils

package com.hayaizo.shortlink.admin.util;
​
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
​
import java.util.Base64;
import java.util.Date;
​
@Component
public class JwtTokenUtil {
​
    private final String SECRET_KEY = "gd9mxJUKoTnkcYdiLl9kqjR3XFIvRf4IymaCju0b1PQ="; // Base64 编码的密钥
​
    // 将 Base64 编码的密钥解码为字节数组
    private byte[] getSecretKeyBytes() {
        return Base64.getDecoder().decode(SECRET_KEY);
    }
​
    // 生成访问Token
    public String generateAccessToken(String username) {
        return createToken(username, 1000 * 60 * 15); // 15分钟过期
    }
​
    // 生成刷新Token
    public String generateRefreshToken(String username) {
        return createToken(username, 1000 * 60 * 60 * 24); // 1天过期
    }
​
    // 验证Token
    public Boolean validateToken(String token, String username) {
        final String tokenUsername = extractUsername(token);
        return (tokenUsername.equals(username) && !isTokenExpired(token));
    }
​
    // 从Token中提取用户名
    public String extractUsername(String token) {
        return extractAllClaims(token).getSubject();
    }
​
    // 检查Token是否过期
    public Boolean isTokenExpired(String token) {
        try{
            return extractExpiration(token).before(new Date());
        }catch (ExpiredJwtException e){
            return true;
        }
    }
​
    // 提取Token中的所有声明
    private Claims extractAllClaims(String token) {
        return Jwts.parser()
                .setSigningKey(getSecretKeyBytes()) // 使用解码后的字节数组
                .parseClaimsJws(token)
                .getBody();
    }
​
    // 提取Token中的过期时间
    private Date extractExpiration(String token) {
        return extractAllClaims(token).getExpiration();
    }
​
    // 创建Token
    private String createToken(String subject, long expiration) {
        return Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + expiration))
                .signWith(SignatureAlgorithm.HS256, getSecretKeyBytes()) // 使用解码后的字节数组进行签名
                .compact();
    }
}

RedisTokenStore

package com.hayaizo.shortlink.admin.util;
​
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
​
import java.util.concurrent.TimeUnit;
​
@Component
public class RedisTokenStore {
​
    @Autowired
    private StringRedisTemplate redisTemplate;
​
    // 存储访问Token
    public void storeAccessToken(String username, String accessToken) {
        redisTemplate.opsForValue().set("access_token:" + username, accessToken, 15, TimeUnit.MINUTES);
    }
​
    // 存储刷新Token
    public void storeRefreshToken(String username, String refreshToken) {
        redisTemplate.opsForValue().set("refresh_token:" + username, refreshToken, 1, TimeUnit.DAYS);
    }
​
    // 获取访问Token
    public String getAccessToken(String username) {
        return redisTemplate.opsForValue().get("access_token:" + username);
    }
​
    // 获取刷新Token
    public String getRefreshToken(String username) {
        return redisTemplate.opsForValue().get("refresh_token:" + username);
    }
​
    // 删除Token
    public void deleteTokens(String username) {
        redisTemplate.delete("access_token:" + username);
        redisTemplate.delete("refresh_token:" + username);
    }
}

用户是否登陆

判断redis中的access_token或access_token是否过期

    @Override
    public boolean checkLogin(String username, String token) {
        String access_token = stringRedisTemplate.opsForValue().get("access_token:" + username);
        if(access_token!=null||!jwtTokenUtil.isTokenExpired(access_token)){
            return true;
        }
        return false;
    }

用户登出功能

    @Override
    public void logout(String username, String token) {
        // 首先判断是否是登陆状态
        if(checkLogin(username,token)){
            // 删除token
            redisTokenStore.deleteTokens(username);
            return;
        }
        throw new ClientException("用户未登陆");
    }