Skip to content
On this page

JWT

JWT(JSON WEB TOKEN)是一个开放标准(RFC 7519)方法实现,它通过Json对象进行网络信息传输,其所传输信息是可以被加密和验证以保证数据安全。JWT常用于授权验证和信息传输。

1. JWT Token

1.1 JWT 认证

JWT常用于身份认证,其工作过程与Asp.Net的基于票据的认证模型吻合,具体流程如下图所示。

JWT验证流程图

  1. 客户端(浏览器等)提交用户凭证(用户名密码等)进行登录
  2. 服务端在确定对方真实身份后,将用户身份信息写到一个Json对象中并进行签名生成安全令牌( Token)
  3. 服务端返回安全令牌给给客户端
  4. 客户端携带安全令牌(一般通过请求头)并以此令牌所携带身份执行目标操作或者访问目标资源
  5. 服务端校验安全令牌签名确认信息未被篡改并获取身份信息,检查授权无误后处理客户端请求
  6. 服务端返回请求响应给客户端

1.2 JWT与Session

传统在Web开发中常使用Session进行用户认证,因而很多人常会比较基于JWT的认证模型与Session的异同优劣。

首先要清楚一点,Session与Asp.Net提供的基于Cookie的认证方案完全不同。Session会话机制并不包含完整的认证过程,它仅是一种记录用户会话状态的方法,完全可以用于认证无关的场景,在认证场景中,我们在确认用户身份后将数据存储在服务器内存并返回session_id标识给客户端,可以简单的认为这是认证的一部分过程。基于Cookie的认证方案则与JWT有一定相似,认证方颁发的包含用户数据的的票据对应JWT Token,两者都存储在客户端,两者数据加密方式不同,令牌的传输和存储也有所区别。Cookie认证方案使用Cookie进行存储和传输票据,JWT多使用localStorage存储,使用请求头进行数据传输

1.2.1 Session

Session机制下用户信息记录到称为Session的服务端内存中,Session是一个key-value集合,key一般名称为session_id唯一标识用户的一次会话,服务端会把session_id记录到Cookie中并返回给客户端,之后客户端每次请求都会带上这个session_id,服务端则可以根据session_id值来识别用户。

因为数据存储在服务端,Session在一定程度上可以避免敏感数据的泄露,提高了数据安全性。Session机制下我们也可以非常方便的控制用户的在线状态。除了以上提到的两点优势,Session还存在着以下问题,正因如此使用Session鉴权的方式也在逐渐淡出市场。

服务端内存开销

Session的实现原理决定了它会造成服务器内存开销,随着认证用户量的增长,服务端的开销会明显增大。进程内Session还存在多实例的状态丢失问题,当然开发者有可以使用Redis等进程外Session来解决。

非Web平台支持度低

因为Session是基于Cookie实现的,Cookie也会带来一定的问题。Cookie在Web开发中使用较广泛,但在其它平台如移动端中则较少使用。

XSS/XSRF漏洞

由于 Cookie可以被JavaScript读取导致session_id泄露,而作为后端识别用户的标识,Cookie的泄露意味着用户信息不再安全。设置 httpOnlyCookie将不能被 JS 读取,那么XSS注入的问题也基本不用担心了。浏览器会自动的把它加在请求的header当中,设置secure的话,Cookie就只允许通过HTTPS传输。secure选项可以过滤掉一些使用HTTP协议的XSS注入,但并不能完全阻止,而且还存在XSRF风险。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容,因为Cookie默认被发了出去。

跨域问题

前后端分离的架构中,Cookie会阻止域共享访问,需要开发人员解决跨域问题。

1.2.2 JWT

相比于SessionJWT最大的不同是其数据会被签名后存储在客户端。

优势:

  • 节省服务器内存开销。
  • SSO。因为用户数据保存在客户端,只要保证服务端鉴权逻辑统一即可实现SSO
  • 跨平台/跨语言支持。不同开发平台和语言对JWT支持良好
  • 无跨域问题。Token多通过请求报文头传输可以避免跨域问题

劣势:

  • 不能强制客户端下线。配置不变且Token未过期前,无法让客户端下线
  • 不可存储敏感信息。数据存储在客户端,虽有签名不可篡改,但信息对用户透明,故不可存储敏感数据
  • 不可存储大量数据。每次请求都携带TokenPayload中数据过多会降低网络传输效率。

2. JWT结构

JWT结构图

如上图所示,JWTHeaderPayloadSignature三部分构成。

2.1 Header

属性含义
alg声明加密的算法 通常使用HMACSHA256
typ声明类型,这里是JWT

2.2 Payload

这部分是我们存放信息的地方。 包含三个部分"标准注册声明"、"公共声明"、"私有声明"。

标准注册声明是固定名称,存放固定内容但不强制使用。

属性含义
iss签发者
sub所面向的用户
aud接收方
exp过期时间,这个过期时间必须要大于签发时间
nbf定义在什么时间之前,该JWT都是不可用的.
iat签发时间
jti唯一身份标识,主要用来作为一次性Token,从而回避重放攻击

公共声明可以添加任何的信息,一般添加用户的相关信息或其它业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。私有声明是提供者和消费者所共同定义的声明。

2.3 Signature

这部分是防篡改签名。base64编码HeaderPayload后使用.连接组成的字符串,然后通过Header中声明的加密方式进行加盐SecretKey组合加密,然后就构成了签名。

