Я использую JWT
токен, отправляю запрос на получение токена, получаю токен, как мне сделать перенаправление в удобной форме после успешного прохождения процедуры аутентификации? Я пробовал это сделать через js (решение работает на странице входа, получаем токен и ссылку для перехода, делаем запрос на выборку и встраиваем ответ на страницу, я понимаю, что это фигня), я бы хотелось бы узнать, есть ли в ASP .NET Core MVC механизм, который сам будет встраивать токен во все запросы, если это потребуется, а не писать для этого везде различные js-скрипты. Я также хочу знать, можно ли как-то перенаправить на другую ссылку и при этом получить токен одним методом, в моем случае методом Login
.
Login.cshtml
@model LoginViewModel
<h2>Login</h2>
<form id = "loginForm" method = "post" asp-controller = "Account" asp-action = "Login" asp-route-returnUrl = "@Model.ReturnUrl">
<div asp-validation-summary = "ModelOnly" class = "text-danger"></div>
<div>
<label asp-for = "UserName"></label><br />
<input asp-for = "UserName" />
<span asp-validation-for = "UserName"></span>
</div>
<div>
<label asp-for = "Password"></label><br />
<input asp-for = "Password" />
<span asp-validation-for = "Password"></span>
</div>
<div>
<label asp-for = "RememberMe"></label><br />
<input asp-for = "RememberMe" />
</div>
<div>
<input type = "submit" value = "Login" />
</div>
</form>
<div id = "content"></div>
@section Scripts {
<script>
document.getElementById('loginForm').addEventListener('submit', async function (event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
const username = formData.get('UserName');
const password = formData.get('Password');
const response = await fetch('/Account/Login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
UserName: username,
Password: password
})
});
if (response.ok) {
const data = await response.json();
localStorage.setItem('token', data.token.access_token);
const returnUrl = form.getAttribute('asp-route-returnUrl');
var newUrl = returnUrl ? returnUrl : '/Users/Index';
loadPage(newUrl);
} else {
const errorText = await response.text();
alert('Login failed: ' + errorText);
}
});
async function loadPage(url) {
const token = localStorage.getItem('token');
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const content = await response.text();
document.getElementById('content').innerHTML = content;
} else {
alert('Failed to load the page: ' + response.statusText);
}
}
</script>
}
AccountController.cs
обеспечивает регистрацию, вход, выход
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using MySteamDBMetacritic.Db;
using MySteamDBMetacritic.Models;
using MySteamDBMetacritic.ViewModels;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Runtime.Intrinsics.Arm;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace MySteamDBMetacritic.Controllers
{
public class AccountController : Controller
{
private readonly ILogger<AccountController> _logger;
private readonly ApplicationDbContext _context;
private readonly UserManager<User> _userManager;
private readonly SignInManager<User> _signInManager;
private readonly IConfiguration _configuration;
public AccountController(ILogger<AccountController> logger, ApplicationDbContext context, UserManager<User> userManager, SignInManager<User> signInManager, IConfiguration configuration)
{
_context = context;
_logger = logger;
_userManager = userManager;
_signInManager = signInManager;
_configuration = configuration;
}
[HttpGet]
public IActionResult Register()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Register([FromBody]RegisterViewModel model)
{
if (ModelState.IsValid)
{
User user = new User { Email = model.Email, UserName = model.UserName };
if (_userManager.Users.FirstOrDefault(x=>x.Email==user.Email) != default(User))
{
return View(model);
}
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
result = await _userManager.AddToRoleAsync(user, "User");
if (result.Succeeded)
{
_context.SaveChanges();
await _signInManager.SignInAsync(user, false);
return Json(new { token = Token(model.UserName, model.Password), returnUrl = Url.Action("Index", "Game") });
}
else
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
}
else
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
}
return View(model);
}
[HttpGet]
public IActionResult Login(string returnUrl = null)
{
return View(new LoginViewModel { ReturnUrl = returnUrl });
}
[HttpPost]
public async Task<IActionResult> Login([FromBody]LoginViewModel model)
{
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, false);
if (result.Succeeded)
{
if (!string.IsNullOrEmpty(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
{
return Json(new { token = ((JsonResult)Token(model.UserName, model.Password)).Value, returnUrl = model.ReturnUrl});
}
else
{
return Json(new { token = ((JsonResult)Token(model.UserName, model.Password)).Value, returnUrl = Url.Action("Index", "Game") });
}
}
else
{
ModelState.AddModelError("", "Incorrect username or password");
}
}
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
return RedirectToAction("Index", "Game");
}
public async Task<IActionResult> ChangePassword(int id)
{
User user = await _userManager.FindByIdAsync(id.ToString());
if (user == null)
{
return NotFound();
}
ChangePasswordViewModel model = new ChangePasswordViewModel { Id = user.Id, Email = user.Email };
return View(model);
}
[HttpPost]
public async Task<IActionResult> ChangePassword(ChangePasswordViewModel model)
{
if (ModelState.IsValid)
{
User user = await _userManager.FindByIdAsync(model.Id.ToString());
if (user != null)
{
var _passwordValidator = HttpContext.RequestServices.GetService(typeof(IPasswordValidator<User>)) as IPasswordValidator<User>;
var _passwordHasher = HttpContext.RequestServices.GetService(typeof(IPasswordHasher<User>)) as IPasswordHasher<User>;
IdentityResult result = await _passwordValidator.ValidateAsync(_userManager, user, model.NewPassword);
if (result.Succeeded)
{
user.PasswordHash = _passwordHasher.HashPassword(user, model.NewPassword);
await _userManager.UpdateAsync(user);
return RedirectToAction("Index");
}
else
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
}
else
{
ModelState.AddModelError(string.Empty, "User is missing");
}
}
return View(model);
}
public IActionResult Token(string username, string password)
{
var identity = GetIdentity(username, password).GetAwaiter().GetResult();
if (identity == null)
{
return BadRequest(new { errorText = "Invalid username or password." });
}
var now = DateTime.Now;
var jwt = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(
issuer: _configuration["Jwt:Issuer"],
audience: _configuration["Jwt:Audience"],
//notBefore: now,
claims: identity.Claims,
//expires: now.Add(TimeSpan.FromMinutes(double.Parse(_configuration["Jwt:ExpiresMinutes"]))),
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:Key"])), SecurityAlgorithms.HmacSha256));
var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
var response = new
{
access_token = encodedJwt,
username = identity.Name
};
return Json(response);
}
private async Task<ClaimsIdentity> GetIdentity(string username, string password)
{
User user = await _userManager.FindByNameAsync(username);
if (user != null)
{
var claims = new List<Claim>
{
new Claim(ClaimsIdentity.DefaultNameClaimType, user.UserName)
};
claims.AddRange(_userManager.GetRolesAsync(user).Result.Select(x => new Claim(ClaimsIdentity.DefaultRoleClaimType, x)));
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, "Token", ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType);
return claimsIdentity;
}
return null;
}
}
}
UsersController.cs
Функционал контроллера я решил разделить на две части, в AccountController пользователь регистрируется и может авторизоваться, в UserController пользователь с ролью администратора может менять пароль, создавать/удалять пользователей.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using MySteamDBMetacritic.Models;
using MySteamDBMetacritic.ViewModels;
using System.Security.Claims;
namespace MySteamDBMetacritic.Controllers
{
[Authorize(Roles = "Admin")]
public class UsersController : Controller
{
UserManager<User> _userManager;
public UsersController(UserManager<User> userManager)
{
_userManager = userManager;
}
public IActionResult Index()
{
var cl = HttpContext.User.Claims;
return View(_userManager.Users.ToList());
}
public IActionResult Create() => View();
[HttpPost]
public async Task<IActionResult> Create(CreateUserViewModel model)
{
if (ModelState.IsValid)
{
User user = new User { Email = model.Email, UserName = model.UserName };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
return RedirectToAction("Index");
}
else
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
}
return View(model);
}
public async Task<IActionResult> Edit(int id)
{
User user = await _userManager.FindByIdAsync(id.ToString());
if (user == null)
{
return NotFound();
}
EditUserViewModel model = new EditUserViewModel { Id = user.Id, Email = user.Email, UserName = user.UserName };
return View(model);
}
[HttpPost]
public async Task<IActionResult> Edit(EditUserViewModel model)
{
if (ModelState.IsValid)
{
User user = await _userManager.FindByIdAsync(model.Id.ToString());
if (user != null)
{
user.Email = model.Email;
user.UserName = model.UserName;
var result = await _userManager.UpdateAsync(user);
if (result.Succeeded)
{
return RedirectToAction("Index");
}
else
{
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
}
}
return View(model);
}
[HttpPost]
public async Task<ActionResult> Delete(int id)
{
User user = await _userManager.FindByIdAsync(id.ToString());
if (user != null)
{
IdentityResult result = await _userManager.DeleteAsync(user);
}
return RedirectToAction("Index");
}
}
}
Я также открыт для любых советов по приложению в целом, возможно, есть ошибки, которых следует избегать в будущем. Ссылка на мой проект.
Я уже получил токен, сохранил его через js и использую SignInManger или я что-то не понял?
Почему вы смешиваете ядро asp.net Identity и Jwt вместе? Есть ли какая-то конкретная причина? Куда вы хотите перенаправиться после успешного входа в систему?
Я хотел использовать только jwt, но еще добавил код из некоторых статей, в которых почему-то использовалась идентичность, просто я новичок в asp, подумал, что может быть в jwt это тоже как-то используется. Если подскажете, как использовать только jwt, и при этом сохранить весь функционал, буду благодарен
Вы используете правильный способ, не знаете, что именно вы хотите знать или с какими проблемами вы сталкиваетесь. Но в целом, если вы хотите использовать jwt для выполнения всего с использованием токена, вам необходимо передать токен во всем запросе сервера, чтобы безопасно получить доступ к конечной точке.
1. То есть я могу удалить SignInManager, UserManager и написать модели для хранения информации и сделать миграцию, тогда мне придется самому писать логику добавления/изменения/удаления пользователей и проверять, есть она или нет при входе в систему, или я что-то упускаю? 2. А по поводу добавления токена ко всем запросам, есть ли способ сделать это максимально корректно и просто, или мне действительно придется вставлять js в каждый View, который должен будет как-то перехватить запрос и встроить токен в заголовок авторизации?
Я хотел бы знать, есть ли в ASP .NET Core MVC механизм, который сам будет вставлять токен во все запросы, если это потребуется, вместо того, чтобы везде для этого писать различные js-скрипты.
Что ж, чтобы установить заголовок запроса Authorization
при отправке запроса в API по всему миру, это довольно просто, если вы используете ajax-запрос с атрибутом beforeSend, где вы можете установить свой токен один раз, а затем нет, где вам не нужно это делать.
Поскольку выборка не поддерживает beforeSend
— это особенность jQuery's $.ajax
. При извлечении вам необходимо включить заголовки непосредственно в options object
, чтобы глобально установить заголовок авторизации для всех запросов API выборки в вашем файле JavaScript.
Вы можете создать функцию-оболочку вокруг API выборки, которая автоматически включает токен в заголовки.
Итак, в вашем файле js-скрипта у вас должна быть функция, как показано ниже, и где бы вам ни потребовалось передать заголовок аутентификации, вы можете связать скрипт, вызвать функцию и передать URL-адрес запроса.
async function fetchWithAuth(url, options = {}) {
const token = localStorage.getItem('token');
const headers = new Headers(options.headers || {});
if (token) {
headers.append('Authorization', `Bearer ${token}`);
}
const fetchOptions = {
...options,
headers: headers
};
try {
const response = await fetch(url, fetchOptions);
if (response.ok) {
const contentType = response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
} else if (contentType && contentType.includes('text/html')) {
return await response.text();
} else {
throw new Error('Unsupported content type: ' + contentType);
}
} else {
if (response.status === 403) {
window.location.href = '/Users/AccessDenied';
} else {
throw new Error('Network response was not ok.');
}
}
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
Учитывая ваш сценарий, вы можете вызвать функцию следующим образом:
async function loadPage(url) {
const token = localStorage.getItem('token');
const content = await fetchWithAuth(url, { method: 'GET' });
document.getElementById('content').innerHTML = content;
}
UserController — пользователь с ролью администратора может изменить пароль, создавать/удалять пользователей.
Что ж, в этом сценарии перед выполнением создания и удаления вы можете проверить userRole. Например, вы можете написать [Authorize(Roles = "Admin")]
перед действием контроллера. То же самое касается и другой операции.
Кроме того, основываясь на другом вашем вопросе, я уже объяснил и дал вам указание выше. Если вы хотите напрямую выполнить операцию чтения и записи внутри контроллера, вы можете использовать назначенную роль в атрибуте Authorize
.
Да, остальная часть вашего кода кажется в порядке.
Примечание. Если вам нужна дополнительная информация , обратитесь к этому официальному документу.
Хорошо, а как насчет того, что вы сказали, что я использую авторизацию на основе JWT Token
и Identity
одновременно, что мне нужно переделать, чтобы осталась только чистая аутентификация по токену jwt, мне нужно перестать использовать UserManager
/SignInManager
и просто создать БД, в которой я буду хранить скажем логин и пароль и самостоятельно проверять соответствие введенных данных в Login.cshtml
реальным данным в базе, потом выдавать пользователю jwt токен и все будет работать???
Это просто не то, чего я хочу. Я хочу, чтобы после получения токена пользователь мог сам перейти на другую ссылку и токен был отправлен вместе с запросом, но я так понимаю, предложенная вами реализация будет работать только в том случае, если на странице есть js-скрипт, который выполнит запрос и интегрирует ответ в текущую страницу. После небольшого поиска пока что наиболее подходящий вариант, который я нашел, это передать токен в куки, а затем добавить его в поле Авторизация с помощью промежуточного программного обеспечения или OnMessageReceived
, или это не правильно делать при работе с JWT
?
Прежде всего, вам не нужно ничего переделывать, просто используйте утверждения токенов, чтобы позволить пользователю делать свои дела. Я не понимаю, почему вы хотите избавиться от UserManager/SignInManager
. Выпуск токена обеспечит то, что вы запланировали. Да, повторно используя токен, вы также можете использовать cookie, чтобы проверить свой токен в заголовке запроса.
Не будет ли лишним использовать ASP .NET Core Identity в связке с JWT, я просто использовал Identity для создания базы данных для хранения пользовательских данных, возможно, мне следовало написать всю обработку самому, не используя готовый, более функциональный решение для этих целей или это не то, на что стоит обратить внимание??
Любой вариант подойдет, это зависит от требований и предпочтений. Все, что вам нужно учитывать при получении запроса, — это проверить, авторизован ли пользователь и имеет ли он правильную роль для доступа к конечной точке, которую он запрашивал. Вот и все.
А как насчет уязвимости CSRF
токена, если я передам его через Cookie? Моя цель – сделать все максимально правильно и безопасно. Когда ты сам получаешь токен и сам делаешь запросы через Postman
и сам передаешь токен - все хорошо и просто, но когда ты хочешь сделать все удобно для обычного человека, который хочет авторизоваться и попасть на сайт, возникают проблемы с реализацией. Возможно, ваше решение более удобно для SPA. И еще, Cookies тоже можно заблокировать на получение, в этом случае мой токен не может быть получен через него сервером, потому что пользователь его не сохранил?
В этом сценарии используйте атрибуты Secure, HttpOnly и SameSite для файлов cookie, чтобы повысить безопасность и снизить риск CSRF.
Чтобы выполнить перенаправление после входа в учетную запись и внедрения токена JWT в последующие запросы в ASP.NET Core MVC, настройте аутентификацию JWT в
Startup.cs
, используйтеSignInManager
вAccountController
для выдачи токенов и сохраните токен вlocalStorage
с помощью JavaScript на будущее. Запросы.