首页 > 解决方案 > JWT 和刷新令牌不工作

问题描述

我是 .Net 的新手,我正在尝试制作一个 jwt 令牌和一个刷新令牌。我的登录创建了一个令牌和一个刷新令牌,但是当我尝试使用刷新令牌更新令牌时,它不会更新,并给我错误,让用户再次登录。启动.cs

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<JwtConfig>(Configuration.GetSection("JwtConfig"));

        services.AddDbContext<ApiDbContext>(options =>
            options.UseSqlite(
                Configuration.GetConnectionString("DefaultConnection")
            ));

        var key = Encoding.ASCII.GetBytes(Configuration["JwtConfig:Secret"]);

        var tokenValidationParams = new TokenValidationParameters {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                ValidateLifetime = true,
                RequireExpirationTime = false,
                ClockSkew = TimeSpan.Zero
            };

        services.AddSingleton(tokenValidationParams);

        services.AddAuthentication(options => {
            options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(jwt => {
            jwt.SaveToken = true;
            jwt.TokenValidationParameters = tokenValidationParams;
        });

        services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
                    .AddEntityFrameworkStores<ApiDbContext>();

        services.AddControllers();
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo { Title = "TodoApp", Version = "v1" });
            c.AddSecurityDefinition("BearerAuth", new OpenApiSecurityScheme
            {
                Type = SecuritySchemeType.Http,
                Scheme = JwtBearerDefaults.AuthenticationScheme.ToLowerInvariant(),
                In = ParameterLocation.Header,
                Name = "Authorization",
                BearerFormat = "JWT",
                Description = "JWT Authorization header using the Bearer scheme."
            });

            c.OperationFilter<AuthResponsesOperationFilter>();
        });

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

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseSwagger();
            app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TodoApp v1"));
        }

        app.UseHttpsRedirection();

        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseCors("Open");

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

internal class AuthResponsesOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var attributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
                            .Union(context.MethodInfo.GetCustomAttributes(true));

        if (attributes.OfType<IAllowAnonymous>().Any())
        {
            return;
        }

        var authAttributes = attributes.OfType<IAuthorizeData>();

        if (authAttributes.Any())
        {
            operation.Responses["401"] = new OpenApiResponse { Description = "Unauthorized" };

            if (authAttributes.Any(att => !String.IsNullOrWhiteSpace(att.Roles) || !String.IsNullOrWhiteSpace(att.Policy)))
            {
                operation.Responses["403"] = new OpenApiResponse { Description = "Forbidden" };
            }

            operation.Security = new List<OpenApiSecurityRequirement>
            {
                new OpenApiSecurityRequirement
                {
                    {
                        new OpenApiSecurityScheme
                        {
                            Reference = new OpenApiReference
                            {
                                Id = "BearerAuth",
                                Type = ReferenceType.SecurityScheme
                            }
                        },
                        Array.Empty<string>()
                    }
                }
            };
        }
    }
}

应用设置.json

 {
  "ConnectionStrings": {
    "DefaultConnection" : "DataSource=app.db; Cache=Shared"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "JwtConfig": {
    "Secret": "llvudfvkwvepwkdnsnwmuulyvtrawppf"
  },
  "AllowedHosts": "*"
}

刷新令牌.cs

public class RefreshToken
{
    public int Id { get; set; }
    public string UserId { get; set; }
    public string Token { get; set; }
    public string JwtId { get; set; }
    public bool IsUsed { get; set; }
    public bool IsRevorked { get; set; }
    public DateTime AddedDate { get; set; }
    public DateTime ExpiryDate { get; set; }

    [ForeignKey(nameof(UserId))]
    public IdentityUser User {get;set;}
}

注册响应.cs

    public class RegistrationResponse : AuthResult
{
    
}

令牌请求.cs

 public class TokenRequest
{
    [Required]
    public string Token { get; set; }

     [Required]
    public string RefreshToken { get; set; }
}

用户登录请求.cs

public class UserLoginRequest
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }
    [Required]
    public string Password { get; set; }
}

UserRegistrationDto.cs

public class UserRegistrationDto
{
    [Required]
    public string Username { get; set; }
    [Required]
    [EmailAddress]
    public string Email { get; set; }
    [Required]
    public string Password { get; set; }
}

