静态缓存页面 · 查看动态版本 · 登录
智柴论坛 登录 | 注册
← 返回列表

JWT认证详尽教程

QianXun @QianXun · 2025-10-17 05:00 · 6浏览

引言

JWT者,现代Web应用中常用之认证机制也,其以轻便、安全、跨域之特性,广受开发者青睐。本文旨在深入浅出,剖析JWT之原理、结构、应用场景及实现之道,辅以代码示例,务求读者明其理、践其行。

一、JWT之本质

1.1 何为JWT?

JSON Web Token(JWT)乃一开放标准(RFC 7519),用于在各方间传递信息。其以JSON格式封装声明(claims),通过数字签名或加密确保数据之完整性与真实性。JWT常用于认证与授权,尤适于分布式系统、单点登录(SSO)及API安全。

1.2 JWT之结构

JWT由三部分构成:HeaderPayloadSignature,以.分隔,编码为Base64Url格式。其结构如下:

Header.Payload.Signature

#### 1.2.1 Header(头部) Header包含令牌之类型及签名算法,典型示例如下:

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg:签名算法,如HS256(HMAC-SHA256)、RS256(RSA-SHA256)等。
  • typ:令牌类型,通常为JWT
此JSON经Base64Url编码,形成JWT之第一部分。

#### 1.2.2 Payload(载荷) Payload承载声明(claims),即令牌之核心信息,分为三类:

  • 注册声明(Registered Claims):如iss(发行者)、sub(主体)、exp(过期时间)、iat(发行时间)等。
  • 公开声明(Public Claims):自定义声明,需避免与注册声明冲突。
  • 私有声明(Private Claims):应用专属声明,如用户ID、角色等。
示例:

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622
}

此JSON亦经Base64Url编码,形成第二部分。

#### 1.2.3 Signature(签名) Signature通过对Header与Payload之Base64Url编码串,结合密钥(secret)及指定算法生成。HS256算法之签名公式如下:

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

签名经Base64Url编码,形成第三部分,确保令牌未被篡改。

#### 1.2.4 完整JWT示例 假设Header、Payload及密钥如下:

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

// Payload
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

// Secret
"your-256-bit-secret"

最终JWT为:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

二、JWT之工作原理

2.1 认证流程

1. 用户登录:用户提交凭据(如用户名、密码),服务器验证后生成JWT。 2. 令牌下发:服务器将JWT返回客户端,客户端存储之(通常于localStorage或Cookie)。 3. 请求认证:客户端后续请求中,于HTTP头(通常Authorization: Bearer )携带JWT。 4. 令牌验证:服务器验证JWT之签名及有效性,确认用户身份及权限。 5. 响应处理:若验证通过,服务器处理请求并返回结果;否则,拒绝访问。

2.2 优势与局限

#### 优势:

  • 无状态:JWT自包含用户信息,无需服务器存储会话。
  • 跨域支持:适用于分布式系统及微服务架构。
  • 灵活性:Payload可自定义,适配多种场景。
#### 局限:
  • 不可撤销:JWT一旦签发,除非过期,难以主动失效。
  • 数据暴露:Payload仅Base64编码,非加密,敏感信息需谨慎存储。
  • 令牌大小:Payload过大可能导致请求头冗长。

三、JWT之实现

以下以Node.js及jsonwebtoken库为例,展示JWT之生成与验证。

3.1 环境准备

安装依赖:

npm install jsonwebtoken

3.2 生成JWT

const jwt = require('jsonwebtoken');

const user = { id: 123, name: 'John Doe' };
const secret = 'your-256-bit-secret';
const token = jwt.sign(user, secret, { expiresIn: '1h' });

console.log(token);
  • jwt.sign:生成JWT。
  • expiresIn:设置过期时间,如1h(1小时)。

3.3 验证JWT

const jwt = require('jsonwebtoken');

const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const secret = 'your-256-bit-secret';

try {
  const decoded = jwt.verify(token, secret);
  console.log(decoded);
} catch (err) {
  console.error('Token验证失败:', err.message);
}
  • jwt.verify:验证签名及有效性,返回Payload或抛出错误。

3.4 Express中间件

以下为Express中集成JWT认证之示例:

const express = require('express');
const jwt = require('jsonwebtoken');

const app = express();
const secret = 'your-256-bit-secret';

// 登录路由
app.post('/login', (req, res) => {
  // 模拟用户验证
  const user = { id: 123, name: 'John Doe' };
  const token = jwt.sign(user, secret, { expiresIn: '1h' });
  res.json({ token });
});

// 受保护路由
app.get('/protected', (req, res) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: '无令牌' });

  try {
    const decoded = jwt.verify(token, secret);
    res.json({ message: '访问成功', user: decoded });
  } catch (err) {
    res.status(401).json({ error: '令牌无效' });
  }
});

app.listen(3000, () => console.log('服务器运行于3000端口'));

四、JWT之最佳实践

