出典:https://www.cnblogs.com/FlyLolo/p/ASPNETCore2_26.html
この記事では、どのようにユーザー認証と(JWTのASP.NET Coreアプリケーションにおけるトークンリフレッシュプログラムの実用的な例を挙げて紹介しますASP.NETコアシリーズカタログ)
まず、JWTは何ですか?
オープンな標準(RFC 7519)に基づいて、JWT(JSONウェブトークン)は、ステートレスな分散認証方式は、主に安全なネットワーク環境中に文を配信するために使用されています。これは、JSONに基づいており、それは、.NET、JAVA、JavaScriptが,, PHPと他の言語として、JSONのように使用することができます。
なぜJWTを使うのか?
従来のWebアプリケーションでは、一般的に、認証のためにクッキー+セッションを使用します。しかし、アプリケーション、アプレットやその他のアプリケーションが増え、ステートレスのサーバー通常、RESTfulなタイプ、それらに対応するAPIのために、この認証方法を使用することは非常に便利ではありません。ステートレス分散認証方式は、まさにこの需要に沿ったものであることをJWT。
二つは、JWTの構成は次のとおりです。
JWTは、どのように見えるかですか?それは次のような文字列である:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJuYmYiOjE1NjU5MjMxMjIsImV4cCI6MTU2NTkyMzI0MiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1NDIxNCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTQyMTUifQ.Mrta7nftmfXeo_igBVd4rl2keMmm0rg0WkqRXoVAeik
「」これは、2つの文字列を形成するために一緒に接続された3つのセグメントの『歪み』から構成されています。公式サイトのhttps://jwt.io/は、その認証を提供します
ヘッダ、ペイロードと署名図の右側にある三つの部分に対応し、その3つの文字列。
ヘッダ:
ヘッダ: { "ALG": "HS256"、 "標準": "JWT" }
HS256、トークン型JWTとして識別暗号化は、本実施形態の第1列はJSONはBase64Urlを符号化することによって形成されています
ペイロード
ペイロードは多くの文(特許請求の範囲)が含ま情報記憶部用JWTあります。
あなたがペイロードに追加複数の宣言をカスタマイズすることができ、システムはまた、いくつかのデフォルトの型を提供
発行者:ISS(発行者)
EXP(有効期限):有効期限の
サブ(件名):テーマ
AUD(聴衆):オーディエンス
NBF(前ではなくを):効果的な時間
の時間の問題:(発行時)IAT
JTI(JWT ID):いいえ。
コードBase64Urlによって生成された第2の列のこの部分。
署名
署名の検証は、トークンのためです。このように、その値式:(「」base64UrlEncode(ヘッダ)+ + base64UrlEncode(ペイロード)、秘密)署名= HMACSHA256、それは新しい文字列によって生成され、あるは2前後で暗号化されています文字列。
そのため、最初の二つの文字列を通じて同じ文字列を得るために、同じ暗号化キーを持っている人だけが、この方法は、トークンの信頼性を確保します。
第三に、認証プロセス
おそらくプロセスはこれです:
- 認証サーバー:ユーザー認証とトークンの発行のログイン。
- アプリケーションサーバー:ビジネス・データ・インタフェース。保護されたAPI。
- クライアント:一般APP、小さな手続き。
認定プロセス:
- ユーザートークンを取得するには、ロギング、認証サーバによって最初に。
- APIサーバーへのアプリケーションアクセスすると、取得しトークンをリクエストのヘッダーに配置されます。
- アプリケーションサーバは、トークン、対応するポストによって返される結果を検証します。
説明:これは、プログラムの一例であり、実際のプロジェクトは変更になる場合があります。
- 小規模なプロジェクトでは、それが一緒にサービスやアプリケーションサービスを認定することができます。私たちはより良い2間の認証プロセスを理解できるように、この例では、別々の方法によって達成されます。
- より複雑なプロジェクトでは、複数のアプリケーションサービスがあるかもしれない、ユーザーはトークンを取得する複数の分散サービスで認証され、これは、JWTの利点の一つであることができます。
ここJWTの多くの記事があまりにも多くを導入していません。それはのASP.NETコアに適用する方法で実用的な外観の一例を以下によります。
第四に、応用例
1上図:三つの部分のクライアントに関わる「JWT認証プロセス」、認証サーバ、アプリケーションサーバ、これらの三つの部分の以下のシミュレーションを例に挙げます:
- 認証サーバー:FlyLolo.JWT.Server呼ばWEBAPIソリューションを作成します。
- アプリケーションサーバー:FlyLolo.JWT.API呼ばWEBAPIソリューションを作成します。
- クライアント:フィドラーは、テストを行うための要求でここに送ら。
認証サービス
まず、新しいASP.NETコアソリューションのWEBAPIソリューションを作成
FlyLolo.JWT.Serverという名前を付けます。
最初の発行のための新しいログイン名とトークンTokenControllerを作成します。
[ルート( "API / [コントローラー]")] publicクラスTokenController:コントローラ { プライベートITokenHelper tokenHelper = NULL; 公共TokenController(ITokenHelper _tokenHelper) { tokenHelper = _tokenHelper。 } [HTTPGET] パブリックIActionResult取得(文字列コード、ストリングPWD) { ユーザーユーザー= TemporaryData.GetUser(コード)。 IF(ヌル=ユーザ&& user.Password.Equals(PWD)!) { リターンOK(tokenHelper.CreateToken(ユーザ))。 } )(BadRequestを返します。 } }
これは、名前が検証した後、認証のために提出されたユーザ名とパスワードを受信するためのアクションを取得しており、CreateToken生成トークンTokenHelper呼び出す方法が返されます。
ここでは、ユーザーにして二つのクラスTokenHelper関連。
ユーザー関連:
パブリッククラスユーザー { 公共の文字列コード{取得します。セットする; } パブリック文字列名前{得ます。セットする; } パブリック文字列のパスワード{取得します。セットする; } }
唯一のデモ以来、ユーザークラスはわずか3つの以上のフィールドが含まれています。ユーザーデータは、アナログクラスTemporaryDataで行われます
/// <まとめ> ///ダミーデータ、ユーザは、データベースまたはアナログキャッシュから読み出される /// </要約> パブリック静的クラスTemporaryData { プライベート静的リスト<ユーザー>ユーザーの(新しい新しい= <ユーザー>のリスト){新しい新規ユーザー{コード= "001"、 NAME = " ジョー・スミス"、パスワード= "111111"} 、新しいユーザー{コード= "002"、NAME = " ジョン・ドウ"、パスワード= "222222"}}; 公共静的GETUSERユーザ(文字コード) { 戻りUsers.FirstOrDefault(M => m.Code.Equals(コード)); } }
これが唯一のアナログデータであり、実際のプロジェクトでは、データベース、またはキャッシュから読まれるべきです。
TokenHelper:
パブリッククラスTokenHelper:ITokenHelper { プライベートIOptions <JWTConfig> _options。 公共TokenHelper(IOptions <JWTConfig>オプション) { _options =オプション。 } パブリックトークンCreateToken(ユーザユーザ) { 請求[]クレーム= {新しい項(ClaimTypes.NameIdentifier、user.Code)、新たな請求項(ClaimTypes.Name、user.Name)}。 CreateToken(クレーム)を返します。 } プライベートトークンCreateToken(請求項[]の特許請求の範囲) { VAR今= DateTime.Now; varは= now.Add(TimeSpan.FromMinutes(_options.Value.AccessTokenExpiresMinutes))を満了します。 新しいJwtSecurityToken(=トークンVAR 発行者:_options.Value.Issuer、 観客:_options.Value.Audience、 請求:請求、 notBeforeの:今、 期限が切れる:満了し、 signingCredentials:新しいSigningCredentials(新SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey))、 SecurityAlgorithms.HmacSha256)); 戻り新しいトークン{。TokenContent =新しいJwtSecurityTokenHandler()WriteToken(トークン)、有効期限=満了し}。 } }
CreateTokenメソッドによって作成されたトークン、ここではいくつかの重要なパラメータは、次のとおりです。
- トークンの発行者の発行者
- 観客トークンの受信者
- 有効期限が満了します
- IssuerSigningKey署名鍵
次のようにコードを対応するトークンです。
パブリッククラストークン { 公共の文字列TokenContent {取得します。セットする; } 公共の日時が有効期限{GET。セットする; } }
これは、トークンはTokenHelper CreateToken方法でクライアントに返さ生成します。今まで、一見すべての作業が完了しました。ない場合、我々はまだスタートアップファイルにいくつかの設定を行う必要があります。
パブリッククラススタートアップ {
// ......此处省略部分代码
ます。public void ConfigureServices(IServiceCollectionサービス) {
//读取配置信息 services.AddSingleton <ITokenHelper、TokenHelper>(); services.Configure <JWTConfig>(Configuration.GetSection( "JWT")); //启用JWT services.AddAuthentication(オプション=> { Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; })。 AddJwtBearer(); services.AddMvc()SetCompatibilityVersion(CompatibilityVersion.Version_2_2)。 } 無効に設定公開(APP IApplicationBuilder、IHostingEnvironment ENV) { IF(env.IsDevelopment()) { app.UseDeveloperExceptionPage(); }
//認証中間イネーブル app.UseAuthenticationを(); app.UseMvc(); } }
これは、認証情報に設定情報を使用して、次のようにappsettings.json設定があります:
"JWT":{ "発行者": "FlyLolo"、 "観客": "TestAudience"、 "IssuerSigningKey": "FlyLolo1234567890"、 "AccessTokenExpiresMinutes": "30" }
プロジェクトを実行し、道のAPI /トークンコード= 002&PWD = 222222でFidderを取得するためのアクセス、次のような結果が返さ?:
{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8
yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL
3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJuYmYiOjE1NjY3OTg0NzUsImV4cCI6MTU2NjgwMDI
3NSwiaXNzIjoiRmx5TG9sbyIsImF1ZCI6IlRlc3RBdWRpZW5jZSJ9.BVf3gOuW1E9RToqKy8XXp8uIvZKL-lBA-q9fB9QTEZ4",
"expires":"2019-08-26T21:17:55.1183172+08:00"}
客户端登录成功并成功返回了一个Token,认证服务创建完成
应用服务
新建一个WebApi的解决方案,名为FlyLolo.JWT.API。
添加BookController用作业务API。
[Route("api/[controller]")] [Authorize] public class BookController : Controller { // GET: api/<controller> [HttpGet] [AllowAnonymous] public IEnumerable<string> Get() { return new string[] { "ASP", "C#" }; } // POST api/<controller> [HttpPost] public JsonResult Post() { return new JsonResult("Create Book ..."); } }
对此Controller添加了[Authorize]标识,表示此Controller的Action被访问时需要进行认证,而它的名为Get的Action被标识了[AllowAnonymous],表示此Action的访问可以跳过认证。
在Startup文件中配置认证:
public class Startup { // 省略部分代码 public void ConfigureServices(IServiceCollection services) { #region 读取配置 JWTConfig config = new JWTConfig(); Configuration.GetSection("JWT").Bind(config); #endregion #region 启用JWT认证 services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }). AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = config.Issuer, ValidAudience = config.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.IssuerSigningKey)), ClockSkew = TimeSpan.FromMinutes(1) }; }); #endregion services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseAuthentication(); app.UseMvc(); } }
这里同样用到了配置:
public class JWTConfig { public string Issuer { get; set; } public string Audience { get; set; } public string IssuerSigningKey { get; set; } public int AccessTokenExpiresMinutes { get; set; } }
appsettings.json:
"JWT": { "Issuer": "FlyLolo", "Audience": "TestAudience", "IssuerSigningKey": "FlyLolo1234567890", "AccessTokenExpiresMinutes": "30" }
关于JWT认证,这里通过options.TokenValidationParameters对认证信息做了设置,ValidIssuer、ValidAudience、IssuerSigningKey这三个参数用于验证Token生成的时候填写的Issuer、Audience、IssuerSigningKey,所以值要和生成Token时的设置一致。
ClockSkew默认值为5分钟,它是一个缓冲期,例如Token设置有效期为30分钟,到了30分钟的时候是不会过期的,会有这么个缓冲时间,也就是35分钟才会过期。为了方便测试(不想等太长时间),这里我设置了1分钟。
TokenValidationParameters还有一些其他参数,在它的构造方法中已经做了默认设置,代码如下:
public TokenValidationParameters() { RequireExpirationTime = true; RequireSignedTokens = true; SaveSigninToken = false; ValidateActor = false; ValidateAudience = true; //是否验证接受者 ValidateIssuer = true; //是否验证发布者 ValidateIssuerSigningKey = false; //是否验证秘钥 ValidateLifetime = true; //是否验证过期时间 ValidateTokenReplay = false; }
访问api/book,正常返回了结果
["ASP","C#"]
通过POST方式访问,返回401错误。
这就需要使用获取到的Toke了,如下图方式再次访问
添加了“Authorization: bearer Token内容”这样的Header,可以正常访问了。
至此,简单的JWT认证示例就完成了,代码地址https://github.com/FlyLolo/JWT.Demo/releases/tag/1.0。
这里可能会有个疑问,例如:
1.Token被盗了怎么办?
答: 在启用Https的情况下,Token被放在Header中还是比较安全的。另外Token的有效期不要设置过长。例如可以设置为1小时(微信公众号的网页开发的Token有效期为2小时)。
2. Token到期了如何处理?
答:理论上Token过期应该是跳到登录界面,但这样太不友好了。可以在后台根据Token的过期时间定期去请求新的Token。下一节来演示一下Token的刷新方案。
五、Token的刷新
为了使客户端能够获取到新的Token,对上文的例子进行改造,大概思路如下:
- 用户登录成功的时候,一次性给他两个Token,分别为AccessToken和RefreshToken,AccessToken用于正常请求,也就是上例中原有的Token,RefreshToken作为刷新AccessToken的凭证。
- AccessToken的有效期较短,例如一小时,短一点安全一些。RefreshToken有效期可以设置长一些,例如一天、一周等。
- 当AccessToken即将过期的时候,例如提前5分钟,客户端利用RefreshToken请求指定的API获取新的AccessToken并更新本地存储中的AccessToken。
所以只需要修改FlyLolo.JWT.Server即可。
首先修改Token的返回方案,新增一个Model
public class ComplexToken { public Token AccessToken { get; set; } public Token RefreshToken { get; set; } }
包含AccessToken和RefreshToken,用于用户登录成功后的Token结果返回。
修改 appsettings.json,添加两个配置项:
"RefreshTokenAudience": "RefreshTokenAudience", "RefreshTokenExpiresMinutes": "10080" //60*24*7
RefreshTokenExpiresMinutes用于设置RefreshToken的过期时间,这里设置了7天。RefreshTokenAudience用于设置RefreshToken的接受者,与原Audience值不一致,作用是使RefreshToken不能用于访问应用服务的业务API,而AccessToken不能用于刷新Token。
修改TokenHelper:
public enum TokenType { AccessToken = 1, RefreshToken = 2 } public class TokenHelper : ITokenHelper { private IOptions<JWTConfig> _options; public TokenHelper(IOptions<JWTConfig> options) { _options = options; } public Token CreateAccessToken(User user) { Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Code), new Claim(ClaimTypes.Name, user.Name) }; return CreateToken(claims, TokenType.AccessToken); } public ComplexToken CreateToken(User user) { Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Code), new Claim(ClaimTypes.Name, user.Name) //下面两个Claim用于测试在Token中存储用户的角色信息,对应测试在FlyLolo.JWT.API的两个测试Controller的Put方法,若用不到可删除 , new Claim(ClaimTypes.Role, "TestPutBookRole"), new Claim(ClaimTypes.Role, "TestPutStudentRole") }; return CreateToken(claims); } public ComplexToken CreateToken(Claim[] claims) { return new ComplexToken { AccessToken = CreateToken(claims, TokenType.AccessToken), RefreshToken = CreateToken(claims, TokenType.RefreshToken) }; } /// <summary> /// 用于创建AccessToken和RefreshToken。 /// 这里AccessToken和RefreshToken只是过期时间不同,【实际项目】中二者的claims内容可能会不同。 /// 因为RefreshToken只是用于刷新AccessToken,其内容可以简单一些。 /// 而AccessToken可能会附加一些其他的Claim。 /// </summary> /// <param name="claims"></param> /// <param name="tokenType"></param> /// <returns></returns> private Token CreateToken(Claim[] claims, TokenType tokenType) { var now = DateTime.Now; var expires = now.Add(TimeSpan.FromMinutes(tokenType.Equals(TokenType.AccessToken) ? _options.Value.AccessTokenExpiresMinutes : _options.Value.RefreshTokenExpiresMinutes));//设置不同的过期时间 var token = new JwtSecurityToken( issuer: _options.Value.Issuer, audience: tokenType.Equals(TokenType.AccessToken) ? _options.Value.Audience : _options.Value.RefreshTokenAudience,//设置不同的接受者 claims: claims, notBefore: now, expires: expires, signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_options.Value.IssuerSigningKey)), SecurityAlgorithms.HmacSha256)); return new Token { TokenContent = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires }; } public Token RefreshToken(ClaimsPrincipal claimsPrincipal) { var code = claimsPrincipal.Claims.FirstOrDefault(m => m.Type.Equals(ClaimTypes.NameIdentifier)); if (null != code ) { return CreateAccessToken(TemporaryData.GetUser(code.Value.ToString())); } else { return null; } } }
在登录后,生成两个Token返回给客户端。在TokenHelper添加了一个RefreshToken方法,用于生成新的AccessToken。对应在TokenController中添加一个名为Post的Action,用于调用这个RefreshToken方法刷新Token
[HttpPost] [Authorize] public IActionResult Post() { return Ok(tokenHelper.RefreshToken(Request.HttpContext.User)); }
这个方法添加了[Authorize]标识,说明调用它需要RefreshToken认证通过。既然启用了认证,那么在Startup文件中需要像上例的业务API一样做JWT的认证配置。
public void ConfigureServices(IServiceCollection services) { #region 读取配置信息 services.AddSingleton<ITokenHelper, TokenHelper>(); services.Configure<JWTConfig>(Configuration.GetSection("JWT")); JWTConfig config = new JWTConfig(); Configuration.GetSection("JWT").Bind(config); #endregion #region 启用JWT services.AddAuthentication(Options => { Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }). AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = config.Issuer, ValidAudience = config.RefreshTokenAudience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.IssuerSigningKey)) }; }); #endregion services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); }
注意这里的ValidAudience被赋值为config.RefreshTokenAudience,和FlyLolo.JWT.API中的不一致,用于防止AccessToken和RefreshToken的混用。
再次访问/api/token?code=002&pwd=222222,会返回两个Token:
{"accessToken":{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8y
MDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUva
WRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW
1zL3JvbGUiOlsiVGVzdFB1dEJvb2tSb2xlIiwiVGVzdFB1dFN0dWRlbnRSb2xlIl0sIm5iZiI6MTU2NjgwNjQ3OSwiZXhwIjoxNTY2ODA4Mjc5LCJ
pc3MiOiJGbHlMb2xvIiwiYXVkIjoiVGVzdEF1ZGllbmNlIn0.wlMorS1V0xP0Fb2MDX7jI7zsgZbb2Do3u78BAkIIwGg",
"expires":"2019-08-26T22:31:19.5312172+08:00"},
"refreshToken":{"tokenContent":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8y
MDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjAwMiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUva
WRlbnRpdHkvY2xhaW1zL25hbWUiOiLmnY7lm5siLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW
1zL3JvbGUiOlsiVGVzdFB1dEJvb2tSb2xlIiwiVGVzdFB1dFN0dWRlbnRSb2xlIl0sIm5iZiI6MTU2NjgwNjQ3OSwiZXhwIjoxNTY3NDExMjc5LCJ
pc3MiOiJGbHlMb2xvIiwiYXVkIjoiUmVmcmVzaFRva2VuQXVkaWVuY2UifQ.3EDi6cQBqa39-ywq2EjFGiM8W2KY5l9QAOWaIDi8FnI",
"expires":"2019-09-02T22:01:19.6143038+08:00"}}
可以使用RefreshToken去请求新的AccessToken
测试用AccessToken可以正常访问FlyLolo.JWT.API,用RefreshToken则不可以。
至此,Token的刷新功能改造完成。代码地址:https://github.com/FlyLolo/JWT.Demo/releases/tag/1.1
疑问:RefreshToken有效期那么长,被盗了怎么办,和直接将AccessToken的有效期延长有什么区别?
考えて:. 1 RefreshToken要求の大半で使用されているようAccessTokenません。2.漏れの可能性をおそらくもっとあることを、対応するサービスが(ある)よりAPIのように適用し、そう大きくなります。