JWT 认证简单介绍
关于 Jwt 的介绍网上很多, 此处不在赘述, 我们主要看看 jwt 的结构.
JWT 主要由三部分组成, 如下:
HEADER.PAYLOAD.SIGNATURE
HEADER 包含 token 的元数据, 主要是加密算法, 和签名的类型, 如下面的信息, 说明了
加密的对象类型是 JWT, 加密算法是 HMAC SHA-256
{"alg":"HS256","typ":"JWT"}
然后需要通过 BASE64 编码后存入 token 中
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload 主要包含一些声明信息 (claim), 这些声明是 key-value 对的数据结构.
通常如用户名, 角色等信息, 过期日期等, 因为是未加密的, 所以不建议存放敏感信息.
{"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name":"admin","exp":1578645536,"iss":"webapi.cn","aud":"WebApi"}
也需要通过 BASE64 编码后存入 token 中
eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJleHAiOjE1Nzg2NDU1MzYsImlzcyI6IndlYmFwaS5jbiIsImF1ZCI6IldlYkFwaSJ9
Signaturejwt 要符合 jws(JSON Web Signature) 的标准生成一个最终的签名. 把编码后的 Header 和 Payload 信息加在一起, 然后使用一个强加密算法, 如 HmacSHA256, 进行加密. HS256(BASE64(Header).Base64(Payload),secret)
2_akEH40LR2QWekgjm8Tt3lesSbKtDethmJMo_3jpF4
最后生成的 token 如下
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJleHAiOjE1Nzg2NDU1MzYsImlzcyI6IndlYmFwaS5jbiIsImF1ZCI6IldlYkFwaSJ9.2_akEH40LR2QWekgjm8Tt3lesSbKtDethmJMo_3jpF4
开发环境
框架: ASP.NET 3.1
IDE:VS2019
ASP.NET 3.1 Webapi 中使用 JWT 认证
命令行中执行执行以下命令, 创建 webapix 项目:
dotnet new webapi -n Webapi -o WebApi
特别注意的时, 3.x 默认是没有 jwt 的 Microsoft.AspNetCore.Authentication.JwtBearer 库的, 所以需要手动添加 NuGet Package, 切换到项目所在目录, 执行 .net cli 命令
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 3.1.0
创建一个简单的 POCO 类, 用来存储签发或者验证 jwt 时用到的信息
- using Newtonsoft.JSON;
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading.Tasks;
- namespace Webapi.Models
- {
- public class TokenManagement
- {
- [JsonProperty("secret")]
- public string Secret { get; set; }
- [JsonProperty("issuer")]
- public string Issuer { get; set; }
- [JsonProperty("audience")]
- public string Audience { get; set; }
- [JsonProperty("accessExpiration")]
- public int AccessExpiration { get; set; }
- [JsonProperty("refreshExpiration")]
- public int RefreshExpiration { get; set; }
- }
- }
然后在 appsettings.Development.JSON 增加 jwt 使用到的配置信息 (如果是生成环境在 appsettings.JSON 添加即可)
- "tokenManagement": {
- "secret": "123456",
- "issuer": "webapi.cn",
- "audience": "WebApi",
- "accessExpiration": 30,
- "refreshExpiration": 60
- }
然后再 startup 类的 ConfigureServices 方法中增加读取配置信息
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddControllers();
- services.Configure<TokenManagement>(Configuration.GetSection("tokenManagement"));
- var token = Configuration.GetSection("tokenManagement").Get<TokenManagement>();
- }
到目前为止, 我们完成了一些基础工作, 下面再 webapi 中注入 jwt 的验证服务, 并在中间件管道中启用 authentication 中间件.
startup 类中要引用 jwt 验证服务的命名空间
- using Microsoft.AspNetCore.Authentication.JwtBearer;
- using Microsoft.IdentityModel.Tokens;
然后在 ConfigureServices 方法中添加如下逻辑
- services.AddAuthentication(x =>
- {
- x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
- x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
- }).AddJwtBearer(x =>
- {
- x.RequireHttpsMetadata = false;
- x.SaveToken = true;
- x.TokenValidationParameters = new TokenValidationParameters
- {
- ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(token.Secret)),
- ValidIssuer = token.Issuer,
- ValidAudience = token.Audience,
- ValidateIssuer = false,
- ValidateAudience = false
- };
- });
再 Configure 方法中启用验证
- public void Configure(IApplicationBuilder App, IWebHostEnvironment env)
- {
- if (env.IsDevelopment())
- {
- App.UseDeveloperExceptionPage();
- }
- App.UseHttpsRedirection();
- App.UseAuthentication();
- App.UseRouting();
- App.UseAuthorization();
- App.UseEndpoints(endpoints =>
- {
- endpoints.MapControllers();
- });
- }
上面完成了 JWT 验证的功能, 下面就需要增加签发 token 的逻辑. 我们需要增加一个专门用来用户认证和签发 token 的控制器, 命名成 AuthenticationController, 同时增加一个请求的 DTO 类
- public class LoginRequestDTO
- {
- [Required]
- [JsonProperty("username")]
- public string Username { get; set; }
- [Required]
- [JsonProperty("password")]
- public string Password { get; set; }
- }
- [Route("api/[controller]")]
- [ApiController]
- public class AuthenticationController : ControllerBase
- {
- [AllowAnonymous]
- [HttpPost, Route("requestToken")]
- public ActionResult RequestToken([FromBody] LoginRequestDTO request)
- {
- if (!ModelState.IsValid)
- {
- return BadRequest("Invalid Request");
- }
- return Ok();
- }
- }
目前上面的控制器只实现了基本的逻辑, 下面我们要创建签发 token 的服务, 去完成具体的业务. 第一步我们先创建对应的服务接口, 命名为 IAuthenticateService
- public interface IAuthenticateService
- {
- bool IsAuthenticated(LoginRequestDTO request, out string token);
- }
接下来, 实现接口
- public class TokenAuthenticationService : IAuthenticateService
- {
- public bool IsAuthenticated(LoginRequestDTO request, out string token)
- {
- throw new NotImplementedException();
- }
- }
在 Startup 的 ConfigureServices 方法中注册服务
services.AddScoped<IAuthenticateService, TokenAuthenticationService>();
在 Controller 中注入 IAuthenticateService 服务, 并完善 action
- public class AuthenticationController : ControllerBase
- {
- private readonly IAuthenticateService _authService;
- public AuthenticationController(IAuthenticateService authService)
- {
- this._authService = authService;
- }
- [AllowAnonymous]
- [HttpPost, Route("requestToken")]
- public ActionResult RequestToken([FromBody] LoginRequestDTO request)
- {
- if (!ModelState.IsValid)
- {
- return BadRequest("Invalid Request");
- }
- string token;
- if (_authService.IsAuthenticated(request, out token))
- {
- return Ok(token);
- }
- return BadRequest("Invalid Request");
- }
- }
正常情况, 我们都会根据请求的用户和密码去验证用户是否合法, 需要连接到数据库获取数据进行校验, 我们这里为了方便, 假设任何请求的用户都是合法的.
这里单独加个用户管理的服务, 不在 IAuthenticateService 这个服务里面添加相应逻辑, 主要遵循了职责单一原则. 首先和上面一样, 创建一个服务接口 IUserService
- public interface IUserService
- {
- bool IsValid(LoginRequestDTO req);
- }
实现 IUserService 接口
- public class UserService : IUserService
- {
- // 模拟测试, 默认都是人为验证有效
- public bool IsValid(LoginRequestDTO req)
- {
- return true;
- }
- }
同样注册到容器中
services.AddScoped<IUserService, UserService>();
接下来, 就要完善 TokenAuthenticationService 签发 token 的逻辑, 首先要注入 IUserService 和 TokenManagement, 然后实现具体的业务逻辑, 这个 token 的生成还是使用的 Jwt 的类库提供的 API, 具体不详细描述.
特别注意下 TokenManagement 的注入是已 IOptions 的接口类型注入的, 还记得在 Startpup 中吗? 我们是通过配置项的方式注册 TokenManagement 类型的.
- public class TokenAuthenticationService : IAuthenticateService
- {
- private readonly IUserService _userService;
- private readonly TokenManagement _tokenManagement;
- public TokenAuthenticationService(IUserService userService, IOptions<TokenManagement> tokenManagement)
- {
- _userService = userService;
- _tokenManagement = tokenManagement.Value;
- }
- public bool IsAuthenticated(LoginRequestDTO request, out string token)
- {
- token = string.Empty;
- if (!_userService.IsValid(request))
- return false;
- var claims = new[]
- {
- new Claim(ClaimTypes.Name,request.Username)
- };
- var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_tokenManagement.Secret));
- var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
- var jwtToken = new JwtSecurityToken(_tokenManagement.Issuer, _tokenManagement.Audience, claims, expires: DateTime.Now.AddMinutes(_tokenManagement.AccessExpiration), signingCredentials: credentials);
- token = new JwtSecurityTokenHandler().WriteToken(jwtToken);
- return true;
- }
- }
准备好测试试用的 API, 打上 Authorize 特性, 表明需要授权!
- [ApiController]
- [Route("[controller]")]
- [Authorize]
- public class WeatherForecastController : ControllerBase
- {
- private static readonly string[] Summaries = new[]
- {
- "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
- };
- private readonly ILogger<WeatherForecastController> _logger;
- public WeatherForecastController(ILogger<WeatherForecastController> logger)
- {
- _logger = logger;
- }
- [HttpGet]
- public IEnumerable<WeatherForecast> Get()
- {
- var rng = new Random();
- return Enumerable.Range(1, 5).Select(index => new WeatherForecast
- {
- Date = DateTime.Now.AddDays(index),
- TemperatureC = rng.Next(-20, 55),
- Summary = Summaries[rng.Next(Summaries.Length)]
- })
- .ToArray();
- }
- }
支持我们可以测试验证了, 我们可以使用 postman 来进行 http 请求, 先启动 http 服务, 获取 url, 先测试一个访问需要授权的接口, 但没有携带 token 信息, 返回是 401, 表示未授权
下面我们先通过认证接口, 获取 token, 居然报错, 查询了下, 发现 HS256 算法的秘钥长度最新为 128 位, 转换成字符至少 16 字符, 之前设置的秘钥是 123456, 所以导致异常.
System.ArgumentOutOfRangeException: IDX10603: Decryption failed. Keys tried: 'HS256'. Exceptions caught: '128'. token: '48' (Parameter 'KeySize') at
更新秘钥
- "tokenManagement": {
- "secret": "123456123456123456",
- "issuer": "webapi.cn",
- "audience": "WebApi",
- "accessExpiration": 30,
- "refreshExpiration": 60
- }
重新发起请求, 成功获取 token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJleHAiOjE1Nzg2NDUyMDMsImlzcyI6IndlYmFwaS5jbiIsImF1ZCI6IldlYkFwaSJ9.AehD8WTAnEtklof2OJsvg0U4_o8_SjdxmwUjzAiuI-o
把 token 带到之前请求的 API 中, 重新测试, 成功获取数据
总结
基于 token 的认证方式, 让我们构建分布式 / 松耦合的系统更加容易. 任何地方生成的 token, 只有拥有相同秘钥, 就可以再任何地方进行签名校验.
当然要用好 jwt 认证方式, 还有其他安全细节需要处理, 比如 palyload 中不能存放敏感信息, 使用 https 的加密传输方式等等, 可以根据业务实际需要再进一步安全加固!
同时我们也发现使用 token, 就可以摆脱 cookie 的限制, 所以 JWT 是移动 App 开发的首选!
来源: https://www.cnblogs.com/liuww/p/12177272.html