Цель такая: написать бэкенд ASP.Net Core MVC* SPA для работы с ReactJS и дальнейшей возможностью переиспользовать существующий API для создания, скажем, Android приложения.
Платформа: .Net Core 2.1
.
* - Если такое ещё можно назвать MVC, учитывая, что View не будет, а будет отдельная директория ClientApp
со всем содержимым фронтэнда.
Пошуршав интернеты, наткнулся на то, что ASP.Net Core Identity не актуален. Звучит логично, учитывая, что тот сильно опирается на куки, а при общении через API куки таскать неудобно. Хотя многие примеры нижеупомянутого JWT всё же используют IdentityUser
.
Много инструкций с использованием JWT. Приличная часть из них слишком зациклена на фронтенд реализации и практически ничего не говорит о бэкенде. Не нашёл примеров с OAuth2, везде свой велосипед, причём Demo и не пригодный для реального использования.
В тех же примерах по JWT используются Issuer
, Audience
и SecretKey
, но ни слова о том, по каким правилам их надо выбирать и/или генерировать, ну и где безопасно хранить (если исключить примеры с хардкодом, то их обычно хранили в appsettings.json
).
Также в процессе гугления (конкретно: попытке найти инфу об JwtSecurityTokenHandler
из System.IdentityModel.Tokens
) MSDN Microsoft выдаёт:
We’re no longer updating this content regularly. Check the Microsoft Product Lifecycle for information about how this product, service, or technology is supported.
Что это значит? Microsoft более не поддерживает JWT? Технология в принципе уже неактуальна? Что же тогда использовать?
В итоге мне теперь не совсем понятно как строить авторизацию в своём приложении:
IdentityUser
из Microsoft.AspNetCore.Identity
?IdentityDbContext
из Microsoft.AspNetCore.Identity.EntityFrameworkCore
?Хотелось бы сохранить доступность авторизованного пользователя из HttpContext.User
и к его Claims
, чтобы не мучать БД лишний раз для получения Id/UserName/Avatar/LastOnline/etc.
А также учесть то, что активно будут использоваться роли пользователей.
UPD: Немного обновлю конечные цели, чтобы стало понятнее:
Вот проверенный код, который работает. Итак, настройка аутентификации в Startup
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)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
services.AddDbContext<AppDbContext>(options =>
options.UseMySql(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<AppDbContext>();
services.Configure<IdentityOptions>(options =>
{
//........
});
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
// укзывает, будет ли валидироваться издатель при валидации токена
ValidateIssuer = false,
// будет ли валидироваться потребитель токена
ValidateAudience = false,
// будет ли валидироваться время существования
ValidateLifetime = true,
// установка ключа безопасности
IssuerSigningKey = AuthOptions.GetSymmetricSecurityKey(),
// валидация ключа безопасности
ValidateIssuerSigningKey = true,
};
});
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
// .........
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// ...............
app.UseAuthentication();
// ................
}
}
Класс AuthOptions
public class AuthOptions
{
const string KEY = "mysupersecret_secretkey!"; // ключ для шифрации
public const int LIFETIME = 300; // время жизни токена - 5 часов
public static SymmetricSecurityKey GetSymmetricSecurityKey()
{
return new SymmetricSecurityKey(Encoding.ASCII.GetBytes(KEY));
}
}
Моделька для запроса токена
public class TokenRequest
{
public string UserName { get; set; }
public string Password { get; set; }
}
Контроллер для получения токена
[Route("api/[controller]")]
public class AccountController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
[HttpPost("[action]")]
public async Task<ActionResult> Auth([FromBody] TokenRequest tokenRequest)
{
var username = tokenRequest.UserName;
var password = tokenRequest.Password;
var principal = await GetPrincipal(username, password);
if (principal == null)
{
return StatusCode(400, "Invalid username or password.");
}
var now = DateTime.UtcNow;
// создаем JWT-токен
var jwt = new JwtSecurityToken(
notBefore: now,
claims: principal.Claims,
expires: now.Add(TimeSpan.FromMinutes(AuthOptions.LIFETIME)),
signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256));
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
var response = new
{
token = encodedJwt,
username = principal.Identity.Name
};
return Json(response);
}
private async Task<ClaimsPrincipal> GetPrincipal(string username, string password)
{
var user = await _userManager.FindByNameAsync(username);
if (user != null)
{
var check = await _userManager.CheckPasswordAsync(user, password);
if (check)
{
var principal = await _signInManager.CreateUserPrincipalAsync(user);
return principal;
}
}
return null;
}
}
И, по моему, это все. С этим все остальные вещи типа атрибутов авторизации, роли и прочее работает из коробки. Насколько я помню, ничего дополнительно делать не надо.
Для начала, ASP.Net Core Identity никаким образом не опирается на куки! ASP.Net Core Identity - это прежде всего система для хранения и обработки информации о пользователях.
Все что делает Identity во время аутентификации - это загружает из БД информацию о пользователе, заполняя набор утверждений о нем, после чего передает его ASP.NET Core.
Соответственно, нет никакого смысла искать как использовать ASP.Net Core Identity вместе с Jwt - это попросту не связанные друг с другом вещи.
Теперь про OAuth. Технология OAuth предназначена для того, чтобы сторонний разработчик мог создать приложение которое бы работало с вашим сайтом, но при этом не имел доступа к паролям ваших пользователей. Если же вы собрались создавать приложение на Андроиде самостоятельно - использование OAuth будет для вас избыточно!
Все что вам нужно - это action в контроллере, который принимает логин с паролем и выдает токен.
Ну а если вы решили дать доступ к своему API именно для сторонних разработчиков - Identity Server вам в помощь.
Наконец, про JWT.
Audience
- это параметр токена, который означает целевую систему куда его можно предъявлять. Иными словами, это URL вашего сайта. Однако, нет никакого смысла в его использовании в ситуации когда у вас только один сайт: он нужен когда есть несколько сайтов со своими API и эти сайты не доверяют друг другу.
Issuer
- это URI провайдера, который выдал JWT. То есть, опять-таки, URL вашего сайта. Это поле используется только в тех случаях когда доступ к API можно получить при помощи токенов от разных поставщиков. В остальных случаях это поле вообще-то не обязательное.
SecretKey
- это, скорее всего, ключ, которым вы шифруете или подписываете JWT. Хранить его можно в appsettings.json или в переменных окружения. Главное - не публикуйте его на github. Вообще, желательно использовать разные ключи для разработки и на "боевом" сервере.
Отталкивался я от ответа @tym32167 (поэтому и пометил его как правильный), но на всякий случай добавлю конкретно свою реализацию. Вдруг будет кому-то полезна.
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// подключение DbContext'ов
// подключение Identity
services.Configure<IdentityOptions>(options =>
{
// ...
});
// игнорирование регистра (url без заглавных букв)
services.Configure<RouteOptions>(options => options.LowercaseUrls = true);
// подключение репозиториев
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
//ValidIssuer = "",
ValidateAudience = false,
//ValidAudience = "",
ValidateLifetime = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["Jwt:Key"])),
ValidateIssuerSigningKey = true,
};
});
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error"); //ToDo: do it better
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
// актуально для ReactJS
spa.UseReactDevelopmentServer(npmScript: "start");
}
});
}
}
appsettings.json
{
"Jwt": {
"Key": "my_very_secret_key",
"LifeTime": "300"
}
}
Jwt:Key
должен быть больше 16 символов (128 бит) иначе выбрасывает исключение при генерации токена. По крайней мере, при использовании шифрования HmacSha256.
TokenRequest.cs
public class TokenRequest
{
[Required(ErrorMessage = "")]
[JsonProperty("login")]
public string Login { get; set; }
[Required(ErrorMessage = "")]
[DataType(DataType.Password)]
[JsonProperty("password")]
public string Password { get; set; }
}
RegisterModel.cs
public class RegisterModel
{
[Required(ErrorMessage = "")]
[JsonProperty("username")]
public string UserName { get; set; }
[Required(ErrorMessage = "")]
[EmailAddress]
[DataType(DataType.EmailAddress)]
[JsonProperty("email")]
public string Email { get; set; }
[Required(ErrorMessage = "")]
[DataType(DataType.Password)]
[JsonProperty("password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Compare("Password", ErrorMessage = "")]
[JsonProperty("confirm_password")]
public string ConfirmPassword { get; set; }
}
AccountController.cs
[Route("api/[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
private readonly UserManager<UserAccount> userManager;
private readonly SignInManager<UserAccount> signInManager;
private readonly IConfiguration configuration;
public AccountController(
UserManager<UserAccount> userManager,
SignInManager<UserAccount> signInManager,
IConfiguration configuration)
{
this.userManager = userManager;
this.signInManager = signInManager;
this.configuration = configuration;
}
[HttpPost("[action]")]
public async Task<IActionResult> Register([FromBody] RegisterModel model)
{
if (!ModelState.IsValid)
{
var errors = ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage));
return BadRequest(errors);
}
var userAccount = new UserAccount { Email = model.Email, UserName = model.UserName };
var result = await userManager.CreateAsync(userAccount, model.Password);
if (result.Succeeded)
{
var token = await GetTokenAsync(userAccount);
return Ok(new { token });
}
return BadRequest(result.Errors.Select(e => e.Description));
}
[HttpPost("[action]")]
public async Task<IActionResult> Auth([FromBody] TokenRequest tokenRequest)
{
var userAccount = tokenRequest.Login.Contains('@') ?
await userManager.FindByEmailAsync(tokenRequest.Login) :
await userManager.FindByNameAsync(tokenRequest.Login);
if (userAccount == null)
{
return NotFound(new { message = $"User with login {tokenRequest.Login} not found!" });
}
var passValided = await userManager.CheckPasswordAsync(userAccount, tokenRequest.Password);
if (!passValided)
{
return UnprocessableEntity(new { message = "Invalid username or password." });
}
var token = await GetTokenAsync(userAccount);
return Ok(token);
}
private async Task<string> GetTokenAsync(UserAccount userAccount)
{
var principal = await signInManager.CreateUserPrincipalAsync(userAccount);
var identity = (ClaimsIdentity)principal.Identity;
if (identity == null)
{
return null;
}
var now = DateTime.UtcNow;
// created JWT-token
var jwt = new JwtSecurityToken(
notBefore: DateTime.UtcNow,
claims: identity.Claims,
expires: now.Add(TimeSpan.FromMinutes(int.Parse(configuration["Jwt:LifeTime"]))),
signingCredentials: new SigningCredentials(
new SymmetricSecurityKey(
Encoding.ASCII.GetBytes(configuration["Jwt:Key"])),
SecurityAlgorithms.HmacSha256));
var token = new JwtSecurityTokenHandler().WriteToken(jwt);
return token;
}
}
UserAccount
наследник IdentityUser
, а UserRole
наследник IdentityRole
Виртуальный выделенный сервер (VDS) становится отличным выбором
Сервер отдаёт дату вот в таком формате: 2015-04-24T07:00:51ZМне нужно узнать, не старше ли эта дата, например, трёх дней
Я хочу сделать так, чтобы можно было отсортировать двумерные точкиТо есть объекты, у которых есть 2 числовых значения
Мне нужно постоянно пинговать около 400 машин и при это м чтобы другой функционал программы работалПишу так :
Здравсвуйте, нужно реализовать внутрениий EventBus (не микросервисное взаимодействие на основе например RabbitMq)Все издатели пишут на общую шину...