Я никогда раньше не работал с ASP.NET Core. Пока я просто возюсь, у меня есть желание сделать свою авторизацию с помощью JWT в ASP.NET Core MVC. В итоге я посмотрел несколько гайдов, прочитал несколько статей, возможно я чего-то не понял, но у меня почему-то не работает авторизация, хотя токен я получаю, я сразу делаю запрос контролеру с Authorize
атрибут и получить ответ
Ошибка носителя = "invalid_token"
Получите токен, затем скопируйте токен и выполните метод публикации, требующий авторизации.
POST https://localhost:7081/Account/Login
Content-Type: application/json
{
"UserName": "Admin",
"Password": "aA12345!"
}
###
GET https://localhost:7081/Users/Index
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiQWRtaW4iLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOiJBZG1pbiIsIm5iZiI6MTcyMDk1MDc1NSwiZXhwIjoxNzIwOTU0MzU1LCJpc3MiOiJNeVN0ZWFtREJEYXRhYmFzZSIsImF1ZCI6Ik15U3RlYW1EQkRhdGFiYXNlIn0.lgXz-1z7CJwJiJsqjn_q7WMQy6-rZ_EvbYlEDh9JrGk
Код контроллера
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;
namespace MySteamDBMetacritic.Controllers
{
public class AccountController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly ApplicationDbContext _context;
private readonly UserManager<User> _userManager;
private readonly SignInManager<User> _signInManager;
private readonly IConfiguration _configuration;
public AccountController(ILogger<HomeController> 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)
{
_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);
}
}
}
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 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),
new Claim(ClaimsIdentity.DefaultRoleClaimType, string.Join(',',_userManager.GetRolesAsync(user).Result))
};
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, "Token",
ClaimsIdentity.DefaultNameClaimType,
ClaimsIdentity.DefaultRoleClaimType);
return claimsIdentity;
}
return null;
}
}
}
Я раньше не работал с EF, он создавал свои таблицы, я так понимаю, некоторые здесь тоже используются для авторизации, возможно проблема в том, что он там ничего не сохраняет, но почему-то во всех гайдах, который я смотрел, все как-то работает.
Думаю, не стоит выкладывать сюда весь код, но если вдруг понадобится что-то посмотреть, вот оно https://github.com/Drobovik04/MySteamDBMetacritic
Также еще один вопрос - есть ли какой-то простой механизм отправки токена в запросах, а то почему-то много мест, где к представлению привязывают JS и завязывают на нажатие кнопок на форме, а затем описывают запрос через fetch, меня просто очень смущает такая реализация, наверное есть более удобный и правильный способ
Пробовал: переписать код
Ожидается: успешная авторизация
Результат: получение токена, сохранение его, но ошибка носителя.
проверено, подпись действительна
пробовал IncludeErrorDetails ?
проверил с помощью IncludeErrorDetails, но ничего не показывает, также попробовал выполнить аутентификацию на webapi, это сработало, но то же самое в mvc не работает почтальон ничего не показывает может быть есть что-то, что переопределяет какой-то код
WebApi, который работал github.com/Drobovik04/WebApiTest, пробовал сделать как здесь в mvc, но не получилось или я допустил ошибку
Проверив ваш репозиторий, я обнаружил, что в вашем проекте есть две проблемы.
1. Я обнаружил ошибку IDX1400 с помощью метода OnAuthenticationFailed
, вот вам образец.
.AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = false,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
var result = Newtonsoft.Json.JsonConvert.SerializeObject(new { message = "Authentication failed" });
return context.Response.WriteAsync(result);
}
};
});
Решение
Добавьте пакет System.IdentityModel.Tokens.Jwt
и используйте его в своем методе Token
.
2. window.location.href не поддерживает токен на предъявителя. Итак, ниже приведен мой тестовый код, просто для справки. Не душа для window.location.href
.
@model LoginViewModel
<h2>Вход в приложение</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>
}
может я что-то не так сделал, но это не работает? ошибка
Также, не могли бы вы дать мне совет в целом по разработке на asp net core, может быть я неправильно строю архитектуру приложения и ее можно лучше организовать, чтобы было проще работать? И еще, можно ли как-то автоматически включать токен во все запросы. То есть как только я залогинюсь, какой-то скрипт будет отправлять токен на все последующие запросы к серверу, даже если этот токен не нужен? Также, возможно, я что-то делаю не так, и в asp net core есть способы более удобно и красиво реализовать jwt авторизацию?
Привет @CriticalError JWT очень распространен в проектах веб-API, поскольку он обеспечивает механизм аутентификации без сохранения состояния, подходящий для передачи пользовательской информации между клиентами и серверами.
@CriticalError Конечно, у нас также должна быть возможность добавлять токен jwt при отправке всех http-запросов, как и в других внешних и внутренних проектах. Возможно, вы сможете провести небольшое исследование о том, как работать с jwt в проекте webapi. Если вы просто хотите добавить аутентификацию в свой проект asp.net core mvc, вам следует сначала проверить официальный документ.
написал простой WebApi, все работает, попробовал сделать то же самое с моим проектом, но не помогло. WebApi: github.com/Drobovik04/WebApiTest. Скопируйте все, что участвует в аутентификации, и вставьте. Отредактированный исходный проект не был отправлен на github.
отладчик говорит idx 14100, что точек нет, но точки в токене есть, я не понимаю 🤯
После некоторых исследований я нашел это https://github.com/dotnet/aspnetcore/issues/52286. Похоже проблема была не у меня, а у .NET 8. Вам необходимо реализовать свой собственный TokenHandler.
Есть ли у вас какие-либо журналы, показывающие, почему токен недействителен, какие-либо дополнительные сведения? Пробовали ли вы отлаживать или проверять компоненты токена на jwt.io?