c# - 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());
}
}
解决方案
推荐阅读
- python - 在 python 中存储和操作时间戳数组的最有效方法
- google-sheets - 以不同的格式呈现 2 列组/子项数据
- java - Google云端硬盘未显示目录中的所有文件
- ofbiz - OfBiz 后端 UI 是打算直接使用还是仅作为后备使用?
- java - 计算和初始化外部配置属性
- flutter - Flutter Getx List Object 数据在屏幕之间移动时发生变化
- python - 从嵌套列表中填充字典
- java - 我怎样才能阻止我的 gradle 导致这个错误?
- datetime - GENERATESERIES() 以半小时为增量
- embedded - CMSIS FIR 系数的抽取似乎不正确