-
C#网络编程中的身份认证(JWT、OAuth2、OpenID Connect)
第17章 加密与解密
17.4 身份认证(JWT、OAuth2、OpenID Connect)
一、我踩过的“身份认证坑”:从Session到JWT的血泪史
刚做分布式系统时,用Session做身份认证——结果服务器集群部署后,Session共享成了噩梦,用户登录后换个服务器就需要重新登录,后来用Redis存Session又遇到了Redis单点故障。换成JWT后,一开始图方便用HS256对称加密,还把用户密码存在Payload里,结果被安全扫vb.net教程C#教程python教程SQL教程access 2010教程描测出“JWT敏感数据泄露”、“签名密钥硬编码”漏洞。再后来做第三方登录,用OAuth2授权码模式,一开始没搞懂授权码和AccessToken的区别,直接把AccessToken返回给前端,导致被劫持。最后用OpenID Connect做统一身份认证,才终于搞定了所有场景。这节我把自己从“认证小白”到“安全合规”的踩坑经验揉进去,用大白话讲透JWT、OAuth2、OpenID Connect的核心原理,结合C#实战代码逐行讲解,以及常见坑和最佳实践,让你的身份认证既安全又好用。
二、JWT:分布式系统的“无状态身份凭证”
JWT(JSON Web Token)是一种无状态的身份认证协议——它把用户身份信息存在JSON格式的Token里,用签名保证Token不被篡改,适合分布式系统(比如微服务、前后端分离),不需要共享Session,每个服务都能独立验证Token。
先搞懂JWT的核心结构(拓展知识)
JWT由三部分组成,用.分隔:
Header.Payload.Signature
比如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
-
Header(头部):指定签名算法和Token类型
json
{
"alg": "HS256", // 签名算法:HS256(对称加密)、RS256(非对称加密)
"typ": "JWT" // Token类型:JWT
}
然后Base64Url编码成第一部分。
2. Payload(载荷):存储用户身份信息和声明
Payload是JSON格式,包含标准声明和自定义声明:
json
{
"sub": "1234567890", // 标准声明:用户唯一ID
"name": "John Doe", // 标准声明:用户名
"iat": 1516239022, // 标准声明:Token签发时间(Unix时间戳)
"exp": 1516242622, // 标准声明:Token过期时间(必须设置,否则永久有效)
"role": "admin" // 自定义声明:用户角色
}
然后Base64Url编码成第二部分。注意:Payload是Base64Url编码,不是加密,任何人都能解码,所以绝对不能存敏感数据(比如密码、银行卡号)!
3. Signature(签名):保证Token不被篡改
签名是用Header指定的算法,把Header和Payload的Base64Url编码用.连接,再用密钥加密:
HMACSHA256(
base64UrlEncode(Header) + "." + base64UrlEncode(Payload),
"your-secret-key"
)
签名的作用是验证Token是否被篡改——如果Header或Payload被修改,签名就会失效,服务端验证时会拒绝。
实战1:用C#生成和验证JWT(RS256非对称加密)
推荐用RS256非对称加密——私钥只在授权服务器,公钥可以公开给所有服务,比HS256对称加密更安全(HS256的密钥如果泄露,任何人都能生成Token)。
步骤1:生成RSA密钥对(PowerShell命令)
powershell
# 生成RSA密钥对,保存到文件
ssh-keygen -t rsa -b 2048 -m PEM -f jwtRS256.key
# 生成公钥
openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
步骤2:用System.IdentityModel.Tokens.Jwt库生成和验证JWT
csharp
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using Microsoft.IdentityModel.Tokens;
namespace JwtDemo;
class Program
{
static void Main(string[] args)
{
// 1. 生成JWT Token(授权服务器用私钥签名)
string privateKeyPath = @"C:keysjwtRS256.key";
string token = GenerateJwtToken(privateKeyPath);
Console.WriteLine("生成的JWT Token:");
Console.WriteLine(token);
// 2. 验证JWT Token(服务端用公钥验证)
string publicKeyPath = @"C:keysjwtRS256.key.pub";
bool isValid = ValidateJwtToken(token, publicKeyPath, out ClaimsPrincipal principal);
Console.WriteLine($"
Token验证结果:{isValid}");
if (isValid)
{
Console.WriteLine("用户信息:");
foreach (var claim in principal.Claims)
{
Console.WriteLine($"{claim.Type}: {claim.Value}");
}
}
}
static string GenerateJwtToken(string privateKeyPath)
{
// 加载RSA私钥
var rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText(privateKeyPath));
// 1. 设置Token的声明(Claims)
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, "123456"), // 用户唯一ID
new Claim(JwtRegisteredClaimNames.Name, "张三"), // 用户名
new Claim(JwtRegisteredClaimNames.Email, "zhangsan@example.com"), // 邮箱
new Claim(ClaimTypes.Role, "admin"), // 自定义声明:角色
new Claim("department", "技术部") // 自定义声明:部门
};
// 2. 设置Token的过期时间(必须设置,比如1小时)
var expires = DateTime.UtcNow.AddHours(1);
// 3. 创建签名凭据(用RSA私钥)
var signingCredentials = new SigningCredentials(
new RsaSecurityKey(rsa),
SecurityAlgorithms.RsaSha256 // RS256算法
);
// 4. 创建JWT Token
var token = new JwtSecurityToken(
issuer: "https://auth.example.com", // 签发者(授权服务器地址)
audience: "https://api.example.com", // 受众(服务端地址)
claims: claims,
notBefore: DateTime.UtcNow, // 生效时间
expires: expires,
signingCredentials: signingCredentials
);
// 5. 序列化Token为字符串
return new JwtSecurityTokenHandler().WriteToken(token);
}
static bool ValidateJwtToken(string token, string publicKeyPath, out ClaimsPrincipal principal)
{
principal = null;
try
{
// 加载RSA公钥
var rsa = RSA.Create();
rsa.ImportFromPem(File.ReadAllText(publicKeyPath));
// 设置验证参数
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = true, // 验证签发者
ValidIssuer = "https://auth.example.com",
ValidateAudience = true, // 验证受众
ValidAudience = "https://api.example.com",
ValidateLifetime = true, // 验证过期时间
ClockSkew = TimeSpan.FromMinutes(5), // 允许的时间偏差(比如服务器时间不同步)
IssuerSigningKey = new RsaSecurityKey(rsa) // 用公钥验证签名
};
// 验证Token
var handler = new JwtSecurityTokenHandler();
principal = handler.ValidateToken(token, validationParameters, out _);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"Token验证失败:{ex.Message}");
return false;
}
}
}
代码逐行讲解:
生成Token部分:
1.RSA.Create():加载RSA私钥,用于签名Token;
2.Claims:存储用户身份信息,标准声明用JwtRegisteredClaimNames,自定义声明用任意字符串;
3.expires:必须设置过期时间,避免Token永久有效,一般设置1-2小时;
4.SigningCredentials:指定签名算法和私钥,RS256是安全的非对称加密算法;
5.JwtSecurityToken:创建Token实例,指定签发者、受众、声明、过期时间、签名凭据;
6.WriteToken:把Token序列化为字符串。
验证Token部分:
1.TokenValidationParameters:设置验证规则——必须验证签发者、受众、过期时间,用公钥验证签名;
2.ClockSkew:允许的时间偏差,比如服务器时间不同步,设置5分钟;
3.ValidateToken:验证Token,返回ClaimsPrincipal,包含用户的所有声明。
实战2:在ASP.NET Core中用JWT做身份认证
csharp
// Program.cs(ASP.NET Core 8.0)
var builder = WebApplication.CreateBuilder(args);
// 1. 配置JWT身份认证
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = builder.Configuration["Jwt:Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5),
// 从配置文件加载公钥
IssuerSigningKey = new RsaSecurityKey(RSA.Create().ImportFromPem(builder.Configuration["Jwt:PublicKey"]))
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication(); // 启用身份认证
app.UseAuthorization(); // 启用授权
// 受保护的API,需要JWT Token
app.MapGet("/api/protected", (ClaimsPrincipal user) =>
{
return Results.Ok(new
{
Message = "这是受保护的API",
UserId = user.FindFirstValue(JwtRegisteredClaimNames.Sub),
UserName = user.FindFirstValue(JwtRegisteredClaimNames.Name)
});
}).RequireAuthorization(); // 要求授权
app.Run();
appsettings.json配置:
json
{
"Jwt": {
"Issuer": "https://auth.example.com",
"Audience": "https://api.example.com",
"PublicKey": "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"
}
}
JWT的常见坑与最佳实践
1.不要存敏感数据:Payload是Base64Url编码,不是加密,绝对不能存密码、银行卡号等敏感数据;
2.用RS256非对称加密:不要用HS256对称加密,除非你能保证密钥绝对安全;
3.设置合理的过期时间:一般1-2小时,过期后用Refresh Token刷新,不要设置太长;
4.Refresh Token的存储:Refresh Token要存在数据库,绑定用户ID,过期后失效,避免Refresh Token泄露;
5.用HTTPS传输Token:Token是身份凭证,必须用HTTPS传输,避免被劫持;
6.不要把Token存在LocalStorage:前端建议把Token存在HttpOnly Cookie里,防止XSS攻击;
7.验证所有声明:服务端必须验证签发者、受众、过期时间,不要只验证签名。
三、OAuth2:第三方授权登录的“标准协议”
OAuth2(开放授权2.0)是第三方授权登录的标准协议——它允许用户授权第三方应用访问自己的资源(比如微信登录,允许微信授权第三方应用获取用户的昵称、头像),而不需要把密码告诉第三方应用。
先搞懂OAuth2的核心角色和授权模式(拓展知识)
-
核心角色
资源所有者(Resource Owner):用户,比如你自己;
客户端(Client):第三方应用,比如用微信登录的电商网站;
授权服务器(Authorization Server):微信的授权服务器,负责验证用户身份,颁发AccessToken;
资源服务器(Resource Server):微信的API服务器,负责提供用户资源(比如昵称、头像)。 -
四种授权模式
模式 适用场景 安全级别
授权码模式(Authorization Code) 服务器端应用、前后端分离应用 最高
密码模式(Password) 内部系统、信任的应用 中等
客户端凭证模式(Client Credentials) 服务间调用、后台任务 高
隐式模式(Implicit) 旧的前端应用(已淘汰) 低
最常用的是授权码模式,流程如下:
1.用户点击“微信登录”,跳转到微信授权服务器;
2.用户登录微信,授权第三方应用访问自己的资源;
3.微信授权服务器返回授权码(Authorization Code)给第三方应用;
4.第三方应用用授权码向微信授权服务器请求AccessToken;
5.微信授权服务器返回AccessToken给第三方应用;
6.第三方应用用AccessToken向微信资源服务器请求用户资源。
实战:用OpenIddict搭建OAuth2授权服务器(授权码模式)
OpenIddict是ASP.NET Core的轻量级OAuth2/OpenID Connect框架,比IdentityServer4更简洁,适合搭建授权服务器。
步骤1:安装OpenIddict NuGet包
bash
Install-Package OpenIddict.AspNetCore
Install-Package OpenIddict.EntityFrameworkCore
步骤2:配置OpenIddict(Program.cs)
csharp
var builder = WebApplication.CreateBuilder(args);
// 添加DB上下文(用SQL Server或SQLite)
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
options.UseOpenIddict(); // 启用OpenIddict的EF Core支持
});
// 添加OpenIddict
builder.Services.AddOpenIddict()
// 启用核心服务
.AddCore(options =>
{
options.UseEntityFrameworkCore().UseDbContext<ApplicationDbContext>();
})
// 启用服务器服务
.AddServer(options =>
{
// 启用授权码模式
options.AllowAuthorizationCodeFlow();
// 启用刷新Token
options.AllowRefreshTokenFlow();
// 启用AccessToken的JWT格式
options.UseJsonWebTokens();
// 用RSA私钥签名Token(之前生成的jwtRS256.key)
options.AddSigningKey(RSA.Create().ImportFromPem(File.ReadAllText(@"C:keysjwtRS256.key")));
// 用RSA公钥验证Token(可选,因为签名是用私钥,验证用公钥)
options.AddEncryptionKey(RSA.Create().ImportFromPem(File.ReadAllText(@"C:keysjwtRS256.key.pub")));
// 配置端点
options.SetAuthorizationEndpointUris("/connect/authorize");
options.SetTokenEndpointUris("/connect/token");
options.SetUserinfoEndpointUris("/connect/userinfo");
// 允许从本地主机回调(测试用,生产环境换成真实域名)
options.RegisterScopes(OpenIddictConstants.Scopes.OpenId, OpenIddictConstants.Scopes.Email, OpenIddictConstants.Scopes.Profile);
options.AllowLocalhostRedirectUris("https://localhost:5001/signin-oidc");
options.AllowPostLogoutRedirectUris("https://localhost:5001/signout-callback-oidc");
// 配置ASP.NET Core集成
options.UseAspNetCore()
.EnableAuthorizationEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough();
})
// 启用验证服务(用于验证Token)
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
// 添加身份认证和授权
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// 配置授权端点的UI(简化版,实际项目用Razor Pages或MVC)
app.MapGet("/connect/authorize", async (HttpContext context) =>
{
// 简化逻辑:直接授权,实际项目需要用户登录和确认授权
var request = context.GetOpenIddictServerRequest();
var response = await context.AuthenticateAsync();
return Results.Ok(new { code = "authorization-code-123" });
});
// 配置Token端点
app.MapPost("/connect/token", async (HttpContext context) =>
{
var request = context.GetOpenIddictServerRequest();
if (request.GrantType == OpenIddictConstants.GrantTypes.AuthorizationCode)
{
// 验证授权码(简化逻辑,实际项目从数据库查询)
if (request.Code != "authorization-code-123")
{
return Results.BadRequest(new { error = "invalid_grant" });
}
// 生成JWT AccessToken
var token = new JwtSecurityToken(
issuer: "https://auth.example.com",
audience: "https://api.example.com",
claims: new[] { new Claim(ClaimTypes.Name, "张三") },
expires: DateTime.UtcNow.AddHours(1),
signingCredentials: new SigningCredentials(
new RsaSecurityKey(RSA.Create().ImportFromPem(File.ReadAllText(@"C:keysjwtRS256.key"))),
SecurityAlgorithms.RsaSha256
)
);
return Results.Ok(new
{
access_token = new JwtSecurityTokenHandler().WriteToken(token),
token_type = "Bearer",
expires_in = 3600,
refresh_token = "refresh-token-123"
});
}
return Results.BadRequest(new { error = "unsupported_grant_type" });
});
app.Run();
代码逐行讲解:
1.AddOpenIddict:配置OpenIddict的核心、服务器、验证服务;
2.AllowAuthorizationCodeFlow:启用授权码模式;
3.UseJsonWebTokens:设置AccessToken为JWT格式;
4.AddSigningKey:用RSA私钥签名Token;
5.SetAuthorizationEndpointUris:配置授权端点、Token端点、用户信息端点;
6.MapGet("/connect/authorize"):简化的授权端点,实际项目需要用户登录和确认授权;
7.MapPost("/connect/token"):Token端点,验证授权码,生成AccessToken和Refresh Token。
OAuth2的常见坑与最佳实践
1.不要用隐式模式:隐式模式直接返回AccessToken给前端,容易被劫持,已淘汰;
2.授权码模式的安全:授权码必须用HTTPS传输,第三方应用必须验证授权码的合法性;
3.AccessToken的存储:前端不要把AccessToken存在LocalStorage,存在HttpOnly Cookie里;
4.Refresh Token的安全:Refresh Token要存在数据库,绑定用户ID和客户端ID,过期后失效;
5.客户端密钥的保密:服务器端应用的客户端密钥要存在环境变量,不要硬编码;
6.不要用密码模式给第三方应用:密码模式需要用户输入密码,只适合内部系统。
四、OpenID Connect:统一身份认证的“标准协议”
OpenID Connect(OIDC)是基于OAuth2的身份认证协议——它在OAuth2的基础上增加了ID Token,解决了OAuth2没有身份信息的问题,适合统一身份认证(比如用一个账号登录多个系统)。
先搞懂OpenID Connect的核心概念(拓展知识)
-
ID Token:身份凭证
ID Token是JWT格式,包含用户的身份信息,比如用户ID、用户名、邮箱,用于验证用户身份:
json
{
"sub": "123456", // 用户唯一ID
"name": "张三",
"email": "zhangsan@example.com",
"iss": "https://auth.example.com", // 签发者
"aud": "client-id-123", // 受众(客户端ID)
"exp": 1516242622, // 过期时间
"iat": 1516239022 // 签发时间
}
-
OIDC的流程(基于授权码模式)
1.用户点击“统一登录”,跳转到OIDC授权服务器;
2.用户登录,授权第三方应用获取身份信息;
3.授权服务器返回授权码给第三方应用;
4.第三方应用用授权码请求AccessToken和ID Token;
5.授权服务器返回AccessToken、ID Token、Refresh Token;
6.第三方应用用ID Token验证用户身份,用AccessToken获取用户资源。
实战:在ASP.NET Core中用OIDC做客户端(微信登录)
csharp
// Program.cs(ASP.NET Core 8.0)
var builder = WebApplication.CreateBuilder(args);
// 添加OIDC客户端
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie() // 用Cookie存储身份信息
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
// 微信OIDC授权服务器的配置(实际用微信的真实地址)
options.Authority = "https://open.weixin.qq.com/connect/oauth2/authorize";
options.ClientId = "your-weixin-client-id";
options.ClientSecret = "your-weixin-client-secret";
options.ResponseType = "code"; // 授权码模式
options.Scope.Add("openid"); // OIDC必须的Scope
options.Scope.Add("profile"); // 获取用户昵称、头像
options.Scope.Add("email"); // 获取用户邮箱
// 回调地址(必须和微信后台配置的一致)
options.CallbackPath = "/signin-oidc";
options.SignedOutCallbackPath = "/signout-callback-oidc";
// 配置Token端点的地址
options.TokenEndpoint = "https://api.weixin.qq.com/sns/oauth2/access_token";
options.UserInformationEndpoint = "https://api.weixin.qq.com/sns/userinfo";
// 配置JWT验证
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://open.weixin.qq.com",
ValidateAudience = true,
ValidAudience = "your-weixin-client-id"
};
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// 受保护的页面
app.MapGet("/protected", (ClaimsPrincipal user) =>
{
return Results.Ok(new
{
Message = "这是受保护的页面",
UserId = user.FindFirstValue(ClaimTypes.NameIdentifier),
UserName = user.FindFirstValue(ClaimTypes.Name)
});
}).RequireAuthorization();
// 登录页面
app.MapGet("/login", (HttpContext context) =>
{
return Results.Challenge(new AuthenticationProperties { RedirectUri = "/" });
});
app.Run();
代码逐行讲解:
1.AddOpenIdConnect:配置OIDC客户端,指定授权服务器地址、客户端ID、客户端密钥;
2.ResponseType = "code":用授权码模式;
3.Scope.Add("openid"):必须添加openid Scope,否则OIDC授权服务器不会返回ID Token;
4.CallbackPath:回调地址,必须和授权服务器后台配置的一致;
5.TokenValidationParameters:验证ID Token的签发者、受众,确保ID Token合法。
OpenID Connect的常见坑与最佳实践
1.必须验证ID Token:ID Token是身份凭证,必须验证签名、签发者、受众、过期时间;
2.用HTTPS传输ID Token:ID Token包含用户身份信息,必须用HTTPS传输;
3.不要存ID Token在前端:前端建议把ID Token存在HttpOnly Cookie里,防止XSS攻击;
4.统一身份认证的场景:适合企业内部系统、SaaS应用,用一个账号登录多个系统;
5.支持多租户:OIDC支持多租户,不同租户用不同的客户端ID和密钥。
五、总结:三种协议的适用场景对比
| 协议 | 核心特性 | 适用场景 |
|---|---|---|
| JWT | 无状态身份凭证,验证简单 | 分布式系统、微服务、前后端分离 |
| OAuth2 | 第三方授权登录,资源访问 | 微信登录、GitHub登录、服务间调用 |
| OpenID Connect | 统一身份认证,ID Token | 统一登录、多系统身份认证 |
最佳实践:
1.分布式系统用JWT:配合RS256非对称加密,设置合理的过期时间;
2.第三方登录用OAuth2授权码模式:不要用密码模式或隐式模式;
3.统一身份认证用OpenID Connect:在OAuth2的基础上增加ID Token,验证用户身份;
4.安全第一:所有凭证必须用HTTPS传输,不要存敏感数据,验证所有声明和Token。
下一节我们会学习网络编程的安全防护:防火墙、入侵检测、DDoS防护,让你的网络系统固若金汤。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49539.html










