【问题标题】:User.Identity.Name is empty with JWT when method is no decorated with Authorize in Asp.NET Core 3.0 API Controller当方法没有在 Asp.NET Core 3.0 API 控制器中使用 Authorize 修饰时,User.Identity.Name 为空,JWT
【发布时间】:2021-02-19 11:53:27
【问题描述】:

我在 .net core 3.1 中有一个 Web Api 项目,并且我添加了 JwT 身份验证。 身份验证和授权工作得很好,但我需要在每个请求中获取 UserId。当方法用 Authorize 属性修饰时,效果很好。

[HttpGet]
    [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
    public IEnumerable<WeatherForecast> Get()
    {
        string user = User.Identity.Name; //Get a value
        //Do something
    }

但是我有一些不需要身份验证的方法,但是如果经过身份验证的用户发出请求,我想获取 userId,但在这种情况下,user.Identity.Name 始终为空。

[HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        string user = User.Identity.Name; //null
        //Do somwthing
    }

我在 statur 文件中的配置是:

private void ConfigureJwt(IServiceCollection services)
    {
        //Add Auth scheme
        services.AddAuthorization(options =>
        {
            var defaultAuthorizationPolicyBuilder = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme);
            defaultAuthorizationPolicyBuilder = defaultAuthorizationPolicyBuilder.RequireAuthenticatedUser();
            options.DefaultPolicy = defaultAuthorizationPolicyBuilder.Build();
        });

        AuthSettings authSettings = Configuration.GetSection("AuthSettings").Get<AuthSettings>();
        JwtIssuerOptions jwtIssuerOptions = Configuration.GetSection("JwtIssuerOptions").Get<JwtIssuerOptions>();

        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 = jwtIssuerOptions.Issuer,
                ValidAudience = jwtIssuerOptions.Audience,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings.SecretKey))
            };
            options.Events = new JwtBearerEvents
            {
                OnTokenValidated = context =>
                {
                    //When method is no decorated with Authorize, it not working
                    var userId = int.Parse(context.Principal.Identity.Name);
                    return System.Threading.Tasks.Task.CompletedTask;
                }
            };
        });

        services.AddTransient<ITokenService, TokenService>(x =>
        {
            return new TokenService(Configuration);
        });
    } 

TokenService 类:

public class TokenService : ITokenService
{
    IConfiguration configuration = null;
    AuthSettings authSettings = null;
    public TokenService(IConfiguration _configuration)
    {
        configuration = _configuration;
        authSettings = configuration.GetSection("AuthSettings").Get<AuthSettings>();
    }

    public string GenerateAccessToken(IEnumerable<Claim> claims, ref JwtIssuerOptions jwtIssuerOptions)
    {
        //var authSettings = configuration.GetSection(nameof(AuthSettings));
        //var authSettings = configuration.GetSection("EmailSettings").Get<AuthSettings>();
        jwtIssuerOptions = configuration.GetSection("JwtIssuerOptions").Get<JwtIssuerOptions>();

        var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings.SecretKey));
        var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);

        var tokeOptions = new JwtSecurityToken (
            issuer: jwtIssuerOptions.Issuer,
            audience: jwtIssuerOptions.Audience,
            claims: claims,
            expires: jwtIssuerOptions.Expiration,
            //expires: DateTime.Now.AddMinutes(5),
            signingCredentials: signinCredentials
        );

        var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
        return tokenString;
    }

    public string GenerateRefreshToken()
    {
        var randomNumber = new byte[32];
        using (var rng = RandomNumberGenerator.Create())
        {
            rng.GetBytes(randomNumber);
            return Convert.ToBase64String(randomNumber);
        }
    }
    public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
    {
        TokenValidationParameters tokenValidationParameters = GetValidationParameters();

        var tokenHandler = new JwtSecurityTokenHandler();
        SecurityToken securityToken;
        var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
        var jwtSecurityToken = securityToken as JwtSecurityToken;
        if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
            throw new SecurityTokenException("Invalid token");

        return principal;
    }

    private TokenValidationParameters GetValidationParameters()
    {
        var tokenValidationParameters = new TokenValidationParameters
        {
            ValidateAudience = false, //you might want to validate the audience and issuer depending on your use case
            ValidateIssuer = false,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(authSettings.SecretKey)),
            ValidateLifetime = false //here we are saying that we don't care about the token's expiration date
        };
        return tokenValidationParameters;
    }
}

