ChaperTwo-整合 SaToken 实现 JWT 登录功能

SaToken是什么?

是一个轻量级的Java权限认证框架,主要解决:登录认证、权限认证、单点认证、OAuth2.0、分布式Session会话、微服务网关鉴权等权限相关问题

Sa-Token 目前主要五大功能模块:登录认证、权限认证、单点登录、OAuth2.0、微服务鉴权。

  • 登录认证 ------ 单端登录、多端登录、同端互斥登录、七天内免登录。
  • 权限认证 ------ 权限认证、角色认证、会话二级认证。
  • 踢人下线 ------ 根据账号id踢人下线、根据Token值踢人下线。
  • 注解式鉴权 ------ 优雅的将鉴权与业务代码分离。
  • 路由拦截式鉴权 ------ 根据路由拦截鉴权,可适配 restful 模式。
  • Session会话 ------ 全端共享Session,单端独享Session,自定义Session,方便的存取值。
  • 持久层扩展 ------ 可集成 Redis,重启数据不丢失。
  • 前后台分离 ------ APP、小程序等不支持 Cookie 的终端也可以轻松鉴权。
  • Token风格定制 ------ 内置六种 Token 风格,还可:自定义 Token 生成策略。
  • 记住我模式 ------ 适配 记住我 模式,重启浏览器免验证。
  • 二级认证 ------ 在已登录的基础上再次认证,保证安全性。
  • 模拟他人账号 ------ 实时操作任意用户状态数据。
  • 临时身份切换 ------ 将会话身份临时切换为其它账号。
  • 同端互斥登录 ------ 像QQ一样手机电脑同时在线,但是两个手机上互斥登录。
  • 账号封禁 ------ 登录封禁、按照业务分类封禁、按照处罚阶梯封禁。
  • 密码加密 ------ 提供基础加密算法,可快速 MD5、SHA1、SHA256、AES 加密。
  • 会话查询 ------ 提供方便灵活的会话查询接口。
  • Http Basic认证 ------ 一行代码接入 Http Basic、Digest 认证。
  • 全局侦听器 ------ 在用户登陆、注销、被踢下线等关键性操作时进行一些AOP操作。
  • 全局过滤器 ------ 方便的处理跨域,全局设置安全响应头等操作。
  • 多账号体系认证 ------ 一个系统多套账号分开鉴权(比如商城的 User 表和 Admin 表)
  • 单点登录 ------ 内置三种单点登录模式:同域、跨域、同Redis、跨Redis、前后端分离等架构都可以搞定。
  • 单点注销 ------ 任意子系统内发起注销,即可全端下线。
  • OAuth2.0认证 ------ 轻松搭建 OAuth2.0 服务,支持openid模式 。
  • 分布式会话 ------ 提供共享数据中心分布式会话方案。
  • 微服务网关鉴权 ------ 适配Gateway、ShenYu、Zuul等常见网关的路由拦截认证。
  • RPC调用鉴权 ------ 网关转发鉴权,RPC调用鉴权,让服务调用不再裸奔
  • 临时Token认证 ------ 解决短时间的 Token 授权问题。
  • 独立Redis ------ 将权限缓存与业务缓存分离。
  • Quick快速登录认证 ------ 为项目零代码注入一个登录页面。
  • 标签方言 ------ 提供 Thymeleaf 标签方言集成包,提供 beetl 集成示例。
  • jwt集成 ------ 提供三种模式的 jwt 集成方案,提供 token 扩展参数能力。
  • RPC调用状态传递 ------ 提供 dubbo、grpc 等集成包,在RPC调用时登录状态不丢失。
  • 参数签名 ------ 提供跨系统API调用签名校验模块,防参数篡改,防请求重放。
  • 自动续签 ------ 提供两种Token过期策略,灵活搭配使用,还可自动续签。
  • 开箱即用 ------ 提供SpringMVC、WebFlux、Solon 等常见框架集成包,开箱即用。
  • 最新技术栈 ------ 适配最新技术栈:支持 SpringBoot 3.x,jdk 17。

在父项目pom添加依赖,后在auth模块引入依赖。配置Sa-Token