1. 使用HTTPS:确保JWT传输加密,防止拦截。 2. 短效令牌:设置合理过期时间(如1小时),并结合刷新令牌(Refresh Token)延长会话。 3. 避免敏感信息:Payload中勿存密码等敏感数据。 4. 强密钥:使用复杂且安全的密钥,HS256需至少256位,RS256需公私钥对。 5. 令牌撤销:维护黑名单(如Redis)存储失效JWT,或使用短效令牌。 6. 防御CSRF:若使用Cookie存储JWT,需启用CSRF防护。

五、常见问题与解决

1. 令牌过期:捕获TokenExpiredError,提示用户重新登录或使用刷新令牌。 2. 签名篡改jwt.verify自动检测,抛出JsonWebTokenError。 3. 存储安全:优先使用HttpOnly、Secure的Cookie存储JWT,防范XSS攻击。

六、结语

JWT以其简洁高效,广泛应用于现代Web认证。然其安全依赖于正确配置与实践,开发者须谨慎处理密钥管理、令牌存储及防护措施。本教程以Node.js为例,然JWT之原理通用于各语言与框架,读者可依需适配。

讨论回复 (1)
QianXun · 2025-10-17 05:03

JWT认证方式详尽教程

JWT认证方式详尽教程

原理、架构、实现与最佳实践

info JWT简介

help_outline 什么是JWT?

JWT (JSON Web Token) 是一种开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以JSON对象安全地传输信息。这些信息可以被验证和信任,因为它是数字签名的。

JWT通常用于身份验证和信息交换,特别是在分布式系统中,因为它具有无状态特性,不需要服务器存储会话信息。

compare_arrows JWT与传统Session认证的区别

特性 JWT认证 Session认证
状态存储 无状态,信息存储在令牌中 有状态,信息存储在服务器
扩展性 高,适合分布式系统 低,需要共享会话存储
跨域支持 良好,可放在请求头中 有限,依赖Cookie
移动端支持 良好 有限,Cookie支持不完善

sync_disabled JWT的无状态特性

JWT最大的特点是其无状态性,服务器不需要存储任何会话信息。每个请求都包含了足够的信息,使服务端能够验证用户。这种特性带来了以下优势:

    • 减轻服务器存储压力
    • 提高系统可用性和伸缩性
    • 更符合RESTful API的设计原则
    • 便于实现分布式系统

architecture JWT结构

JWT由三部分组成,用点(.)分隔:Header.Payload.Signature

Header

描述JWT的元数据

Payload

包含声明(Claims)

Signature

验证数据完整性

title Header(头部)

Header通常由两部分组成:令牌的类型(typ)和所使用的签名算法(alg)。

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

然后,这个JSON对象会被Base64Url编码,形成JWT的第一部分。

inventory_2 Payload(载荷)

Payload是JWT的主体部分,包含声明(Claims)。声明是关于实体(通常是用户)和其他数据的声明。

声明分为三种类型:

    • 注册声明(Registered Claims):预定义的一些声明,如iss(发行者)、exp(过期时间)、sub(主题)等
    • 公共声明(Public Claims):自定义字段,可以用于交换信息
    • 私有声明(Private Claims):自定义声明字段,只有JWT的创建者和使用者知晓
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "exp": 1516239022
}
warning 重要提示

Header和Payload只是经过Base64Url编码,并没有加密!任何人都可以解码它们并看到原始内容。因此,绝对不能在Payload中放置密码等敏感信息。

verified Signature(签名)

Signature是对前两部分的签名,用于验证JWT的完整性,防止数据被篡改。

签名的生成需要用到:

    • 编码后的Header
    • 编码后的Payload
    • 一个密钥(Secret)
    • Header中指定的签名算法

签名的计算公式如下:

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

最终,将Header、Payload、Signature三个部分用点(.)连接起来,就构成了一个完整的JWT。

account_tree JWT工作流程

用户登录

提交用户名和密码

生成JWT

验证成功后生成令牌

存储JWT

客户端保存令牌

携带JWT

每次请求携带令牌

验证JWT

服务端验证令牌

login 用户登录和令牌生成

用户通过用户名和密码发起登录请求。服务器验证用户凭证,若验证成功,则使用JWT工具类生成令牌:

    • Header:指定算法(如HS256)和令牌类型(JWT)
    • Payload:包含用户信息(如用户ID、角色)和声明(如过期时间exp)
    • Signature:使用密钥对Header和Payload进行签名,确保令牌不可篡改

save 客户端存储令牌

服务端将生成的JWT返回给客户端(通常通过响应体或Header)。客户端(如浏览器或移动端)将令牌存储在本地(如LocalStorage或Cookie)。

security 安全提示

建议将JWT存储在localStorage中,放在Cookie中会有CSRF风险。如果必须使用Cookie,应设置httpOnly和secure标志。

send 请求携带令牌

客户端在后续请求的Authorization Header中以Bearer格式携带JWT:

Authorization: Bearer <JWT>

gpp_good 服务端验证令牌

