微服务系列之授权认证(三) JWT
1.JWT简介
官方定义:JWT是JSON Web Token的缩写,JSON Web Token是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,可以将各方之间的信息作为JSON对象安全地传输。该信息可以被验证和信任,因为它是经过加密的。
实际上,Oauth2.0中的access token一般就是jwt格式。
token由三部分组成,通过"."分隔,分别是:
● 标头
● 有效载荷
● 签名
所以JWT表示为:aaaaa.bbbbb.ccccc组成。
1)标头,Header通常由两部分组成:使用的加密算法 "alg" 以及Token的种类 "typ"。如下:
{ "alg": "HS256", "typ": "JWT" }
此JSON被Base64Url编码以形成JWT的第一部分。
2)有效荷载,Payload主要包含了声明Claims,声明实际就是key:value数据,主要包含以下三种声明:
Registered Claims: 注册声明,为IANA JSON Web Token 注册表中预先定义好的声明,这些声明非强制性,但是建议使用,如
-
● iss(issuer):签发人
● exp(expiration time) :过期时间
● sub(subject):主题
● aud(audience):受众
● nbf(not befaore):生效时间
● lat(issued at):签发时间
● jti(jwt id):编号
Public Claims:公共声明,名称可以被任意定义。为了防止重复,任何新的Claim名称都应该被定义在IANA JSON Web Token Registry中或者使用一个包含不易重复命名空间的URI。
Private Claims:私有声明,是在团队中约定使用的自定义Claims,既不属于Registered也不属于Public。
此JSON进行Base64Url编码形成JWT的第二部分
3)Signature,签名是将第一部分(header)、第二部分(payload)、密钥(key)通过指定算法(HMAC、RSA)进行加密生成的。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
生成的签名就是JWT的第三部分。
将这三部分拼接在一起并使用"."分隔后形成的字符串就是Token。如:
可以使用jwt.io的Debugger解码,验证或生成JWT。
注意:虽然签名过后的Token可以防止篡改,但是Token的信息是公开的,任何人都可以读取,所以尽量不要在有效载荷或标头传递敏感信息(如密码)。
2.使用场景
1)目前来说,几乎所有之前使用cookie,session的地方,都可以换成jwt。
2)标准的对C或者对B的微服务系统,这个和之前讲的Oauth2.0协议最大的不同之处,jwt只是一个传输令牌,oauth2.0是一个授权协议,jwt可以理解为是oauth2.0的一部分。。。
3)信息安全交换,由于签名防篡改机制,可以验证其发行人和收件人。
3.Jwt的优势
1)无需存储,无服务器压力,轻量级使用,简单上手。
2)无视跨域,可多端使用,不像cookie、session依赖浏览器。
4.前端滑动登录状态管理方案
jwt token的过期时间如果短了,很影响前端用户操作体验,所以一般情况都是中长期的,以前的session管理登录状态,是滑动的,而现在jwt是无状态的,那么怎么才能做到滑动管理呢,具体细节分析请看这篇文章.NET Core WebAPI 认证授权之JWT(四):JWT续期问题 - 不落阁 (leo96.com) ,虽然没有解决,但是问题抛出的很细致,,我来说说我们现在正在使用的方案。
其实也很简单,用户请求后端服务数据时,作为有效动作,并且触发2小时的计时器,没到2小时的定时器清除并且重新启动,如果2小时内用户没有任何动作,认为是可以退出登录。
5..net core使用jwt
nuget安装System.IdentityModel.Tokens.Jwt
新建一个service写一个生成token的方法
public class TokenService : ITokenService { private IConfiguration configuration; public TokenService(IConfiguration configuration) { this.configuration = configuration; } public async Task<string> MakeJwtToken(long userId) { var claims = new[] { // 角色需要在这里填写 new Claim(ClaimTypes.Role, "Admin"), // 多个角色可以重复写,生成的 JWT 会是一个数组 new Claim(ClaimTypes.Role, "SuperAdmin"), //其他声明 new Claim("uid", userId.ToString()) }; //私钥,验证方也需要使用这个进行验证。 var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Auth:SecurityKey"])); //加密方式 var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: "AESCR",//发行人 audience: "AESCR",//接收人 claims: claims, expires: DateTime.Now.AddMonths(30),//过期时间 signingCredentials: creds); var res = new JwtSecurityTokenHandler().WriteToken(token); return await Task.FromResult<string>(res); } }
启动类认证注入
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true,//是否验证发行人 ValidateAudience = true,//是否验证收件人 ValidateLifetime = true,//是否验证失效时间 ValidateIssuerSigningKey = true,//是否验证SecurityKey ValidAudience = "AESCR",//Audience ValidIssuer = "AESCR",//Issuer,这两项和后面签发jwt的设置一致 ClockSkew = TimeSpan.Zero, // // 默认允许 300s 的时间偏移量,设置为0 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Auth:SecurityKey"]))//与创建者密钥一致 }; });
app.UseAuthentication();//添加认证中间件
控制器代码
////// 编辑用户信息 /// /// /// /// [HttpPost("{userId}/edit/userInfo")] [ProducesResponseType(typeof(Users), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)] [Authorize]//token认证标签,如果需要角色认证,[Authorize(Roles ="admin")] public async Task EditUserInfo([FromRoute] long userId, [FromBody] EditUserInfoCommand command) { if (!this.CheckUser(command.UserId)) return BadRequest("您没有权限访问"); var result = await userService.EditUserInfo(command); if (!result.IsSuccess) return BadRequest(result.FailureReason); return Ok(result.GetData()); }
以上代码就完事了,简单吧,在请求的时候,带上token就可以了。
这里有一个细节问题,由于我们这里用户ID,都是在jwt的Payload中,那么是否还需要请求接口的时候在参数中传输呢?个人理解是这样:
1.首先先看下token验证过后,怎么取claims的声明
public static class ControllerExtensions { public static long GetUserId(this ControllerBase controllerBase) { var claim = controllerBase.User.Claims.Where(p => p.Type == "uid").FirstOrDefault(); if (claim == null) return 0; long res = 0; long.TryParse(claim.Value, out res); return res; } public static bool CheckUser(this ControllerBase controllerBase, long userId) { return controllerBase.GetUserId() == userId; } }
我可以从payload中获取到创建token时带进去的用户id---uid,回到问题,我认为即使可以拿到用户ID,也需要从接口参数中传递过来,因为安全认证是一个切面拦截,我们的服务如果去掉切面,要保障正常运行,我们只需要在加一个传递参数中的uid和声明里的uid是否一致,来判断是否是当前用户的操作。