java 复制代码
Plain Text
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
  # token 名称(同时也是 cookie 名称)
  token-name: satoken
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
  timeout: 2592000
  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  active-timeout: -1
  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
  is-share: true
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: uuid
  # 是否输出操作日志
  is-log: true

通过调用来测试

java 复制代码
StpUtil.login(userId);
SptUtil.isLogin();

|---|
| |

整合RedisTemplate

在父项目和子模块添加完依赖后,在application-dev.yml配置

java 复制代码
YAML
spring:
  datasource:
        // 省略...
  data:
    redis:
      database: 0 # Redis 数据库索引(默认为 0)
      host: 127.0.0.1 # Redis 服务器地址
      port: 6379 # Redis 服务器连接端口
      password: qwe123!@# # Redis 服务器连接密码(默认为空)
      timeout: 5s # 读超时时间
      connect-timeout: 5s # 链接超时时间
      lettuce:
        pool:
          max-active: 200 # 连接池最大连接数
          max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
          min-idle: 0 # 连接池中的最小空闲连接
          max-idle: 10 # 连接池中的最大空闲连接    

接下来便是创建Config文件进行RedisTemplate的自定义配置:

主要是:设置连接工厂->用StringRedisSerializer来序列化和反序列化redis的key,确保key以可读的形式进行保存->用Jackson2JsonRedisSerializer来序列化和反序列化redis的value,确保value以JSON存储

java 复制代码
package com.quanxiaoha.xiaohashu.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @author: 犬小哈
 * @date: 2024/4/6 15:51
 * @version: v1.0.0
 * @description: RedisTemplate 配置
 **/
@Configuration
public class RedisTemplateConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        // 设置 RedisTemplate 的连接工厂
        redisTemplate.setConnectionFactory(connectionFactory);

        // 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值,确保 key 是可读的字符串
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());

        // 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值, 确保存储的是 JSON 格式
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashValueSerializer(serializer);

        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

获取手机短信验证码接口开发

整个流程逻辑为:

  1. 前端以手机号作为入参,请求验证码
  1. 后端拿到手机号,构造redis的verificationkey
  1. 查询redis中是否有这个key,如果有说明验证码还未过期,提示用户请求过于频繁
  1. 若不存在,生成随机6位验证码,调用阿里云短信发送服务发送验证码,同时存储到redis中设置过期时间为3分钟,这样可以用于判断用户输入的验证码正确性已经是否请求频繁

接口地址:

|-----------------------------------------------|
| Java java POST /verification/code/send |

入参:

