首页 > 编程语言 >JAVA拦截器配合JWT、ThreadLocal的登录校验

JAVA拦截器配合JWT、ThreadLocal的登录校验

时间:2024-10-21 17:18:58浏览次数:3  
标签:令牌 拦截器 JAVA JWT 线程 return public

@TOC

拦截器配合JWT、ThreadLocal的登录校验

关于为什么要写这篇文章,今天在做项目的时候发现配置了拦截器,但是不生效,最后排查半天发现引入包有问题,遂决定写一篇详细的拦截器的使用。举例也都是根据案例写的,可能会有些许阅读困难,这里面的示例的TOKEN是在请求头里面的。

使用技术

  • 拦截器Interceptor
  • 令牌Jwt
  • ThreadLocal

技术简介

拦截器:Spring Mvc提供的一种设计模式和编程结构,用于在程序的特定点(方法调用之前和调用之后)插入逻辑,以扩展或修改方法的基本行为。拦截器通常用于日志记录、权限检查、事务处理等场景。

JWT令牌:JWT是一种用于在网络上安全传输信息的令牌,通过‌数字签名的方式,以JSON对象为载体,在不同的服务终端之间安全地传输信息。

ThreadLocal:Java中的一个工具类,提供了线程局部变量的支持。它的主要功能是在多个线程之间隔离变量,使每个线程都拥有自己的变量副本,避免线程之间的数据共享和冲突。

JWT令牌

基本构成

JWT有三部分构成,由.间隔。三个部分分别是请求头(Header)、有效负载(Payload)、签名(Signature)。例如eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ind3dy5iZWpzb24uY29tIiwic3ViIjoiZGVtbyIsImlhdCI6MTcyOTQ5NjY2NywibmJmIjoxNzI5NDk2NjY3LCJleHAiOjE3Mjk1ODMwNjd9.bHGHTdcaQrcOJEv0F_pJ48_VBbboUtjQCKBcSdjN3r4(这个JWT是从网站jwt解密/加密 - bejson在线工具拿到的一个示例数据)

请求头包含令牌的类型和签名所使用的算法(一般使用的是HS256,这个算法不是加密,可以编码和解码)。

例如上面的头存储的是

{
    "alg": "HS256",
    "typ": "JWT"
}

负载包含所需传递的数据或声明(Claims)。声明可以是关于实体(例如用户)及其权限的信息,或者是自定义信息。有效负载中的数据并没有加密,因此不应包含敏感信息。

例如上面的负载存储的是

{
    "username": "www.bejson.com",
    "sub": "demo",
    "iat": 1729496667,
    "nbf": 1729496667,
    "exp": 1729583067
}

签名是用头部和有效负载的内容以及密钥创建,以防止数据篡改。签名的生成过程是将头部和有效负载进行编码,并用声明的算法和密钥进行加密。值得一提的是,签名也可以指定有效时间,从生成后多久后失效!

令牌密钥的保存方式

令牌密钥有很多种保存方式,但是不管用啥方式,都要保证密钥别丢了,常见的有这几种

  • 直接写在类里(不安全,但是方便项目转移),直接定义就行,不举例了。
  • 写在配置文件里面(如properties文件、yml文件,也不安全,方便转移),如果在yml中配置好了如下
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: 密钥
    # 设置jwt过期时间
    admin-ttl: 时间(单位毫秒)

就可以在一个封装类中使用@ConfigurationProperties注解获取,例如

@Component
@ConfigurationProperties(prefix = "jwt")
@Data
public class JwtProperties {

    /**
     * 管理端员工生成jwt令牌相关配置
     */
    private String adminSecretKey;
    private long adminTtl;
}
  • 写在环境变量里面(和计算机绑在一起,相对安全),如果要写在环境变量里面运行cmd命令行,分别执行下面三条语句。
 set JWT_SECRET=你想要的密钥
 
 setx JWT_SECRET %JWT_SECRET%
 
 echo %JWT_SECRET%

然后在Java中使用以下代码就能够获取了

String signKey = System.getenv(JWT_SSECRET);

Java中生成JWT令牌

使用之前自然是引入依赖(不同版本在实际定义的时候可能会有不同的写法)

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

Java中生成令牌和解析令牌,尤其是解析令牌,可能会非常常用,这种非常常用的方法就最好封装成工具类,到时候直接调用封装好的方法就可以了。提供一个封装好的JWT令牌生成方法:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

}