对头部以及负载内容进行签名,可以防止内容被窜改。虽然HeaderPayload可以使用base64解码后得到明文,但由于不知道SecretKey所以客户端或任何第三方篡改内容后无法获得正确签名,服务端校验签名不正确便会得知认证内容被篡改了进而拒绝请求。

SecretKey保存在服务器端,用来进行JWT的签发和验证,务必确保其安全,一旦泄漏,任何人都可以自我签发JWT

3. JWT.NET

3.1 创建和验证JWT

我们可以通过以下方式手动创建和验证JWT。参考JWT.NET

csharp
public static string CreateJwt(Dictionary<string, object> payload, string secret)
{
    var builder = new JwtBuilder()
        .WithAlgorithm(new HMACSHA256Algorithm())
        .WithSecret(secret);

    foreach (var key in payload.Keys)
        builder.AddClaim(key, payload[key]);

    return builder.Build();
}

public static bool VerifyJwt(string token, string secret, out IDictionary<string, object> payload)
{
    try
    {
        payload = new JwtBuilder()
            .WithSecret(secret)
            .MustVerifySignature()
            .Decode<IDictionary<string, object>>(token);

        return true;
    }
    catch (TokenExpiredException)
    {
        //JWT过期
        payload = null;
        return false;
    }
    catch (SignatureVerificationException)
    {
        //签名错误
        payload = null;
        return false;
    }
}

3.2 JWT 认证方案

Asp.Net在Microsoft.AspNetCore.Authentication.JwtBearer中提供了JwtBearer认证方案。接下来我们通过一个WebAPI项目基于JwtBearer认证方案来重构一下 上一节认证授权案例

csharp
public class Startup
{
    public Startup(IConfiguration configuration) =>
        Configuration = configuration;

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.Configure<JwtOptions>(Configuration.GetSection(nameof(JwtOptions)));
        services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                var jwt = Configuration.GetSection(nameof(JwtOptions)).Get<JwtOptions>();
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidIssuer = jwt.ValidIssuer,
                    ValidAudience = jwt.ValidAudience,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwt.IssuerSigningKey)),
                    ValidateIssuerSigningKey = true
                };
            });
            services.AddAuthorization(options => options.AddPolicy("admin", policy =>
            {
                policy.RequireAuthenticatedUser();
                policy.RequireRole("Administrator");
            }));
        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1", new OpenApiInfo {Title = "ColinChang.ApiSample", Version = "v1"});
        });
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseSwagger();
            app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ColinChang.ApiSample v1"));
        }

        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();
        app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
    }
}

注册JWT认证服务并读取appsettings.json中声明的以下配置初始化JWT基础配置选项。

json
{
  "JwtOptions": {
    "ValidIssuer": "https://a-nomad.com",
    "ValidAudience": "https://a-nomad.com",
    "IssuerSigningKey": "~!@#$%^&*()_+[];",
    "Expires": 21600
  }
}

在以下API中使用不同的授权认证,但用户未获得授权时API会响应401 Unauthorized,当无权访问时API会响应403 Forbidden

csharp
[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{
    [Authorize]
    [HttpGet]
    public string Get() => $"{User.Identity.Name} is authenticated";

    [Authorize("admin")]
    [HttpPost]
    public string Post() => $"{User.Identity.Name} is authorized with policy admin";

    [Authorize(Roles = "Administrator")]
    [HttpPut]
    public string Put() => $"{User.Identity.Name} is authorized with role Administrator\nroles:{string.Join(",", User.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value))}";
}

下面是最关键的JWT认证过程。需要注意的是JWT认证方案中核心认证处理器JwtBearerHandler类型继承自AuthenticationHandler<JwtBearerOptions>,但并未实现IAuthenticationSignOutHandlerIAuthenticationSignInHandler,也就没有提供SignInSignOut方法。

csharp
[ApiController]
[Route("[controller]")]
public class AccountController : ControllerBase
{
    private static readonly IEnumerable<User> Users = new[]
    {
        new User("Colin", "123123", new[] {new Role("Administrator")}),
        new User("Robin", "123123", new[] {new Role("User")})
    };

    [HttpPost]
    public IActionResult Post([FromServices] IOptions<JwtOptions> options, [FromBody]User user)
    {
        if (user == null)
            return BadRequest("user cannot be null");

        var usr = Users.SingleOrDefault(u =>
            string.Equals(u.Username, user.Username, StringComparison.OrdinalIgnoreCase));
        if (!string.Equals(usr?.Password, user.Password, StringComparison.OrdinalIgnoreCase))
            return BadRequest("invalid username or password");

        var claims = new List<Claim> {new Claim(ClaimTypes.Name, user.Username)};
        claims.AddRange(usr.Roles.Select(role => new Claim(ClaimTypes.Role, role.Name)));

        var jwtOptions = options.Value;
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.IssuerSigningKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var token = new JwtSecurityToken(jwtOptions.ValidIssuer, jwtOptions.ValidAudience, claims, DateTime.Now,
            DateTime.Now.AddMinutes(jwtOptions.Expires), credentials);
        var jwt = new JwtSecurityTokenHandler().WriteToken(token);

        return Ok(jwt);
    }
}

JWT认证方案中生成Token时直接使用声明对象,而不需要开发人员构建IIdentity身份或IPrincipal用户对象。但在客户端携带Token发起请求时,服务端依然会自动decode并解析客户端用户信息到HttpContext.User对象中。

以上案例的模型类和相关视图不涉及认证授权逻辑,此处不再展示。完整案例代码参见Github

Released under the MIT License.