【.NET Core项目实战-统一认证平台】第十二章 受权篇-深刻理解JWT生成及验证流程

原文: 【.NET Core项目实战-统一认证平台】第十二章 受权篇-深刻理解JWT生成及验证流程

【.NET Core项目实战-统一认证平台】开篇及目录索引

上篇文章介绍了基于Ids4密码受权模式,从使用场景、原理分析、自定义账户体系集成完整的介绍了密码受权模式的内容,并最后给出了三个思考问题,本篇就针对第一个思考问题详细的讲解下Ids4是如何生成access_token的,如何验证access_token的有效性,最后咱们使用.net webapi来实现一个外部接口(原本想用JAVA来实现的,奈何没学好,就当抛砖引玉吧,有会JAVA的朋友根据我写的案例使用JAVA来实现一个案例)。html

.netcore项目实战交流群(637326624),有兴趣的朋友能够在群里交流讨论。java

1、JWT简介

  1. 什么是JWT?
    JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于做为JSON对象在各方之间安全地传输信息。该信息能够被验证和信任,由于它是数字签名的。web

  2. 何时使用JWT?算法

1)、认证,这是比较常见的使用场景,只要用户登陆过一次系统,以后的请求都会包含签名出来的token,经过token也能够用来实现单点登陆。json

2)、交换信息,经过使用密钥对来安全的传送信息,能够知道发送者是谁、放置消息是否被篡改。c#

  1. JWT的结构是什么样的?

JSON Web Token由三部分组成,它们之间用圆点(.)链接。这三部分分别是:后端

  • Header
  • Payload
  • Signature

Header
header典型的由两部分组成:token的类型(“JWT”)和算法名称(好比:HMAC SHA256或者RSA等等)。api

例如:缓存

{
    "alg": "RS256",
    "typ": "JWT"
}

而后,用Base64对这个JSON编码就获得JWT的第一部分安全

Payload

JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(一般是用户)和其余数据的声明。声明有三种类型: registered, public 和 private。

  • Registered claims : 这里有一组预约义的声明,它们不是强制的,可是推荐。好比:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
  • Public claims : 能够随意定义。
  • Private claims : 用于在赞成使用它们的各方之间共享信息,而且不是注册的或公开的声明。

下面是一个例子:

{
 "nbf": 1545919058,
 "exp": 1545922658,
 "iss": "http://localhost:7777",
 "aud": [
     "http://localhost:7777/resources",
     "mpc_gateway"
 ],
 "client_id": "clienta",
 "sub": "1",
 "auth_time": 1545919058,
 "idp": "local",
 "nickname": "金焰的世界",
 "email": "541869544@qq.com",
 "mobile": "13888888888",
 "scope": [
     "mpc_gateway",
     "offline_access"
 ],
 "amr": [
     "pwd"
 ]
}

对payload进行Base64编码就获得JWT的第二部分

注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的。

Signature

为了获得签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的                那个,然对它们签名便可。

例如:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

签名是用于验证消息在传递过程当中有没有被更改,而且,对于使用私钥签名的token,它还能够验证JWT的发送方是否为它所称的发送方。

2、IdentityServer4是如何生成jwt的?

在了解了JWT的基本概念介绍后,咱们要知道JWT是如何生成的,加密的方式是什么,咱们如何使用本身的密钥进行加密。

IdentityServer4的加密方式?

Ids4目前使用的是RS256非对称方式,使用私钥进行签名,而后客户端经过公钥进行验签。可能有的人会问,咱们在生成Ids4时,也没有配置证书,为何也能够运行起来呢?这里就要讲解证书的使用,以及Ids4使用证书的加密流程。

一、加载证书

Ids4默认使用临时证书来进行token的生成,使用代码 .AddDeveloperSigningCredential(),这里会自动给生成tempkey.rsa证书文件,因此项目若是使用默认配置的根目录能够查看到此文件,实现代码以下:

