首页 > 其他分享 >使用JWT、拦截器与ThreadLocal实现在任意位置获取Token中的信息,并结合自定义注解实现对方法的鉴权

使用JWT、拦截器与ThreadLocal实现在任意位置获取Token中的信息,并结合自定义注解实现对方法的鉴权

时间:2023-11-14 14:57:57浏览次数:33  
标签:拦截器 return 自定义 JWT userType ThreadLocal public

1. 简介

1.1 JWT

JWT,即JSON Web Token,是一种用于在网络上传递声明的开放标准(RFC 7519)。JWT 可以在用户和服务器之间传递安全可靠的信息,通常用于身份验证和信息交换。

  1. 声明(Claims): JWT 包含一组称为声明的信息,声明描述了一些数据。有三种类型的声明:
    • 注册声明(Registered Claims):这是一些预定义的声明,包括标准的声明,例如"iss"(签发者)、"sub"(主题)和"exp"(过期时间)等。
    • 公共声明(Public Claims):这些声明是由使用 JWT 的双方定义的,并且必须遵守一定的规定,以防止冲突。
    • 私有声明(Private Claims):这些是自定义的声明,用于在双方之间共享信息。
  2. 编码结构: JWT 由三部分组成,使用点号(.)分隔开:
    • Header(头部):包含了令牌的元数据,例如算法和令牌类型。
    • Payload(载荷):包含了声明,即实际传输的数据。
    • Signature(签名):用于验证发送方的身份以及确保消息的完整性。签名由前两部分的内容和密钥组成,以防篡改。
  3. 编码方法: JWT 可以使用不同的编码方法,包括:
    • Base64 URL encoding:用于编码头部和载荷。
    • HMACSHA256:用于生成签名,以确保消息完整性。
  4. 使用场景
    • 身份验证:用户登录后,服务器生成一个包含用户信息的JWT,并将其发送给客户端。客户端在后续请求中将JWT包含在请求头中,服务器验证JWT以确保请求的合法性。
    • 信息交换:JWT还可以包含其他信息,例如用户的角色、访问权限等,这些信息可以在不需要查询数据库的情况下进行快速访问。

1.2 拦截器

拦截器(Interceptor)是一种用于处理请求的机制,可以让你在请求的处理过程中进行预处理和后处理。拦截器类似于过滤器(Filter),但相比过滤器,拦截器更加专注于处理控制器层面的请求,可以对处理器的执行过程进行更加细粒度的控制。

使用场景

  • 身份验证和授权:拦截器可以用于检查用户是否已经登录,是否具有足够的权限访问某个资源。
  • 日志记录:拦截器可以用于记录请求和响应的日志信息,方便调试和监控。
  • 性能监控:通过拦截器,你可以记录请求的处理时间,帮助进行性能监控。
  • 执行顺序:拦截器可以配置多个,它们的执行顺序由配置时的顺序决定。
  • 异步请求:拦截器也能处理异步请求,需要实现AsyncHandlerInterceptor接口。

1.3 ThreadLocal

ThreadLocal 是 Java 中的一个类,主要用于提供了线程本地变量。 ThreadLocal 创建的变量只能被当前线程访问,其他线程无法直接访问或修改它。ThreadLocal 主要用于保持线程封闭性,即每个线程都拥有自己独立的变量副本,不同线程之间不会相互干扰。

应用场景

  • 线程安全的数据传递:在多线程环境中,通过 ThreadLocal 可以轻松地将数据在方法调用间传递,而无需将数据作为参数传递。
  • 数据库连接管理:在数据库连接的管理中,可以使用 ThreadLocal 来存储每个线程的数据库连接,确保每个线程使用的是自己的连接。
  • 事务管理ThreadLocal 可以用于事务管理,确保事务的一致性。

注意:

  • ThreadLocal可以在虚拟线程环境下使用
  • ThreadLocal 应该谨慎使用,避免滥用。过多的使用可能导致代码难以理解和维护。
  • 避免在 ThreadLocal 中存储大对象,以防止内存泄漏。
  • 在使用线程池时,需要注意清理 ThreadLocal,以防止线程复用时出现数据污染。

2. 代码实战

2.1 引入依赖

<!-- java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>${jwt.version}</version>
</dependency>

2.2 获取token的函数

推荐使用@Value的方式从application.yml中获取密钥和过期时间

// refresh-token密钥
@Value("${refresh-token.secret}")
private String REFRESH_TOKEN_SECRET;

// refresh-token过期时间
@Value("${refresh-token.expire-time}")
private int REFRESH_TOKEN_EXPIRE_TIME;

// refresh-token密钥
@Value("${access-token.secret}")
private String ACCESS_TOKEN_SECRET;

// refresh-token过期时间
@Value("${access-token.expire-time}")
private int ACCESS_TOKEN_EXPIRE_TIME;

