【Java】使用 JWT 实现自定义的权限校验

jdk 1.8
idea 2022.2.3
maven 3.6.3
springboot 2.3.7.RELEASE

Spring Security 和 Apache Shiro 可以实现权限校验,但是不够简洁,比较重,小项目根本不需要这么多功能,可以自己使用 JWT 实现简洁的权限校验


先在 pom.xml 里引入 jwt 依赖

    <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

写一个 TokenUtil 工具类,封装 jwt 功能

import io.jsonwebtoken.*;
import io.jsonwebtoken.impl.DefaultClaims;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
 * Token工具
 *
 * <p>生成token和解析token的工具类</p>
 *
 * @author zhangxuetu
 * @date 2022/12/29
 **/
public class TokenUtil {

    /***密钥明文*/
    private static final String SECRET_CLEARTEXT = "YI_TONG_KE_JI_2022";

    /***token的签发者*/
    private static final String ISSUER = "YI_TONG";

    /***存放信息、数据的key*/
    private static final String DATA_KEY = "data";

    /***默认过期时间,1天后过期*/
    private static final long DEFAULT_EXPIRATION_TIME = 1000L * 60 * 60 * 24 * 1L;


    /**
     * 以Base64编码获取一个AES算法密钥
     */
    private static SecretKey getSecretKey() {
        //以Base64编码获取到明文密钥的字节  以该数组生成一个AES算法的的密钥
        return new SecretKeySpec(Base64.getMimeDecoder().decode(SECRET_CLEARTEXT), "AES");
    }

    /**
     * 获取 UUID 作为token的唯一ID
     */
    private static String getUUID() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    /**
     * 生成一个以默认的过期时间的token
     *
     * @param data 存放在token里的信息
     */
    public static String generateToken(Object data) {
        return getJwtBuilder(data).compact();
    }

    /**
     * 生成一个以指定的过期时间的 token
     *
     * @param data  存放在token里的信息
     * @param mills 指定多少毫秒后过期
     * @return 返回生成的 token
     */
    public static String generateToken(Object data, long mills) {
        return getJwtBuilder(data, mills).compact();
    }

    /**
     * 生成以一个以指定天数过期的时间的 token
     *
     * @param data 存放信息
     * @param day  天数
     * @return 返回生成的 token
     */
    public static String generateTokenByDay(Object data, long day) {
        return generateToken(data, 1000 * 60 * 60 * 24 * day);
    }

    /**
     * 获取一个JWT的构造器
     *
     * @param data 存放在token里的信息
     */
    public static JwtBuilder getJwtBuilder(Object data) {
        return getJwtBuilder(data, DEFAULT_EXPIRATION_TIME);
    }

    /**
     * 获取一个token的构造器
     *
     * @param data  存在token里的数据
     * @param mills 过期时间
     */
    public static JwtBuilder getJwtBuilder(Object data, long mills) {
        if (data == null) {
            throw new RuntimeException("实体数据为空");
        }
        //获取到AES算法的密钥
        long nowMills = System.currentTimeMillis();
        DefaultClaims defaultClaims = new DefaultClaims();
        defaultClaims.put(DATA_KEY, data);
        return Jwts.builder()//一个构造器 下面为必要属性的设置
                .setId(getUUID())           //唯一的ID
                .setSubject("token")        //主题为 token
                .setIssuer(ISSUER)      // 签发者
                .setClaims(defaultClaims)   //数据存放
                .setIssuedAt(new Date(nowMills))      // 签发时间设置为当前
                .signWith(SignatureAlgorithm.HS256, getSecretKey()) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(new Date(nowMills + mills));//设置过期时间
    }

    /**
     * 从给定的token中获取存入的信息
     *
     * @return 返回这个Token存储的信息
     */
    public static Object getData(String token) {
        return getData(token, Object.class);
    }

    /**
     * 从给定的token中获取存入的信息
     *
     * @param token  token值
     * @param tClass 这个数据的类型
     * @param <T>    数据类
     * @return 返回这个Token存储的信息
     * @throws UnsupportedJwtException  不支持的格式异常
     * @throws MalformedJwtException    平台jwt异常
     * @throws SignatureException       签名异常
     * @throws ExpiredJwtException      超时异常
     * @throws IllegalArgumentException 非法参数异常
     */
    public static <T> T getData(String token, Class<T> tClass)
            throws UnsupportedJwtException,
            MalformedJwtException,
            SignatureException,
            ExpiredJwtException,
            IllegalArgumentException {
        return Jwts.parser()//token的语法分析器
                .setSigningKey(getSecretKey())//设置签名验证所用的密钥
                .parseClaimsJws(token)//处理token
                .getBody()//获取存入的token里的所有信息
                .get(DATA_KEY, tClass);//获取claims里面存放的msg数据
    }

    /**
     * 获取到达过期时间的剩余时间
     *
     * @param token token值
     * @return 返回到达过期的剩余毫秒时间
     */
    public static long getTimeLeft(String token) throws
            UnsupportedJwtException,
            MalformedJwtException,
            SignatureException,
            ExpiredJwtException,
            IllegalArgumentException {
        Claims claims = Jwts.parser()//token的语法分析器
                .setSigningKey(getSecretKey())//设置签名验证所用的密钥
                .parseClaimsJws(token)
                .getBody();

        long expirationTime = claims.getExpiration().getTime();
        long now = new Date(System.currentTimeMillis()).getTime();
        return expirationTime - now;
    }

    /**
     * 令牌是否过期
     */
    public static boolean isExpiration(String token) {
        try {
            return getTimeLeft(token) <= 0;
        } catch (Exception e) {
            return true;
        }
    }

}

