上篇文章介紹了基於
Ids4
密碼受權模式,從使用場景、原理分析、自定義賬戶體系集成完整的介紹了密碼受權模式的內容,並最後給出了三個思考問題,本篇就針對第一個思考問題詳細的講解下Ids4
是如何生成access_token的,如何驗證access_token的有效性,最後咱們使用.net webapi來實現一個外部接口(原本想用JAVA來實現的,奈何沒學好,就當拋磚引玉吧,有會JAVA的朋友根據我寫的案例使用JAVA來實現一個案例)。html.netcore項目實戰交流羣(637326624),有興趣的朋友能夠在羣裏交流討論。java
什麼是JWT?
JSON Web Token (JWT)是一個開放標準(RFC 7519),它定義了一種緊湊的、自包含的方式,用於做爲JSON對象在各方之間安全地傳輸信息。該信息能夠被驗證和信任,由於它是數字簽名的。web
何時使用JWT?算法
1)、認證,這是比較常見的使用場景,只要用戶登陸過一次系統,以後的請求都會包含簽名出來的token,經過token也能夠用來實現單點登陸。json
2)、交換信息,經過使用密鑰對來安全的傳送信息,能夠知道發送者是誰、放置消息是否被篡改。c#
JSON Web Token由三部分組成,它們之間用圓點(.)鏈接。這三部分分別是:後端
Header
header典型的由兩部分組成:token的類型(「JWT」)和算法名稱(好比:HMAC SHA256或者RSA等等)。api
例如:緩存
{ "alg": "RS256", "typ": "JWT" }
而後,用Base64對這個JSON編碼就獲得JWT的第一部分安全
Payload
JWT的第二部分是payload,它包含聲明(要求)。聲明是關於實體(一般是用戶)和其餘數據的聲明。聲明有三種類型: registered, public 和 private。
下面是一個例子:
{ "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的發送方是否爲它所稱的發送方。
在瞭解了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
。
知道了如何生成後,最主要的目的仍是要直接咱們服務端是如何來保護接口安全的,爲何服務端只要加入下代碼就可以保護配置的資源呢?
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是否合法 //一、獲取公鑰 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")]
代碼便可。
使用其餘語言實現的原理基本一致,就是公鑰來驗籤,只要經過驗證證實是容許訪問的請求,因爲公鑰一直不變(除非認證服務器更新了證書),因此咱們請求到後能夠緩存到本地,這樣驗籤時能夠省去每次都獲取公鑰這步操做。
本篇咱們介紹了JWT
的基本原理和Ids4
的JWT
實現方式,而後使用.NET webapi
實現了使用Ids4
保護接口,其餘語言實現方式同樣,這樣咱們就能夠把網關部署後,後端服務使用任何語言開發,而後接入到網關便可。
有了這些知識點,感受是否是對Ids4
的理解更深刻了呢?JWT
確實方便,可是有些特殊場景是咱們但願Token
在有效期內經過人工配置的方式當即失效,若是按照現有Ids4
驗證方式是沒有辦法作到,那該如何實現呢?我將會在下一篇來介紹如何實現強制token
失效,敬請期待吧。