/**
 * 获取access_token
 * @param userId
 * @param userType
 * @return
 */
private String getAccessToken(int userId, int userType){
    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.HOUR, ACCESS_TOKEN_EXPIRE_TIME);
    return JWT.create()
            .withClaim("userId", userId)
            .withClaim("userType", userType)
            .withExpiresAt(calendar.getTime())
            .sign(Algorithm.HMAC512(ACCESS_TOKEN_SECRET));
}

/**
 * 获取refresh_token
 * @param userId
 * @param userType
 * @return
 */
private String getRefreshToken(int userId, int userType){
    Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.HOUR, REFRESH_TOKEN_EXPIRE_TIME);
    return JWT.create()
            .withClaim("userId", userId)
            .withClaim("userType", userType)
            .withExpiresAt(calendar.getTime())
            .sign(Algorithm.HMAC512(REFRESH_TOKEN_SECRET));
}

2.3 ThreadLocal工具类

/**
 * <p>
 * 用户SessionVO
 * </p>
 *
 * @author jonil
 * @since 2023/11/10 16:03
 */
@Data
public class UserSessionVO {

    // 用户id
    Integer userId;
    // 用户类型
    Integer userType;
}

/**
 * <p>
 * 用户session上下文
 * </p>
 *
 * @author jonil
 * @since 2023/11/10 16:02
 */
public class UserSessionContext {
    private static ThreadLocal<UserSessionVO> userSessionVOThreadLocal = new ThreadLocal<>();

    public static void set(UserSessionVO userSessionVO) {
        userSessionVOThreadLocal.set(userSessionVO);
    }

    public static UserSessionVO get() {
        return userSessionVOThreadLocal.get();
    }

    public static void remove() {
        userSessionVOThreadLocal.remove();
    }
}

2.4 拦截器

此处拦截器为处理HTTP请求 各阶段需要做的操作。HTTP请求进来的时候将JWT解析的数据放进ThreadLocal,出去的时候需要将数据从ThreadLocal移除,否则会造成内存泄漏。

/**
 * <p>
 * JWT拦截器
 * </p>
 *
 * @author jonil
 * @since 2023/11/10 15:55
 */
public class JWTInterceptor implements HandlerInterceptor {

    // refresh-token密钥
    @Value("${access-token.secret}")
    private String ACCESS_TOKEN_SECRET;

