HttpClient调用受JWT保护的Api
准备工作:
一个Asp.Net Core Api 程序,程序的功能大概有两个:模拟验证用户登录,权限认证模块给用户颁发Jwt,用户带token来调用Api资源。
首先简单介绍一下JWT的数据结构,JWT由头部、载荷与签名这三部分组成,中间以「.」分隔。
头部以 JSON 格式表示,用于指明令牌类型和加密算法。形式如下,表示使用 JWT 格式,加密算法采用 HS256。
{ "alg": "HS256", "typ": "JWT" }
载荷用来存储服务器需要的数据,比如用户信息,要注意的是重要的机密信息最好不要放到这里,比如密码。
{ "name": "zhouxieyi", "introduce": "a test token" }
另外,JWT 还规定了 7 个字段供开发者选用。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
签名使用HMACSHA256
算法计算得出,这个方法有两个参数,前一个参数是 (base64 编码的头部 + base64 编码的载荷)用点号相连,后一个参数是自定义的字符串密钥,密钥不要暴露在客户端。
我们在appsettings.json中存放了我们自定义的token信息如下:
"tokenManagement": { "issuer": "webapi.cn", "name": "zhouxieyi", "introduce": "a test token", "audience": "WebApi", "secret": "qwertyuiopasdfghjklzxcvbnm", "accessExpiration": 30, "refreshExpiration": 60 }
下一步配置StartUp.cs,将身份认证中间件加入引用,并引入NuGet:Microsoft.AspNetCore.Authentication.JwtBearer,在Configure中配置好服务。
创建用户User,UserService来模拟用户登录,这里我们假使所有的用户都合法。用户通过校验后,利用AuthenticationService颁发token,根据这两步逻辑,我们编写接口和控制器来完成。
代码如下:
using System.Text; using authapi.Models; using authapi.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; namespace authapi { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.Configure<TokenManagement>(Configuration.GetSection("tokenManagement")); var token = Configuration.GetSection("tokenManagement").Get<TokenManagement>(); services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.SaveToken = true; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(token.Secret)), ValidIssuer = token.Issuer, ValidAudience = token.Audience, ValidateIssuer = false, ValidateAudience = false }; }); services.AddScoped<IUserService, UserService>(); services.AddScoped<IAuthenticationService, TokenAuthenticateService>(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); } } }
public interface IAuthenticationService { bool IsAuthenticated(User user, out string token); } public class TokenAuthenticateService : IAuthenticationService { private readonly IUserService _userService; private readonly TokenManagement _tokenManagement; public TokenAuthenticateService(IUserService userService, IOptions<TokenManagement> tokenManagement) { _userService = userService; _tokenManagement = tokenManagement.Value; } public bool IsAuthenticated(User user, out string token) { token = string.Empty; if (!_userService.Validate(user)) return false; var claims = new[] { new Claim(ClaimTypes.Name,user.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; } }
public interface IUserService { bool Validate(User user); } public class UserService : IUserService { public bool Validate(User user) { return true; } }
using authapi.Models; using authapi.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace authapi.Controllers { [ApiController] [Route("api/[controller]")] public class AuthenticationController : ControllerBase { private readonly IAuthenticationService _authenticationService; public AuthenticationController(IAuthenticationService authenticationService) { _authenticationService = authenticationService; } [AllowAnonymous] [HttpPost, Route("requestToken")] public ActionResult RequestToken([FromBody] User user) { if (!ModelState.IsValid) { return BadRequest("Model Error"); } if (_authenticationService.IsAuthenticated(user, out var token)) { return Ok(token); } return BadRequest("Invalid Request"); } } }
using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Authorization; namespace authapi.Controllers { [ApiController] [Route("api/[controller]")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; [Authorize] [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(); } } }
public class TokenManagement { [JsonPropertyName("secret")] public string Secret { get; set; } [JsonPropertyName("issuer")] public string Issuer { get; set; } [JsonPropertyName("audience")] public string Audience { get; set; } [JsonPropertyName("accessExpiration")] public int AccessExpiration { get; set; } [JsonPropertyName("refreshExpiration")] public int RefreshExpiration { get; set; } } public class User { [Required] [JsonPropertyName("username")] public string UserName { get; set; } [Required] [JsonPropertyName("password")] public string PassWord { get; set; } } public class WeatherForecast { public DateTime Date { get; set; } public int TemperatureC { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public string Summary { get; set; } }
运行程序,使用PostMan测试:
先利用用户获取Token
设置Token类型,输入我们刚刚获取的Token,访问Api成功获取数据。
HttpClient:
创建一个Console程序,创建HttpClient,HttpClient类实例充当发送 HTTP 请求的会话。每个 HttpClient 实例都使用其自己的连接池,并从其他实例所执行的请求隔离其请求 HttpClient。
利用.NET Json处理拓展包:using System.Net.Http.Json,可以很方便的带Json对象发送请求。具体逻辑流程是使用client带用户信息请求token,获取成功后将token绑定到Header中:Authorization : Bearer + token即可,在请求Api即可获取资源。
using System; using System.Collections.Generic; using System.Diagnostics; using System.Net.Http; using System.Net.Http.Json; using System.Threading.Tasks; namespace any { public class Program { static async Task Main(string[] args) { var client = new HttpClient(); var uri = new Uri("https://localhost:5001"); var user = new User { UserName = "client", PassWord = "123321" }; //利用SendAsync发送请求,在Request中设置Content或Header传入。 //var postRequest = new HttpRequestMessage( // HttpMethod.Post, // uri.AbsoluteUri + "api/Authentication/RequestToken") //{ // Content = JsonContent.Create(user), //}; //var response = await client.SendAsync(postRequest); //使用PostAsJsonAsync传递json var response = await client.PostAsJsonAsync(uri.AbsoluteUri + "api/Authentication/RequestToken", user); var token = await response.Content.ReadAsStringAsync(); Console.WriteLine($"token: {token}"); //设置Header client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}"); var result = await client.GetStringAsync(uri.AbsoluteUri + "api/WeatherForecast"); Console.WriteLine($"string result: {result}"); var jsonResponse = await client.GetAsync(uri.AbsoluteUri + "api/WeatherForecast"); var jsonObjs = await jsonResponse.Content.ReadFromJsonAsync<List<WeatherForecast>>(); if (jsonObjs != null) { foreach (var weatherForecast in jsonObjs) { Console.WriteLine($"Date is {weatherForecast.Date}, is {weatherForecast.Summary} day"); } } } } }
result: