Spring Boot:用JWT令牌和拦截器实现登录认证(含测试过程和关键注解讲解)

在前后端分离的架构中,传统的 Session 认证因为依赖服务端存储,难以应对分布式和跨域场景。JWT 凭借其无状态、自包含的特性,成为了现代 Web 开发的主流选择。

本文将带你使用 Spring Boot 结合 JWT 和自定义拦截器,快速搭建一套安全、高效的登录认证系统

一、配置依赖

复制代码
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/big_event
    username: root
    password: 123456

<dependencies>
      <!--jwt依赖-->
      <dependency>
          <groupId>com.auth0</groupId>
          <artifactId>java-jwt</artifactId>
          <version>4.4.0</version>
      </dependency>
  </dependencies>

二、为什么要登录认证和登录认证的流程

登录认证的设计主要是为安全性考虑:

1.首先,为了保护用户信息和防止数据泄露,篡改,我们必须保证用户的身份,划分权限,只有有权限才能提供相应服务,有些场景甚至需要实名认证,以保证用户信息的真实,合法,有效。

2.其次,若用户数据异常、系统故障,可以根据登录信息记录定位责任人,便于问题排查和责任追溯。

3.最后,保障服务合规性,多数场景(如政务服务、金融、电商)需通过登录认证确认用户身份,符合行业规范和安全要求,建立服务方与用户之间的合法服务关系。

登录认证流程:

用户提交凭证

用户在登录页面输入用户名(或邮箱/手机号)和密码。

后端校验与比对

服务器接收到请求后,首先会验证数据的完整性。随后,系统会根据用户名在数据库中查询对应的用户记录。

令牌颁发

一旦密码比对成功,服务器会生成一个临时的身份凭证------通常是 JSON Web Token。这个令牌包含了用户的 ID、过期时间等元数据,并经过数字签名以防篡改。

客户端存储与后续交互

服务器将生成的令牌返回给客户端。前端通常会将其存储在本地存储或 Cookie 中。然后前端就能在请求头中携带这个令牌,以证明用户身份,从而实现无状态认证。

三、JWT令牌

令牌的要求:

看完登录认证的流程以后,我们可以想一想,一个令牌应当具有哪些功能,有什么要求?

1、首先,令牌必须可靠,不能被篡改,也不能被破解,作为令牌其本身必须要足够令人信服。

2、其次,令牌还要包含用户的所有必要信息,这样后端就无需频繁查数据库,减轻数据库压力。

3、它必须能被任何平台解密,只要具备相应算法和密钥就能解开。

JWT 的结构(三段式)

JWT 的字符串格式非常独特,由三部分组成,中间用点号 . 分隔:Header.Payload.Signature

  1. Header(头部):声明令牌的类型(JWT)以及加密算法(如 HMAC SHA256)。
  2. Payload(载荷) :存放有效信息的地方。这里存放用户 ID、过期时间等。注意:这里只是 Base64 编码,不是加密,所以千万不要放密码等敏感信息。
  3. Signature(签名):这是防伪标识。它是用"头部 + 载荷 + 服务器私有的密钥"通过算法生成的。一旦前两部分被篡改,签名就会对不上,服务器就会拒绝请求

创建JWT

在实际开发中,我们通常会写一个JWT工具类,以实现令牌的生成和解密。

java 复制代码
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

import java.util.Date;
import java.util.Map;

public class JwtUtil {

    private static final String KEY = "hzy";

    //接收业务数据,生成token并返回
    public static String genToken(Map<String, Object> claims) {
        return JWT.create()
                .withClaim("claims", claims)//数据的名字和存的东西
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))//设置过期时间
                .sign(Algorithm.HMAC256(KEY));//加密方式
    }

    //接收token,验证token,并返回业务数据
    public static Map<String, Object> parseToken(String token) {
        return JWT.require(Algorithm.HMAC256(KEY))
                .build()//创建检查器
                .verify(token)//验证
                .getClaim("claims")//获取名为claims的数据
                .asMap();//强制转换
    }

}

四、拦截器的建立与注册

在此之前,先介绍一个工具,ThreadLocal,可以将从令牌中解密的用户信息放入其中。这个工具是线程安全的,我们给用户提供服务的时候通常是一个用户开一个线程,有了这个传参会很方便。

下面展示ThreadLocal工具类的实现:

java 复制代码
package org.example.utils;
import java.util.HashMap;
import java.util.Map;

/**
 * ThreadLocal 工具类
 */
@SuppressWarnings("all")
public class ThreadLocalUtil {
    //提供ThreadLocal对象,
    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

    //根据键获取值
    public static <T> T get(){
        return (T) THREAD_LOCAL.get();
    }

    //存储键值对
    public static void set(Object value){
        THREAD_LOCAL.set(value);
    }


    //清除ThreadLocal 防止内存泄漏
    public static void remove(){
        THREAD_LOCAL.remove();
    }
}

值得注意的是ThreadLocal用完以后必须要清空,防止数据泄露。