这个方法提供了生成TOKEN和解析TOKEN的方法,生成TOKEN调用JwtUtil.createJWT(密钥,过期时间,负载)这个方法,解析TOKEN就调用JwtUtil.parseJWT(密钥,TOKEN)这个方法。

下面是一个生成TOKEN的实例,使用的是令牌密钥保存方式的第二种

@PostMapping("/login")
@ApiOperation(value = "员工登录")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
    log.info("员工登录:{}", employeeLoginDTO);

    Employee employee = employeeService.login(employeeLoginDTO);

    //登录成功后,生成jwt令牌,没成功已经在service层抛出了自定义异常,结束了方法
    Map<String, Object> claims = new HashMap<>();
    claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
    String token = JwtUtil.createJWT(//主要是这里,这里生成了Token
        jwtProperties.getAdminSecretKey(),
        jwtProperties.getAdminTtl(),
        claims);

    EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()//这个是封装返回给前端的数据了,把Token封装在了里面
        .id(employee.getId())
        .userName(employee.getUsername())
        .name(employee.getName())
        .token(token)
        .build();

    return Result.success(employeeLoginVO);
}

Java中解析JWT令牌

解析令牌用到的还是上面提到的工具类,放一个示例,具体是要在拦截器中实现的,并且加入userId变量到ThreadLocal中:

//2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:{}", empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }

ThreadLocal

ThreadLocal适用于以下环境:

  • 多线程环境下,需要保证线程安全性的数据访问
  • 多个方法之间需要共享数据,但又不希望使用传递参数的形式
  • 常用语例如数据库链接、用户会话、线程上下文信息传递等

优点:

  • 简化代码:提供了一种简单的方式来为每个线程存储和访问独立的变量,提高了代码的可读性和易维护性
  • 避免并发问题:由于每个线程都有自己的变量副本,避免了并发修改和数据竞争问题

缺点:

  • 内存泄漏:如果使用不当,例如线程没有及时调用remove(),可能会导致内存泄露,特别是在长生命周期的线程(线程池)中
  • 调试困难:由于每个线程都有自己的副本,可能导致调试和测试变得更加复杂,难以追踪数据流动
  • 不可恢复的状态:在某些情况下,线程中的数据是不可恢复的,一旦丢失,其他线程无法访问

简单实例

public class ThreadLocalTest {
    //创建ThreadLocal对象
    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            THREAD_LOCAL.set("这是第一个线程的数据");//设置局部变量的值
            getData("t1");//获取局部变量的值
        }, "t1");

        Thread t2 = new Thread(() -> {
            THREAD_LOCAL.set("这是第二个线程的数据");//设置局部变量的值
            getData("t2");//获取局部变量的值
        }, "t2");

        //两个打印顺序不一定相同
        t1.start();//打印结果:t1-这是第一个线程的数据
        t2.start();//打印结果:t2-这是第二个线程的数据

    }

    private static void getData(String threadName){
        Object data = THREAD_LOCAL.get();
        System.out.println(threadName+"-"+data);
    }
}

这个在实际开发中也是可以定义一个封装类的,里面写好set()、get()、remove()的方法,到时候直接调用就可以,不同再从定义一个ThreadLocal对象开始。

例如:

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

拦截器

补充一点:拦截器和AOP是两个密切相关的概念,但是它们不是完全相同的东西。拦截器是一种设计模式,主要用于方法调用前后进行额外操作;AOP是一种编程范式,旨在通过将跨切关注点(如事务处理、日志记录、权限校验等)分离出来,从而提高代码的模块化程度。可以认为,拦截器是实现AOP的一个重要手段。AOP提供了一种更为全面的编程范式,涵盖了拦截器的使用以及切面、连接点、切入点等更多的概念。

拦截器的实现分三步:

  1. 自定义拦截器,实现拦截器接口HandlerInterceptor
  2. 将拦截器添加到容器中,一般是一个实现WebMvcConfigurer接口的配置类,这个类重写addInterceptors()方法
  3. 配置拦截器的拦截规则

自定义拦截器

写一个类,实现接口HandlerInterceptor,记得加上注解@Component,里面的三个方法按需要重写

