JWT详解:从登录认证到令牌验证

一、引入

回想一下传统的Web应用是怎么做登录认证的:

用户输入用户名密码登录成功,服务器在session里记下"用户已登录",然后把sessionId通过cookie返回给浏览器。浏览器下次请求时带上这个cookie,服务器就能识别出用户身份。

这套方案在单机时代没问题,但到了分布式时代就尴尬了:

  • 用户登录了A服务器,下次请求被负载均衡到B服务器,B服务器不认识这个session

  • 解决方法是session共享,要么存数据库,要么用Redis,但多了一次网络开销

  • 如果做移动端App,cookie用起来更别扭

那有没有一种办法,让服务器不保存登录状态,用户自己把身份信息带过来,服务器验证一下就行?这就是JWT(JSON Web Token)的核心理念。

二、JWT是什么?

JWT全称是JSON Web Token,它本质上是一个字符串,由三部分组成,用点(.)连接:

这个字符串就是用户的"身份证"。用户登录成功后,服务器生成这个令牌返回给客户端。客户端以后每次请求都带着它,服务器只要验证令牌合法,就知道用户是谁。

整个过程中,服务器不需要保存任何session数据,所以特别适合分布式系统和微服务架构。

三、JWT的三段结构

把JWT拆开看,每一段都有自己的作用:

3.1 Header(头部)

Header通常由两部分组成:

  • typ:令牌类型,固定是"JWT"

  • alg:签名算法,比如HS256、RS256

json

复制代码
{
  "alg": "HS256",
  "typ": "JWT"
}

这个JSON对象经过Base64Url编码,就成了JWT的第一段。

打个比方,Header就像是身份证上的"中华人民共和国"这几个字,告诉别人这是个什么证件。

3.2 Payload(载荷)

Payload里放的是实际需要传递的数据,比如用户ID、用户名、过期时间等。

json

复制代码
{
  "sub": "1234567890",
  "name": "张三",
  "exp": 1516239022,
  "iss": "auth-server"
}

这里有几个预定义的字段(但都不是强制的):

  • sub:主题(通常是用户ID)

  • exp:过期时间

  • iat:签发时间

  • iss:签发者

  • aud:接收方

你还可以加任意自定义字段,比如roleavatar等。

这个JSON同样经过Base64Url编码,成了JWT的第二段。

注意 :Payload只是Base64编码,不是加密!里面的内容任何人只要解码就能看到。所以千万不要放密码、手机号等敏感信息

3.3 Signature(签名)

第三段是签名,它的作用是防止数据被篡改

签名的生成过程:

  1. 把编码后的Header和Payload用点连接:header.payload

  2. 用Header里声明的算法(比如HS256),加上一个只有服务器知道的密钥,对上面的字符串进行加密

  3. 得到的结果就是签名

java 复制代码
HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret
)

服务器收到JWT后,会重新计算一遍签名。如果计算出来的签名和JWT自带的签名一致,说明数据没有被篡改;如果不一致,说明这个令牌被改过了,直接拒绝。

四、JWT的工作流程

用一个具体的例子走一遍流程:

4.1 用户登录

java 复制代码
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request) {
    // 1. 验证用户名密码
    User user = userService.login(request.getUsername(), request.getPassword());
    
    // 2. 生成JWT
    String jwt = JwtUtil.createToken(user.getId(), user.getUsername());
    
    // 3. 返回给客户端
    return Result.success(jwt);
}

4.2 服务器生成JWT

服务器生成JWT的步骤:

java 复制代码
public class JwtUtil {
    // 密钥(应该配置在配置文件里,不要写死在代码里)
    private static final String SECRET = "mySecretKey";
    
    // 过期时间(1小时)
    private static final long EXPIRE_TIME = 60 * 60 * 1000;
    
    public static String createToken(Long userId, String username) {
        // 1. 设置过期时间
        Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        
        // 2. 创建JWT Builder
        JwtBuilder builder = Jwts.builder()
                // 设置Header(算法)
                .setHeaderParam("alg", "HS256")
                .setHeaderParam("typ", "JWT")
                // 设置Payload
                .setSubject(String.valueOf(userId))
                .claim("username", username)
                .setIssuedAt(new Date())
                .setExpiration(expireDate)
                // 签名
                .signWith(SignatureAlgorithm.HS256, SECRET);
        
        // 3. 生成token
        return builder.compact();
    }
}

生成的JWT大概是这个样子:

复制代码
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOiLlvKDkuIkiLCJleHAiOjE2MTUyMzkwMjIsImlhdCI6MTYxNTIzNTQyMn0.Lw1qQ_RqFbPJ4H5X7Y8Z9a0b1c2d3e4f5g6h7i8j9k

4.3 客户端存储JWT

客户端收到JWT后,需要保存起来。不同端有不同的存法:

  • Web端:存在localStorage或sessionStorage

  • App端:存在SharedPreferences或Keychain

  • 小程序:存在storage

以后每次请求,客户端都要在HTTP头里带上JWT:

javascript 复制代码
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;

4.4 服务器验证JWT

服务器收到请求后,从Header里取出JWT,验证合法性:

