You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

ASP.NET MVC客户端如何结合WebApi实现授权与认证?

嘿,这个场景我之前帮不少开发者梳理过,刚好可以给你一套完整的实现方案,一步步来拆解你的问题:

核心思路总览

你的架构是典型的「MVC客户端 + API服务端」分离模式,所有授权认证逻辑都由API掌控,MVC只负责界面展示和调用API。核心要做的就是让MVC的授权系统能识别API返回的Token,并且自动把Token携带到所有API请求中,同时让[Authorize]特性正常生效。

1. Token的全局携带(不用每个请求手动封装)

最优方案是通过DelegatingHandler全局处理,让所有从MVC发往API的请求自动带上Token,不用重复写代码。

实现自定义Token Handler

public class ApiTokenHandler : DelegatingHandler
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ApiTokenHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // 从Session或Cookie中取出存储的Token
        var token = _httpContextAccessor.HttpContext.Session.GetString("ApiToken");
        if (!string.IsNullOrEmpty(token))
        {
            // 把Token加到请求头的Authorization字段
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        }
        return await base.SendAsync(request, cancellationToken);
    }
}

注册Handler与HttpClient

Program.cs(.NET 6+)或Startup.cs中配置:

// 注册HttpContextAccessor用于获取Session
builder.Services.AddHttpContextAccessor();
// 启用Session存储Token
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromHours(2);
    options.Cookie.HttpOnly = true;
});
// 注册自定义Handler
builder.Services.AddScoped<ApiTokenHandler>();
// 配置全局HttpClient,自动携带Token
builder.Services.AddHttpClient("ApiClient", client =>
{
    client.BaseAddress = new Uri("https://your-api-base-url/");
})
.AddHttpMessageHandler<ApiTokenHandler>();

之后MVC中注入命名为ApiClient的HttpClient发请求时,都会自动带上Token,完全不用手动处理。

2. 让MVC的[Authorize]特性适配API Token

要让MVC的授权逻辑识别API的Token,需要自定义认证Handler,替换掉默认的本地认证逻辑。

方案1:调用API验证Token(适合非JWT场景)

如果你的API用的是自定义Token,每次验证都需要调用API接口:

public class ApiTokenAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    private readonly IHttpClientFactory _httpClientFactory;

    public ApiTokenAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IHttpClientFactory httpClientFactory)
        : base(options, logger, encoder, clock)
    {
        _httpClientFactory = httpClientFactory;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var token = Context.Session.GetString("ApiToken");
        if (string.IsNullOrEmpty(token))
        {
            return AuthenticateResult.Fail("未找到有效Token");
        }

        // 调用API的Token验证接口,获取用户信息和角色
        var client = _httpClientFactory.CreateClient("ApiClient");
        var response = await client.GetAsync($"api/auth/validate?token={token}");
        if (!response.IsSuccessStatusCode)
        {
            return AuthenticateResult.Fail("Token无效");
        }

        var userInfo = await response.Content.ReadFromJsonAsync<UserInfoDto>();
        if (userInfo == null)
        {
            return AuthenticateResult.Fail("获取用户信息失败");
        }

        // 构建MVC识别的Claims身份信息
        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, userInfo.UserId.ToString()),
            new Claim(ClaimTypes.Name, userInfo.UserName)
        };
        // 添加用户角色
        foreach (var role in userInfo.Roles)
        {
            claims.Add(new Claim(ClaimTypes.Role, role));
        }

        var identity = new ClaimsIdentity(claims, Scheme.Name);
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, Scheme.Name);

        return AuthenticateResult.Success(ticket);
    }
}

// 对应API返回的用户信息DTO
public class UserInfoDto
{
    public int UserId { get; set; }
    public string UserName { get; set; }
    public List<string> Roles { get; set; }
}

方案2:本地验证JWT Token(高效推荐)

如果API用的是JWT Token,MVC可以直接本地验证签名,不用每次调用API,性能更好:

