You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET API中IsSignedIn始终返回true的问题求助

问题:调用Logout接口后IsSignedIn仍返回true的原因及解决办法

我确信自己忽略了某个明显的要点,想请教为何在我的.NET API中,调用Logout接口后,IsSignedIn仍始终返回true?

相关代码

AuthenticationController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using Registry_Backend.DTO;
using Registry_Backend.Models;
using Registry_Backend.Shared;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Security.Claims;
using System.Text;

namespace Registry_Backend.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class AuthenticationController : ControllerBase
    {
        private readonly SignInManager<IdentityUser> signInManager;
        private readonly RegistryContext dbContext;
        private readonly UserManager<IdentityUser> userManager;

        public AuthenticationController(RegistryContext dbContext, SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager)
        {
            this.dbContext = dbContext;
            this.signInManager = signInManager;
            this.userManager = userManager;
        }

        [HttpPost("Login")]
        [ProducesResponseType(typeof(JWTTokenResponse), StatusCodes.Status200OK)]
        public async Task<IActionResult> LoginAsync([FromBody] LoginData loginData)
        {
            if (loginData is null)
            {
                throw new AppException("Invalid user request!!!");
            }
            else
            {
                var result = await signInManager.PasswordSignInAsync(loginData.UserName, loginData.Password, false, false);

                if (result.Succeeded)
                {
                    var user = await userManager.FindByNameAsync(loginData.UserName);

                    if (user != null) {
                        var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(ConfigurationManagerExtension.AppSetting["JWT:Secret"]));
                        var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
                        var tokeOptions = new JwtSecurityToken(
                            issuer: ConfigurationManagerExtension.AppSetting["JWT:ValidIssuer"],
                            audience: ConfigurationManagerExtension.AppSetting["JWT:ValidAudience"], claims: new List<Claim>(),
                            expires: DateTime.Now.AddMinutes(6),
                            signingCredentials: signinCredentials);
                        var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
                        return Ok(new JWTTokenResponse
                        {
                            Token = tokenString,
                            UserId = user.Id
                        });
                    }
                }
            }

            return Unauthorized();
        }

        [HttpPost("Logout")]
        [ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
        public async Task<IActionResult> LogoutAsync()
        {
            await signInManager.SignOutAsync();
            return Ok("OK");
        }

        [HttpGet("IsLoggedIn")]
        [ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
        public async Task<IActionResult> IsLoggedIn([FromQuery] string userId)
        {
            var user = await userManager.FindByIdAsync(userId);
            if(user != null)
            {
                var claims = await signInManager.CreateUserPrincipalAsync(user);
                return Ok(signInManager.IsSignedIn(claims));
            }

            return Ok(false);
        }
    }
}

Program.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Registry_Backend;
using Registry_Backend.Models;
using Registry_Backend.Shared;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options => {

    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Scheme = "Bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Name = "Authorization",
        Description = "Bearer Authentication with JWT Token",
        Type = SecuritySchemeType.Http
    });
    options.AddSecurityRequirement(new OpenApiSecurityRequirement {
        {
            new OpenApiSecurityScheme {
                Reference = new OpenApiReference {
                    Id = "Bearer",
                        Type = ReferenceType.SecurityScheme
                }
            },
            new List < string > ()
        }
    });
});


builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowAll", builder =>
    {
        builder.AllowAnyOrigin()
               .AllowAnyMethod()
               .AllowAnyHeader();
    });
});

builder.Services.AddDbContext<RegistryContext>(opt => opt.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
                options.Lockout.AllowedForNewUsers = false
            ).AddEntityFrameworkStores<RegistryContext>();


builder.Services.AddAuthentication(opt => {
    opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => {
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = ConfigurationManagerExtension.AppSetting["JWT:ValidIssuer"],
        ValidAudience = ConfigurationManagerExtension.AppSetting["JWT:ValidAudience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(ConfigurationManagerExtension.AppSetting["JWT:Secret"]))
    };
});


var app = builder.Build();
app.UseCors("AllowAll");
app.UseMiddleware<ErrorHandlerMiddleware>();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();


app.MapControllers();

app.Run();

问题原因分析

  1. Logout接口无效:signInManager.SignOutAsync()是针对Cookie认证的注销逻辑,但你的API使用的是JWT认证。JWT是无状态令牌,服务端不会存储用户会话信息,所以这个方法无法清除JWT的认证状态。

  2. IsLoggedIn逻辑错误:你通过signInManager.CreateUserPrincipalAsync(user)手动创建了一个用户主体,再调用IsSignedIn(claims)判断登录状态。但IsSignedIn方法的正确用法是检查当前请求上下文(HttpContext.User)中的用户是否已认证,而你手动创建的主体和当前请求的认证状态完全无关,所以无论用户是否注销,这个方法都会返回true。

解决方案

1. 修正Logout接口

JWT的注销只能由客户端完成:客户端需要删除本地存储的JWT令牌(比如localStorage、sessionStorage),服务端无需做额外操作。修改Logout接口如下:

[HttpPost("Logout")]
[ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
public IActionResult LogoutAsync()
{
    // JWT无状态,通知客户端删除令牌即可
    return Ok("请客户端删除本地存储的JWT令牌");
}

2. 修正IsLoggedIn接口

需要通过当前请求的认证状态判断,要求客户端携带JWT令牌访问该接口:

[HttpGet("IsLoggedIn")]
[Authorize] // 必须携带有效JWT才能访问
public IActionResult IsLoggedIn()
{
    // 直接判断当前请求的用户是否已认证
    return Ok(User.Identity.IsAuthenticated);
}

3. 额外优化(可选)

如果需要主动失效JWT令牌,可以实现令牌黑名单机制(比如用Redis存储已注销的令牌ID,直到令牌过期),但你的令牌过期时间只有6分钟,短期令牌可以依赖过期时间,无需额外实现。


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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 08:04:56