ApiDbContext.cs

 public class ApiDbContext : IdentityDbContext
{
    public virtual DbSet<ItemData> Items {get;set;}
    public virtual DbSet<RefreshToken> RefreshTokens {get;set;}

    public ApiDbContext(DbContextOptions<ApiDbContext> options)
        : base(options)
    {
        
    }
}

验证结果.cs

 public class AuthResult
{
    public string Token { get; set; }
    public string RefreshToken { get; set; }
    public bool Success { get; set; }
    public List<string> Errors { get; set; }
}

JwtConfig.cs

public class JwtConfig
{
    public string Secret { get; set; }
}

AuthController.cs

[Route("api/[controller]")] // api/authManagement
[ApiController]
public class AuthManagementController : ControllerBase
{
    private readonly UserManager<IdentityUser> _userManager;
    private readonly JwtConfig _jwtConfig;
    private readonly TokenValidationParameters _tokenValidationParams;
    private readonly ApiDbContext _apiDbContext;

    public AuthManagementController(
        UserManager<IdentityUser> userManager,
        IOptionsMonitor<JwtConfig> optionsMonitor,
        TokenValidationParameters tokenValidationParams,
        ApiDbContext apiDbContext)
    {
        _userManager = userManager;
        _jwtConfig = optionsMonitor.CurrentValue;
        _tokenValidationParams = tokenValidationParams;
        _apiDbContext = apiDbContext;
    }

    [HttpPost]
    [Route("Register")]
    public async Task<IActionResult> Register([FromBody] UserRegistrationDto user)
    {
        if(ModelState.IsValid)
        {
            // We can utilise the model
            var existingUser = await _userManager.FindByEmailAsync(user.Email);

            if(existingUser != null)
            {
                return BadRequest(new RegistrationResponse(){
                        Errors = new List<string>() {
                            "Email already in use"
                        },
                        Success = false
                });
            }

            var newUser = new IdentityUser() { Email = user.Email, UserName = user.Username};
            var isCreated = await _userManager.CreateAsync(newUser, user.Password);
            if(isCreated.Succeeded)
            {
               var jwtToken = await GenerateJwtToken( newUser);

               return Ok(jwtToken);
            } else {
                return BadRequest(new RegistrationResponse(){
                        Errors = isCreated.Errors.Select(x => x.Description).ToList(),
                        Success = false
                });
            }
        }

        return BadRequest(new RegistrationResponse(){
                Errors = new List<string>() {
                    "Invalid payload"
                },
                Success = false
        });
    }

    [HttpPost]
    [Route("Login")]
    public async Task<IActionResult> Login([FromBody] UserLoginRequest user)
    {
        if(ModelState.IsValid)
        {
            var existingUser = await _userManager.FindByEmailAsync(user.Email);

            if(existingUser == null) {
                    return BadRequest(new RegistrationResponse(){
                        Errors = new List<string>() {
                            "Invalid login request"
                        },
                        Success = false
                });
            }

            var isCorrect = await _userManager.CheckPasswordAsync(existingUser, user.Password);

            if(!isCorrect) {
                  return BadRequest(new RegistrationResponse(){
                        Errors = new List<string>() {
                            "Invalid login request"
                        },
                        Success = false
                });
            }

            var jwtToken  = await GenerateJwtToken(existingUser);

            return Ok(jwtToken);
        }

        return BadRequest(new RegistrationResponse(){
                Errors = new List<string>() {
                    "Invalid payload"
                },
                Success = false
        });
    }

    [HttpPost]
    [Route("RefreshToken")]
    public async Task<IActionResult> RefreshToken([FromBody] TokenRequest tokenRequest)
    {
        if(ModelState.IsValid)
        {
            var result = await VerifyAndGenerateToken(tokenRequest);

            if(result == null) {
                return BadRequest(new RegistrationResponse() {
                    Errors = new List<string>() {
                        "Invalid tokens"
                    },
                    Success = false
                });
            }

            return Ok(result);
        }

        return BadRequest(new RegistrationResponse() {
            Errors = new List<string>() {
                "Invalid payload"
            },
            Success = false
        });
    }