public class JwtTokenAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    private readonly IConfiguration _configuration;

    public JwtTokenAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IConfiguration configuration)
        : base(options, logger, encoder, clock)
    {
        _configuration = configuration;
    }

    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var token = Context.Session.GetString("ApiToken");
        if (string.IsNullOrEmpty(token))
        {
            return AuthenticateResult.Fail("未找到有效Token");
        }

        try
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var validationParams = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = _configuration["Jwt:Issuer"],
                ValidAudience = _configuration["Jwt:Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"]))
            };

            // 本地验证JWT并获取用户Claims
            var principal = tokenHandler.ValidateToken(token, validationParams, out _);
            var ticket = new AuthenticationTicket(principal, Scheme.Name);

            return AuthenticateResult.Success(ticket);
        }
        catch (Exception ex)
        {
            return AuthenticateResult.Fail($"Token验证失败:{ex.Message}");
        }
    }
}

注册认证与授权中间件

Program.cs中配置:

// 注册自定义认证方案
builder.Services.AddAuthentication("ApiTokenScheme")
    .AddScheme<AuthenticationSchemeOptions, ApiTokenAuthenticationHandler>("ApiTokenScheme", options => { });

// 配置授权策略,可自定义角色要求
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
});

// 启用中间件顺序很重要:Session -> Authentication -> Authorization
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();

正常使用[Authorize]特性

现在MVC的授权特性就能正常工作了:

// 要求用户已认证
[Authorize]
public IActionResult Dashboard()
{
    // 获取当前用户信息
    var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
    var userName = User.Identity.Name;
    var isAdmin = User.IsInRole("Admin");
    return View();
}

// 要求Admin角色
[Authorize(Policy = "AdminOnly")]
public IActionResult AdminPanel()
{
    return View();
}
3. 登录流程示例

MVC登录页面提交账号密码,调用API获取Token并存入Session:

public class AccountController : Controller
{
    private readonly IHttpClientFactory _httpClientFactory;

    public AccountController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    [HttpGet]
    public IActionResult Login()
    {
        return View();
    }

    [HttpPost]
    public async Task<IActionResult> Login(LoginViewModel model)
    {
        if (!ModelState.IsValid) return View(model);

        var client = _httpClientFactory.CreateClient("ApiClient");
        var loginReq = new LoginRequestDto { UserName = model.UserName, Password = model.Password };
        var response = await client.PostAsJsonAsync("api/auth/login", loginReq);

        if (!response.IsSuccessStatusCode)
        {
            ModelState.AddModelError("", "用户名或密码错误");
            return View(model);
        }

        var tokenResp = await response.Content.ReadFromJsonAsync<TokenResponseDto>();
        // 存储Token到Session
        HttpContext.Session.SetString("ApiToken", tokenResp.AccessToken);

        return RedirectToAction("Index", "Home");
    }

    public IActionResult Logout()
    {
        HttpContext.Session.Remove("ApiToken");
        return RedirectToAction("Login");
    }
}

// 对应的视图模型和DTO
public class LoginViewModel
{
    [Required]
    public string UserName { get; set; }
    [Required]
    public string Password { get; set; }
}

public class LoginRequestDto
{
    public string UserName { get; set; }
    public string Password { get; set; }
}

public class TokenResponseDto
{
    public string AccessToken { get; set; }
    public int ExpiresIn { get; set; }
}
最优方案建议
  • Token存储:内部系统用Session足够;公开系统建议用HttpOnlySecure的Cookie存储Token,同时设置SameSite=Strict,防止XSS和CSRF攻击。
  • Token验证:优先用JWT本地验证,减少API调用;自定义Token才用API验证方式。
  • 过期处理:可以在ApiTokenHandler中捕获API返回的401 Unauthorized响应,自动跳转到登录页面,或者调用API的刷新Token接口续期。
  • 权限对齐:MVC的授权策略要和API的权限体系保持一致,避免出现MVC允许访问但API拒绝的情况。

内容的提问来源于stack exchange,提问作者P_Soltys

火山引擎 最新活动