【.NET Core项目实战-统一认证平台】开篇及目录索引 <https://www.cnblogs.com/jackcao/p/9928879.html>

上篇文章介绍了基于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
<https://www.cnblogs.com/cgzl/p/8270677.html>.

本来准备使用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失效,敬请期待吧。

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信