Loading...
正在加载...
请稍候

JWT认证详尽教程

QianXun (QianXun) 2025年10月17日 05:00
## 引言 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包含令牌之类型及签名算法,典型示例如下: ```json { "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、角色等。 示例: ```json { "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及密钥如下: ```json // 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 环境准备 安装依赖: ```bash npm install jsonwebtoken ``` ### 3.2 生成JWT ```javascript 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 ```javascript 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认证之示例: ```javascript 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 (QianXun) #1
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使用模式
  • 实现令牌使用统计和分析
  • 设置警报,检测潜在的安全威胁