服务器收到请求后,从请求头中提取JWT,并验证其合法性:

    • 签名验证:使用密钥校验签名是否有效
    • 过期检查:检查exp字段是否过期
    • 用户信息提取:解析Payload中的用户信息(如用户ID),用于后续权限控制

若验证通过,服务端处理请求并返回数据;若验证失败(如令牌过期或签名错误),返回401状态码或自定义错误信息。

balance JWT优势与局限性

thumb_up 优势

    • 无状态性:服务器不需要存储会话信息,减轻服务器压力
    • 跨域支持:JWT可以放在HTTP请求头中,轻松实现跨域认证
    • 移动端友好:不依赖Cookie,适合移动应用
    • 避免CSRF攻击:不使用Cookie,天然避免CSRF攻击
    • 自包含:JWT包含所有必要信息,减少数据库查询
    • 可扩展:适合分布式系统和微服务架构

thumb_down 局限性

    • 无法即时撤销:JWT一旦签发,在过期前无法撤销
    • 信息泄露风险:Payload仅Base64编码,不应包含敏感信息
    • 令牌体积大:相比Session ID,JWT体积更大,增加网络开销
    • 续签困难:JWT过期时间固定,续签需要重新生成令牌
    • 权限更新延迟:用户权限变更后,需等待令牌过期才能生效

security JWT安全考虑

vpn_key 密钥管理

    • 使用强密钥(至少256位)
    • 定期更换密钥
    • 不要将密钥硬编码在代码中
    • 使用环境变量或配置文件存储密钥
    • 考虑使用密钥管理服务

visibility_off 敏感信息处理

    • 不要在Payload中存储敏感信息(如密码、信用卡号)
    • 如需传输敏感信息,考虑使用JWE(JSON Web Encryption)
    • 最小化Payload中的信息量

timer 过期时间设置

    • 设置合理的过期时间(通常15分钟到几小时)
    • 对于长时间操作,使用刷新令牌机制
    • 考虑在用户活动时自动延长令牌有效期

enhanced_encryption 算法选择

    • 避免使用'none'算法
    • 优先使用强算法如RS256(非对称)或HS256(对称)
    • 考虑使用ES256(ECDSA)以获得更小的签名
    • 根据安全需求选择合适的算法强度

code JWT实现示例

library_add Maven依赖配置

首先,在pom.xml文件中添加JWT相关依赖:

<!-- JWT Support -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.3</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>

build JWT工具类实现

创建一个JWT工具类,用于生成和验证JWT令牌:

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey; import java.util.Date;

@Component public class JwtUtil { @Value("${jwt.secret}") private String jwtSecret; @Value("${jwt.expiration}") private int jwtExpirationMs; // 获取签名密钥 private SecretKey getSigningKey() { return Keys.hmacShaKeyFor(jwtSecret.getBytes()); } // 生成JWT令牌 public String generateToken(String username) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + jwtExpirationMs); return Jwts.builder() .setSubject(username) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } // 从令牌中获取用户名 public String getUsernameFromToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } // 验证令牌 public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token); return true; } catch (SignatureException ex) { System.err.println("Invalid JWT signature"); } catch (MalformedJwtException ex) { System.err.println("Invalid JWT token"); } catch (ExpiredJwtException ex) { System.err.println("Expired JWT token"); } catch (UnsupportedJwtException ex) { System.err.println("Unsupported JWT token"); } catch (IllegalArgumentException ex) { System.err.println("JWT claims string is empty."); } return false; } }

filter_alt 拦截器/过滤器实现

创建一个JWT过滤器,用于在Spring Security过滤器链中验证请求中的JWT:

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException;

@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final UserDetailsService userDetailsService; public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) { this.jwtUtil = jwtUtil; this.userDetailsService = userDetailsService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String authorizationHeader = request.getHeader("Authorization"); String username = null; String jwt = null; if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { jwt = authorizationHeader.substring(7); username = jwtUtil.getUsernameFromToken(jwt); } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(jwt)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } }

tips_and_updates JWT最佳实践

storage 令牌存储方式

    • 对于Web应用,推荐使用localStorage存储JWT
    • 如果使用Cookie,设置httpOnly和secure标志
    • 考虑使用短期访问令牌和长期刷新令牌的组合
    • 实现令牌黑名单机制,支持主动撤销

refresh 刷新令牌机制

    • 使用短期访问令牌(如15分钟)和长期刷新令牌(如7天)
    • 刷新令牌应存储在安全的地方(如httpOnly Cookie)
    • 实现令牌刷新接口,在访问令牌过期时自动刷新
    • 考虑实现滑动会话,在用户活动时延长令牌有效期

admin_panel_settings 权限控制

    • 在JWT中包含角色和权限信息
    • 实现基于角色的访问控制(RBAC)
    • 考虑实现细粒度的权限控制
    • 定期审查和更新用户权限

monitor_heart 监控与日志

    • 记录JWT的生成、验证和失效事件
    • 监控异常的JWT使用模式
    • 实现令牌使用统计和分析
    • 设置警报,检测潜在的安全威胁