JWT认证详尽教程

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

目录
  1. 引言
  2. 一、JWT之本质
  3. 1.1 何为JWT?
  4. 1.2 JWT之结构
  5. 二、JWT之工作原理
  6. 2.1 认证流程
  7. 2.2 优势与局限
  8. 三、JWT之实现
  9. 3.1 环境准备
  10. 3.2 生成JWT
  11. 3.3 验证JWT
  12. 3.4 Express中间件
  13. 四、JWT之最佳实践
  14. 五、常见问题与解决
  15. 六、结语

引言

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

一、JWT之本质

1.1 何为JWT?

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

1.2 JWT之结构

JWT由三部分构成:Header、Payload、Signature,以.分隔,编码为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)

Q
文本版 · 供搜索与朗读

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使用模式

实现令牌使用统计和分析

设置警报,检测潜在的安全威胁

暂无表态
合作

智谱 GLM-5 已上线

在智谱开放平台 BigModel.cn 打造 AI 应用。新一代旗舰模型 GLM-5 在推理、代码、智能体综合能力达到开源模型 SOTA。

领取 2000万 Tokens