【.NET Core项目实战-统一认证平台】第十二章 授权篇-深入理解JWT生成及验证流程
【.NET Core项目实战-统一认证平台】开篇及目录索引
上篇文章介绍了基于
Ids4密码授权模式,从使用场景、原理分析、自定义帐户体系集成完整的介绍了密码授权模式的内容,并最后给出了三个思考问题,本篇就针对第一个思考问题详细的讲解下Ids4是如何生成access_token的,如何验证access_token的有效性,最后我们使用.net webapi来实现一个外部接口(本来想用JAVA来实现的,奈何没学好,就当抛砖引玉吧,有会JAVA的朋友根据我写的案例使用JAVA来实现一个案例)。.netcore项目实战交流群(637326624),有兴趣的朋友可以在群里交流讨论。
一、JWT简介
什么是JWT?
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。什么时候使用JWT?
1)、认证,这是比较常见的使用场景,只要用户登录过一次系统,之后的请求都会包含签名出来的token,通过token也可以用来实现单点登录。
2)、交换信息,通过使用密钥对来安全的传送信息,可以知道发送者是谁、放置消息是否被篡改。
- JWT的结构是什么样的?
JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:
- Header
- Payload
- Signature
Header
header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。
例如:
{
"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的发送方是否为它所称的发送方。
二、IdentityServer4是如何生成jwt的?
在了解了JWT的基本概念介绍后,我们要知道JWT是如何生成的,加密的方式是什么,我们如何使用自己的密钥进行加密。
IdentityServer4的加密方式?
Ids4目前使用的是RS256非对称方式,使用私钥进行签名,然后客户端通过公钥进行验签。可能有的人会问,我们在生成Ids4时,也没有配置证书,为什么也可以运行起来呢?这里就要讲解证书的使用,以及Ids4使用证书的加密流程。
1、加载证书
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 }));
后面就可以在项目里使用证书的相关操作了,比如加密、验签等。
2、使用证书加密
上篇我介绍了密码授权模式,详细的讲解了流程,当所有信息校验通过,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。
三、如何验证access_token的有效性?
知道了如何生成后,最主要的目的还是要直接我们服务端是如何来保护接口安全的,为什么服务端只要加入下代码就能够保护配置的资源呢?
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority ="http://localhost:7777";
options.RequireHttpsMetadata = false;
options.ApiName = "Api1";
options.SaveToken = true;
});
//启用授权
app.UseAuthentication();
在理解这个前,我们需要了解系统做的验证流程,这里使用一张图可以很好的理解流程了。

