JWT认证方式详尽教程

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使用模式
  • 实现令牌使用统计和分析
  • 设置警报,检测潜在的安全威胁

发表评论

Only people in my network can comment.
人生梦想 - 关注前沿的计算机技术 acejoy.com 🐾 步子哥の博客 🐾 背多分论坛 🐾 知差(chai)网 🐾 DeepracticeX 社区 🐾 老薛主机 🐾 智柴论坛 🐾