A colleague of mine and I have been working on a school project and we ran into a problem with adding a JWT token to our webservice. We tried to recreate a tutorial but we are using the generic repository pattern with a unit of work class. The tutorial we were trying to recreate is this one by json watmore. However, to stay true to the repository pattern we didn't see much sense in creating a IUserService and UserService when there is already a UserRepository which essentially aims to do the same thing. Our UserRepository now looks like this:
public class UserRepository : Repository<User>, IUserRepository
{
public UserRepository(JellyContext context)
: base(context)
{
}
public JellyContext DbContext
{
get { return _context as JellyContext; }
}
public User Create(User user, string password)
{
// validation
if (string.IsNullOrWhiteSpace(password))
throw new AppException("Password is required");
if (_context.Users.Any(x => x.Username == user.Username))
throw new AppException("Username " + "user.Username "+ "is already taken");
byte[] passwordHash, passwordSalt;
CreatePasswordHash(password, out passwordHash, out passwordSalt);
user.PasswordHash = passwordHash;
user.PasswordSalt = passwordSalt;
_context.Users.Add(user);
_context.SaveChanges();
return user;
}
public User Authenticate(string username, string password)
{
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
return null;
User user = _context.Users.SingleOrDefault(x => x.Username == username);
// check if username exists
if (user == null)
return null;
// check if password is correct
if (!VerifyPasswordHash(password, user.PasswordHash, user.PasswordSalt))
return null;
// authentication successful
return user;
}
private static bool VerifyPasswordHash(string password, byte[] storedHash, byte[] storedSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
if (storedHash.Length != 64) throw new ArgumentException("Invalid length of password hash (64 bytes expected).", "passwordHash");
if (storedSalt.Length != 128) throw new ArgumentException("Invalid length of password salt (128 bytes expected).", "passwordHash");
using (var hmac = new System.Security.Cryptography.HMACSHA512(storedSalt))
{
var computedHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
for (int i = 0; i < computedHash.Length; i++)
{
if (computedHash[i] != storedHash[i]) return false;
}
}
return true;
}
private static void CreatePasswordHash(string password, out byte[] passwordHash, out byte[] passwordSalt)
{
if (password == null) throw new ArgumentNullException("password");
if (string.IsNullOrWhiteSpace(password)) throw new ArgumentException("Value cannot be empty or whitespace only string.", "password");
using (var hmac = new System.Security.Cryptography.HMACSHA512())
{
passwordSalt = hmac.Key;
passwordHash = hmac.ComputeHash(System.Text.Encoding.UTF8.GetBytes(password));
}
}
public async Task<IEnumerable<Project>> getProjectsFromUser(int userId)
{
List<UserProject> uP = await DbContext.Set<UserProject>().Where(c => c.FKEY_User == userId).ToListAsync();
IList<Project> projects = new List<Project>();
foreach (UserProject item in uP)
{
Project project = await DbContext.Set<Project>().SingleOrDefaultAsync(c => c.ID == item.FKEY_Project);
if (project != null)
{
projects.Add(project);
}
}
return projects;
}
public async Task<IEnumerable<Hour>> getHoursFromUser(int userId)
{
return await DbContext.Set<Hour>().Where(c => c.FKEY_User == userId).ToListAsync();
}
//example method implementation:
//public IEnumerable<User> GetUserHours(int id){
// return DbContext.User.Where(x => x.Irgendwas == soundsowoascheh);
//}
}
UserController中的实现如下所示:
[AllowAnonymous]
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]AuthenticateModel model)
{
User user = _unitOfWork.User.Authenticate(model.Username, model.Password);
if (user == null) return BadRequest(new { message = "Username or password is incorrect" });
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[] {
new Claim(ClaimTypes.Name, user.ID.ToString())
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
return Ok(new
{
ID = user.ID,
Username = user.Username,
Email = user.Email,
Token = tokenString
});
}
[AllowAnonymous]
[HttpPost("register")]
public IActionResult Register([FromBody]RegisterModel model)
{
// map model to entity
var user = _mapper.Map<User>(model);
try
{
// create user
//_unitOfWork.User.Create(user, model.Password);
_unitOfWork.User.Create(user, model.Password);
return Ok();
}
catch (AppException ex)
{
// return error message if there was an exception
return BadRequest(new { message = ex.Message });
}
}
解决方案不会编译,但会引发错误,我们不确定原因。我们的启动类如下所示:
public class Startup
{
private readonly IConfiguration _configuration;
public Startup(IWebHostEnvironment env, 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.AddDbContext<JellyContext>(opts => opts.UseLazyLoadingProxies().UseMySql(Configuration["ConnectionString:JellyDB"]));
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddCors();
services.AddControllersWithViews();
services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
var appSettingSection = _configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingSection);
var appSettings = appSettingSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.Events = new JwtBearerEvents
{
OnTokenValidated = context =>
{
var userService = context.HttpContext.RequestServices.GetRequiredService<IUnitOfWork>();
var userId = int.Parse(context.Principal.Identity.Name);
var user = userService.User.Get(userId);
if (user == null)
{
context.Fail("Unauthorized");
}
return Task.CompletedTask;
}
};
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "JellyAPI", Version = "v1" });
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
c.IncludeXmlComments(xmlPath);
});
services.AddControllers().AddNewtonsoftJson(options =>
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
}
// 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();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "JellyAPI v1");
});
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseCors(x => x
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
}
引发的错误与自动映射器有关,它说
System.Reflection.ReflectionTypeLoadException:'无法加载一个或多个请求的类型。 无法加载文件或程序集'Microsoft.EntityFrameworkCore.Relational,Version = 3.1.3.0,Culture = neutral,PublicKeyToken = sometoken'。该系统找不到指定的文件。 无法加载文件或程序集'Microsoft.EntityFrameworkCore.Relational,Version = 3.1.3.0,Culture = neutral,PublicKeyToken = sometoken'。该系统找不到指定的文件。'
Try installing
Microsoft.EntityFrameworkCore
to the project using Nuget Package Managerhttps://www.nuget.org/packages/Microsoft.EntityFrameworkCore