<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JWT认证方式详尽教程</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&family=Roboto+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root {
--primary-color: #1565c0;
--primary-light: #e3f2fd;
--secondary-color: #455a64;
--accent-color: #ff5722;
--background-color: #f5f7fa;
--card-color: #ffffff;
--text-color: #263238;
--code-bg: #263238;
--code-color: #aed581;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans SC', sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
}
.poster-container {
width: 960px;
min-height: 1600px;
margin: 0 auto;
padding: 40px;
background: linear-gradient(135deg, #f5f7fa 0%, #e3f2fd 100%);
position: relative;
overflow: hidden;
}
.background-accent {
position: absolute;
width: 500px;
height: 500px;
border-radius: 50%;
background: radial-gradient(circle, rgba(21, 101, 192, 0.1) 0%, rgba(21, 101, 192, 0) 70%);
z-index: 0;
}
.accent-1 {
top: -200px;
right: -200px;
}
.accent-2 {
bottom: -100px;
left: -200px;
}
.content {
position: relative;
z-index: 1;
}
.header {
text-align: center;
margin-bottom: 40px;
padding: 20px;
background-color: var(--primary-color);
color: white;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.header h1 {
font-size: 48px;
margin-bottom: 10px;
letter-spacing: -0.5px;
}
.header p {
font-size: 20px;
opacity: 0.9;
}
.section {
margin-bottom: 40px;
background-color: var(--card-color);
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 32px;
color: var(--primary-color);
margin-bottom: 20px;
display: flex;
align-items: center;
}
.section-title .material-icons {
margin-right: 10px;
font-size: 32px;
}
.subsection {
margin-bottom: 25px;
}
.subsection-title {
font-size: 24px;
color: var(--secondary-color);
margin-bottom: 15px;
display: flex;
align-items: center;
}
.subsection-title .material-icons {
margin-right: 8px;
font-size: 24px;
}
p {
font-size: 18px;
margin-bottom: 15px;
}
ul, ol {
margin-left: 25px;
margin-bottom: 15px;
}
li {
font-size: 18px;
margin-bottom: 8px;
}
.highlight {
background-color: var(--primary-light);
padding: 2px 5px;
border-radius: 4px;
font-weight: 500;
}
.code-block {
background-color: var(--code-bg);
color: var(--code-color);
padding: 20px;
border-radius: 8px;
font-family: 'Roboto Mono', monospace;
font-size: 16px;
overflow-x: auto;
margin: 20px 0;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
.code-block pre {
margin: 0;
white-space: pre-wrap;
}
.jwt-structure {
display: flex;
justify-content: space-between;
margin: 20px 0;
}
.jwt-part {
flex: 1;
padding: 15px;
margin: 0 5px;
background-color: var(--primary-light);
border-radius: 8px;
text-align: center;
}
.jwt-part h4 {
margin-bottom: 10px;
color: var(--primary-color);
}
.jwt-flow {
display: flex;
justify-content: space-between;
align-items: center;
margin: 20px 0;
}
.flow-step {
flex: 1;
padding: 15px;
background-color: var(--primary-light);
border-radius: 8px;
text-align: center;
position: relative;
}
.flow-step:not(:last-child)::after {
content: '\e5cc';
font-family: 'Material Icons';
position: absolute;
right: -20px;
top: 50%;
transform: translateY(-50%);
color: var(--primary-color);
font-size: 24px;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
margin: 20px 0;
}
.comparison-table th, .comparison-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.comparison-table th {
background-color: var(--primary-light);
color: var(--primary-color);
font-weight: 500;
}
.comparison-table tr:nth-child(even) {
background-color: #f9f9f9;
}
.note {
background-color: #fff8e1;
border-left: 4px solid #ffc107;
padding: 15px;
margin: 20px 0;
border-radius: 0 8px 8px 0;
}
.note-title {
font-weight: 500;
color: #f57c00;
margin-bottom: 5px;
display: flex;
align-items: center;
}
.note-title .material-icons {
margin-right: 5px;
font-size: 20px;
}
</style>
</head>
<body>
<div class="poster-container">
<div class="background-accent accent-1"></div>
<div class="background-accent accent-2"></div>
<div class="content">
<header class="header">
<h1>JWT认证方式详尽教程</h1>
<p>原理、架构、实现与最佳实践</p>
</header>
<section class="section">
<h2 class="section-title">
<i class="material-icons">info</i>
JWT简介
</h2>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">help_outline</i>
什么是JWT?
</h3>
<p>JWT (JSON Web Token) 是一种开放标准 (RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以JSON对象安全地传输信息。这些信息可以被验证和信任,因为它是数字签名的。</p>
<p>JWT通常用于身份验证和信息交换,特别是在分布式系统中,因为它具有无状态特性,不需要服务器存储会话信息。</p>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">compare_arrows</i>
JWT与传统Session认证的区别
</h3>
<table class="comparison-table">
<tr>
<th>特性</th>
<th>JWT认证</th>
<th>Session认证</th>
</tr>
<tr>
<td>状态存储</td>
<td>无状态,信息存储在令牌中</td>
<td>有状态,信息存储在服务器</td>
</tr>
<tr>
<td>扩展性</td>
<td>高,适合分布式系统</td>
<td>低,需要共享会话存储</td>
</tr>
<tr>
<td>跨域支持</td>
<td>良好,可放在请求头中</td>
<td>有限,依赖Cookie</td>
</tr>
<tr>
<td>移动端支持</td>
<td>良好</td>
<td>有限,Cookie支持不完善</td>
</tr>
</table>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">sync_disabled</i>
JWT的无状态特性
</h3>
<p>JWT最大的特点是其无状态性,服务器不需要存储任何会话信息。每个请求都包含了足够的信息,使服务端能够验证用户。这种特性带来了以下优势:</p>
<ul>
<li>减轻服务器存储压力</li>
<li>提高系统可用性和伸缩性</li>
<li>更符合RESTful API的设计原则</li>
<li>便于实现分布式系统</li>
</ul>
</div>
</section>
<section class="section">
<h2 class="section-title">
<i class="material-icons">architecture</i>
JWT结构
</h2>
<p>JWT由三部分组成,用点(.)分隔:Header.Payload.Signature</p>
<div class="jwt-structure">
<div class="jwt-part">
<h4>Header</h4>
<p>描述JWT的元数据</p>
</div>
<div class="jwt-part">
<h4>Payload</h4>
<p>包含声明(Claims)</p>
</div>
<div class="jwt-part">
<h4>Signature</h4>
<p>验证数据完整性</p>
</div>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">title</i>
Header(头部)
</h3>
<p>Header通常由两部分组成:令牌的类型(typ)和所使用的签名算法(alg)。</p>
<div class="code-block">
<pre><code class="json">{
"alg": "HS256",
"typ": "JWT"
}</code></pre>
</div>
<p>然后,这个JSON对象会被Base64Url编码,形成JWT的第一部分。</p>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">inventory_2</i>
Payload(载荷)
</h3>
<p>Payload是JWT的主体部分,包含声明(Claims)。声明是关于实体(通常是用户)和其他数据的声明。</p>
<p>声明分为三种类型:</p>
<ul>
<li><span class="highlight">注册声明(Registered Claims)</span>:预定义的一些声明,如iss(发行者)、exp(过期时间)、sub(主题)等</li>
<li><span class="highlight">公共声明(Public Claims)</span>:自定义字段,可以用于交换信息</li>
<li><span class="highlight">私有声明(Private Claims)</span>:自定义声明字段,只有JWT的创建者和使用者知晓</li>
</ul>
<div class="code-block">
<pre><code class="json">{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"exp": 1516239022
}</code></pre>
</div>
<div class="note">
<div class="note-title">
<i class="material-icons">warning</i>
重要提示
</div>
<p>Header和Payload只是经过Base64Url编码,并没有加密!任何人都可以解码它们并看到原始内容。因此,绝对不能在Payload中放置密码等敏感信息。</p>
</div>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">verified</i>
Signature(签名)
</h3>
<p>Signature是对前两部分的签名,用于验证JWT的完整性,防止数据被篡改。</p>
<p>签名的生成需要用到:</p>
<ul>
<li>编码后的Header</li>
<li>编码后的Payload</li>
<li>一个密钥(Secret)</li>
<li>Header中指定的签名算法</li>
</ul>
<p>签名的计算公式如下:</p>
<div class="code-block">
<pre><code class="text">HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)</code></pre>
</div>
<p>最终,将Header、Payload、Signature三个部分用点(.)连接起来,就构成了一个完整的JWT。</p>
</div>
</section>
<section class="section">
<h2 class="section-title">
<i class="material-icons">account_tree</i>
JWT工作流程
</h2>
<div class="jwt-flow">
<div class="flow-step">
<h4>用户登录</h4>
<p>提交用户名和密码</p>
</div>
<div class="flow-step">
<h4>生成JWT</h4>
<p>验证成功后生成令牌</p>
</div>
<div class="flow-step">
<h4>存储JWT</h4>
<p>客户端保存令牌</p>
</div>
<div class="flow-step">
<h4>携带JWT</h4>
<p>每次请求携带令牌</p>
</div>
<div class="flow-step">
<h4>验证JWT</h4>
<p>服务端验证令牌</p>
</div>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">login</i>
用户登录和令牌生成
</h3>
<p>用户通过用户名和密码发起登录请求。服务器验证用户凭证,若验证成功,则使用JWT工具类生成令牌:</p>
<ul>
<li>Header:指定算法(如HS256)和令牌类型(JWT)</li>
<li>Payload:包含用户信息(如用户ID、角色)和声明(如过期时间exp)</li>
<li>Signature:使用密钥对Header和Payload进行签名,确保令牌不可篡改</li>
</ul>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">save</i>
客户端存储令牌
</h3>
<p>服务端将生成的JWT返回给客户端(通常通过响应体或Header)。客户端(如浏览器或移动端)将令牌存储在本地(如LocalStorage或Cookie)。</p>
<div class="note">
<div class="note-title">
<i class="material-icons">security</i>
安全提示
</div>
<p>建议将JWT存储在localStorage中,放在Cookie中会有CSRF风险。如果必须使用Cookie,应设置httpOnly和secure标志。</p>
</div>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">send</i>
请求携带令牌
</h3>
<p>客户端在后续请求的Authorization Header中以Bearer格式携带JWT:</p>
<div class="code-block">
<pre><code class="http">Authorization: Bearer <JWT></code></pre>
</div>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">gpp_good</i>
服务端验证令牌
</h3>
<p>服务器收到请求后,从请求头中提取JWT,并验证其合法性:</p>
<ul>
<li>签名验证:使用密钥校验签名是否有效</li>
<li>过期检查:检查exp字段是否过期</li>
<li>用户信息提取:解析Payload中的用户信息(如用户ID),用于后续权限控制</li>
</ul>
<p>若验证通过,服务端处理请求并返回数据;若验证失败(如令牌过期或签名错误),返回401状态码或自定义错误信息。</p>
</div>
</section>
<section class="section">
<h2 class="section-title">
<i class="material-icons">balance</i>
JWT优势与局限性
</h2>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">thumb_up</i>
优势
</h3>
<ul>
<li><span class="highlight">无状态性</span>:服务器不需要存储会话信息,减轻服务器压力</li>
<li><span class="highlight">跨域支持</span>:JWT可以放在HTTP请求头中,轻松实现跨域认证</li>
<li><span class="highlight">移动端友好</span>:不依赖Cookie,适合移动应用</li>
<li><span class="highlight">避免CSRF攻击</span>:不使用Cookie,天然避免CSRF攻击</li>
<li><span class="highlight">自包含</span>:JWT包含所有必要信息,减少数据库查询</li>
<li><span class="highlight">可扩展</span>:适合分布式系统和微服务架构</li>
</ul>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">thumb_down</i>
局限性
</h3>
<ul>
<li><span class="highlight">无法即时撤销</span>:JWT一旦签发,在过期前无法撤销</li>
<li><span class="highlight">信息泄露风险</span>:Payload仅Base64编码,不应包含敏感信息</li>
<li><span class="highlight">令牌体积大</span>:相比Session ID,JWT体积更大,增加网络开销</li>
<li><span class="highlight">续签困难</span>:JWT过期时间固定,续签需要重新生成令牌</li>
<li><span class="highlight">权限更新延迟</span>:用户权限变更后,需等待令牌过期才能生效</li>
</ul>
</div>
</section>
<section class="section">
<h2 class="section-title">
<i class="material-icons">security</i>
JWT安全考虑
</h2>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">vpn_key</i>
密钥管理
</h3>
<ul>
<li>使用强密钥(至少256位)</li>
<li>定期更换密钥</li>
<li>不要将密钥硬编码在代码中</li>
<li>使用环境变量或配置文件存储密钥</li>
<li>考虑使用密钥管理服务</li>
</ul>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">visibility_off</i>
敏感信息处理
</h3>
<ul>
<li>不要在Payload中存储敏感信息(如密码、信用卡号)</li>
<li>如需传输敏感信息,考虑使用JWE(JSON Web Encryption)</li>
<li>最小化Payload中的信息量</li>
</ul>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">timer</i>
过期时间设置
</h3>
<ul>
<li>设置合理的过期时间(通常15分钟到几小时)</li>
<li>对于长时间操作,使用刷新令牌机制</li>
<li>考虑在用户活动时自动延长令牌有效期</li>
</ul>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">enhanced_encryption</i>
算法选择
</h3>
<ul>
<li>避免使用'none'算法</li>
<li>优先使用强算法如RS256(非对称)或HS256(对称)</li>
<li>考虑使用ES256(ECDSA)以获得更小的签名</li>
<li>根据安全需求选择合适的算法强度</li>
</ul>
</div>
</section>
<section class="section">
<h2 class="section-title">
<i class="material-icons">code</i>
JWT实现示例
</h2>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">library_add</i>
Maven依赖配置
</h3>
<p>首先,在pom.xml文件中添加JWT相关依赖:</p>
<div class="code-block">
<pre><code class="xml"><!-- 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></code></pre>
</div>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">build</i>
JWT工具类实现
</h3>
<p>创建一个JWT工具类,用于生成和验证JWT令牌:</p>
<div class="code-block">
<pre><code class="java">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;
<span class="mention-invalid">@Component</span>
public class JwtUtil {
<span class="mention-invalid">@Value</span>("${jwt.secret}")
private String jwtSecret;
<span class="mention-invalid">@Value</span>("${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;
}
}</code></pre>
</div>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">filter_alt</i>
拦截器/过滤器实现
</h3>
<p>创建一个JWT过滤器,用于在Spring Security过滤器链中验证请求中的JWT:</p>
<div class="code-block">
<pre><code class="java">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;
<span class="mention-invalid">@Component</span>
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;
}
<span class="mention-invalid">@Override</span>
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);
}
}</code></pre>
</div>
</div>
</section>
<section class="section">
<h2 class="section-title">
<i class="material-icons">tips_and_updates</i>
JWT最佳实践
</h2>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">storage</i>
令牌存储方式
</h3>
<ul>
<li>对于Web应用,推荐使用localStorage存储JWT</li>
<li>如果使用Cookie,设置httpOnly和secure标志</li>
<li>考虑使用短期访问令牌和长期刷新令牌的组合</li>
<li>实现令牌黑名单机制,支持主动撤销</li>
</ul>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">refresh</i>
刷新令牌机制
</h3>
<ul>
<li>使用短期访问令牌(如15分钟)和长期刷新令牌(如7天)</li>
<li>刷新令牌应存储在安全的地方(如httpOnly Cookie)</li>
<li>实现令牌刷新接口,在访问令牌过期时自动刷新</li>
<li>考虑实现滑动会话,在用户活动时延长令牌有效期</li>
</ul>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">admin_panel_settings</i>
权限控制
</h3>
<ul>
<li>在JWT中包含角色和权限信息</li>
<li>实现基于角色的访问控制(RBAC)</li>
<li>考虑实现细粒度的权限控制</li>
<li>定期审查和更新用户权限</li>
</ul>
</div>
<div class="subsection">
<h3 class="subsection-title">
<i class="material-icons">monitor_heart</i>
监控与日志
</h3>
<ul>
<li>记录JWT的生成、验证和失效事件</li>
<li>监控异常的JWT使用模式</li>
<li>实现令牌使用统计和分析</li>
<li>设置警报,检测潜在的安全威胁</li>
</ul>
</div>
</section>
</div>
</div>
</body>
</html>