    /**
     * 将用户基本信息添加到ThreadLocal
     * @param request
     * @param response
     * @param handler
     * @return
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String accessToken = request.getHeader("Authorization");
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC512(ACCESS_TOKEN_SECRET)).build();
        DecodedJWT decodedJWT = jwtVerifier.verify(accessToken);

        Integer userId = decodedJWT.getClaim("userId").asInt();
        Integer userType = decodedJWT.getClaim("userType").asInt();

        UserSessionVO userSessionVO = new UserSessionVO();
        userSessionVO.setUserId(userId);
        userSessionVO.setUserType(userType);
        UserSessionContext.set(userSessionVO);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) {

    }

    /**
     * 将用户的基本信息从ThreadLocal移除
     * @param request
     * @param response
     * @param handler
     * @param ex
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) {
        UserSessionContext.remove();
    }
}

2.5 使用

在需要用到JWT内容的地方,使用以下代码即可获取对应的内容

UserSessionVO userSessionVO = UserSessionContext.get();
Integer userId = userSessionVO.getUserId();

3. 进阶

3.1 结合自定义注解实现对方法的鉴权

角色枚举类

public enum UserType {

    systemAdmin(0),
    userAdmin(1),
    maintenancePersonnel(2),
    vipUser(3),
    user(4),
    none(5);

    UserType(int code) {
        this.code = code;
    }

    private int code;

    public int getCode() {
        return code;
    }
}

自定义注解类

/**
 * <p>
 * 自定义校验注解
 * </p>
 *
 * @author jonil
 * @since 2023/11/13 20:05
 */
@Documented
@Target({ ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {PermissionValidator.class})
public @interface Permission {

    UserType type() default UserType.none;

    /**
     * 是否强制校验
     * @return
     */
    boolean required() default true;

    /**
     * 校验不通过时的报错信息
     * @return
     */
    String message() default "权限不足!";

    /**
     * 分组
     * @return
     */
    Class<?>[] groups() default {};

    /**
     * bean的负载
     * @return
     */
    Class<? extends Payload>[] payload() default {};
}

自定义注解实现类

/**
 * <p>
 * 自定义注解实现
 * </p>
 *
 * @author jonil
 * @since 2023/11/13 20:05
 */
public class PermissionValidator implements ConstraintValidator<Permission, UserType> {

    @Override
    public void initialize(Permission constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
    }

    /**
     * 判断当前是否有权限
     * @param userType object to validate
     * @param context context in which the constraint is evaluated
     *
     * @return
     */
    @Override
    public boolean isValid(UserType userType, ConstraintValidatorContext context) {
        return UserSessionContext.get().getUserType() <= userType.getCode();
    }
}

使用

/**
 * 获取信息接口
 */
@Permission(type = UserType.user)
@GetMapping("/info")
public R getInfo() {
    return R.success(service.getInfo());
}

注解会从ThreadLocal获取当前用户的类型,然后进行比对,当然你的判断逻辑可以比这个更加复杂,只需要符合业务实现就好了。

4. 注意

倘若你是在微服务环境或分布式环境下使用这一套逻辑,需要注意在网关(流量网关、业务网关)和OpenFeign中携带原生的Header,否则获取不到该用户的信息。

标签:拦截器,return,自定义,JWT,userType,ThreadLocal,public
From: https://www.cnblogs.com/jonil/p/17831566.html

相关文章

  • SpringBoot定义拦截器+自定义注解+Redis实现接口防刷(限流)
    实现思路在拦截器Interceptor中拦截请求通过地址+请求uri作为调用者访问接口的区分在Redis中进行计数达到限流目的简单实现定义参数访问周期最大访问次数禁用时长#接口防刷配置,时间单位都是秒.如果second秒内访问次数达到times,就禁用lockTime秒access:lim......
  • Oracle sql自定义统计月范围
     思路: 1,使用SUBSTR(to_char(INSPECTION_DATE,'yyyy-mm-dd'),-2)取出天数, 2,使用case……when……then……判断取出的天数是否大于等于25号,如果是则将日期设置成下月第一天 如果小于等于24号,则设置成当月第一天 3,使用TRUNC(ADD_MONTHS(INSPECTION_DATE,1),'mm')增......
  • 自定义GPT已经出现,并将影响人工智能的一切,做好被挑战的准备了吗?
    原创|文BFT机器人OpenAI凭借最新突破:定制GPT站在创新的最前沿。预示着个性化数字协助的新时代到来,ChatGPT以前所未有的精度来满足个人需求和专业需求。自定义GPT具有变革性的特点——可以被定制为任何领域或任务的专家。我们可以想象一下为SEO研究量身定制的数字助理、能够筛选......
  • 微信小程序--自定义tabbar切换页面时,保留数据方案
    自定义的tabbar组件,每次切换页面时都会重新加载页面和数据,需要通过一些方法把tabbar菜单的数据保留下来,不要每次都请求数据。方案一:在app.js文件里定义全局数据(本次项目采用的是可以在后台管理里配置的数据,所以采用了方案一)1、在app.js文件里定义一个全局变量App({  globa......
  • 十三、自定义类型
    用户定义数据类型通俗定义:用户自己设计并实现的数据类型就称为用户自定义数据类型,即使这些数据类型基于系统数据类型。也可以理解为基础类型的一个延伸。用户定义数据类型三要素:1.数据类型的名称2.所基于的系统数据类型3.数据类型的可空性(是否可以为空)USE[Advanc......
  • JWT浅了解
    JWT通过数字签名的方式(让我想起了软考),以json对象为载体,在不同的服务终端之间安全传输信息是一种授权认证生成token的原理:通过header的加密方式,对payload进行加密。然后把header和payload再次加密生成signature贴一下我的settokenreturnJWT.create().withAudience(userId)//......
  • 拦截器
    packagecom.comen.interceptor;importcom.comen.edata.bean.User;importcom.comen.edata.tools.JwtUtil;importorg.springframework.web.servlet.HandlerInterceptor;importorg.springframework.web.servlet.ModelAndView;importjavax.servlet.http.HttpServletR......
  • nuclei 快速&可自定义的基于DSL的漏洞扫描工具
    nuclei是基于golang开发的,可以使用基于yaml定义的dsl,支持扫描不少协议(tcp,dns,http,ssl,file,whois,websocket,headless,以及code)同时nuclei也提供了不少模版可以方便快速使用说明nuclei使用简单,主要包含两步,定义yaml文件,运行,同时提供了大量可用的模版是一个很不错的安全工具,很值......
  • identityserver,OAuth2.0,JWT之间的关系
    OAuth2.0是一种授权框架,用于应用程序之间安全的共享用户资源,它允许用户授权第三方应用程序访问他们的资源,列如照片,视频,联系人列表等,而不必将用户名和密码提供给第三方应用程序,OAuth2.0通过令牌来代表用户授权,这些令牌可以被第三方应用程序用于访问用户资源,而不必知道用户的凭据。O......
  • Android自定义View使用系统自有属性
    原文链接:Android自定义View使用系统自有属性-Stars-One的杂货小窝本篇默认各位有自定义View的相关知识,本篇只作为一个小知识点补充有这样一种情况,比如说我们的一个自定义View中有个maxLines的属性,但是我们会注意到这个maxLines其实Android里面已经存在了(如TextView中),我们能......