В июне 2016 вышел релиз ASP.Net Core 1.0 и теперь, если вас не пугает возраст нового фреймворка, можно аккуратно запустить микросервис в продакшн (все ведь используют микросервисную архитектуру, не так ли?). Для того, чтобы ограничить доступ к вашему микросервису для третьих лиц, необходимо сделать аутентификацию, используя довольно распространенный способ — токены. В статье под катом мы расскажем подробнее о том, как это сделать с помощью JSON Web Token (JWT), а также о плюсах и минусах этого подхода.
Обычно токен — это случайно сгенерированная строка, которая связанная с определенным пользователем и для получения его данных (например, id или email) необходимо сделать запрос к базе данных (БД). Но, что если нам не нужно делать лишний запрос к БД за данными пользователя, а нужно хранить их прямо внутри токена? Такое возможно с помощью JWT. Разберем, что такое JWT и создадим тестовый проект.
JWT — это подписанный объект JSON, содержащий что-либо полезное (например, id пользователя, его права/роли), закодированный в base64 и состоящий из трех частей разделенный точками .
: Header, Payload, Signature и обычно выглядит так aaaaaaa.bbbbbb.cccccc
. Более полную информацию можно найти на jwt.io или RFC 7519.
{ "alg": "HS256", "typ": "JWT" }
{ "email": "temp@jwt.ru", "user_id": "57dc51a3389b30fed1b13f91" }
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
project.json
добавим следующие зависимости:"Microsoft.AspNetCore.Authentication.JwtBearer": "1.0.0",
"Microsoft.AspNetCore.Mvc.Core": "1.0.0",
"Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.0"
Microsoft.AspNetCore.Mvc.Core
вместо Microsoft.AspNetCore.Mvc
для того чтоб не тащить лишние (для нашего rest сервиса) зависимости в виде Razor, TagHelper и т.д.Startup.cs
, немного подправим его:public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseMvc().UseMvcWithDefaultRoute();
}
}
Startup.cs
и допишем следующее:public class Startup {
public void ConfigureServices(IServiceCollection services) {
services.AddMvcCore()
.AddAuthorization();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
var key = Encoding.UTF8
.GetBytes("401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429090fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1");
var options = new JwtBearerOptions {
TokenValidationParameters = {
ValidIssuer = "ExampleIssuer",
ValidAudience = "ExampleAudience",
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}
};
app.UseJwtBearerAuthentication(options);
app.UseMvcWithDefaultRoute();
}
}
ConfigureServices
все довольно очевидно, мы добавляем использование сервиса авторизации, а так же регистрируем HttpContextAccessor
. Для чего нам понадобилась явная регистрация HttpContextAccessor, узнаем чуть позже. Перейдем к методу Configure
, в котором и происходит оснавная настройка параметров для валидации токена JWT. Сейчас, нам больше всего интересны три параметра:IssuerSigningKey
— ключ, которым должен быть подписан наш токен. Для примера выбрали SymmetricSecurityKey, но также можно указать X509SecurityKey() или JsonWebKey, если вы обладаете большой любовью к JSON.ValidateIssuerSigningKey
— указываем, что будем проверять ключ которым подписывали токен JWT.ValidateLifetime
— ставим true, так как хотим контролировать время жизни токена.HomeController.cs
: [Route("/")]
public class HomeController {
private readonly IHttpContextAccessor _context;
public HomeController(IHttpContextAccessor context) {
_context = context;
}
[HttpGet("token")]
public dynamic GetToken() {
var handler = new JwtSecurityTokenHandler();
var sec = "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429090fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1";
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(sec));
var signingCredentials = new SigningCredentials(securityKey,SecurityAlgorithms.HmacSha256Signature);
var identity = new ClaimsIdentity(new GenericIdentity("temp@jwt.ru"), new[] { new Claim("user_id", "57dc51a3389b30fed1b13f91") });
var token = handler.CreateJwtSecurityToken(subject: identity,
signingCredentials: signingCredentials,
audience: "ExampleAudience",
issuer: "ExampleIssuer",
expires: DateTime.UtcNow.AddSeconds(42));
return handler.WriteToken(token);
}
[Authorize, HttpGet("secure")]
public dynamic Secret() {
var currentUser = _context.HttpContext.User;
return currentUser.Identity.Name;
}
}
AspNetCore.Mvc.Core
, то единственный способ (хотя может быть есть и другой) добраться до HttpContext — как раз через IHttpContextAccessor
, который мы регистрировали ранее.signingCredentials
— создаем ключ которым подпишем наш токен, он должен быть одинаковый с тем, который мы указывали в Startup.cs
при настройке JWT-параметров.identity
— создаем наш payload
. Безусловно, в реальном приложении достанем данные из хранилища, перед этим проверив их, а сейчас устроим немного хардкода.expires: DateTime.UtcNow.AddSeconds(42)
, довольно тривиально и гибко.curl -X GET "http://localhost:<your_port>/token"
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1bmlxdWVfbmFtZSI6InRlc3RAdGVzdC5ydSIsInVzZXJfaWQiOiI1N2RjNTFhMzM4OWIzMGZlZDFiMTNmOTEiLCJuYmYiOjE0NzQyMTU4MDAsImV4cCI6MTQ3NDIxNTgzNSwiaWF0IjoxNDc0MjE1ODAwLCJpc3MiOiJFeGFtcGxlSXNzdWVyIiwiYXVkIjoiRXhhbXBsZUF1ZGllbmNlIn0.9NhOkoalaE70nIb-erH_waWx8rk6QJta5N19EiBLETQ
curl -X GET "http://localhost:<your_port>/secure"
401 Unauthorized
, что и ожидаемо.curl -X GET -H "Authorization: Bearer token_should_be_here" "http://localhost:<your_port>/secure"
GenericIdentity
.401 Unauthorized
и в хедере WWW-Authenticate
будет значение: Bearer error="invalid_token", error_description="The token is expired"
, сообщающее нам об истекшем токене.blacklist
, который будет содержать невалидные токены. Но, теперь нам все равно придется делать лишний запрос для проверки нашего токена.refresh
— токен на более длительное время. Совсем не красивое решение на мой взгляд.К сожалению, не доступен сервер mySQL