public static IIdentityServerBuilder AddDeveloperSigningCredential(this IIdentityServerBuilder builder, bool persistKey = true, string filename = null)
{
    if (filename == null)
    {
        filename = Path.Combine(Directory.GetCurrentDirectory(), "tempkey.rsa");
    }

    if (File.Exists(filename))
    {
        var keyFile = File.ReadAllText(filename);
        var tempKey = JsonConvert.DeserializeObject<TemporaryRsaKey>(keyFile, new JsonSerializerSettings { ContractResolver = new RsaKeyContractResolver() });

        return builder.AddSigningCredential(CreateRsaSecurityKey(tempKey.Parameters, tempKey.KeyId));
    }
    else
    {
        var key = CreateRsaSecurityKey();

        RSAParameters parameters;

        if (key.Rsa != null)
            parameters = key.Rsa.ExportParameters(includePrivateParameters: true);
        else
            parameters = key.Parameters;

        var tempKey = new TemporaryRsaKey
        {
            Parameters = parameters,
            KeyId = key.KeyId
        };

        if (persistKey)
        {
            File.WriteAllText(filename, JsonConvert.SerializeObject(tempKey, new JsonSerializerSettings { ContractResolver = new RsaKeyContractResolver() }));
        }

        return builder.AddSigningCredential(key);
    }
}

这也就能够理解为何没有配置证书也同样可使用了。

注意:在生产环境咱们最好使用本身配置的证书。

若是咱们已经有证书了,可使用以下代码实现,至于证书是如何生成的,网上资料不少,这里就不介绍了。

.AddSigningCredential(new X509Certificate2(Path.Combine(basePath,"test.pfx"),"123456"));

而后注入证书相关信息,代码以下:

builder.Services.AddSingleton<ISigningCredentialStore>(new DefaultSigningCredentialsStore(credential));
            builder.Services.AddSingleton<IValidationKeysStore>(new DefaultValidationKeysStore(new[] { credential.Key }));

后面就能够在项目里使用证书的相关操做了,好比加密、验签等。

二、使用证书加密

上篇我介绍了密码受权模式,详细的讲解了流程,当全部信息校验经过,Claim生成完成后,就开始生成token了,核心代码以下。

