在 Springboot 3 统一校验Token【完整代码】

346 阅读1分钟

需求

在 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 {
        // ...
    }
}