赞
踩
JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案之一,今天我们一起来揭开它神秘的面纱!
一、故事起源
说起 JWT,我们先来谈一谈基于传统session认证的方案以及瓶颈。
传统session交互流程,如下图:
当浏览器向服务器发送登录请求时,验证通过之后,会将用户信息存入seesion中,然后服务器会生成一个sessionId放入cookie中,随后返回给浏览器。
当浏览器再次发送请求时,会在请求头部的cookie中放入sessionId,将请求数据一并发送给服务器。
服务器就可以再次从seesion获取用户信息,整个流程完毕!
通常在服务端会设置seesion的时长,例如 30 分钟没有活动,会将已经存放的用户信息从seesion中移除。
session.setMaxInactiveInterval(30 * 60);//30分钟没活动,自动移除
同时,在服务端也可以通过seesion来判断当前用户是否已经登录,如果为空表示没有登录,直接跳转到登录页面;如果不为空,可以从session中获取用户信息即可进行后续操作。
在单体应用中,这样的交互方式,是没啥问题的。
但是,假如应用服务器的请求量变得很大,而单台服务器能支撑的请求量是有限的,这个时候就容易出现请求变慢或者OOM。
解决的办法,要么给单台服务器增加配置,要么增加新的服务器,通过负载均衡来满足业务的需求。
如果是给单台服务器增加配置,请求量继续变大,依然无法支撑业务处理。
显而易见,增加新的服务器,可以实现无限的水平扩展。
但是增加新的服务器之后,不同的服务器之间的sessionId是不一样的,可能在A服务器上已经登录成功了,能从服务器的session中获取用户信息,但是在B服务器上却查不到session信息,此时肯定无比的尴尬,只好退出来继续登录,结果A服务器中的session因为超时失效,登录之后又被强制退出来要求重新登录,想想都挺尴尬~~
面对这种情况,几位大佬于是合起来商议,想出了一个token方案。
将各个应用程序与内存数据库redis相连,对登录成功的用户信息进行一定的算法加密,生成的ID被称为token,将token还有用户的信息存入redis;等用户再次发起请求的时候,将token还有请求数据一并发送给服务器,服务端验证token是否存在redis中,如果存在,表示验证通过,如果不存在,告诉浏览器跳转到登录页面,流程结束。
token方案保证了服务的无状态,所有的信息都是存在分布式缓存中。基于分布式存储,这样可以水平扩展来支持高并发。
二、JWT是什么
上文中,我们谈到的session还有token的方案,在集群环境下,他们都是靠第三方缓存数据库redis来实现数据的共享。
那有没有一种方案,不用缓存数据库redis来实现用户信息的共享,以达到一次登录,处处可见的效果呢?
答案肯定是有的,就是我们今天要介绍的JWT!
JWT是 JSON Web Token 的缩写,是一个开放标准(RFC 7519),[RFC 7519(https://tools.ietf.org/html/rfc7519)。特别适用于分布式站点的单点登录(SSO)场景。它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
这里我们通过一张图了解它的工作流程。
从上图中我们可以看出它是基于Token的身份认证,具体流程:客户端携带用户名和密码请求访问 - 服务器校验用户凭据 - 应用提供一个token给客户端 - 客户端存储token,并且在随后的每一次请求中都带着它 -服务器校验token并返回数据。
1.头部 Header
2.载荷 Payload
3.签名 Signature
Jwt Token包含了使用.分隔的三部分
{Header 头部}.{Payload 负载}.{Signature 签名}
1.头部 Header
Header 一般由两个部分组成:
alg
typ
alg是是所使用的hash算法,如:HMAC SHA256或RSA,typ是Token的类型,在这里就是:JWT。
{
“alg”: “HS256”,
“typ”: “JWT”
}
然后使用Base64Url编码成第一部分
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..
2.载荷 Payload
这一部分是JWT主要的信息存储部分,其中包含了许多种的声明(claims)。
Claims的实体一般包含用户和一些元数据,这些claims分成三种类型:
reserved claims:预定义的 一些声明,并不是强制的但是推荐,它们包括 iss (issuer), exp (expiration time), sub (subject),aud(audience) 等(这里都使用三个字母的原因是保证 JWT 的紧凑)。
public claims: 公有声明,这个部分可以随便定义,但是要注意和 IANA JSON Web Token 冲突。
private claims: 私有声明,这个部分是共享被认定信息中自定义部分。
一个简单的Pyload可以是这样子的:
{
“user_name”: “admin”,
“scope”: [
“read”,“write”,“del”
],
“organization”: “admin”,
“exp”: 1531975621,
“authorities”: [
“ADMIN”
],
“jti”: “23408d38-8cdc-4460-beac-24c76dc7629a”,
“client_id”: “webapp”
}
这部分同样使用Base64Url编码成第二部分
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
3.签名 Signature
Signature是用来验证发送者的JWT的同时也能确保在期间不被篡改。
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名。
使用Base64编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload),
secret)
通过下图,我们可以直观的看到JWT的组成。
它本质上是一个独立的身份验证令牌,可以包含用户标识、用户角色和权限等信息,以及您可以存储任何其他信息(自包含)。任何人都可以轻松读取和解析,并使用密钥来验证真实性。
客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header Authorization字段中。
Authorization: Bearer JWT_TOKEN
当跨域时,也可以将JWT被放置于POST请求的数据主体中。
通用:因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
紧凑:JWT的构成非常简单,字节占用很小,可以通过 GET、POST 等放在 HTTP 的 header 中,非常便于传输。
扩展:JWT是自我包涵的,包含了必要的所有信息,不需要在服务端保存会话信息, 非常易于应用的扩展。
一.Asp.net MVC/WebApi + Cookie
1、 在Models中创建AuthInfo.cs、LoginRequest.cs、TokenInfo.cs三个类
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace apiToken.Models { /// <summary> /// 身份验证信息 模拟JWT的payload /// </summary> public class AuthInfo { /// <summary> /// 用户名 /// </summary> public string UserName { get; set; } /// <summary> /// 角色 /// </summary> public List<string> Roles { get; set; } /// <summary> /// 是否管理员 /// </summary> public bool IsAdmin { get; set; } /// <summary> /// 口令过期时间 /// </summary> public DateTime? ExpiryDateTime { get; set; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace apiToken.Models { /// <summary> /// 登录用户信息 /// </summary> public class LoginRequest { /// <summary> /// 用户名 /// </summary> public string UserName { get; set; } /// <summary> /// 密码 /// </summary> public string Password { get; set; } } } using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace apiToken.Models { /// <summary> /// 生成的口令信息 /// </summary> public class TokenInfo { /// <summary> /// 是否成功 /// </summary> public bool Success { get; set; } /// <summary> /// 令牌 /// </summary> public string Token { get; set; } /// <summary> /// 错误信息 /// </summary> public string Message { get; set; } } }
2、在Controllers文件夹下创建TokenController.cs文件,生成口令
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using JWT; using JWT.Algorithms; using JWT.Serializers; using apiToken.Models; using System.Text; namespace apiToken.Controllers { [RoutePrefix("api/Token")] public class TokenController : ApiController { /// <summary> /// 登录 /// </summary> /// <param name="loginRequest"></param> /// <returns></returns> [HttpPost] [Route("Login")] public TokenInfo Login([FromBody] LoginRequest loginRequest) { TokenInfo tokenInfo = new TokenInfo();//需要返回的口令信息 if (loginRequest != null) { string userName = loginRequest.UserName; string passWord = loginRequest.Password; bool isAdmin = (userName == "admin")?true:false; //模拟数据库数据,真正的数据应该从数据库读取 //身份验证信息 AuthInfo authInfo = new AuthInfo { UserName=userName,Roles=new List<string> {"admin","commonrole"}, IsAdmin= isAdmin, ExpiryDateTime=DateTime.Now.AddHours(2)}; const string secretKey = "Hello World";//口令加密秘钥 try { byte[] key = Encoding.UTF8.GetBytes(secretKey); IJwtAlgorithm algorithm = new HMACSHA256Algorithm();//加密方式 IJsonSerializer serializer = new JsonNetSerializer();//序列化Json IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();//base64加解密 IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);//JWT编码 var token = encoder.Encode(authInfo, key);//生成令牌 //口令信息 tokenInfo.Success = true; tokenInfo.Token = token; tokenInfo.Message = "OK"; } catch(Exception ex) { tokenInfo.Success = false; tokenInfo.Message = ex.Message.ToString(); } } else { tokenInfo.Success = false; tokenInfo.Message = "用户信息为空"; } return tokenInfo; } } }
3、在项目中添加AuthAttributes文件夹,并且在文件夹在创建ApiAuthorizeAttribute.cs文件,用于创建身份拦截器
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using System.Web.Http.Controllers; using JWT; using JWT.Serializers; using apiToken.Models; using System.Text; using System.Net; using System.Net.Http; namespace apiToken.AuthAttributes { /// <summary> /// 身份认证拦截器 /// </summary> public class ApiAuthorizeAttribute: AuthorizeAttribute { /// <summary> /// 指示指定的控件是否已获得授权 /// </summary> /// <param name="actionContext"></param> /// <returns></returns> protected override bool IsAuthorized(HttpActionContext actionContext) { //前端请求api时会将token存放在名为"auth"的请求头中 var authHeader = from t in actionContext.Request.Headers where t.Key == "auth" select t.Value.FirstOrDefault(); if (authHeader != null) { const string secretKey = "Hello World";//加密秘钥 string token = authHeader.FirstOrDefault();//获取token if (!string.IsNullOrEmpty(token)) { try { byte[] key = Encoding.UTF8.GetBytes(secretKey); IJsonSerializer serializer = new JsonNetSerializer(); IDateTimeProvider provider = new UtcDateTimeProvider(); IJwtValidator validator = new JwtValidator(serializer, provider); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder); //解密 var json = decoder.DecodeToObject<AuthInfo>(token, key, verify: true); if (json != null) { //判断口令过期时间 if (json.ExpiryDateTime < DateTime.Now) { return false; } actionContext.RequestContext.RouteData.Values.Add("auth", json); return true; } return false; } catch (Exception ex) { return false; } } } return false; } /// <summary> /// 处理授权失败的请求 /// </summary> /// <param name="actionContext"></param> protected override void HandleUnauthorizedRequest(HttpActionContext actionContext) { var erModel = new { Success="false", ErrorCode="401" }; actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.OK, erModel, "application/json"); } /// <summary> /// 为操作授权时调用 /// </summary> /// <param name="actionContext"></param> //public override void OnAuthorization(HttpActionContext actionContext) //{ //} } }
4、创建UserInfoController.cs 用于测试身份验证
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using apiToken.AuthAttributes; using Newtonsoft.Json; namespace apiToken.Controllers { [RoutePrefix("api/UserInfo")] [ApiAuthorize] public class UserInfoController : ApiController { /// <summary> /// 获取用户信息 /// </summary> /// <returns></returns> [HttpGet] [Route("GetUserInfo")] public string GetUserInfo() { var userInfo = new { UserName="test", Tel="123456789", Address="testddd" }; return JsonConvert.SerializeObject(userInfo); } } }
二. Asp.net Core WebApi + Authorization: Bearer JWT_TOKEN
在一个Controller的方法中生成 jwt token ,一般在用户登陆 验证方法中,如果用户登陆验证成功 , 就 返回一个 token 。
using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; namespace JWTTest.Controllers { [Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { private readonly IConfiguration _configuration; public ValuesController(IConfiguration configuration) { _configuration = configuration; } // GET api/values [HttpGet] //[Authorize] public ActionResult<IEnumerable<string>> Get() { return new string[] { "value1", "value2" }; } // POST api/values [HttpPost] public IActionResult Post(string username, string password) { if (username == "AngelaDaddy" && password == "123456") { // push the user’s name into a claim, so we can identify the user later on. var claims = new[] { new Claim(ClaimTypes.Name, username) }; //sign the token using a secret key.This secret will be shared between your API and anything that needs to check that the token is legit. var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["SecurityKey"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); //.NET Core’s JwtSecurityToken class takes on the heavy lifting and actually creates the token. /** * Claims (Payload) Claims 部分包含了一些跟这个 token 有关的重要信息。 JWT 标准规定了一些字段,下面节选一些字段: iss: The issuer of the token,token 是给谁的 发送者 audience: 接收的 sub: The subject of the token,token 主题 exp: Expiration Time。 token 过期时间,Unix 时间戳格式 iat: Issued At。 token 创建时间, Unix 时间戳格式 jti: JWT ID。针对当前 token 的唯一标识 除了规定的字段外,可以包含其他任何 JSON 兼容的字段。 * */ var token = new JwtSecurityToken( issuer: "jwttest", audience: "jwttest", claims: claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) }); } return BadRequest("用户名密码错误"); } } }
在appsettings.json中:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"SecurityKey": "dd%88*377f6d&f£$$£$FdddFF33fssDG^!3"
}
测试生成token :
使用jwt验证
在Startup.cs中配置 服务 ,添加jwt 验证 服务:
using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; namespace JWTTest { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { //添加jwt验证: services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true,//是否验证Issuer ValidateAudience = true,//是否验证Audience ValidateLifetime = true,//是否验证失效时间 ValidateIssuerSigningKey = true,//是否验证SecurityKey ValidAudience = "jwttest",//Audience ValidIssuer = "jwttest",//Issuer,这两项和前面签发jwt的设置一致 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["SecurityKey"]))//拿到SecurityKey }; }); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseHsts(); } app.UseAuthentication();//启用验证 app.UseHttpsRedirection(); app.UseMvc(); } } }
测试
新建一个Conroller , 在需要验证的地方加上 [Authorize] :
using System.Collections.Generic; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace JWTTest.Controllers { [Route("api/[controller]")] [ApiController] public class TestController : ControllerBase { // GET api/values [HttpGet] [Authorize]//添加Authorize标签,可以加在方法上,也可以加在类上 public ActionResult<IEnumerable<string>> Get() { return new string[] { "value1", "value2" }; } // GET api/values/5 [HttpGet("{id}")] public string Get(int id) { return "value"; } } }
经过本人亲自测试,可行,如下图:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。