拦截器是什么:

拦截器就像一个大门的安检员,它负责在请求到达业务代码前,对请求进行拦截和检查,统一进行身份校验。

为什么这么做?这样可以保证从始至终只进行一次JWT的解码,降低代码耦合度。我们没有必要在每个业务代码前都安置一个检查令牌的安检员。

拦截器的具体实现类:

结合我们前面提到的ThreadLocal工具:

java 复制代码
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import net.bytebuddy.asm.Advice;
import org.example.pojo.Result;
import org.example.utils.JwtUtil;
import org.example.utils.ThreadLocalUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.util.Map;

@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //令牌验证
        String token= request.getHeader("Authorization");
        try {
            Map<String, Object> claims = JwtUtil.parseToken(token);
            //把业务数据加入到TreadLocal
            ThreadLocalUtil.set(claims);
            return true;
        } catch (Exception e) {
            //http响应状态码为401
            response.setStatus(401);
            //不放行
            return false;
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //释放threadlocal
        ThreadLocalUtil.remove();;
    }
}
工作原理:三段式生命周期

拦截器不仅仅是在"之前"拦截,它其实贯穿了请求的整个生命周期。在 Spring Boot 中,我们通常实现 HandlerInterceptor 接口,它包含三个关键方法:

  1. preHandle(事前拦截 - 最重要)
    • 时机 :Controller 方法执行之前
    • 作用 :这是做登录认证的最佳位置。我们在这里校验 JWT 令牌。
    • 返回值 :返回 true 表示放行(继续往下走);返回 false 表示拦截(请求终止)。
  2. postHandle(事后处理)
    • 时机 :Controller 方法执行之后 ,但在视图渲染或数据返回给用户之前
    • 作用:可以对数据进行微调,或者添加一些通用的模型数据。
  3. afterCompletion(最终完成)
    • 时机:整个请求结束之后(视图渲染完毕)。
    • 作用:通常用于资源清理,比如记录日志、计算接口耗时、释放数据库连接等。

拦截器注册:

具体注册方式就是让WebConfig实现WebMvcConfigurer接口,并重写方法

注册时可以规定拦截器可以直接放行哪些业务代码,比如说登录功能就不能被拦截器拦截,因为不能因为用户没登录就不让用户登录。

java 复制代码
import org.example.interceptors.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","/user/register");
    }
}

五、Controller层实现

登录接口的实现:

java 复制代码
@PostMapping("/login")
    public Result<String> Login(@Pattern(regexp = "^\\S{5,16}$") String username,@Pattern(regexp = "^\\S{5,16}$") String password){
        //根据用户名查询用户
        User Loginuser= userService.findByUserName(username);
        //该用户是否存在
        if(Loginuser==null){
            return Result.error("用户名错误");
        }
        //密码是否正确
        if(Md5Util.getMD5String(password).equals(Loginuser.getPassword())){
            Map<String,Object> claims=new HashMap<>();
            claims.put("id",Loginuser.getId());
            claims.put("username",Loginuser.getUsername());
            //登录成功
            String token=JwtUtil.genToken(claims);
            return Result.success( token);
        }
        return Result.error("密码错误");
    }

密码正确后直接生成令牌返回到前端。

获取用户信息接口的实现:

java 复制代码
@GetMapping("/userInfo")
    public Result<User> userInfo(@RequestHeader(name="Authorization") String token){
        Map<String,Object>map=ThreadLocalUtil.get();
        String username= (String)map.get("username");
        User user= userService.findByUserName(username);
        return Result.success(user);
    }

从请求中获取带有Authorization请求头的令牌,获取用户名后去数据库中查找用户信息。

六、总结

至此,登录认证的功能就完成了,这套方案利用 JWT 的无状态特性,配合拦截器的"守门员"机制,实现了轻量级且安全的接口保护。

相关推荐
小兔崽子去哪了2 小时前
华为 IODT 设备接入
java·华为
摇滚侠2 小时前
Groovy 如何给集合中添加元素
java·开发语言·windows·python
Java水解2 小时前
Go语言中的Pool:对象复用的艺术
后端·go
无巧不成书02182 小时前
Java异常体系与处理全解:核心原理、实战用法、避坑指南
java·开发语言·异常处理·java异常处理体系
8Qi83 小时前
RabbitMQ高级篇:消息可靠性、幂等性与延迟消息
java·分布式·微服务·中间件·rabbitmq·springcloud
yxl_num3 小时前
Docker 完整部署一个包含 Spring Boot(依赖 JDK)、MySQL、Redis、Nginx 的整套服务
java·spring boot·docker
大鹏说大话3 小时前
Go语言Channel并发编程实战:从基础通信到高级模式
开发语言·后端·golang
Jacky-0083 小时前
Rust安装(MinGw64编译器安装)
开发语言·后端·rust
好家伙VCC3 小时前
**发散创新:基于Python的自动化恢复演练框架设计与实战**在现代软件系统运维中,
java·开发语言·python·自动化