|----------------------------------------------------|
| Java java { "phone": "18019939108" // 手机号 } |

出参:

java 复制代码
{
        "success": false,
        "message": "请求太频繁,请3分钟后再试",
        "errorCode": "AUTH-20000",
        "data": null
}

根据流程,先进行VO的创建

java 复制代码
package com.quanxiaoha.xiaohashu.auth.model.vo.verificationcode;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SendVerificationCodeReqVO {

    @NotBlank(message = "手机号不能为空")
    private String phone;

}

加上无参构造、全参构造等注解,方便调用

接下来需要完善redis中的verificationkey,为了方便使用以及保证代码的规范性,创建constants类来管理rediskey,加上属性和静态方法便于其他的调用

java 复制代码
package com.quanxiaoha.xiaohashu.auth.constant;


public class RedisKeyConstants {

    /**
     * 验证码 KEY 前缀
     */
    private static final String VERIFICATION_CODE_KEY_PREFIX = "verification_code:";

    /**
     * 构建验证码 KEY
     * @param phone
     * @return
     */
    public static String buildVerificationCodeKey(String phone) {
        return VERIFICATION_CODE_KEY_PREFIX + phone;
    }
}

由于有可能出现"请求频繁"的错误,需要在ResponeEnum类中添加业务状态码

接下来便是具体业务的开发,创建Controller和Service。

Service需要结合阿里云的号码认证服务,先添加相关依赖;在阿里云创建AccessKey并配置到.yml文件;封装sms工具类,进行短信服务,包括Properties、Config与具体的实现类。其中Properties主要用于管理AccessKey,方便使用的同时防止硬编码导致的信息不安全问题

java 复制代码
package com.quanxiaoha.xiaohashu.auth.sms;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;


@ConfigurationProperties(prefix = "aliyun")
@Component
@Data
public class AliyunAccessKeyProperties {
    private String accessKeyId;
    private String accessKeySecret;
}

其中的注释

java 复制代码
@ConfigurationProperties(prefix = "aliyun")

指定了从.yml哪个地方加载具体值

Config类主要用于配置发送短信的必要信息

java 复制代码
Java
package com.quanxiaoha.xiaohashu.auth.sms;

import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author: 犬小哈
 * @Date: 2025/10/12 17:29
 * @Version: v1.0.0
 * @Description: 短信发送客户端
 **/
@Configuration
@Slf4j
public class AliyunSmsClientConfig {

    @Resource
    private AliyunAccessKeyProperties aliyunAccessKeyProperties;

    @Bean
    public com.aliyun.dypnsapi20170525.Client smsClient() {
        try {
            com.aliyun.credentials.Client credential = new com.aliyun.credentials.Client();

            com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
                    .setCredential(credential);

            // Endpoint 请参考 https://api.aliyun.com/product/Dypnsapi
            config.endpoint = "dypnsapi.aliyuncs.com";

            config.accessKeyId = aliyunAccessKeyProperties.getAccessKeyId(); // 必填
            config.accessKeySecret = aliyunAccessKeyProperties.getAccessKeySecret(); // 必填

            return new com.aliyun.dypnsapi20170525.Client(config);
        } catch (Exception e) {
            log.error("初始化阿里云短信发送客户端错误: ", e);
            return null;
        }
    }
}

其中的config作为中间件来存放具体信息,避免了麻烦的配置与冗余的代码

主要信息是:credential、endpoint、accessKeyId、accessKeySecret

最后是具体的发送类:

java 复制代码
package com.quanxiaoha.xiaohashu.auth.sms;

import com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeResponse;
import com.quanxiaoha.framework.common.util.JsonUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * @Author: 犬小哈
 * @Date: 2025/10/12 17:43
 * @Version: v1.0.0
 * @Description: 短信发送工具类
 **/
@Component
@Slf4j
public class AliyunSmsHelper {

    @Resource
    private com.aliyun.dypnsapi20170525.Client client;

    /**
     * 发送短信
     * @param signName
     * @param templateCode
     * @param phone
     * @param templateParam
     * @return
     */
    public boolean sendMessage(String signName, String templateCode, String phone, String templateParam) {
        com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeRequest sendSmsVerifyCodeRequest = new com.aliyun.dypnsapi20170525.models.SendSmsVerifyCodeRequest()
                .setSignName(signName)
                .setTemplateCode(templateCode)
                .setPhoneNumber(phone)
                .setTemplateParam(templateParam);
        
        com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();

        try {
            log.info("==> 开始短信发送, phone: {}, signName: {}, templateCode: {}, templateParam: {}", phone, signName, templateCode, templateParam);

            // 发送短信
            SendSmsVerifyCodeResponse response = client.sendSmsVerifyCodeWithOptions(sendSmsVerifyCodeRequest, runtime);

            log.info("==> 短信发送成功, response: {}", JsonUtils.toJsonString(response));
            return true;
        } catch (Exception error) {
            log.error("==> 短信发送错误: ", error);
            return false;
        }
    }
}

到这里,就可以实现service的发送短信功能了。但是为了可以提升性能,增加系统的响应速度,可以采用异步发送短信的方法。为了实现异步发送,需要进行自定义线程池

自定义线程池

创建Config类,进行线程池的配置

配置完成后就可以开始实现具体业务了

主要流程是:

  1. 获取手机号并构建rediskey
  1. 根据rediskey来判断redis中是否已经发送验证码,已发送则终止流程并抛出验证码发送频繁错误
  1. 若没有,生成验证码,调用阿里云验证码工具发送验证码
  1. 存储验证码到redis,TTL3分钟
java 复制代码
package com.quanxiaoha.xiaohashu.auth.service.impl;

import cn.hutool.core.util.RandomUtil;
import com.quanxiaoha.framework.common.exception.BizException;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.xiaohashu.auth.constant.RedisKeyConstants;
import com.quanxiaoha.xiaohashu.auth.enums.ResponseCodeEnum;
import com.quanxiaoha.xiaohashu.auth.model.vo.verificationcode.SendVerificationCodeReqVO;
import com.quanxiaoha.xiaohashu.auth.service.VerificationCodeService;
import com.quanxiaoha.xiaohashu.auth.sms.AliyunSmsHelper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class VerificationCodeServiceImpl implements VerificationCodeService {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource(name = "taskExecutor")
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    @Resource
    private AliyunSmsHelper aliyunSmsHelper;

    /**
     * 发送短信验证码
     *
     * @param sendVerificationCodeReqVO
     * @return
     */
    @Override
    public Response<?> send(SendVerificationCodeReqVO sendVerificationCodeReqVO) {
        // 手机号
        String phone = sendVerificationCodeReqVO.getPhone();

        // 构建验证码 redis key
        String key = RedisKeyConstants.buildVerificationCodeKey(phone);

        // 判断是否已发送验证码
        boolean isSent = redisTemplate.hasKey(key);
        if (isSent) {
            // 若之前发送的验证码未过期,则提示发送频繁
            throw new BizException(ResponseCodeEnum.VERIFICATION_CODE_SEND_FREQUENTLY);
        }

        // 生成 6 位随机数字验证码
        String verificationCode = RandomUtil.randomNumbers(6);

        log.info("==> 手机号: {}, 已生成验证码:【{}】", phone, verificationCode);

        // 调用第三方短信发送服务
        threadPoolTaskExecutor.submit(() -> {
            String signName = "速通互联验证码"; // 签名,个人测试签名无法修改
            String templateCode = "100001"; // 短信模板编码
            // 短信模板参数,code 表示要发送的验证码;min 表示验证码有时间时长,即 3 分钟
            String templateParam = String.format("{\"code\":\"%s\",\"min\":\"3\"}", verificationCode);
            aliyunSmsHelper.sendMessage(signName, templateCode, phone, templateParam);
        });

        // 存储验证码到 redis, 并设置过期时间为 3 分钟
        redisTemplate.opsForValue().set(key, verificationCode, 3, TimeUnit.MINUTES);

        return Response.success();
    }
}

自定义 @PhoneNumber 手机号校验注解

自定义注解的通用流程:

  1. 创建自定义校验类,实现ConstraintValidator<PhoneNumber, String>接口
  • PhoneNumber:自定义注解类型。
  • String:被校验的属性类型。

用于添加自己的校验逻辑,在isValid 方法实现具体的自定义校验

  1. 创建自定义注解类
java 复制代码
package com.quanxiaoha.framework.common.validator;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.*;

/**
 * @author: 犬小哈
 * @date: 2024/4/15 22:22
 * @version: v1.0.0
 * @description: 自定义手机号校验注解
 **/
@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneNumberValidator.class)
public @interface PhoneNumber {