授权控制器

    [HttpPost, Route("login")]
    public async Task<IActionResult> Login([FromBody] LoginModel loginModel)
    {
        if (loginModel == null)
            return BadRequest("Invalid client request");

        var sessionInfo = await userBo.LoginUser(loginModel);
        if (sessionInfo == null)
            return Unauthorized();

        var claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, sessionInfo.User.BusinessEntityId.ToString()),
            new Claim(ClaimTypes.Role, sessionInfo.User.RoleCode)
        };

        JwtIssuerOptions tokeOptions = null;
        var accessToken = tokenService.GenerateAccessToken(claims, ref tokeOptions);
        var refreshToken = tokenService.GenerateRefreshToken();

        await tokenBo.SaveToken(
            new Token()
            {
                BusinessEntityId = sessionInfo.Person.BusinessEntityId,
                RefreshToken = refreshToken,
                RefreshTokenExpiryTime = tokeOptions.Expiration
            }
        );

        sessionInfo.TokenInfo = new TokenInfo()
        {
            AccessToken = accessToken,
            RefreshToken = refreshToken
        };
        return Ok(sessionInfo);
    }
}

感谢您的帮助!

【问题讨论】:

标签: asp.net-core .net-core jwt


【解决方案1】:

据我所知,如果控制器不需要授权,它不会将用户信息添加到管道声明中,因此用户名始终为空。

为了解决这个问题,我建议你可以尝试添加一个自定义中间件来检查请求是否包含授权标头。如果它包含您可以获取用户名并将其添加到 http 上下文项中。

那么你可以直接在api控制器中获取用户名,而不是从User.Identity.Name中获取。

更多细节,您可以参考以下代码:

将以下中间件添加到startup.cs配置方法中:

        app.Use(async (context, next) =>
        {
            // you could get from token or get from session. 
            string token = context.Request.Headers["Authorization"];
            if (!string.IsNullOrEmpty(token))
            {
                var tok = token.Replace("Bearer ", "");
                var jwttoken = new JwtSecurityTokenHandler().ReadJwtToken(tok);

                 var jti = jwttoken.Claims.First(claim => claim.Type == ClaimTypes.Name).Value;
                context.Items.Add("Username", jti);
            }
    
            await next();

        });

控制器获取用户名:

        object value;
        ControllerContext.HttpContext.Items.TryGetValue("Username", out value);

        var username = value.ToString();

结果:

【讨论】:

    【解决方案2】:

    在将应用程序从使用基于 cookie 的身份验证更改为使用 JWT 后,我遇到了这个问题。您可以通过创建一个没有要求的authorization handler 来解决它——有点——从而允许匿名用户访问。 ASP.NET 管道不知道需要哪些要求,因此如果用户的凭据出现在请求中,它将提供用户的凭据。最终结果是允许匿名用户,但如果提供凭据,它们将可用。

    琐碎的要求:

    class RequireNothing : IAuthorizationRequirement { }
    

    和处理程序:

    class RequireNothingHandler : AuthorizationHandler<RequireNothing>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequireNothing requirement)
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }
    }
    

    如果请求包含凭据,它们将在 User 对象中可用,但该要求还允许匿名用户访问。

    要使用该要求,您可以创建一个策略(并将处理程序添加到 DI 容器):

    services
        .AddAuthorization(options => options
            .AddPolicy("AlsoAllowAnonymous", policy => policy
                .AddRequirements(new RequireNothing())))
        .AddSingleton<IAuthorizationHandler, RequireNothingHandler>();
    

    要结合身份验证和匿名访问,您可以使用属性装饰操作或控制器:

    [Authorize(Policy = "AlsoAllowAnonymous")]
    

    很遗憾,这可能效果不佳。如果您使用的是长期 JWT 刷新令牌和短期访问令牌,它们在收到 401 质询时刷新,则在访问令牌过期后将没有质询,并且用户将匿名访问端点,可能会导致即使用户已通过身份验证并拥有刷新令牌来证明这一点,用户体验也会下降。

    这个问题并不是使用授权处理程序所独有的,您可以通过提供两个不同的端点获得更多控制:一个用于匿名用户,另一个用于经过身份验证的用户。您需要在客户端使用一些额外的逻辑来选择正确的 API 以使事情正常进行。

    【讨论】:

      猜你喜欢
      • 2018-03-30
      • 1970-01-01
      • 2021-10-11
      • 2020-07-31
      • 2023-03-15
      • 1970-01-01
      • 2019-10-18
      • 2021-11-24
      • 1970-01-01
      相关资源
      最近更新 更多