方法名执行时机
preHandle()方法在请求处理之前被调用,即在控制器方法执行之前。实现处理器的预处理(如登录检查)
postHandle()方法在控制器方法执行之后,但在视图渲染之前调用。只要preHandle()返回true,这个一定执行。
afterCompletion()方法在视图渲染完成后调用,即整个请求处理流程的最后阶段。不管preHandle()返回true还是false,这个一定执行。

所以关键要重写的方法就是preHandle(),如果用到了ThreadLocal并且设置了线程变量,那么一定记得要在afterCompletion()方法中使用remove()把那个设置的值给删除,不然会内存泄露!

然后在这要注意一件事,如果你要判断拦截到的是Controller的方法或者是其它动态资源,会用到一个多态的判断,判断这个方法是不是动态方法,这里导入的包应该是org.springframework.web.method.HandlerMethod,如果不是这个包,可能会出现一个问题,就是不管什么方法都放行!!这也是我写这个文章的原因,后面如果有多个包需要引入对应的我可能还会往上加,或者写新的笔记

示例:定义一个拦截器,进行令牌校验,然后在ThreadLocal里面设置好用户的ID,可以按需设置

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;//前面JWT令牌的保存方式定义了

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {//一定要导对包,看上面的加粗提示
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:{}", empId);
            //这个BaseContext在ThreadLocal的简单示例里面也定义了
            BaseContext.setCurrentId(empId);//在这里给线程定义好了Id,被拦截器拦截的方法就可以直接获取Id了
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        BaseContext.removeCurrentId();//写上这个,防止内存泄漏!一定要写!
    }
}

拦截器添加到容器中,同时定义拦截路径

这里采用的方式是一个实现WebMvcConfigurer的接口的类,记得在类上加@Configuration注解,声明这是一个配置类,重写里面的addInterceptors()方法。

例如:这个只是一个举例,是单独的,再下面那个举例才是和上面的代码相关的

@Configuration
public class WebConfig implements WebMvcConfigurer {
    //需要填充容器
    @Autowired
    private TokenInterceptor tokenInterceptor;