看完后是不是豁然开朗?这里就可以很好的理解/.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是否合法
//1、获取公钥
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();
//2、使用公钥校验是否合法,如果验证失败会抛出异常
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")]代码即可。
使用其他语言实现的原理基本一致,就是公钥来验签,只要通过验证证明是允许访问的请求,由于公钥一直不变(除非认证服务器更新了证书),所以我们请求到后可以缓存到本地,这样验签时可以省去每次都获取公钥这步操作。
四、总结
本篇我们介绍了JWT的基本原理和Ids4的JWT实现方式,然后使用.NET webapi实现了使用Ids4保护接口,其他语言实现方式一样,这样我们就可以把网关部署后,后端服务使用任何语言开发,然后接入到网关即可。
有了这些知识点,感觉是不是对Ids4的理解更深入了呢?JWT确实方便,但是有些特殊场景是我们希望Token在有效期内通过人工配置的方式立即失效,如果按照现有Ids4验证方式是没有办法做到,那该如何实现呢?我将会在下一篇来介绍如何实现强制token失效,敬请期待吧。
【.NET Core项目实战-统一认证平台】第十二章 授权篇-深入理解JWT生成及验证流程的更多相关文章
- 【.NET Core项目实战-统一认证平台】第十三章 授权篇-如何强制有效令牌过期
[.NET Core项目实战-统一认证平台]开篇及目录索引 上一篇我介绍了JWT的生成验证及流程内容,相信大家也对JWT非常熟悉了,今天将从一个小众的需求出发,介绍如何强制令牌过期的思路和实现过程. ...
- 【.NET Core项目实战-统一认证平台】第十一章 授权篇-密码授权模式
[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了基于Ids4客户端授权的原理及如何实现自定义的客户端授权,并配合网关实现了统一的授权异常返回值和权限配置等相关功能,本篇将介绍 ...
- 【.NET Core项目实战-统一认证平台】第四章 网关篇-数据库存储配置(2)
[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我们介绍了如何扩展Ocelot网关,并实现数据库存储,然后测试了网关的路由功能,一切都是那么顺利,但是有一个问题未解决,就是如果网关 ...
- 【.NET Core项目实战-统一认证平台】第七章 网关篇-自定义客户端限流
[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我介绍了如何在网关上增加自定义客户端授权功能,从设计到编码实现,一步一步详细讲解,相信大家也掌握了自定义中间件的开发技巧了,本篇我们 ...
- 【.NET Core项目实战-统一认证平台】第三章 网关篇-数据库存储配置(1)
[.NET Core项目实战-统一认证平台]开篇及目录索引 本篇将介绍如何扩展Ocelot中间件实现自定义网关,并使用2种不同数据库来演示Ocelot配置信息存储和动态更新功能,内容也是从实际设计出发 ...
- 【.NET Core项目实战-统一认证平台】第五章 网关篇-自定义缓存Redis
[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我们介绍了2种网关配置信息更新的方法和扩展Mysql存储,本篇我们将介绍如何使用Redis来实现网关的所有缓存功能,用到的文档及源码 ...
- 【.NET Core项目实战-统一认证平台】第六章 网关篇-自定义客户端授权
[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章我们介绍了网关使用Redis进行缓存,并介绍了如何进行缓存实现,缓存信息清理接口的使用.本篇我们将介绍如何实现网关自定义客户端授权, ...
- 【.NET Core项目实战-统一认证平台】第十章 授权篇-客户端授权
[.NET Core项目实战-统一认证平台]开篇及目录索引 上篇文章介绍了如何使用Dapper持久化IdentityServer4(以下简称ids4)的信息,并实现了sqlserver和mysql两种 ...
- 【.NET Core项目实战-统一认证平台】第十六章 网关篇-Ocelot集成RPC服务
[.NET Core项目实战-统一认证平台]开篇及目录索引 一.什么是RPC RPC是"远程调用(Remote Procedure Call)"的一个名称的缩写,并不是任何规范化的 ...
随机推荐
- msxfs.dll函数加载代码
msxfs.dll函数加载代码 #include "stdafx.h" #include "WSXFSLoader.h" NS_AWP_DEVICE_WOSA_ ...
- Java之hashCode的作用和equals方法的重构规则
这个是博主对hashcode的初步理解,以后加深了会再来更新: 1.hashcode是什么? hashcode是对象的散列码,不同的对象几乎不一样,说几乎是因为还是可以一样的. 特点:每一个对象都有h ...
- Android第三次作业
制作音乐播放器 实现的功能: 歌曲的播放.暂停.停止.上一首.下一首.歌单列表的显示(获取本地歌曲). 成品图: 功能代码实现: 实现歌曲的播放.暂停.停止.上一首.下一首: public void ...
- js 面向对象的三大特性:封装,继承、多态
一:什么是封装? 封装的定义:就是对象内部的变化对外界是透明的,不可见的. 封装的场景: 在写项目的过程中,有时候不同页面,会有相同的功能,我们还需要每个页面都写一遍吗?额,,,,其实也可以写的,只不 ...
- css与html基础收集
1.css去掉iPhone.iPad默认按钮样式 nput[type="button"], input[type="submit"], input[type=& ...
- HttpClient 调用WebAPI时,传参的三种方式
public void Post() { //方法一,传json参数 var d = new { username = " ", password = " ", ...
- 【二代示波器教程】第14章 uCOS-III操作系统版本二代示波器实现
第14章 uCOS-III操作系统版本二代示波器实现 本章教程为大家讲解uCOS-III操作系统版本的二代示波器实现.主要讲解RTOS设计框架,即各个任务实现的功能,任务间的通信方案选择,任 ...
- Android OpenSL ES 开发:使用 OpenSL 播放 PCM 数据
OpenSL ES 是基于NDK也就是c语言的底层开发音频的公开API,通过使用它能够做到标准化, 高性能,低响应时间的音频功能实现方法. 这次是使用OpenSL ES来做一个音乐播放器,它能够播放m ...
- 音视频编解码技术(二):AAC 音频编码技术
一.AAC编码概述 AAC是高级音频编码(Advanced Audio Coding)的缩写,出现于1997年,最初是基于MPEG-2的音频编码技术,目的是取代MP3格式.2000年,MPEG-4标准 ...
- HTTP 400 错误 - 请求无效 (Bad request)
在ajax请求后台数据时有时会报 HTTP 400 错误 - 请求无效 (Bad request);出现这个请求无效报错说明请求没有进入到后台服务里: 原因:1)前端提交数据的字段名称或者是字段类型和 ...