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
JWT是 JSON Web Token 的缩写,是一个开放标准(RFC 7519),[RFC 7519(https://tools.ietf.org/html/rfc7519)。特别适用于分布式站点的单点登录(SSO)场景。它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
JWT工作流程
这里我们通过一张图了解它的工作流程。
从上图中我们可以看出它是基于Token的身份认证,具体流程:客户端携带用户名和密码请求访问 - 服务器校验用户凭据 - 应用提供一个token给客户端 - 客户端存储token,并且在随后的每一次请求中都带着它 -服务器校验token并返回数据。
JWT结构
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的工作过程
客户端接收服务器返回的JWT,将其存储在Cookie或localStorage中。
此后,客户端将在与服务器交互中都会带JWT。如果将它存储在Cookie中,就可以自动发送,但是不会跨域,因此一般是将它放入HTTP请求的Header Authorization字段中。
Authorization: Bearer JWT_TOKEN
当跨域时,也可以将JWT被放置于POST请求的数据主体中。
使用JWT具有如下好处
通用:因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
紧凑:JWT的构成非常简单,字节占用很小,可以通过 GET、POST 等放在 HTTP 的 header 中,非常便于传输。
扩展:JWT是自我包涵的,包含了必要的所有信息,不需要在服务端保存会话信息, 非常易于应用的扩展。
JWT Bearer Token应用场景
一.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";
}
}
}
经过本人亲自测试,可行,如下图: