需求
在 Springboot 3 中实现一个@Token 注解,对需要校验的接口自动检验,并把Token对应的User存在ThreadLocal中备用。
定义@Token注解
package cc.wanghl.ebook.annotation;
import java.lang.annotation.*;
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Token{
}
定义一个切面用于具体实现逻辑
package cc.wanghl.ebook.aop;
import cc.wanghl.ebook.constant.ResponseCode;
import cc.wanghl.ebook.entity.User;
import cc.wanghl.ebook.utils.RedisUtil;
import cc.wanghl.ebook.utils.ThreadLocalUtil;
import cc.wanghl.ebook.vo.WrappedResp;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
@Slf4j
@Aspect // 切面注qev
public class TokenChecker {
@Value("${token.expire-time}")
private Long tokenExpireTime; // token 过期时间,来自配置文件
@Autowired
HttpServletRequest request;
@Autowired
RedisUtil redisUtil;
// 定义切点,表示当一个“方法”上有@Token注解时执行
@Pointcut("@annotation(cc.wanghl.ebook.annotation.Token)")
public void methodPointCut() {
}
// 定义切点,表示当一个“类”上有@Token注解时执行
@Pointcut("@within(cc.wanghl.ebook.annotation.Token)")
public void classPointCut() {
}
// 切面逻辑
@Around("methodPointCut() || classPointCut()")
public Object aroundTokenValidate(ProceedingJoinPoint joinPoint) throws Throwable {
String token = request.getHeader("token");
WrappedResp wrappedResp;
if (StringUtils.isEmpty(token)) { // 请求头里没传token直接返失败
wrappedResp = WrappedResp.fail(ResponseCode.FAIL_NO_TOKEN);
return wrappedResp;
} else {
if(!redisUtil.hasKey(token)){ // redis里没找到这个token,返失败
wrappedResp = WrappedResp.fail(ResponseCode.FAIL_TOKEN_EXPIRED);
return wrappedResp;
}else{
User u = JSON.parseObject(redisUtil.get(token), User.class);
log.debug("user [{}] operation [{}]", u.getUsername(), joinPoint.getSignature());
redisUtil.expire(token, tokenExpireTime); // 刷新token有效期
ThreadLocalUtil.setValue(u); //把redis里面的user信息存到 ThreadLocal 中
}
}
return joinPoint.proceed();
}
}
ThreadLocalUtil 工具类
简单贴一下 ThreadLocalUtil 工具类,不在主流程逻辑中。
package cc.wanghl.ebook.utils;
import cc.wanghl.ebook.entity.User;
public class ThreadLocalUtil {
private static final ThreadLocal<User>threadLocalUser= new ThreadLocal<>();
public static void setValue(User value) {
threadLocalUser.set(value);
}
public static User getValue() {
returnthreadLocalUser.get();
}
public static void clear() {
threadLocalUser.remove();
}
}
@Token的使用
加在类上,表示整个类里面的所有的方法都会进行Token校验
package cc.wanghl.ebook.controller;
@Slf4j
@RestController
@Token
@RequestMapping(value = "epub/")
public class EPUBController {
public WrappedResp addBook(@RequestPart Long catalogueId, @RequestPart String md5, @RequestPart MultipartFile file) throws IOException {
// ...
}
}
加在方法上,表示中有这个API进行Token校验
package cc.wanghl.ebook.controller;
@Slf4j
@RestController
@RequestMapping(value = "epub/")
public class EPUBController {
@PostMapping("xxxxxxx")
@Token
public WrappedResp addBook(@RequestPart Long catalogueId, @RequestPart String md5, @RequestPart MultipartFile file) throws IOException {
// ...
}
}