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 属性设置完成后执行的初始化逻辑。
相关推荐
Scott9999HH6 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔6 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社7 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海7 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖8 小时前
JDK 26 新特性详解
java·开发语言
马优晨9 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区10 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大11 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai11 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说11 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json