public virtual async Task<string> CreateTokenAsync(Token token)
{
    var header = await CreateHeaderAsync(token);
    var payload = await CreatePayloadAsync(token);
    return await CreateJwtAsync(new JwtSecurityToken(header, payload));
}
//使用配置的证书生成JWT头部
protected virtual async Task<JwtHeader> CreateHeaderAsync(Token token)
{
    var credential = await Keys.GetSigningCredentialsAsync();

    if (credential == null)
    {
        throw new InvalidOperationException("No signing credential is configured. Can't create JWT token");
    }

    var header = new JwtHeader(credential);

    // emit x5t claim for backwards compatibility with v4 of MS JWT library
    if (credential.Key is X509SecurityKey x509key)
    {
        var cert = x509key.Certificate;
        if (Clock.UtcNow.UtcDateTime > cert.NotAfter)
        {//若是证书过时提示
            Logger.LogWarning("Certificate {subjectName} has expired on {expiration}", cert.Subject, cert.NotAfter.ToString(CultureInfo.InvariantCulture));
        }
        header["x5t"] = Base64Url.Encode(cert.GetCertHash());
    }

    return header;
}
//生成内容
public static JwtPayload CreateJwtPayload(this Token token, ISystemClock clock, ILogger logger)
{
    var payload = new JwtPayload(
        token.Issuer,
        null,
        null,
        clock.UtcNow.UtcDateTime,
        clock.UtcNow.UtcDateTime.AddSeconds(token.Lifetime));

    foreach (var aud in token.Audiences)
    {
        payload.AddClaim(new Claim(JwtClaimTypes.Audience, aud));
    }

    var amrClaims = token.Claims.Where(x => x.Type == JwtClaimTypes.AuthenticationMethod);
    var scopeClaims = token.Claims.Where(x => x.Type == JwtClaimTypes.Scope);
    var jsonClaims = token.Claims.Where(x => x.ValueType == IdentityServerConstants.ClaimValueTypes.Json);

    var normalClaims = token.Claims
        .Except(amrClaims)
        .Except(jsonClaims)
        .Except(scopeClaims);

    payload.AddClaims(normalClaims);

    // scope claims
    if (!scopeClaims.IsNullOrEmpty())
    {
        var scopeValues = scopeClaims.Select(x => x.Value).ToArray();
        payload.Add(JwtClaimTypes.Scope, scopeValues);
    }

    // amr claims
    if (!amrClaims.IsNullOrEmpty())
    {
        var amrValues = amrClaims.Select(x => x.Value).Distinct().ToArray();
        payload.Add(JwtClaimTypes.AuthenticationMethod, amrValues);
    }

    // deal with json types
    // calling ToArray() to trigger JSON parsing once and so later 
    // collection identity comparisons work for the anonymous type
    try
    {
        var jsonTokens = jsonClaims.Select(x => new { x.Type, JsonValue = JRaw.Parse(x.Value) }).ToArray();

        var jsonObjects = jsonTokens.Where(x => x.JsonValue.Type == JTokenType.Object).ToArray();
        var jsonObjectGroups = jsonObjects.GroupBy(x => x.Type).ToArray();
        foreach (var group in jsonObjectGroups)
        {
            if (payload.ContainsKey(group.Key))
            {
                throw new Exception(string.Format("Can't add two claims where one is a JSON object and the other is not a JSON object ({0})", group.Key));
            }

            if (group.Skip(1).Any())
            {
                // add as array
                payload.Add(group.Key, group.Select(x => x.JsonValue).ToArray());
            }
            else
            {
                // add just one
                payload.Add(group.Key, group.First().JsonValue);
            }
        }

        var jsonArrays = jsonTokens.Where(x => x.JsonValue.Type == JTokenType.Array).ToArray();
        var jsonArrayGroups = jsonArrays.GroupBy(x => x.Type).ToArray();
        foreach (var group in jsonArrayGroups)
        {
            if (payload.ContainsKey(group.Key))
            {
                throw new Exception(string.Format("Can't add two claims where one is a JSON array and the other is not a JSON array ({0})", group.Key));
            }

            var newArr = new List<JToken>();
            foreach (var arrays in group)
            {
                var arr = (JArray)arrays.JsonValue;
                newArr.AddRange(arr);
            }

            // add just one array for the group/key/claim type
            payload.Add(group.Key, newArr.ToArray());
        }

        var unsupportedJsonTokens = jsonTokens.Except(jsonObjects).Except(jsonArrays);
        var unsupportedJsonClaimTypes = unsupportedJsonTokens.Select(x => x.Type).Distinct();
        if (unsupportedJsonClaimTypes.Any())
        {
            throw new Exception(string.Format("Unsupported JSON type for claim types: {0}", unsupportedJsonClaimTypes.Aggregate((x, y) => x + ", " + y)));
        }

        return payload;
    }
    catch (Exception ex)
    {
        logger.LogCritical(ex, "Error creating a JSON valued claim");
        throw;
    }
}
//生成最终的Token
protected virtual Task<string> CreateJwtAsync(JwtSecurityToken jwt)
{
    var handler = new JwtSecurityTokenHandler();
    return Task.FromResult(handler.WriteToken(jwt));
}

知道了这些原理后,咱们就能清楚的知道access_token都放了那些东西,以及咱们能够如何来验证生成的Token

3、如何验证access_token的有效性?

知道了如何生成后,最主要的目的仍是要直接咱们服务端是如何来保护接口安全的,为何服务端只要加入下代码就可以保护配置的资源呢?

services.AddAuthentication("Bearer")
        .AddIdentityServerAuthentication(options =>
            {
               options.Authority ="http://localhost:7777";
               options.RequireHttpsMetadata = false;
               options.ApiName = "Api1";
               options.SaveToken = true;
            });
//启用受权 
app.UseAuthentication();

在理解这个前,咱们须要了解系统作的验证流程,这里使用一张图能够很好的理解流程了。

img
看完后是否是豁然开朗?这里就能够很好的理解/.well-known/openid-configuration/jwks原来就是证书的公钥信息,是经过访问/.well-known/openid-configuration暴露给全部的客户端使用,安全性是用过非对称加密的原理保证,私钥加密的信息,公钥只能验证,因此也不存在密钥泄漏问题。

虽然只是短短的几句代码,就作了那么多事情,这说明Ids4封装的好,减小了咱们不少编码工做。这是有人会问,那若是咱们的项目不是.netcore的,那如何接入到网关呢?

网上有一个Python例子,用 Identity Server 4 (JWKS 端点和 RS256 算法) 来保护 Python web api.

原本准备使用Java来实现,很久没摸已经忘了怎么写了,留给会java的朋友实现吧,原理都是同样。

下面我就已webapi为例来开发服务端接口,而后使用Ids4来保护接口内容。

