实战项目-双端验证

双端验证

在此项目中,User和Admin两端是分别发送了不同格式的Token以区分登陆状态

Admin: Token: <token>

User: Authorization:<token>

所以我们在配置JWT验证规则时,应该分别为其配置验证规则

// 配置Admin的验证规则
.AddJwtBearer("Admin", options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Key"] ??
                                    throw new InvalidOperationException())),

        ValidateIssuer = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidateAudience = true,
        ValidAudience = builder.Configuration["Jwt:Audience"]
    };
    // 使用自定义处理请求头的方式
    options.EventsType = typeof(AdminJwtBearerEvents);
})
    
// 配置User的验证规则
.AddJwtBearer("User", options =>
{
    options.RequireHttpsMetadata = false;
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.ASCII.GetBytes(builder.Configuration["Jwt:Key"] ??
                                    throw new InvalidOperationException())),
        
        ValidateIssuer = true,
        ValidIssuer = builder.Configuration["Jwt:Issuer"],
        ValidateAudience = true,
        ValidAudience = builder.Configuration["Jwt:Audience"]
    };
    // 使用自定义处理请求头的方式
    options.EventsType = typeof(UserJwtBearerEvents);
});

JwtBearerEvents

分别添加Admin身份认证方案和User身份认证方案

// 配置Admin身份认证方案处理请求头中Token的方式
public class AdminJwtBearerEvents : JwtBearerEvents
{
    public override Task MessageReceived(MessageReceivedContext context)
    {
        var token = context.Request.Headers["Token"].FirstOrDefault();
        if (!string.IsNullOrEmpty(token))
        {
            context.Token = token;
        }
        return base.MessageReceived(context);
    }
}

// 配置User身份认证方案处理请求头中Token的方式
public class UserJwtBearerEvents : JwtBearerEvents
{
    public override Task MessageReceived(MessageReceivedContext context)
    {
        var token = context.Request.Headers["Authorization"].FirstOrDefault();
        if (!string.IsNullOrEmpty(token))
        {
            context.Token = token;
        }
        return base.MessageReceived(context);
    }
}

AuthenticationSchemes

我们在Controller中,可以使用不同的身份认证方案来对不同的Action方法进行验证

/// <summary>
/// 员工登出
/// </summary>
/// <returns></returns>
[Authorize(AuthenticationSchemes = "Admin")]
[HttpPost("logout")]
public Result<string> Logout()
{
    return Result<string>.Success();
}

我们也可以将这个方法直接添加到控制器类上,再将登陆方法暴露出来

[ApiController]
[Route("/admin/employee")]
[Authorize(AuthenticationSchemes = "Admin")]
public class EmployeeController : ControllerBase
{
    /// <summary>
    /// 员工登录Action
    /// </summary>
    /// <param name="employeeLoginReq"></param>
    /// <returns></returns>
    [HttpPost("login")]
    [AllowAnonymous]
    public Result<EmployeeLoginResp> Login([FromBody] EmployeeLoginReq employeeLoginReq)
    {
    }

Swagger按钮

#region MyRegion Swagger

builder.Services.AddSwaggerGen(
    options =>
    {
        // 以下配置影响两个输入Token的按钮
        // 添加一个安全定义,定义名称为"Admin"
        options.AddSecurityDefinition("Admin", new OpenApiSecurityScheme
        {
            // 描述如何使用Token进行身份验证
            Description = "Token: <token>",
            // 指定认证头的名称,这里是"Token"
            Name = "Token",
            // 指定认证信息应该放置的位置,这里是HTTP头部
            In = ParameterLocation.Header,
            // 指定安全方案的类型,这里是指API密钥类型
            Type = SecuritySchemeType.ApiKey,
        });
        // 添加安全要求,这将告诉Swagger UI哪些安全方案是必需的
        options.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme
                {
                    // 引用之前定义的安全方案
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "Admin" // 对应于上面定义的安全方案的Id
                    },
                },
                new List<string>() // 空列表表示不需要特定的作用域
            }
        });

        // 添加一个安全定义,定义名称为"User"
        options.AddSecurityDefinition("User", new OpenApiSecurityScheme
        {
            // 描述如何进行身份验证
            Description = "Authorization: <token>",
            // 指定认证头的名称,这里是"Authorization"
            Name = "Authorization",
            // 指定认证信息应该放置的位置,这里是HTTP头部
            In = ParameterLocation.Header,
            // 指定安全方案的类型,这里是指API密钥类型
            Type = SecuritySchemeType.ApiKey,
        });
        // 添加安全要求,这将告诉Swagger UI哪些安全方案是必需的
        options.AddSecurityRequirement(new OpenApiSecurityRequirement
        {
            {
                new OpenApiSecurityScheme
                {
                    // 引用之前定义的安全方案
                    Reference = new OpenApiReference
                    {
                        Type = ReferenceType.SecurityScheme,
                        Id = "User" // 对应于上面定义的安全方案的Id
                    },
                },
                new List<string>() // 空列表表示不需要特定的作用域
            }
        });
    });

#endregion

Swagger.png

Swagger文档

我们应该为Admin和User配置不同的接口文档,以方便测试使用

builder.Services.AddSwaggerGen(
    options =>
    {
        options.SwaggerDoc("admin", new OpenApiInfo { Title = "Admin API", Version = "v1" });
        options.SwaggerDoc("user", new OpenApiInfo { Title = "User API", Version = "v1" });
        //文档逻辑包含
        options.DocInclusionPredicate((docName, apiDesc) =>
        {
            // 检查 apiDesc 的 ActionDescriptor 是否是 ControllerActionDescriptor 类型
            if (apiDesc.ActionDescriptor is ControllerActionDescriptor actionDescriptor)
            {
                // 获取控制器的命名空间
                var controllerNamespace = actionDescriptor.ControllerTypeInfo.Namespace;

                // 确保命名空间不为空,并且包含 "Controllers" 字符串
                if (controllerNamespace != null && controllerNamespace.Contains("Controllers"))
                {
                    // 如果文档名称是 "user"
                    if (docName == "user")
                    {
                        // 只有当控制器命名空间包含 "User" 时才包含该 API 描述
                        return controllerNamespace.Contains("User");
                    }
                    // 如果文档名称是 "admin"

                    if (docName == "admin")
                    {
                        // 只有当控制器命名空间包含 "Admin" 时才包含该 API 描述
                        return controllerNamespace.Contains("Admin");
                    }
                }
            }

            // 如果上述条件都不满足,则返回 false,表示不包含该 API 描述
            return false;
        });
    });
)
配置中间件
app.UseSwaggerUI(options =>
    {
        options.SwaggerEndpoint("/swagger/v1-user/swagger.json", "User API V1");
        options.SwaggerEndpoint("/swagger/v1-admin/swagger.json", "Admin API V1");
    }
);