IdentityServer4 统一授权认证
一、IdentityServer4 基础
1. 什么是 IdentityServer4?
IdentityServer4 是一个用于 ASP.NET Core 的 OpenID Connect 和 OAuth 2.0 框架,用于实现身份认证和授权。
核心功能:
- ✅ 身份认证(Authentication) - 验证用户身份
- ✅ 授权(Authorization) - 控制资源访问权限
- ✅ 单点登录(SSO) - 一次登录,多系统访问
- ✅ API 保护 - 保护 API 资源
- ✅ 令牌管理 - 生成和管理访问令牌、刷新令牌
2. OAuth 2.0 和 OpenID Connect
OAuth 2.0:
- 授权框架,用于获取访问资源的权限
- 不提供身份认证,只提供授权
OpenID Connect(OIDC):
- 基于 OAuth 2.0 的身份认证层
- 在 OAuth 2.0 基础上添加了身份认证功能
关系:
OpenID Connect = OAuth 2.0 + 身份认证3. 核心概念
客户端(Client):
- 请求访问资源的应用程序
- 类型:Web 应用、SPA、移动应用、API
资源(Resource):
- 受保护的资源(API、数据等)
用户(User):
- 资源的所有者
身份服务器(Identity Server):
- 颁发令牌的服务器
令牌(Token):
- 访问令牌(Access Token) - 访问资源的凭证
- 刷新令牌(Refresh Token) - 刷新访问令牌
- ID 令牌(ID Token) - 用户身份信息(OpenID Connect)
二、OAuth 2.0 授权流程
1. 授权码模式(Authorization Code Flow)
最安全的流程,适用于 Web 应用:
1. 用户访问客户端
2. 客户端重定向到 IdentityServer 登录页
3. 用户登录并授权
4. IdentityServer 返回授权码
5. 客户端用授权码换取访问令牌
6. 客户端使用访问令牌访问资源实现步骤:
csharp
// 1. 配置 IdentityServer
public class Config
{
public static IEnumerable<Client> Clients =>
new List<Client>
{
new Client
{
ClientId = "web_client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
AllowedScopes = { "api1", "openid", "profile" },
RedirectUris = { "https://localhost:5001/callback" },
PostLogoutRedirectUris = { "https://localhost:5001" },
RequirePkce = true, // 推荐启用
AllowOfflineAccess = true // 允许刷新令牌
}
};
public static IEnumerable<ApiScope> ApiScopes =>
new List<ApiScope>
{
new ApiScope("api1", "My API")
};
public static IEnumerable<IdentityResource> IdentityResources =>
new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}2. 客户端凭证模式(Client Credentials Flow)
适用于服务间通信(无用户参与):
csharp
// 客户端配置
new Client
{
ClientId = "service_client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ClientCredentials,
AllowedScopes = { "api1" }
}客户端请求令牌:
csharp
var client = new HttpClient();
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = "https://localhost:5001/connect/token",
ClientId = "service_client",
ClientSecret = "secret",
Scope = "api1"
});
var accessToken = tokenResponse.AccessToken;3. 资源所有者密码模式(Resource Owner Password Flow)
不推荐使用,仅用于受信任的客户端:
csharp
new Client
{
ClientId = "password_client",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
AllowedScopes = { "api1" }
}4. 隐式模式(Implicit Flow)
已废弃,不推荐使用。
三、IdentityServer4 配置
1. 安装和配置
安装 NuGet 包:
bash
dotnet add package IdentityServer4
dotnet add package IdentityServer4.AspNetIdentityProgram.cs 配置:
csharp
using IdentityServer4;
using IdentityServer4.Models;
var builder = WebApplication.CreateBuilder(args);
// 添加 IdentityServer
builder.Services.AddIdentityServer()
.AddInMemoryClients(Config.Clients)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddDeveloperSigningCredential(); // 开发环境,生产环境使用证书
var app = builder.Build();
app.UseIdentityServer();
app.Run();2. 客户端配置详解
csharp
new Client
{
ClientId = "web_client", // 客户端唯一标识
ClientName = "Web Application", // 客户端名称
ClientSecrets = { new Secret("secret".Sha256()) }, // 客户端密钥
// 授权类型
AllowedGrantTypes = GrantTypes.Code, // 授权码模式
// 允许的作用域
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
},
// 重定向 URI(授权后回调)
RedirectUris = { "https://localhost:5001/callback" },
// 登出后重定向 URI
PostLogoutRedirectUris = { "https://localhost:5001" },
// 是否要求 PKCE(推荐)
RequirePkce = true,
// 是否允许离线访问(刷新令牌)
AllowOfflineAccess = true,
// 访问令牌生命周期(秒)
AccessTokenLifetime = 3600,
// 刷新令牌使用方式
RefreshTokenUsage = TokenUsage.ReUse, // 或 TokenUsage.OneTimeOnly
// 刷新令牌过期时间(秒)
RefreshTokenExpiration = TokenExpiration.Sliding
}3. API 资源保护
API 项目配置:
csharp
// Program.cs
builder.Services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("scope", "api1");
});
});
// 使用
[Authorize(Policy = "ApiScope")]
[ApiController]
[Route("api/[controller]")]
public class ValuesController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok(new { message = "Protected data" });
}
}四、用户认证
1. 使用 ASP.NET Core Identity
安装包:
bash
dotnet add package IdentityServer4.AspNetIdentity配置:
csharp
// 添加 Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// 配置 IdentityServer
builder.Services.AddIdentityServer()
.AddAspNetIdentity<ApplicationUser>()
.AddInMemoryClients(Config.Clients)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddDeveloperSigningCredential();2. 自定义用户存储
csharp
public class CustomUserStore : IUserStore<User>
{
// 实现 IUserStore 接口
// 从数据库或其他存储读取用户信息
}五、令牌管理
1. 访问令牌(Access Token)
特点:
- 短期有效(通常 1 小时)
- 包含用户信息和权限
- 用于访问受保护的资源
自定义声明:
csharp
public class CustomProfileService : IProfileService
{
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
var user = await _userManager.GetUserAsync(context.Subject);
var claims = new List<Claim>
{
new Claim("email", user.Email),
new Claim("role", user.Role)
};
context.IssuedClaims.AddRange(claims);
}
public async Task IsActiveAsync(IsActiveContext context)
{
var user = await _userManager.GetUserAsync(context.Subject);
context.IsActive = user != null && user.IsActive;
}
}2. 刷新令牌(Refresh Token)
使用刷新令牌:
csharp
var tokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
{
Address = "https://localhost:5001/connect/token",
RefreshToken = refreshToken,
ClientId = "web_client",
ClientSecret = "secret"
});
var newAccessToken = tokenResponse.AccessToken;
var newRefreshToken = tokenResponse.RefreshToken;3. 令牌撤销
csharp
// 撤销刷新令牌
var revocationResponse = await client.RevokeTokenAsync(new TokenRevocationRequest
{
Address = "https://localhost:5001/connect/revocation",
ClientId = "web_client",
ClientSecret = "secret",
Token = refreshToken,
TokenTypeHint = "refresh_token"
});六、客户端集成
1. MVC 客户端
csharp
// Program.cs
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://localhost:5001";
options.ClientId = "web_client";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
options.Scope.Add("api1");
options.Scope.Add("offline_access");
});
// 使用
[Authorize]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}2. API 客户端
csharp
// 使用 HttpClient
var client = new HttpClient();
client.SetBearerToken(accessToken);
var response = await client.GetAsync("https://localhost:5002/api/values");
var content = await response.Content.ReadAsStringAsync();3. JavaScript 客户端(SPA)
javascript
// 使用 oidc-client.js
import { UserManager } from 'oidc-client';
const userManager = new UserManager({
authority: 'https://localhost:5001',
client_id: 'spa_client',
redirect_uri: 'https://localhost:5003/callback',
response_type: 'code',
scope: 'openid profile api1',
post_logout_redirect_uri: 'https://localhost:5003'
});
// 登录
userManager.signinRedirect();
// 处理回调
userManager.signinRedirectCallback().then(user => {
console.log('Access Token:', user.access_token);
});
// 获取用户信息
userManager.getUser().then(user => {
if (user) {
console.log('User:', user);
}
});七、常见面试题
Q1: IdentityServer4 和 JWT 的区别?
| 特性 | IdentityServer4 | JWT |
|---|---|---|
| 定位 | 完整的身份认证和授权框架 | 令牌格式 |
| 功能 | 认证、授权、令牌管理、SSO | 仅令牌格式 |
| 复杂度 | 高 | 低 |
| 适用场景 | 微服务、多系统 | 简单应用 |
IdentityServer4 使用 JWT 作为令牌格式。
Q2: OAuth 2.0 的授权码模式为什么最安全?
- 授权码不直接暴露 - 授权码通过浏览器重定向传递,不包含敏感信息
- 令牌交换在后台 - 客户端用授权码在后端换取令牌,令牌不经过浏览器
- 支持 PKCE - 防止授权码拦截攻击
Q3: 如何实现单点登录(SSO)?
原理:
- 用户在一个系统登录后,IdentityServer 颁发 Cookie
- 访问其他系统时,系统重定向到 IdentityServer
- IdentityServer 检测到已登录,直接颁发令牌,无需再次登录
配置:
csharp
// 所有客户端使用相同的 IdentityServer
options.Authority = "https://ids.example.com";Q4: 如何保护 API?
配置 API 资源
csharpnew ApiResource("api1", "My API")API 项目验证令牌
csharpbuilder.Services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { options.Authority = "https://localhost:5001"; });使用 [Authorize] 特性
csharp[Authorize] [ApiController] public class ValuesController : ControllerBase { }
Q5: 刷新令牌的作用?
- 延长会话 - 访问令牌过期后,使用刷新令牌获取新的访问令牌
- 安全性 - 刷新令牌可以设置更长的过期时间,但需要安全存储
- 用户体验 - 用户无需频繁登录
八、最佳实践
- ✅ 使用 HTTPS - 生产环境必须使用 HTTPS
- ✅ 使用证书签名 - 生产环境使用证书,不要用开发证书
- ✅ 启用 PKCE - 保护授权码流程
- ✅ 合理设置令牌过期时间 - 平衡安全性和用户体验
- ✅ 使用刷新令牌 - 延长会话,提升用户体验
- ✅ 保护客户端密钥 - 不要泄露客户端密钥
- ✅ 监控和日志 - 记录认证和授权事件
- ✅ 定期轮换密钥 - 增强安全性
- ❌ 不要使用隐式模式 - 已废弃,不安全
- ❌ 不要在生产环境使用开发证书