    String message() default "手机号格式不正确, 需为 11 位数字";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

其中

@Target 注解用于指定自定义注解可以应用的 Java 元素类型。在 @PhoneNumber 中,@Target 的参数包括以下几个元素类型:

  • ElementType.METHOD:可以应用于方法。
  • ElementType.FIELD:可以应用于字段。
  • ElementType.ANNOTATION_TYPE:可以应用于其他注解。
  • ElementType.PARAMETER:可以应用于方法参数。

这种组合使得 @PhoneNumber 注解可以被广泛使用在方法、字段、注解和参数上。

@Retention 注解用于指定自定义注解的保留策略。RetentionPolicy.RUNTIME 表示该注解在运行时仍然可用(可以通过反射机制访问)。这对于校验注解非常重要,因为校验框架需要在运行时读取注解并执行相应的校验逻辑。

@Constraint 注解用于指定关联的验证器类。在 @PhoneNumber 中,validatedBy 属性指向 PhoneNumberValidator.class,即自定义注解 @PhoneNumber 使用 PhoneNumberValidator 类进行校验。

message 元素用于定义验证失败时的错误消息。在使用注解时可以覆盖默认消息。default 关键字用于提供该元素的默认值。

鉴权设计:RBAC 权限模型

Role-Base Access Control基于角色的访问控制。通过角色来管理用户的权限。核心是将用户和角色相关联,不同的角色有不同的权限,而不是直接将权限分配给用户

在实际业务中又扩展了RBAC1、RBAC2、RBAC3,在最基础的功能上添加了功能来适应不同场景

要实现RBAC权限控制,需要5张表:用户表、角色表、角色权限关联表、用户角色关联表

由于生产环境中,请求不能直接打到内网,鉴权放置位置有不同选择