新建一个webapi项目,项目名称Czar.AuthPlatform.WebApi,为了让输出的结果为json,咱们须要在WebApiConfig增长config.Formatters.Remove(config.Formatters.XmlFormatter);代码,而后修改默认的控制器ValuesController,修改代码以下。

[Ids4Auth("http://localhost:6611", "mpc_gateway")]
public IEnumerable<string> Get()
{
      var Context = RequestContext.Principal; 
      return new string[] { "WebApi Values" };
}

为了保护api安全,咱们须要增长一个身份验证过滤器,实现代码以下。

using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace Czar.AuthPlatform.WebApi
{
    public class Ids4AuthAttribute : AuthorizationFilterAttribute
    {
        /// <summary>
        /// 认证服务器地址
        /// </summary>
        private string issUrl = "";
        /// <summary>
        /// 保护的API名称
        /// </summary>
        private string apiName = "";

        public Ids4AuthAttribute(string IssUrl,string ApiName)
        {
            issUrl = IssUrl;
            apiName = ApiName;
        }
        /// <summary>
        /// 重写验证方式
        /// </summary>
        /// <param name="actionContext"></param>
        public override void OnAuthorization(HttpActionContext actionContext)
        {
            try
            {
                var access_token = actionContext.Request.Headers.Authorization?.Parameter; //获取请求的access_token
                if (String.IsNullOrEmpty(access_token))
                {//401
                    actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
                    actionContext.Response.Content = new StringContent("{\"errcode\":401,\"errmsg\":\"未受权\"}");
                }
                else
                {//开始验证请求的Token是否合法
                    //一、获取公钥
                    var httpclient = new HttpClient();
                    var jwtKey= httpclient.GetStringAsync(issUrl + "/.well-known/openid-configuration/jwks").Result;
                    //能够在此处缓存jwtkey,不用每次都获取。
                    var Ids4keys = JsonConvert.DeserializeObject<Ids4Keys>(jwtKey);
                    var jwk = Ids4keys.keys;
                    var parameters = new TokenValidationParameters
                    { //能够增长自定义的验证项目
                        ValidIssuer = issUrl,
                        IssuerSigningKeys = jwk ,
                        ValidateLifetime = true,
                        ValidAudience = apiName
                    };
                    var handler = new JwtSecurityTokenHandler();
                    //二、使用公钥校验是否合法,若是验证失败会抛出异常
                    var id = handler.ValidateToken(access_token, parameters, out var _);
                    //请求的内容保存
                    actionContext.RequestContext.Principal = id;
                }
            }
            catch(Exception ex)
            {
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
                actionContext.Response.Content = new StringContent("{\"errcode\":401,\"errmsg\":\"未受权\"}");
            }
        }
    }

    public class Ids4Keys
    {
        public JsonWebKey[] keys { get; set; }
    }
}

代码很是简洁,就实现了基于Ids4的访问控制,如今咱们开始使用PostMan来测试接口地址。

咱们直接请求接口地址,返回401未受权。

而后我使用Ids4生成的access_token再次测试,能够获得咱们预期结果。

为了验证是否是任何地方签发的token均可以经过验证,我使用其余项目生成的access_token来测试,发现提示的401未受权,能够达到咱们预期结果。

如今就能够开心的使用咱们熟悉的webapi开发咱们的接口了,须要验证的地方增长相似[Ids4Auth("http://localhost:6611", "mpc_gateway")]代码便可。

使用其余语言实现的原理基本一致,就是公钥来验签,只要经过验证证实是容许访问的请求,因为公钥一直不变(除非认证服务器更新了证书),因此咱们请求到后能够缓存到本地,这样验签时能够省去每次都获取公钥这步操做。

4、总结

本篇咱们介绍了JWT的基本原理和Ids4JWT实现方式,而后使用.NET webapi实现了使用Ids4保护接口,其余语言实现方式同样,这样咱们就能够把网关部署后,后端服务使用任何语言开发,而后接入到网关便可。

有了这些知识点,感受是否是对Ids4的理解更深刻了呢?JWT确实方便,可是有些特殊场景是咱们但愿Token在有效期内经过人工配置的方式当即失效,若是按照现有Ids4验证方式是没有办法作到,那该如何实现呢?我将会在下一篇来介绍如何实现强制token失效,敬请期待吧。

相关文章
相关标签/搜索