VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • 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

  1. 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的核心角色和授权模式(拓展知识)

  1. 核心角色
    资源所有者(Resource Owner):用户,比如你自己;
    客户端(Client):第三方应用,比如用微信登录的电商网站;
    授权服务器(Authorization Server):微信的授权服务器,负责验证用户身份,颁发AccessToken;
    资源服务器(Resource Server):微信的API服务器,负责提供用户资源(比如昵称、头像)。
  2. 四种授权模式
    模式 适用场景 安全级别
    授权码模式(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的核心概念(拓展知识)

  1. 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 // 签发时间
	}
  1. 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


相关教程