找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3198

积分

0

好友

454

主题
发表于 前天 23:58 | 查看: 0| 回复: 0

JWT,全称 JSON Web Token,是目前最流行的跨域身份验证解决方案之一。

在深入探讨 JWT 之前,我们不妨先回顾一下传统的 session 和基于缓存(如 Redis)的 token 认证方案,以及它们在分布式架构下遇到的瓶颈。

在《Session、Cookie 与 Token 的工作原理及区别》一文中对 sessioncookietoken 有更具体的介绍。

传统Session认证的局限性

传统的 session 交互流程如下图所示:

Session认证交互流程图

当浏览器向服务器发起登录请求并验证通过后,用户信息会被存储在服务器内存的 session 中。随后,服务器会生成一个 sessionId 并将其放入响应头的 Set-Cookie 字段,返回给浏览器。

此后,浏览器每次发送请求时,都会自动在请求头的 Cookie 中携带这个 sessionId

浏览器请求携带Cookie示例

服务器通过这个 sessionId 就能查询到对应的 session 信息,从而判断用户是否登录以及获取其身份。

在单服务器架构下,这种模式运作良好。然而,当业务量激增,需要部署多台服务器并通过负载均衡对外服务时,问题就出现了。不同服务器之间的 session 默认是隔离的,用户可能在服务器 A 登录成功,但下次请求被分发到服务器 B 时,却找不到对应的 session 信息,导致需要重新登录,严重影响用户体验。

基于Token(缓存)的方案

为了解决上述问题,引入了基于 token 的方案,其流程如下:

基于Token的认证流程图

在这种方案中,服务器验证用户身份后,会生成一个唯一的 token 返回给客户端。同时,服务器会将这个 token 与用户信息的映射关系存储在如 Redis 这样的分布式缓存中。客户端后续请求时需携带此 token,服务器通过验证缓存中是否存在该 token 来判断其有效性。

这种方案保证了服务的无状态性,易于水平扩展。但无论是 session 还是这种 token 方案,在集群环境下都依赖于 redis 这类第三方缓存来实现数据共享。

那么,有没有一种方案,不需要依赖缓存数据库,就能实现用户信息在集群间的“一次登录,处处可见”呢?答案就是 JWT。

什么是 JWT?

JWT 的主要原理是:服务器在认证用户通过后,生成一个包含用户信息的 JSON 对象,并对其进行签名后发回给用户。一个简化的示例如下:

{
  "sub": "login",
  "userName": "Dylan Smith",
  "userId": "1",
  "userEmail": "junfeng0828@gmail.com",
  "iat": 1730049826
}

之后,用户与服务器通信时,必须在请求(通常在 Authorization 头)中携带这个经过签名的字符串(即 JWT)。服务器通过验证签名来识别用户身份,并直接从 JWT 中读取用户信息,无需查询任何会话存储。这使得服务器完全无状态,易于扩展。

重要提醒:由于 JWT 的负载(Payload)可以被客户端解码,因此切勿在其中存储任何敏感信息(如密码)。

完整的 JWT 认证流程可概括为下图:

JWT认证完整交互流程图

JWT 的组成格式

一个 JWT 由三部分组成,以点(.)分隔:Header(头部)、Payload(负载)和 Signature(签名)。形式如下:

Header.Payload.Signature

JWT结构示意图

1. Header

头部通常是一个 JSON 对象,描述 token 类型和签名算法,例如:

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

该 JSON 经过 Base64URL 编码后形成 JWT 的第一部分。

2. Payload

负载部分也是一个 JSON 对象,用于存放实际需要传递的数据。JWT 官方定义了 7 个标准字段(可选),如签发者 (iss)、过期时间 (exp)、主题 (sub) 等。你还可以在此定义自定义的私有字段。

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

注意:JWT 默认是不加密的,任何人都可以解码看到这部分内容,所以不要存放机密信息。该 JSON 同样会经过 Base64URL 编码,形成 JWT 的第二部分。

3. Signature

签名部分是对前两部分的签名,用于防止数据被篡改。生成签名的算法在 Header 中指定(如 HS256),计算方式如下:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

其中 secret 是只有服务器知道的密钥。计算出的签名再经过 Base64URL 编码,形成 JWT 的第三部分。

最后,将编码后的三部分用点连接起来,就得到了完整的 JWT。

4. Base64URL 编码

Header 和 Payload 使用的 Base64URL 算法与标准 Base64 类似,但为了适应 URL 环境,它将字符 + 替换为 -/ 替换为 _,并省略末尾的填充字符 =

SpringBoot 整合 JWT 实践

理论讲完,接下来我们动手在 Spring Boot 项目中整合 JWT。

第一步:添加依赖

首先创建一个 Spring Boot 项目,并在 pom.xml 中添加 JWT 依赖(这里使用 java-jwt 库):

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>

第二步:定义用户信息载体

创建一个 UserToken 类,用于封装将要存入 JWT 的用户信息:

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class UserToken implements Serializable {
    private static final long serialVersionUID = 1L;
    private String userId;
    private String userEmail;
    private String userName;
}

第三步:编写 JWT 工具类

创建 JwtTokenUtil 工具类,负责生成和验证 Token:

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import java.util.Date;