  1. 每个微服务各自鉴权
  1. 网关统一鉴权
  1. 混合策略

其中混合策略即在网关进行初步鉴权,进行粗粒度的控制,然后在关键微服务中进行细粒度的二次鉴权。这种方式可以兼顾性能和安全性。

用户注册/登录接口开发

接口地址:

java 复制代码
POST /user/login

入参:

java 复制代码
{
    "phone": "18011119108", // 手机号
    "code": "218603", // 登录验证码,验证码登录时,需要填写
    "password": "xx", // 密码登录时,需要填写
    "type": 1 // 登录类型,1表示手机号验证码登录;2表示账号密码登录
}

出参:

java 复制代码
{
        "success": true,
        "message": null,
        "errorCode": null,
        "data": "xxxxx" // 登录成功后,返回 Token 令牌
}

整体流程与验证码获取部分差不多,但是这里要考虑DAO层的操作

service的流程为:

  • 拿到入参实体类中的 type 字段,通过 LoginTypeEnum.valueOf() 方法,获取具体的类型枚举值;
  • 对枚举进行 switch 判断,若是手机号验证码登录;
  • 获取提交上来的验证码,并与存储在 Redis 中的验证码进行比对;
  • 若不一致,返回验证码错误提示信息;
  • 否则,通过手机号查询数据库;
  • 若 userDO 为空,说明是新用户,系统需要自动为该用户注册用户信息。这里代码块中,先写个 todo , 后面小节中,再写具体的逻辑;
  • 若 userDO 不为空,则说明是老用户,获取其用户 ID;
  • 若是账号密码登录,校验密码是否正确;
  • SaToken 登录用户,并返回 token 令牌;

编程式事务使用:更细粒度的事务控制

声明式注解事务失效,主要由以下几点:

  • 方法可见性 :@Transactional 仅在 public 方法上生效。
  • 自调用 :当类中的方法调用同一个类中的另一个 @Transactional 方法时,事务可能不会生效。这是因为事务注解是通过 AOP 实现的,而 Spring 的 AOP 代理机制在这种情况下不会被触发。
  • 异常处理 :只有 RuntimeException 和 Error 类型的异常会触发事务回滚。如果你抛出的是 checked exception,事务不会回滚,除非你明确指定 rollbackFor 属性。
  • 代理对象 :确保你是在 Spring 管理的代理对象上调用方法。如果你直接使用 new 关键字实例化对象,Spring 的 AOP 代理机制将不会被应用。

什么是编程式事务?有哪些优点?

编程式事务(Programmatic Transaction)是一种通过代码显式地管理事务的方式,而不是依赖声明式事务(Declarative Transaction)中使用的注解或 XML 配置。在编程式事务中,开发人员通过编写代码来开启、提交和回滚事务,以精细控制事务的边界和行为。

使用编程式事务优点如下:

