-
C#网络编程之身份认证(JWT、OAuth2、OpenID Connect)
身份认证:JWT、OAuth2、OpenID Connect深度解析
18.9.1 引言:身份认证的核心价值
在Web应用、API服务中,身份认证是保护敏感资源的第一道防线——它vb.net教程C#教程python教程SQL教程access 2010教程解决了“你是谁”的问题。随着分布式系统、微服务的普及,传统的Session-Cookie认证(依赖服务器存储会话状态)已无法满足跨域、跨服务的需求。JWT、OAuth2、OpenID Connect应运而生,成为现代身份认证体系的核心技术:
JWT:轻量级的令牌格式,实现无状态认证;
OAuth2:授权框架,解决“第三方应用如何安全访问用户资源”的问题;
OpenID Connect:基于OAuth2的身份认证协议,实现单点登录(SSO)。
本章将从概念、代码实践、底层原理三个维度,逐一解析这三项技术。
18.9.2 JWT:无状态身份认证令牌
-
什么是JWT?
JWT(JSON Web Token)是一种自包含、无状态的身份认证令牌,通过JSON格式存储用户身份信息,无需服务器存储会话状态。令牌由三部分组成,用.分隔:
Header:令牌类型和签名算法(如{"alg":"HS256","typ":"JWT"});
Payload:用户身份信息(如用户ID、角色)和元数据(如过期时间),称为Claims;
Signature:用Header指定的算法,将Header+Payload用密钥签名,防止令牌被篡改。
JWT的核心优势:
无状态:服务器无需存储会话,降低分布式系统的复杂度;
跨域:令牌通过HTTP Header传输,支持跨域、跨服务认证;
自包含:令牌本身包含用户信息,无需额外查询数据库。 -
代码实践:JWT的生成与验证(C#)
使用.NET官方库System.IdentityModel.Tokens.Jwt实现JWT的生成与验证,代码逐行讲解。
步骤1:安装NuGet包
bash
Install-Package System.IdentityModel.Tokens.Jwt
Install-Package Microsoft.IdentityModel.Tokens
步骤2:生成JWT令牌
csharp
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
class JwtGenerator
{
// 密钥:必须保密,生产环境应从配置文件读取,避免硬编码
private const string SecretKey = "YourSuperSecretKey@321"; // 长度≥16字节(HS256要求)
static void Main()
{
// 1. 配置签名密钥:使用HMAC-SHA256算法
var securityKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(SecretKey));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
// 2. 设置Claims(用户身份信息)
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, "123"), // 用户ID(标准Claim类型)
new Claim(ClaimTypes.Name, "张三"), // 用户名
new Claim(ClaimTypes.Role, "Admin"), // 用户角色
new Claim("CustomClaim", "CustomValue") // 自定义Claim
};
// 3. 配置JWT令牌参数
var token = new JwtSecurityToken(
issuer: "https://your-auth-server.com", // 令牌颁发者(可选)
audience: "https://your-resource-server.com", // 令牌受众(可选)
claims: claims, // 身份信息
expires: DateTime.UtcNow.AddHours(1), // 过期时间(UTC时间,避免时区问题)
signingCredentials: credentials // 签名凭证
);
// 4. 生成JWT字符串
var tokenString = new JwtSecurityTokenHandler().WriteToken(token);
Console.WriteLine("生成的JWT令牌:");
Console.WriteLine(tokenString);
}
}
逐行讲解:
1.SymmetricSecurityKey:将字符串密钥转换为字节数组,用于HMAC-SHA256签名(对称加密算法,密钥需在服务器和客户端共享);
2.Claims:存储用户身份信息,分为标准Claim(如NameIdentifier、Role)和自定义Claim(如CustomClaim);
3.JwtSecurityToken:配置令牌的核心参数:
oissuer:令牌颁发者,验证时可校验是否来自信任的服务器;
oaudience:令牌受众,验证时可校验是否允许访问当前资源服务器;
oexpires:过期时间,必须设置为UTC时间,避免时区差异导致的过期错误;
4.JwtSecurityTokenHandler().WriteToken(token):将JwtSecurityToken转换为字符串格式的JWT令牌。
生成的JWT令牌示例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjEyMyIsIm5iZiI6MTcxOTg0MjQwMCwiZXhwIjoxNzE5ODQ2MDAwLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDEifQ.7Z8X7Z8X7Z8X7Z8X7Z8X7Z8X7Z8X7Z8X7Z8X7Z8X
步骤3:验证JWT令牌
csharp
using System;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
class JwtValidator
{
private const string SecretKey = "YourSuperSecretKey@321"; // 与生成时的密钥一致
static void Main(string[] args)
{
string tokenString = args[0]; // 传入生成的JWT令牌
var tokenHandler = new JwtSecurityTokenHandler();
try
{
// 1. 配置验证参数
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true, // 验证颁发者
ValidIssuer = "https://your-auth-server.com", // 信任的颁发者
ValidateAudience = true, // 验证受众
ValidAudience = "https://your-resource-server.com", // 信任的受众
ValidateIssuerSigningKey = true, // 验证签名密钥
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(SecretKey)), // 签名密钥
ValidateLifetime = true, // 验证过期时间
ClockSkew = TimeSpan.Zero // 允许的时间偏差(生产环境可设为5分钟,避免服务器时间差异)
};
// 2. 验证令牌并解析Claims
SecurityToken validatedToken;
var claimsPrincipal = tokenHandler.ValidateToken(tokenString, validationParameters, out validatedToken);
// 3. 解析用户信息
var userId = claimsPrincipal.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var userName = claimsPrincipal.FindFirst(ClaimTypes.Name)?.Value;
var role = claimsPrincipal.FindFirst(ClaimTypes.Role)?.Value;
Console.WriteLine("令牌验证成功:");
Console.WriteLine($"用户ID:{userId}");
Console.WriteLine($"用户名:{userName}");
Console.WriteLine($"角色:{role}");
}
catch (SecurityTokenExpiredException)
{
Console.WriteLine("令牌已过期");
}
catch (SecurityTokenInvalidSignatureException)
{
Console.WriteLine("令牌签名无效(可能被篡改)");
}
catch (Exception ex)
{
Console.WriteLine($"令牌验证失败:{ex.Message}");
}
}
}
逐行讲解:
1.TokenValidationParameters:配置验证规则,核心是验证签名(确保令牌未被篡改)和验证过期时间(确保令牌有效);
2.tokenHandler.ValidateToken():验证令牌的合法性,若验证通过则返回ClaimsPrincipal(包含用户身份信息);
3.claimsPrincipal.FindFirst():从验证后的Claims中提取用户信息,无需查询数据库(JWT自包含特性)。
3. 基础知识拓展
JWT的优缺点
| 优点 | 缺点 |
|---|---|
| 无状态,服务器无需存储会话 | 无法主动吊销令牌(除非服务器维护黑名单) |
| 跨域、跨服务支持好 | 令牌长度较长(包含用户信息),增加网络传输开销 |
| 自包含,减少数据库查询 | 敏感信息不能放在Payload中(Base64编码可解码,非加密) |
安全注意事项
HTTPS传输:JWT令牌通过HTTP Header传输,必须使用HTTPS防止令牌被窃听;
短有效期:设置较短的过期时间(如1小时),减少令牌泄露的风险;
签名密钥保密:对称加密的密钥必须严格保密,生产环境应使用非对称加密(如RS256,私钥签名,公钥验证);
避免敏感信息:Payload仅存储非敏感信息(如用户ID、角色),密码、手机号等敏感信息不能放在JWT中。
18.9.3 OAuth2:授权框架(解决“第三方应用如何访问用户资源”)
-
什么是OAuth2?
OAuth2(Open Authorization 2.0)是一种授权框架,而非身份认证协议。它解决了“第三方应用如何在不获取用户密码的情况下,安全访问用户资源”的问题。例如:
微信小程序请求获取用户的微信头像(用户资源);
GitHub授权第三方应用访问用户的代码仓库。
OAuth2的核心角色
资源所有者:用户,拥有资源的所有权;
客户端:第三方应用,请求访问用户资源;
授权服务器:颁发授权令牌的服务器(如微信授权服务器);
资源服务器:存储用户资源的服务器(如微信的用户头像服务器)。
OAuth2的四种授权模式
| 模式 | 适用场景 | 流程复杂度 | 安全性 |
| ---- | ---- | ---- | ---- |
| 授权码模式 | Web应用、移动应用 | 高 | 最高 |
| 密码模式 | 信任的客户端(如内部系统) | 低 | 低(需用户提供密码) |
| 客户端凭证模式 | 服务间通信(无用户参与) | 低 | 高 |
| 隐式模式 | 纯前端应用(无后端) | 中 | 中(令牌暴露在前端) | -
核心实践:授权码模式(最常用)
授权码模式是OAuth2最安全、最常用的模式,流程如下:
1.用户访问客户端,客户端重定向用户到授权服务器的授权页面;
2.用户登录并授权客户端访问资源;
3.授权服务器返回授权码给客户端;
4.客户端使用授权码向授权服务器请求访问令牌(Access Token);
5.授权服务器返回Access Token给客户端;
6.客户端使用Access Token向资源服务器请求用户资源。
代码示例:ASP.NET Core配置OAuth2客户端
以下示例展示如何在ASP.NET Core Web应用中配置OAuth2客户端,使用GitHub作为授权服务器:
csharp
// Program.cs
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
// 1. 配置Cookie认证(存储用户会话)
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie()
// 2. 配置OAuth2客户端(GitHub)
.AddOAuth("GitHub", options =>
{
// 客户端ID和密钥(从GitHub开发者平台获取)
options.ClientId = builder.Configuration["GitHub:ClientId"];
options.ClientSecret = builder.Configuration["GitHub:ClientSecret"];
// 授权服务器端点
options.CallbackPath = "/signin-github"; // 授权成功后的回调地址
options.AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
options.TokenEndpoint = "https://github.com/login/oauth/access_token";
options.UserInformationEndpoint = "https://api.github.com/user"; // 获取用户信息的端点
// 3. 配置授权范围(请求访问的资源)
options.Scope.Add("user:email"); // 请求访问用户邮箱
// 4. 从UserInfo端点解析用户信息
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
options.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
// 5. 验证令牌(可选,GitHub返回的Access Token无需签名验证)
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
// 请求UserInfo端点获取用户信息
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
context.RunClaimActions(user); // 解析用户信息到Claims
}
};
});
var app = builder.Build();
app.UseAuthentication(); // 启用认证中间件
app.UseAuthorization(); // 启用授权中间件
app.MapGet("/login", async context =>
{
// 重定向到GitHub授权页面
await context.ChallengeAsync("GitHub", new AuthenticationProperties { RedirectUri = "/" });
});
app.MapGet("/", (ClaimsPrincipal user) =>
{
if (user.Identity.IsAuthenticated)
{
return Results.Ok(new
{
UserId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value,
UserName = user.FindFirst(ClaimTypes.Name)?.Value,
Email = user.FindFirst(ClaimTypes.Email)?.Value
});
}
return Results.Redirect("/login");
});
app.Run();
逐行讲解:
1.AddAuthentication().AddCookie():配置Cookie认证,用于存储用户登录后的会话状态;
2.AddOAuth("GitHub"):配置GitHub作为OAuth2授权服务器,核心参数:
oClientId/ClientSecret:从GitHub开发者平台注册应用后获取;
oCallbackPath:授权成功后,授权服务器重定向到客户端的地址;
oScope:请求访问的资源范围(如user:email表示请求访问用户邮箱);
3.OnCreatingTicket:事件回调,在获取Access Token后,请求GitHub的UserInfo端点获取用户信息,并解析到Claims中;
4.context.ChallengeAsync("GitHub"):触发OAuth2授权流程,重定向用户到GitHub的授权页面。
3. 基础知识拓展
OAuth2与身份认证的区别
OAuth2是授权框架,解决“第三方应用如何访问用户资源”的问题,不直接提供身份认证功能(无法确认用户是谁)。例如:
OAuth2的Access Token仅用于访问资源服务器的API,不包含用户身份信息;
若需要身份认证,需结合JWT或OpenID Connect。
四种授权模式的适用场景
授权码模式:Web应用、移动应用(最安全,授权码通过后端传输,避免令牌暴露在前端);
密码模式:内部系统(如公司OA系统,用户信任客户端,可直接输入密码);
客户端凭证模式:服务间通信(如微服务A请求微服务B的API,无用户参与);
隐式模式:纯前端应用(如Vue/React单页应用,无后端,令牌直接返回给前端)。
18.9.4 OpenID Connect:基于OAuth2的身份认证协议(解决“单点登录”)
-
什么是OpenID Connect?
OpenID Connect(OIDC)是基于OAuth2的身份认证协议,它在OAuth2的基础上增加了身份认证的功能,解决了“第三方应用如何确认用户身份”的问题。OIDC的核心是ID Token(包含用户身份信息的JWT令牌),实现了单点登录(SSO):用户登录一次,即可访问多个关联应用。
OIDC的核心组件
ID Token:JWT格式的身份令牌,包含用户身份信息(如用户ID、姓名、邮箱);
Access Token:OAuth2的访问令牌,用于访问资源服务器的API;
Refresh Token:用于刷新Access Token和ID Token;
UserInfo端点:用于获取更详细的用户信息(如头像、手机号)。 -
核心实践:OIDC授权码模式
OIDC的授权流程基于OAuth2的授权码模式,增加了获取ID Token的步骤:
1.用户访问客户端,客户端重定向用户到OIDC授权服务器的授权页面;
2.用户登录并授权客户端;
3.授权服务器返回授权码给客户端;
4.客户端使用授权码请求ID Token、Access Token、Refresh Token;
5.客户端验证ID Token的合法性,确认用户身份;
6.客户端使用Access Token访问资源服务器的API。
代码示例:ASP.NET Core配置OIDC客户端
以下示例展示如何在ASP.NET Core Web应用中配置OIDC客户端,使用IdentityServer4作为授权服务器:
csharp
// Program.cs
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie()
// 配置OpenID Connect客户端
.AddOpenIdConnect("OIDC", options =>
{
// OIDC授权服务器地址
options.Authority = "https://your-oidc-server.com";
options.ClientId = "your-client-id"; // 客户端ID
options.ClientSecret = "your-client-secret"; // 客户端密钥
options.ResponseType = "code"; // 授权码模式
options.Scope.Add("openid"); // 必须包含的OIDC范围
options.Scope.Add("profile"); // 请求用户基本信息(姓名、头像)
options.Scope.Add("email"); // 请求用户邮箱
// 回调地址(需在授权服务器注册)
options.CallbackPath = "/signin-oidc";
// 登出回调地址
options.SignedOutCallbackPath = "/signout-callback-oidc";
// 配置ID Token验证参数
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true, // 验证颁发者
ValidateAudience = true, // 验证受众
ValidateLifetime = true, // 验证过期时间
NameClaimType = "name", // 用户名对应的Claim类型
RoleClaimType = "role" // 用户角色对应的Claim类型
};
// 事件回调:获取ID Token后处理用户信息
options.Events = new OpenIdConnectEvents
{
OnTokenValidated = context =>
{
// 从ID Token中提取用户信息
var userId = context.Principal.FindFirst("sub")?.Value; // OIDC标准Claim:用户唯一ID
var userName = context.Principal.FindFirst("name")?.Value;
Console.WriteLine($"用户登录成功:{userName}(ID:{userId})");
return Task.CompletedTask;
}
};
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/login", async context =>
{
// 触发OIDC登录流程
await context.ChallengeAsync("OIDC", new AuthenticationProperties { RedirectUri = "/" });
});
app.MapGet("/", (ClaimsPrincipal user) =>
{
if (user.Identity.IsAuthenticated)
{
return Results.Ok(new
{
UserId = user.FindFirst("sub")?.Value,
UserName = user.FindFirst("name")?.Value,
Email = user.FindFirst("email")?.Value
});
}
return Results.Redirect("/login");
});
app.Run();
逐行讲解:
1.AddOpenIdConnect("OIDC"):配置OIDC客户端,核心参数:
oAuthority:OIDC授权服务器的地址(如IdentityServer4的地址);
oScope:必须包含openid(OIDC标准范围),可选profile、email等;
oResponseType = "code":使用授权码模式(OIDC推荐的模式);
2.TokenValidationParameters:配置ID Token的验证规则,核心是验证签名(确保ID Token来自信任的授权服务器);
3.OnTokenValidated:事件回调,在ID Token验证通过后,提取用户身份信息(OIDC标准Claim如sub(用户唯一ID)、name(用户名))。
3. 基础知识拓展
OIDC与OAuth2的区别
| OAuth2 | OIDC |
|---|---|
| 授权框架,解决“第三方应用如何访问用户资源” | 身份认证协议,解决“第三方应用如何确认用户身份” |
| 核心令牌:Access Token | 核心令牌:ID Token(JWT格式)+ Access Token |
| 无用户身份信息 | 自包含用户身份信息,支持单点登录(SSO) |
OIDC的单点登录(SSO)原理
用户登录一次OIDC授权服务器后,授权服务器会在用户浏览器中设置会话Cookie。当用户访问其他关联的OIDC客户端时,客户端会重定向用户到授权服务器,授权服务器检测到用户已登录,直接返回授权码,无需用户再次登录,实现单点登录。
18.9.5 对比与最佳实践
- 技术选型对比
| 技术 | 核心作用 | 适用场景 |
|---|---|---|
| JWT | 无状态身份认证令牌 | API服务认证、前后端分离应用 |
| OAuth2 | 授权框架 | 第三方应用访问用户资源、服务间通信 |
| OIDC | 身份认证协议(基于OAuth2) | 单点登录(SSO)、跨应用身份认证 |
-
安全最佳实践
优先使用OIDC:若需要身份认证和单点登录,优先使用OIDC而非纯OAuth2;
JWT使用非对称加密:生产环境中JWT的签名算法优先使用RS256(私钥签名,公钥验证),避免对称加密的密钥泄露风险;
OAuth2使用授权码模式:除内部系统外,优先使用授权码模式,避免使用密码模式(需用户提供密码);
令牌存储安全:前端应用中,令牌应存储在HttpOnly Cookie中,避免XSS攻击;后端应用中,令牌应存储在内存或加密的配置中心。
18.9.6 思考题
1.JWT令牌无法主动吊销,如何解决这个问题?(提示:服务器维护令牌黑名单、使用Refresh Token刷新)
2.OAuth2的授权码模式和密码模式的区别是什么?分别适用于什么场景?
3.OIDC的ID Token和Access Token的区别是什么?分别用于什么场景?
4.如何在ASP.NET Core Web API中配置JWT认证,保护敏感接口?
5.设计一个基于OIDC的单点登录系统,包含两个客户端应用和一个OIDC授权服务器,描述核心流程。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49552.html










