Skip to content

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.AspNetIdentity

Program.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 的区别?

特性IdentityServer4JWT
定位完整的身份认证和授权框架令牌格式
功能认证、授权、令牌管理、SSO仅令牌格式
复杂度
适用场景微服务、多系统简单应用

IdentityServer4 使用 JWT 作为令牌格式。

Q2: OAuth 2.0 的授权码模式为什么最安全?

  1. 授权码不直接暴露 - 授权码通过浏览器重定向传递,不包含敏感信息
  2. 令牌交换在后台 - 客户端用授权码在后端换取令牌,令牌不经过浏览器
  3. 支持 PKCE - 防止授权码拦截攻击

Q3: 如何实现单点登录(SSO)?

原理:

  • 用户在一个系统登录后,IdentityServer 颁发 Cookie
  • 访问其他系统时,系统重定向到 IdentityServer
  • IdentityServer 检测到已登录,直接颁发令牌,无需再次登录

配置:

csharp
// 所有客户端使用相同的 IdentityServer
options.Authority = "https://ids.example.com";

Q4: 如何保护 API?

  1. 配置 API 资源

    csharp
    new ApiResource("api1", "My API")
  2. API 项目验证令牌

    csharp
    builder.Services.AddAuthentication("Bearer")
        .AddJwtBearer("Bearer", options =>
        {
            options.Authority = "https://localhost:5001";
        });
  3. 使用 [Authorize] 特性

    csharp
    [Authorize]
    [ApiController]
    public class ValuesController : ControllerBase { }

Q5: 刷新令牌的作用?

  • 延长会话 - 访问令牌过期后,使用刷新令牌获取新的访问令牌
  • 安全性 - 刷新令牌可以设置更长的过期时间,但需要安全存储
  • 用户体验 - 用户无需频繁登录

八、最佳实践

  1. 使用 HTTPS - 生产环境必须使用 HTTPS
  2. 使用证书签名 - 生产环境使用证书,不要用开发证书
  3. 启用 PKCE - 保护授权码流程
  4. 合理设置令牌过期时间 - 平衡安全性和用户体验
  5. 使用刷新令牌 - 延长会话,提升用户体验
  6. 保护客户端密钥 - 不要泄露客户端密钥
  7. 监控和日志 - 记录认证和授权事件
  8. 定期轮换密钥 - 增强安全性
  9. 不要使用隐式模式 - 已废弃,不安全
  10. 不要在生产环境使用开发证书

基于 VitePress 构建 | Copyright © 2026-present