权限注解,用于拦截的时候判断调用的方法或者类是否包含这个权限注解以及值的内容

import java.lang.annotation.*;

/**
 * 自定义权限注解
 *
 * @author zhangxuetu
 * @date 2023-01-10
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Permission {

    /*** and 校验,必须包含有 value 中的所有权限 */
    public final static String AND = "and";
    /*** or 校验,至少有一个 value 中的权限 */
    public final static String OR = "or";

    /**
     * 权限列表,需要里面包含有这些
     */
    String[] value();

    /**
     * 描述
     */
    String message() default "";

    /**
     * 校验方式
     *
     * <p>值为 or 时只要 value 含有其中一个权限即可校验通过,值为 and 时需要 value 中含有所有的值才能通过</p>
     */
    String mode() default "or";

}

拦截器进行拦截并校验权限

import org.springframework.web.method.HandlerMethod;
import com.sun.xml.txw2.IllegalSignatureException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.*;
import java.util.function.Function;

/**
 * 权限校验
 *
 * @author zhangxuetu
 * @date 2022-12-29
 */
@Configuration
@Component
@Slf4j
public class PermissionVerification implements WebMvcConfigurer {

    /**
     * 权限校验方法缓存
     */
    private final static Map<Method, Function<Set<String>, Boolean>> PERMISSION_VERIFY_CACHE = new HashMap<>();

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // 权限拦截器
        HandlerInterceptor permissionInterceptor = new HandlerInterceptor() {

            // 校验 Token 权限
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
                log.info("操作:{}", handler);

                String uri = request.getRequestURI();
                log.info("请求路径:{}", uri);

                // 获取 token
                String token = request.getHeader("token");
                log.info("检验token:{}", token);

                 if (handler instanceof HandlerMethod) {
                    Method method = ((HandlerMethod) handler).getMethod();
                    return verifyPermissions(token, method);
                } else {
                    return false;
                }

            }

        };

        // 放行的路径
        String[] excludePath = {
                "/user/login", "classpath:/META-INF/**", "/file/**"
        };
        String[] swaggerExcludePath = {
                "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**"
        };

        registry.addInterceptor(permissionInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(excludePath)
                .excludePathPatterns(swaggerExcludePath);

    }

    /**
     * 生成这个用户权限的 token 值
     *
     * @param permissionList 这个用户的权限列表值
     * @return 返回 token
     */
    public String generateToken(Collection<String> permissionList) {
        return TokenUtil.generateTokenByDay(permissionList, 1);
    }

    /**
     * 校验权限
     *
     * @param token  用户token值
     * @param method 执行的方法
     * @return 返回是否通过权限校验
     */
    public boolean verifyPermissions(String token, Method method) {
        if (TokenUtil.isExpiration(token)) {
            // 过期了
            throw new IllegalSignatureException("token 已失效!");
        }

        // 如果没有这个校验方法的缓存,则生成并添加进去
        if (!PERMISSION_VERIFY_CACHE.containsKey(method)) {
            // method
            Function<Set<String>, Boolean> methodVerify = null;
            if (method.isAnnotationPresent(Permission.class)) {
                Permission methodPermission = method.getAnnotation(Permission.class);
                methodVerify = generateVerifyFunction(methodPermission);
            }

            // controller
            Function<Set<String>, Boolean> controllerVerify = null;
            Class<?> controller = method.getDeclaringClass();
            if (controller.isAnnotationPresent(Permission.class)) {
                Permission controllerPermission = controller.getAnnotation(Permission.class);
                controllerVerify = generateVerifyFunction(controllerPermission);
            }

            // 加入缓存
            if (methodVerify != null && controllerVerify != null) {
                // Controller 和 Method 上都有权限注解,则两个都校验
                Function<Set<String>, Boolean> finalMethodVerify = methodVerify;
                Function<Set<String>, Boolean> finalControllerVerify = controllerVerify;
                PERMISSION_VERIFY_CACHE.put(method, (userPermissionSet) -> {
                    return finalMethodVerify.apply(userPermissionSet) && finalControllerVerify.apply(userPermissionSet);
                });
            } else if (methodVerify != null) {
                PERMISSION_VERIFY_CACHE.put(method, methodVerify);
            } else if (controllerVerify != null) {
                PERMISSION_VERIFY_CACHE.put(method, controllerVerify);
            }
        }

        // 获取这个权限注解的权限集合
        Function<Set<String>, Boolean> verifyFunction = PERMISSION_VERIFY_CACHE.get(method);
        // 获取用户的权限集合
        Set<String> userPermissionSet = TokenUtil.getData(token, Set.class);
        // 校验这个权限
        return verifyFunction.apply(userPermissionSet);
    }

    /**
     * 生成一个校验方法
     *
     * @param permission 权限注解对象
     * @return 返回校验方法
     */
    public static Function<Set<String>, Boolean> generateVerifyFunction(Permission permission) {
        List<String> permissionSet = Arrays.asList(permission.value());
        permissionSet.remove("");

        if (Permission.OR.equals(permission.mode())) {
            // or 权限校验
            return (userPermissionSet) -> {
                for (String perm : permissionSet) {
                    if (userPermissionSet.contains(perm)) {
                        return true;
                    }
                }
                return false;
            };
        } else if (Permission.AND.equals(permission.mode())) {
            // and 权限校验
            return permissionSet::containsAll;

        } else {
            // 错误的校验方式值
            throw new IllegalArgumentException("错误的权限校验方式值!请检查代码");
        }
    }

}

登录时成功时,获取这个用户的权限列表,然后根据权限列表获取这个 token,返回数据时额外添加这个 token 值

String token = permissionVerification.generateToken(permission);

前端请求时,请求头携带这个 token 值进行校验

发表评论