java 复制代码
@Component
public class JwtInterceptor implements HandlerInterceptor {
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 从Header获取token
        String token = request.getHeader("Authorization");
        if (token == null || !token.startsWith("Bearer ")) {
            throw new RuntimeException("未登录");
        }
        
        token = token.substring(7); // 去掉"Bearer "
        
        // 2. 验证token
        try {
            Claims claims = JwtUtil.parseToken(token);
            // 把用户信息存入请求,后续方法可以直接取用
            request.setAttribute("userId", claims.getSubject());
            request.setAttribute("username", claims.get("username"));
            return true;
        } catch (Exception e) {
            throw new RuntimeException("token无效或已过期");
        }
    }
}

验证的具体实现:

java 复制代码
public static Claims parseToken(String token) {
    return Jwts.parser()
            .setSigningKey(SECRET)  // 用相同的密钥验证签名
            .parseClaimsJws(token)
            .getBody();
}

验证过程做了三件事:

  1. 签名验证:用密钥重新计算签名,对比是否一致

  2. 过期验证:检查exp字段是否晚于当前时间

  3. 完整性验证:解码出来的数据是否完整

如果验证通过,说明这个JWT确实是我们签发的,而且没有被篡改过。

4.5 后续处理

Controller里可以直接取用JWT里的用户信息:

java 复制代码
@GetMapping("/user/info")
public Result getUserInfo(HttpServletRequest request) {
    // 从拦截器设置进去的属性里拿
    Long userId = (Long) request.getAttribute("userId");
    String username = (String) request.getAttribute("username");
    
    // 或者直接注入
    return Result.success(userService.findById(userId));
}

五、JWT的核心优势

对比传统session方案,JWT的优势很明显:

维度 Session方案 JWT方案
存储位置 服务器内存/Redis 客户端自己保存
扩展性 需要session共享,麻烦 天生支持分布式
跨域 需要配置CORS 直接支持
移动端 cookie用起来别扭 Header统一
性能 每次都要查session 只需计算签名

六、JWT的注意事项

6.1 安全性

JWT的Payload是明文的(只是Base64编码)。打开浏览器的开发者工具就能看到:

javascript 复制代码
// 在控制台输入
atob("eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOiLlvKDkuIkiLCJleHAiOjE2MTUyMzkwMjJ9")

所以绝对不能放密码、身份证号、手机号等敏感信息。如果有需要,可以再对Payload加密,但这会增加复杂度。

6.2 长度

JWT比sessionId长得多。一个普通的JWT可能有两三百个字符,如果放在URL里可能太长(浏览器URL长度有限制),所以一般放Header里。

6.3 登出

这是JWT最大的痛点:因为服务器不保存状态,所以无法主动让一个token失效。你点退出登录,服务器只能把客户端的token删掉,但如果别人拿着之前的token来访问,服务器依然认为是有效的。

解决方案:

  • 设置较短的过期时间,配合refresh token机制

  • 维护一个黑名单(但又回到共享状态的问题了)

6.4 续签

token过期了怎么办?不能让用户每过一小时就重新登录一次。

常见做法是refresh token机制:同时返回两个token

  • access token:有效期短(比如1小时),用于正常访问

  • refresh token:有效期长(比如7天),专门用来换取新的access token

客户端发现access token过期了,就拿refresh token去换新的,用户体验不受影响。

七、总结

JWT用一句话总结就是:把用户信息加密成一段字符串,交给客户端保存,服务器通过验证签名来确认身份

它的核心三要素:

  • Header:声明类型和算法

  • Payload:存放实际数据(不能放敏感信息)

  • Signature:防篡改的签名

整个流程:

  1. 用户登录成功,服务器生成JWT返回

  2. 客户端保存JWT,每次请求都带上

  3. 服务器验证JWT,通过就放行,不通过就拒绝

相比传统session方案,JWT更适合分布式系统、移动端App、前后端分离的项目。虽然它也有一些局限(如无法主动登出、长度较大),但只要合理设计过期时间,配合refresh token机制,足以应对大多数场景。


以上是我对JWT的学习理解,如果有不准确的地方,欢迎指正。

相关推荐
修行者Java15 小时前
(八)从“认证混乱难管控”到“JWT高效赋能”——JWT实战进阶指南
认证·jwt
淡笑沐白16 小时前
.NET Core Web API JWT认证实战指南
c#·jwt·.netcoreweb api
曲幽8 天前
不止于JWT:用FastAPI的Depends实现细粒度权限控制
python·fastapi·web·jwt·rbac·permission·depends·abac
源代码•宸17 天前
简版抖音项目——项目需求、项目整体设计、Gin 框架使用、视频模块方案设计、用户与鉴权模块方案设计、JWT
经验分享·后端·golang·音视频·gin·jwt·gorm
玄〤17 天前
个人博客网站搭建day3--Spring Boot JWT Token 认证配置的完整实现详解(漫画解析)
java·spring boot·后端·jwt
玄〤19 天前
个人博客网站搭建day2-Spring Boot 3 + JWT + Redis 实现后台权限拦截与单点登录(漫画解析)
java·spring boot·redis·后端·jwt
小钻风33661 个月前
JWT初识
java·jwt·base64url