public class JwtTokenUtil {
    // 定义 token 返回头部
    public static final String AUTH_HEADER_KEY = "Jwt";
    public static final String TOKEN_PREFIX = "Dylan ";
    public static final String KEY = "test_key";
    public static final Long EXPIRATION_TIME = 1000L * 60 * 60; // 1小时

    /**
     * 创建 TOKEN
     * @param content
     * @return
     */
    public static String createToken(String content) {
        return TOKEN_PREFIX + JWT.create()
               .withSubject(content)
               .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
               .sign(Algorithm.HMAC512(KEY));
    }

    /**
     * 验证 token
     * @param token
     */
    public static String verifyToken(String token) throws Exception {
        try {
            return JWT.require(Algorithm.HMAC512(KEY))
                   .build()
                   .verify(token.replace(TOKEN_PREFIX, ""))
                   .getSubject();
        } catch (TokenExpiredException e) {
            throw new Exception("token已经过期,请重新登录", e);
        } catch (JWTVerificationException e) {
            throw new Exception("token验证失败!", e);
        }
    }
}

第四步:配置拦截器与跨域

创建一个全局配置类 GlobalWebMvcConfig,配置跨域并注册认证拦截器:

@Slf4j
@Configuration
public class GlobalWebMvcConfig implements WebMvcConfigurer {
    /**
     * 配置跨域
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
               .allowedOrigins("*")
               .allowCredentials(true)
               .allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS", "HEAD")
               .allowedHeaders("*")
               .exposedHeaders("Server", "Content-Length", "Authorization", "Access-Token", "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials");
    }

    /**
     * 添加拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/**").excludePathPatterns("/static/**");
    }
}

第五步:实现认证拦截器

创建 AuthenticationInterceptor 拦截器,在请求到达控制器前验证 JWT:

@Slf4j
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从 http 请求头中取出 token
        final String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
        // 如果不是映射到方法直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        // 如果是方法探测(OPTIONS请求),直接通过
        if (HttpMethod.OPTIONS.equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }
        // 如果方法有 JwtIgnore 注解,直接通过
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        if (method.isAnnotationPresent(JwtIgnore.class)) {
            JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class);
            if (jwtIgnore.value()) {
                return true;
            }
        }
        // 判断 token 不为空
        LocalAssert.isStringEmpty(token, "token为空,认证失败!");
        // 验证 token 并获取 token 内部信息
        String userToken = JwtTokenUtil.verifyToken(token);
        // 将 token 放入本地线程上下文
        WebContextUtil.setUserToken(userToken);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 方法结束后,移除缓存的 token
        WebContextUtil.removeUserToken();
    }
}

对于不熟悉拦截器的朋友,可以参考这篇文章:Spring Boot拦截器详解

第六步:创建登录接口

Controller 中实现登录逻辑,成功验证后生成 JWT 并放入响应头:

/**
 * 登录
 * @param userDto
 * @return
 */
@JwtIgnore
@RequestMapping(value = "/login", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
public void login(@RequestBody UserDto userDto, HttpServletResponse response) {
    //... 参数合法性验证
    // 从数据库获取用户信息
    User dbUser = userService.selectByUserId(userDto.getUserId);
    //... 用户名和密码验证
    // 创建 token 并将 token 放入响应头
    UserToken userToken = new UserToken();
    userToken.setUserId(dbUser.getUserId);
    userToken.setUserEmail(dbUser.getUserEmail);
    userToken.setUserName(dbUser.getUserName);
    String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken));
    response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, token);
}

辅助工具类

  1. @JwtIgnore 注解:用于标记无需Token验证的方法(如登录、获取验证码接口)。
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface JwtIgnore {
    boolean value() default true;
    }
  2. WebContextUtil 工具类:利用 ThreadLocal 在请求线程内存储和获取已解析的用户信息。
    public class WebContextUtil {
    // token 本地线程缓存
    private static ThreadLocal<String> local = new ThreadLocal<>();
    /**
     * 设置 token 信息
     * @param content
     */
    public static void setUserToken(String content) {
        removeUserToken();
        local.set(content);
    }
    /**
     * 获取 token 信息
     * @return
     */
    public static UserToken getUserToken() {
        if (local.get() != null) {
            UserToken userToken = JSONObject.parseObject(local.get(), UserToken.class);
            return userToken;
        }
        return null;
    }
    /**
     * 移除 token 信息
     * @return
     */
    public static void removeUserToken() {
        if (local.get() != null) {
            local.remove();
        }
    }
    }

测试验证

启动项目后,使用 Postman 等工具调用登录接口:

Postman测试登录接口返回JWT

可以看到,响应头中已经包含了生成的 JWT(Jwt 字段)。当需要请求其他受保护的接口时,只需在请求头的 Headers 中添加相同的 Jwt 参数即可通过拦截器的验证。

通过以上步骤,我们就实现了一个基于 Spring Boot 和 JWT 的无状态身份认证系统。它有效地解决了分布式环境下的 Session 共享难题,简化了架构,是构建现代微服务和 API 服务的优选方案。如果你在实践过程中遇到问题,欢迎在云栈社区的技术论坛与其他开发者交流探讨。




上一篇:干货分享:Windows Process Monitor实时监控文件注册表网络活动
下一篇:Python 实现 GJR-GARCH 波动率择时策略:完整代码回测与解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-3 01:34 , Processed in 0.323632 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表