  • 精细控制:编程式事务允许开发者通过代码精细地控制事务的生命周期,包括开始、提交和回滚。可以根据具体业务需求,灵活地管理事务。
  • 动态处理:在运行时可以根据业务逻辑的不同情况动态决定事务的行为。特别适合需要在代码执行过程中,根据某些条件来开启、提交或回滚事务的场景。
  • 适用于复杂事务:在一个方法中需要多次开启和关闭事务,或需要嵌套事务的复杂场景中,编程式事务可以提供更大的灵活性和控制力。
  • 灵活性高:能够在代码中实现复杂的事务逻辑,可以精确控制事务的边界和行为。这在需要多个步骤或调用之间共享事务上下文时非常有用。
  • 性能提升:通过精细控制事务的边界,减少不必要的事务开启和提交,从而减少事务开销;通过明确控制事务的开始和结束,可以确保事务范围尽可能小,减少长时间占用数据库资源,提高系统的并发性;通过灵活的事务管理,可以在必要时才进行事务回滚,减少回滚操作带来的性能开销。

四种方式:

  1. 通过 transactionManager.getTransaction 方法获取一个新的事务状态。DefaultTransactionDefinition 用于定义事务的默认属性,例如传播行为和隔离级别。该方法会返回一个TransactionStatus对象,用于管理事务的状态。
  1. TransactionTemplate是一个简化了事务管理的工具类,可以避免直接处理 TransactionStatus

代码优化:Guava Preconditions 参数校验

Guava 是一个广泛使用的 Java 库,提供了许多有用的工具和实用程序,其中包括参数校验工具。Guava 的参数校验功能主要通过 com.google.common.base.Preconditions 类来实现。Preconditions 提供了一组静态方法,用于在方法执行前验证参数的有效性。这些方法在条件不满足时抛出异常,从而确保方法得到合法的输入。

注意checkArgument()方法源码是抛出了IllegaArgumentException,需要在全局异常类中添加捕获IllegaArgumentException的方法

同步【角色-权限集合】数据到 Redis 中

登录成功后redis中会同步用户-角色的关联关系,但是光有角色 ID 是不够的,因为每个角色对应的权限数据,还没有同步到 Redis 中。这块的工作,可以放到项目启动后,同时也将角色-权限数据同步到 Redis 中。

可以通过多种方式在项目启动时执行初始化工作。以下是一些常见的方法:

  1. 使用 @PostConstruct 注解
  1. 实现 ApplicationRunner 接口
  1. 实现 CommandLineRunner 接口
  1. 使用 @EventListener 注解监听 ApplicationReadyEvent
  1. 使用 SmartInitializingSingleton 接口
  1. 使用 Spring Boot 的 InitializingBean 接口

总结:

  • @PostConstruct:适合简单的初始化逻辑,执行时机较早。
  • ApplicationRunner 和 CommandLineRunner:适合需要访问命令行参数的初始化逻辑,执行时机在 Spring Boot 应用启动完成后。
  • ApplicationReadyEvent 监听器:适合在整个应用准备好后执行的初始化逻辑。
  • SmartInitializingSingleton:适合需要在所有单例 bean 初始化完成后执行的初始化逻辑。
  • InitializingBean:适合需要在 bean 属性设置完成后执行的初始化逻辑。
相关推荐
人道领域1 分钟前
【LeetCode刷题日记】90.子集Ⅱ--- 归纳题解
java·开发语言·leetcode
ch.ju7 分钟前
Java Programming Chapter 4——Characteristics of inheritance
java·开发语言
复园电子8 分钟前
企业PDF批量盖章开发集成指南:API对接OA/LIMS系统,高并发落地实战
开发语言·python·pdf
就叫_这个吧9 分钟前
tomcat在idea控制台乱码问题解决
java·tomcat·intellij-idea
SunnyDays101113 分钟前
如何使用 C# 自动调整 Excel 行高和列宽
开发语言·c#·excel
霸道流氓气质18 分钟前
Spring AI Alibaba Skills 完整实战:从零构建智能会议助手
java·人工智能·spring
a诠释淡然26 分钟前
C++模板元编程—现代C++的黑魔法
开发语言·c++
极客先躯29 分钟前
高级java每日一道面试题-2026年02月04日-实战篇[Docker]-如何在容器之间共享数据?
java·运维·网络·docker·容器·自动化·高级面试题
真实的菜29 分钟前
微服务架构痛点
java·微服务·架构
小楊不秃头31 分钟前
Spring:Bean的存储
java·spring·bean