    private async Task<AuthResult> GenerateJwtToken(IdentityUser user)
    {
        var jwtTokenHandler = new JwtSecurityTokenHandler();

        var key = Encoding.ASCII.GetBytes(_jwtConfig.Secret);

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new []
            {
                new Claim("Id", user.Id), 
                new Claim(JwtRegisteredClaimNames.Email, user.Email),
                new Claim(JwtRegisteredClaimNames.Sub, user.Email),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
            }),
            Expires = DateTime.UtcNow.AddSeconds(30), // 5-10 
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };

        var token = jwtTokenHandler.CreateToken(tokenDescriptor);
        var jwtToken = jwtTokenHandler.WriteToken(token);

        var refreshToken = new RefreshToken()
        {
            JwtId = token.Id,
            IsUsed = false,
            IsRevorked = false,
            UserId = user.Id,
            AddedDate = DateTime.UtcNow,
            ExpiryDate = DateTime.UtcNow.AddMonths(6),
            Token = RandomString(35) + Guid.NewGuid()
        };

        await _apiDbContext.RefreshTokens.AddAsync(refreshToken);
        await _apiDbContext.SaveChangesAsync();

        return new AuthResult() {
            Token = jwtToken,
            Success = true,
            RefreshToken = refreshToken.Token
        };
    }

    private async Task<AuthResult> VerifyAndGenerateToken(TokenRequest tokenRequest)
    {
        var jwtTokenHandler = new JwtSecurityTokenHandler();

        try
        {   
            // Validation 1 - Validation JWT token format
            var tokenInVerification = jwtTokenHandler.ValidateToken(tokenRequest.Token, _tokenValidationParams, out var validatedToken);

            // Validation 2 - Validate encryption alg
            if(validatedToken is JwtSecurityToken jwtSecurityToken)
            {
                var result = jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase);

                if(result == false) {
                    return null;
                }
            }

            // Validation 3 - validate expiry date
            var utcExpiryDate = long.Parse(tokenInVerification.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Exp).Value);

            var expiryDate = UnixTimeStampToDateTime(utcExpiryDate);

            if(expiryDate > DateTime.UtcNow) {
                return new AuthResult() {
                    Success = false,
                    Errors = new List<string>() {
                        "Token has not yet expired"
                    }
                };
            }

            // validation 4 - validate existence of the token
            var storedToken = await _apiDbContext.RefreshTokens.FirstOrDefaultAsync(x => x.Token == tokenRequest.RefreshToken);

            if(storedToken == null)
            {
                return new AuthResult() {
                    Success = false,
                    Errors = new List<string>() {
                        "Token does not exist"
                    }
                };
            }

            // Validation 5 - validate if used
            if(storedToken.IsUsed)
            {
                return new AuthResult() {
                    Success = false,
                    Errors = new List<string>() {
                        "Token has been used"
                    }
                };
            }

            // Validation 6 - validate if revoked
            if(storedToken.IsRevorked)
            {
                return new AuthResult() {
                    Success = false,
                    Errors = new List<string>() {
                        "Token has been revoked"
                    }
                };
            }

            // Validation 7 - validate the id
            var jti = tokenInVerification.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Jti).Value;

            if(storedToken.JwtId != jti)
            {
                return new AuthResult() {
                    Success = false,
                    Errors = new List<string>() {
                        "Token doesn't match"
                    }
                };
            }

            // update current token 

            storedToken.IsUsed = true;
            _apiDbContext.RefreshTokens.Update(storedToken);
            await _apiDbContext.SaveChangesAsync();
            
            // Generate a new token
            var dbUser = await _userManager.FindByIdAsync(storedToken.UserId);
            return await GenerateJwtToken(dbUser);
        }
        catch(Exception ex)
        {
            if(ex.Message.Contains("Lifetime validation failed. The token is expired.")) {

                  return new AuthResult() {
                    Success = false,
                    Errors = new List<string>() {
                        "Token has expired please re-login"
                    }
                };
            
            } else {
                  return new AuthResult() {
                    Success = false,
                    Errors = new List<string>() {
                        "Something went wrong."
                    }
                };
            }
        }    
    }

    private DateTime UnixTimeStampToDateTime(long unixTimeStamp)
    {
        var dateTimeVal = new DateTime(1970, 1,1,0,0,0,0, DateTimeKind.Utc);
        dateTimeVal = dateTimeVal.AddSeconds(unixTimeStamp).ToUniversalTime();

        return dateTimeVal;
    }

    private string RandomString(int length)
    {
        var random = new Random();
        var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        return new string(Enumerable.Repeat(chars, length)
            .Select(x => x[random.Next(x.Length)]).ToArray());
    }
}

标签: c#.net

解决方案


推荐阅读