    public void addInterceptors(InterceptorRegistry registry) {
        
        registry.addInterceptor(tokenInterceptor)
            .addPathPatterns("/**")//addPathPatterns是添加拦截路径,写什么具体参考下表
            .excludePathPatterns("/login");//excludePathPatterns是排除路径,添加不需要拦截的路径
    }
}
拦截路径说明
/**所有子路径,例如/user/user/login都可以拦截
/*只拦截一级路径,例如可以拦截/user,但是/user/login就不能拦截

实际使用示例:

/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;

    /**
     * 注册自定义拦截器
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login");//拦截所有非登录的管理员路径
}

到此为止,一个拦截器已经正式配置完成上线了,并且也在放行的动态方法的线程ThreadLocal中加入了ID,可以在前端不传递ID的情况下做一些和ID相关的操作了。

例如:我们要查询一个用户的所有信息,这个请求路径是/admin的GET方法,通过了拦截器,这时候前端就不用传递给我们ID了,我们只需要在service层使用如下方法就可以从数据库获取数据了

@Override
public List<Employee> showShoppingCart() {
    return employeeMapper.getById(BaseContext.getCurrentId());//这个BaseContext的定义去看ThreadLocal里面的示例
}

补充一个指定拦截(指定拦截对一个路径的方法请求,例如同一路径放行GET,但是不放行POST等)

这个一般不常用,因为一般就用Spring Security来做了,如果项目比较简单可以通过以下方式(使用了正则表达式)来实现,这里用到的示例是我以前写的一个简单的系统。

@Component
public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String method = request.getMethod();//获取请求的方法
        String path = request.getRequestURI();//获取请求的路径

        //检查是否是可以直接放行的请求
        if (isAllowedRequest(method, path)) {
            return true;
        } else {
            //获得TOKEN
            String token = request.getHeader("token");
            //判断是否为空,空则返回401并结束方法
            if (!StringUtils.hasLength(token)) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return false;
            }
            //解析TOKEN
            try {
                JwtUtils.parseJWT(token);
            }catch (Exception e) {
                //如果TOKEN过期或者错误也返回401
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                return false;
            }
            //走到这一步一定是正确的了
            return true;
        }
    }

    //用来判断接收的方法和路径是否是可以直接放行的
    //我们在这放行了GET的/tickets路径请求,/tickets/1(这里是数字就行)路径请求,/comments路径请求;POST的/users/login路径请求,/users/register路径请求,/admins/login路径请求。
    private boolean isAllowedRequest(String method, String path) {
        if ("GET".equalsIgnoreCase(method)) {
            return path.matches("^/tickets(/\\d+)?$") || path.matches("/comments");
        } else if ("POST".equalsIgnoreCase(method)) {
            return path.equals("/users/login") || path.equals("/users/register") || path.equals("/admins/login");
        }
        return false;
    }
}

标签:令牌,拦截器,JAVA,JWT,线程,return,public
From: https://blog.csdn.net/2403_86693263/article/details/143117797

相关文章

  • Linux下安装JDK1.8,CentOS7安装JDK1.8/Java8
    一、卸载自带的先检查是否有安装自带的openjdkrpm-qa|grepjava如果有,卸载rpm-e--nodeps#openjdk的名字逐个卸载完之后,确保java-version没有东西二、下载上传下载tar.gz安装包到本地例如zulu的https://www.azul.com/downloads/#downloads-table-zulu例如/usr/bi......
  • 基于java+springboot的智慧博物馆预约平台
    文章目录前言项目介绍技术介绍功能介绍核心代码数据库参考系统效果图前言文章底部名片,获取项目的完整演示视频,免费解答技术疑问项目介绍  伴随着我国社会的发展,人民生活质量日益提高。于是对智慧博物馆预约管理进行规范而严格是十分有必要的,所以许许多多的信息......
  • 自学狂神说java第二点
    计算机硬件Cpu主板内存电源主机箱硬盘显卡鼠标键盘显示器等装机CpuMemory内存Motherboard主板IO设备输入输出设备input输入/output输出冯诺依曼体系结构软件计算机按照其功能分为系统软件和应用软件系统软件:Windows,linux,Mac,Android,ios应用......
  • Java消息队列详解
    消息队列的作用及原理消息队列产生主要是为了解决系统间的异步解耦与确保数据最终一致性问题。通过将主流程与辅助流程分离,使得辅助任务可以并行处理,不仅提高了系统的响应速度,还增强了其可扩展性和稳定性。此外,消息队列机制保证了每条消息至少被消费一次,从而确保了业务逻辑的......
  • Java基础---异常
    1.概述Java异常处理是Java语言的一个重要特性,它可以帮助我们更好地管理程序中的错误和异常情况。本文档将详细介绍Java中的异常处理机制,包括异常的概念、分类、捕获和处理方法。2.异常概念异常(Exception)是在程序执行过程中发生的不正常情况,它会打断程序的正常流程。Java语......
  • Java中的基础知识点---Object
    Object类的常见方法有哪些?Object类是一个特殊的类,是所有类的父类,主要提供了以下11个方法:/** *native方法,用于返回当前运行时对象的Class对象,使用了final关键字修饰,故不允许子类重写。 */publicfinalnativeClass<?>getClass()/** *native方法,用于返回......
  • Java列表list
    List列表创建列表//List的ArrayList实现List<String>list1=newArrayList<>();//List的LinkedList实现List<String>list2=newLinkedList<>();常用方法importjava.util.List;importjava.util.LinkedList;classMain{publicstatic......
  • 【Javaee】网络编程-UDP基础
     前言UDP是一个高效、快速、简单的传输协议,适合于需要低延迟和实时性的应用本篇将介绍UDP相关的api,并使用UDP构建回显服务器程序。一.UDP与TCP特点UDP:无连接,不可靠,面向数据报,全双工。TCP:有连接,可靠,面向字节流,全双工。何为连接?此处所说的连接是抽象的连接,并不是实际......
  • 【java】实现字节数组转int(采用IEEE 754标准)
    /***字节数组转int*采用IEEE754标准**@parambytes*@returnfloat*/publicintbytesToInt(byte[]bytes){//获取字节数组转化成的2进制字符串StringbinaryStr=bytesToBinaryStr(bytes);//......
  • 【最新Java必过毕设选题】基于微信小程序自助购药小程序全套(程序+万字(源码+万字LW+答
    作品简介 Hi,各位同学好呀!今天向大家分享一个最新完成的高质量毕业设计项目作品基于ssm+uniapp的XXX微信小程序项目评分(最低0分,满分5分)难度系数:3分工作量:5分创新点:3分界面美化:5分使用技术小程序框架:uniapp小程序开发软件:HBuilderX小程序运行软件:微信开发者......