回顾:认证方案之初步认识JWTjavascript
在现代Web应用程序中,即分为前端与后端两大部分。当前先后端的趋势日益剧增,前端设备(手机、平板、电脑、及其余设备)层出不穷。所以,为了方便知足前端设备与后端进行通信,就必须有一种统一的机制。因此致使API架构的流行。而RESTful API这个API设计思想理论也就成为目前互联网应用程序比较欢迎的一套方式。html
这种API架构思想的引入,所以,咱们就须要考虑用一种标准的,通用的,无状态的,与语言无关的身份认证方式来实现API接口的认证。前端
HTTP提供了一套标准的身份验证框架:服务端能够用来针对客户端的请求发送质询(challenge),客户端根据质询提供应答身份验证凭证。java
质询与应答的工做流程以下:服务端向客户端返回401(Unauthorized,未受权)状态码,并在WWW-Authenticate头中添加如何进行验证的信息,其中至少包含有一种质询方式。而后客户端能够在请求中添加Authorization头进行验证,其Value为身份验证的凭证信息。git
在本文中,将要介绍的是以Jwt Bearer方式进行认证。
github
本文要介绍的Bearer验证也属于HTTP协议标准验证,它随着OAuth协议而开始流行,详细定义见: RFC 6570。web
+--------+ +---------------+ | |--(A)- Authorization Request ->| Resource | | | | Owner | | |<-(B)-- Authorization Grant ---| | | | +---------------+ | | | | +---------------+ | |--(C)-- Authorization Grant -->| Authorization | | Client | | Server | | |<-(D)----- Access Token -------| | | | +---------------+ | | | | +---------------+ | |--(E)----- Access Token ------>| Resource | | | | Server | | |<-(F)--- Protected Resource ---| | +--------+ +---------------+
A security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can. Using a bearer token does not require a bearer to prove possession of cryptographic key material (proof-of-possession).json
所以Bearer认证的核心是Token,Bearer验证中的凭证称为BEARER_TOKEN
,或者是access_token
,它的颁发和验证彻底由咱们本身的应用程序来控制,而不依赖于系统和Web服务器,Bearer验证的标准请求方式以下:c#
Authorization: Bearer [BEARER_TOKEN]
那么使用Bearer验证有什么好处呢?后端
302
到登陆页面,这在非浏览器状况下很难处理,而Bearer验证则返回的是标准的401 challenge
。上面介绍的Bearer认证,其核心即是BEARER_TOKEN,那么,如何确保Token的安全是重中之重。一种是经过HTTPS的方式,另外一种是经过对Token进行加密编码签名,而最流行的Token编码签名方式即是:JSON WEB TOKEN。
Json web token (Jwt), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。JWT的声明通常被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也能够增长一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT是由.
分割的以下三部分组成:
Header.Payload.Signature
还记得以前说个的一篇认证方案之初步认识JWT吗?没有的,能够看看,对JWT的特色和基本原理介绍,能够进一步的了解。
学习了以前的文章后,咱们能够发现使用JWT的好处在于通用性、紧凑性和可拓展性。
在这里,咱们用微软给咱们提供的JwtBearer认证方式,实现认证服务注册 。
引入nuget包:Microsoft.AspNetCore.Authentication.JwtBearer
注册服务,将服务添加到容器中,
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); var Issurer = "JWTBearer.Auth"; //发行人 var Audience = "api.auth"; //受众人 var secretCredentials = "q2xiARx$4x3TKqBJ"; //密钥 //配置认证服务 services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(o=>{ o.TokenValidationParameters = new TokenValidationParameters { //是否验证发行人 ValidateIssuer = true, ValidIssuer = Issurer,//发行人 //是否验证受众人 ValidateAudience = true, ValidAudience = Audience,//受众人 //是否验证密钥 ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretCredentials)), ValidateLifetime = true, //验证生命周期 RequireExpirationTime = true, //过时时间 }; }); }
注意说明:
一. TokenValidationParameters的参数默认值: 1. ValidateAudience = true, ----- 若是设置为false,则不验证Audience受众人 2. ValidateIssuer = true , ----- 若是设置为false,则不验证Issuer发布人,但建议不建议这样设置 3. ValidateIssuerSigningKey = false, 4. ValidateLifetime = true, ----- 是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比 5. RequireExpirationTime = true, ----- 是否要求Token的Claims中必须包含Expires 6. ClockSkew = TimeSpan.FromSeconds(300), ----- 容许服务器时间偏移量300秒,即咱们配置的过时时间加上这个容许偏移的时间值,才是真正过时的时间(过时时间 +偏移值)你也能够设置为0,ClockSkew = TimeSpan.Zero
调用方法,配置Http请求管道:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseRouting(); //1.先开启认证 app.UseAuthentication(); //2.再开启受权 app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
在JwtBearerOptions
的配置中,一般IssuerSigningKey(签名秘钥)
, ValidIssuer(Token颁发机构)
, ValidAudience(颁发给谁)
三个参数是必须的,后二者用于与TokenClaims中的Issuer
和Audience
进行对比,不一致则验证失败。
建立一个须要受权保护的资源控制器,这里咱们用创建API生成项目自带的控制器,WeatherForecastController.cs, 在控制器上使用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(); } }
由于微软为咱们内置了JwtBearer验证,可是没有提供Token的发放,因此这里咱们要实现生成Token的方法
引入Nugets包:System.IdentityModel.Tokens.Jwt
这里咱们根据IdentityModel.Tokens.Jwt文档给咱们提供的帮助类,提供了方法WriteToken建立Token,根据参数SecurityToken,能够实例化,JwtSecurityToken,指定可选参数的类。
/// <summary> /// Initializes a new instance of the <see cref="JwtSecurityToken"/> class specifying optional parameters. /// </summary> /// <param name="issuer">If this value is not null, a { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in 'claims' if present.</param> /// <param name="audience">If this value is not null, a { aud, 'audience' } claim will be added, appending to any 'aud' claims in 'claims' if present.</param> /// <param name="claims">If this value is not null then for each <see cref="Claim"/> a { 'Claim.Type', 'Claim.Value' } is added. If duplicate claims are found then a { 'Claim.Type', List<object> } will be created to contain the duplicate values.</param> /// <param name="expires">If expires.HasValue a { exp, 'value' } claim is added, overwriting any 'exp' claim in 'claims' if present.</param> /// <param name="notBefore">If notbefore.HasValue a { nbf, 'value' } claim is added, overwriting any 'nbf' claim in 'claims' if present.</param> /// <param name="signingCredentials">The <see cref="SigningCredentials"/> that will be used to sign the <see cref="JwtSecurityToken"/>. See <see cref="JwtHeader(SigningCredentials)"/> for details pertaining to the Header Parameter(s).</param> /// <exception cref="ArgumentException">If 'expires' <= 'notbefore'.</exception> public JwtSecurityToken(string issuer = null, string audience = null, IEnumerable<Claim> claims = null, DateTime? notBefore = null, DateTime? expires = null, SigningCredentials signingCredentials = null) { if (expires.HasValue && notBefore.HasValue) { if (notBefore >= expires) throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX12401, expires.Value, notBefore.Value))); } Payload = new JwtPayload(issuer, audience, claims, notBefore, expires); Header = new JwtHeader(signingCredentials); RawSignature = string.Empty; }
这样,咱们能够根据参数指定内容:
1. string iss = "JWTBearer.Auth"; // 定义发行人 2. string aud = "api.auth"; //定义受众人audience 3. IEnumerable<Claim> claims = new Claim[] { new Claim(JwtClaimTypes.Id,"1"), new Claim(JwtClaimTypes.Name,"i3yuan"), };//定义许多种的声明Claim,信息存储部分,Claims的实体通常包含用户和一些元数据 4. var nbf = DateTime.UtcNow; //notBefore 生效时间 5. var Exp = DateTime.UtcNow.AddSeconds(1000); //expires 过时时间 6. string sign = "q2xiARx$4x3TKqBJ"; //SecurityKey 的长度必须 大于等于 16个字符 var secret = Encoding.UTF8.GetBytes(sign); var key = new SymmetricSecurityKey(secret); var signcreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
好了,经过以上填充参数内容,进行传参赋值获得,完整代码以下:
新增AuthController.cs控制器:
[HttpGet] public IActionResult GetToken() { try { //定义发行人issuer string iss = "JWTBearer.Auth"; //定义受众人audience string aud = "api.auth"; //定义许多种的声明Claim,信息存储部分,Claims的实体通常包含用户和一些元数据 IEnumerable<Claim> claims = new Claim[] { new Claim(JwtClaimTypes.Id,"1"), new Claim(JwtClaimTypes.Name,"i3yuan"), }; //notBefore 生效时间 // long nbf =new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds(); var nbf = DateTime.UtcNow; //expires //过时时间 // long Exp = new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds(); var Exp = DateTime.UtcNow.AddSeconds(1000); //signingCredentials 签名凭证 string sign = "q2xiARx$4x3TKqBJ"; //SecurityKey 的长度必须 大于等于 16个字符 var secret = Encoding.UTF8.GetBytes(sign); var key = new SymmetricSecurityKey(secret); var signcreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var jwt = new JwtSecurityToken(issuer: iss, audience: aud, claims:claims,notBefore:nbf,expires:Exp, signingCredentials: signcreds); var JwtHander = new JwtSecurityTokenHandler(); var token = JwtHander.WriteToken(jwt); return Ok(new { access_token = token, token_type = "Bearer", }); } catch (Exception ex) { throw; } }
注意: 1.SecurityKey 的长度必须 大于等于 16个字符,不然生成会报错。(可经过在线随机生成密钥)
访问获取Token方法,获取获得access_token:
再访问,受权资源接口,能够发现,再没有添加请求头token值的状况下,返回了401没有权限。
此次,在请求头经过Authorization加上以前获取的token值后,再次进行访问,发现已经能够获取访问资源控制器,并返回对应的数据。
在HTTP标准验证方案中,咱们比较熟悉的是"Basic"和"Digest",前者将用户名密码使用BASE64编码后做为验证凭证,后者是Basic的升级版,更加安全,由于Basic是明文传输密码信息,而Digest是加密后传输。
Basic认证是一种较为简单的HTTP认证方式,客户端经过明文(Base64编码格式)传输用户名和密码到服务端进行认证,一般须要配合HTTPS来保证信息传输的安全。
客户端请求须要带Authorization请求头,值为“Basic xxx”,xxx为“用户名:密码”进行Base64编码后生成的值。 若客户端是浏览器,则浏览器会提供一个输入用户名和密码的对话框,用户输入用户名和密码后,浏览器会保存用户名和密码,用于构造Authorization值。当关闭浏览器后,用户名和密码将再也不保存。
凭证为“YWxhzGRpbjpvcGVuc2VzYWl1”,是经过将“用户名:密码”格式的字符串通过的Base64编码获得的。而Base64不属于加密范畴,能够被逆向解码,等同于明文,所以Basic传输认证信息是不安全的。
Basic基础认证图示:
缺陷汇总
1.用户名和密码明文(Base64)传输,须要配合HTTPS来保证信息传输的安全。
2.即便密码被强加密,第三方仍可经过加密后的用户名和密码进行重放攻击。
3.没有提供任何针对代理和中间节点的防御措施。
4.假冒服务器很容易骗过认证,诱导用户输入用户名和密码。
Digest认证是为了修复基本认证协议的严重缺陷而设计的,秉承“毫不经过明文在网络发送密码”的原则,经过“密码摘要”进行认证,大大提升了安全性。
Digest认证步骤以下:
第一步:客户端访问Http资源服务器。因为须要Digest认证,服务器返回了两个重要字段nonce(随机数)和realm。
第二步:客户端构造Authorization请求头,值包含username、realm、nouce、uri和response的字段信息。其中,realm和nouce就是第一步返回的值。nouce只能被服务端使用一次。uri(digest-uri)即Request-URI的值,但考虑到经代理转发后Request-URI的值可能被修改、所以实现会复制一份副本保存在uri内。response也可叫作Request-digest,存放通过MD5运算后的密码字符串,造成响应码。
第三步:服务器验证包含Authorization值的请求,若验证经过则可访问资源。
Digest认证能够防止密码泄露和请求重放,但没办法防假冒。因此安全级别较低。
Digest和Basic认证同样,每次都会发送Authorization请求头,也就至关于从新构造此值。因此二者易用性都较差。
Digest认证图示:
notBefore
和expires
来验证),当access_token
过时后,能够在用户无感知的状况下,使用refresh_token
从新获取access_token
,但这就不属于Bearer认证的范畴了,可是咱们能够经过另外一种方式经过IdentityServer的方式来实现,在后续中会对IdentityServer进行详细讲解。