在.NET Core中使用Jwt对API进行认证
在.NET Core中想给API进行安全认证,最简单的无非就是Jwt,悠然记得一年前写的Jwt Demo,现在拿回来改成.NET Core的,但是在编码上的改变并不大,因为Jwt已经足够强大了。在项目中分为 DotNetCore_Jwt_Server 以及 DotNetCore_Jwt_Client ,从名字就可以看出来是啥意思,博客园高手云集,我就不多诉说,这篇博客就当是一篇记录。
当然本案例是Server&Client双项目,如果你要合成自己发证的形式,那你就自己改下代码玩。
在Server层都会有分发Token的服务,在其中做了用户密码判断,随后根据 Claim 生成 jwtToken 的操作。
其生成Token的服务代码:
namespace DotNetCore_Jwt_Server.Services { public interface ITokenService { string GetToken(User user); } public class TokenService : ITokenService { private readonly JwtSetting _jwtSetting; public TokenService(IOptions<JwtSetting> option) { _jwtSetting = option.Value; } public string GetToken(User user) { //创建用户身份标识,可按需要添加更多信息 var claims = new Claim[] { new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim("id", user.Id.ToString(), ClaimValueTypes.Integer32), new Claim("name", user.Name), new Claim("admin", user.IsAdmin.ToString(),ClaimValueTypes.Boolean) }; //创建令牌 var token = new JwtSecurityToken( issuer: _jwtSetting.Issuer, audience: _jwtSetting.Audience, signingCredentials: _jwtSetting.Credentials, claims: claims, notBefore: DateTime.Now, expires: DateTime.Now.AddSeconds(_jwtSetting.ExpireSeconds) ); string jwtToken = new JwtSecurityTokenHandler().WriteToken(token); return jwtToken; } } }
在获取Token中我们依赖注入服务到控制器中,随后依赖它进行认证并且分发Token,
public class ValuesController : ControllerBase { private readonly IUserService _userService; private readonly ITokenService _tokenService; public ValuesController(IUserService userService, ITokenService tokenService) { _userService = userService; _tokenService = tokenService; } [HttpGet] public async Task<string> Get() { await Task.CompletedTask; return "Welcome the Json Web Token Solucation!"; } [HttpGet("getToken")] public async Task<string> GetTokenAsync(string name, string password) { var user = await _userService.LoginAsync(name, password); if (user == null) return "Login Failed"; var token = _tokenService.GetToken(user); var response = new { Status = true, Token = token, Type = "Bearer" }; return JsonConvert.SerializeObject(response); } }
随后,我们又在项目配置文件中填写了几个字段,相关备注已注释,但值得说明的是有位朋友问我,服务器端生成的Token不需要保存吗,比如Redis或者是Session,其实Jwt Token是无状态的,他们之间的对比第一个是你的token解密出来的信息正确与否,第二部则是看看你 SecurityKey 是否正确,就这样他们的认证才会得出结果。
"JwtSetting": { "SecurityKey": "d0ecd23c-dfdb-4005-a2ea-0fea210c858a", // 密钥 "Issuer": "jwtIssuertest", // 颁发者 "Audience": "jwtAudiencetest", // 接收者 "ExpireSeconds": 20000 // 过期时间 }
随后我们需要DI两个接口以及初始化设置相关字段。
public void ConfigureServices(IServiceCollection services) { services.Configure<JwtSetting>(Configuration.GetSection("JwtSetting")); services.AddScoped<IUserService, UserService>(); services.AddScoped<ITokenService, TokenService>(); services.AddControllers(); }
在Client中,我一般会创建一个中间件用于接受认证结果,AspNetCore Jwt 源码中给我们提供了中间件,我们在进一步扩展,其源码定义如下:
/// <summary> /// Extension methods to expose Authentication on HttpContext. /// </summary> public static class AuthenticationHttpContextExtensions {/// <summary> /// Extension method for authenticate. /// </summary> /// <param name="context">The <see cref="HttpContext"/> context.</param> /// <param name="scheme">The name of the authentication scheme.</param> /// <returns>The <see cref="AuthenticateResult"/>.</returns> public static Task<AuthenticateResult> AuthenticateAsync(this HttpContext context, string scheme) => context.RequestServices.GetRequiredService<IAuthenticationService>().AuthenticateAsync(context, scheme);
}
其该扩展会返回一个 AuthenticateResult 类型的结果,其定义部分是这样的,我们就可以将计就计,给他来个连环套。
连环套直接接受 httpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme) 返回回来的值,随后进行判断返回相应的Http响应码。
public class AuthMiddleware { private readonly RequestDelegate _next; public AuthMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext httpContext) { var result = await httpContext.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme); if (!result.Succeeded) { httpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; await httpContext.Response.WriteAsync("Authorize error"); } else { httpContext.User = result.Principal; await _next.Invoke(httpContext); } } }
当然你也得在Client中添加认证的一些设置,它和Server端的 IssuerSigningKey 一定要对应,否则认证失败。
public void ConfigureServices(IServiceCollection services) { services.AddHttpContextAccessor(); services.AddScoped<IIdentityService, IdentityService>(); var jwtSetting = new JwtSetting(); Configuration.Bind("JwtSetting", jwtSetting); services.AddCors(options => { options.AddPolicy("any", builder => { builder.AllowAnyOrigin() //允许任何来源的主机访问 .AllowAnyMethod() .AllowAnyHeader(); }); }); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = jwtSetting.Issuer, ValidAudience = jwtSetting.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSetting.SecurityKey)), 默认 300s ClockSkew = TimeSpan.Zero }; }); services.AddControllers(); }
随后,你就可以编写带需认证才可以访问的API了,如果认证失败则会返回401的错误响应。
[Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { private readonly IIdentityService _identityService; public ValuesController(IIdentityService identityService) { _identityService = identityService; } [HttpGet] [Authorize] public async Task<string> Get() { await Task.CompletedTask; return $"{_identityService.GetUserId()}:{_identityService.GetUserName()}"; }
值得一提的是,我们可以根据 IHttpContextAccessor 以来注入到我们的Service或者Api中,它是一个当前请求的认证信息上下文,这将有利于你获取用户信息去做该做的事情。
public class IdentityService : IIdentityService { private readonly IHttpContextAccessor _context; public IdentityService(IHttpContextAccessor context) { _context = context; } public int GetUserId() { var nameId = _context.HttpContext.User.FindFirst("id"); return nameId != null ? Convert.ToInt32(nameId.Value) : 0; } public string GetUserName() { return _context.HttpContext.User.FindFirst("name")?.Value; } }
在源码中该类的定义如下,实际上我们可以看到只不过是判断了当前的http上下文吧,所以我们得出,如果认证失败,上下本信息也是空的。
public class HttpContextAccessor : IHttpContextAccessor { private static AsyncLocal<HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextHolder>(); public HttpContext HttpContext { get { return _httpContextCurrent.Value?.Context; } set { var holder = _httpContextCurrent.Value; if (holder != null) { // Clear current HttpContext trapped in the AsyncLocals, as its done. holder.Context = null; } if (value != null) { // Use an object indirection to hold the HttpContext in the AsyncLocal, // so it can be cleared in all ExecutionContexts when its cleared. _httpContextCurrent.Value = new HttpContextHolder { Context = value }; } } } private class HttpContextHolder { public HttpContext Context; } }
如果要通过js来测试代码,您可以添加请求头来进行认证,beforeSend是在请求之前的事件。
beforeSend : function(request) { request.setRequestHeader("Authorization", sessionStorage.getItem("Authorization")); }
好了,今天就说到这,代码地址在https://github.com/zaranetCore/DotNetCore_Jwt 中。