{"id":1154,"date":"2025-05-27T14:48:00","date_gmt":"2025-05-27T06:48:00","guid":{"rendered":"https:\/\/www.hyy.net\/?p=1154"},"modified":"2025-05-27T14:48:00","modified_gmt":"2025-05-27T06:48:00","slug":"ultimate-asp-net-core-web-api-28-refresh-token","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=1154","title":{"rendered":"Ultimate ASP.NET Core Web API 28 REFRESH TOKEN"},"content":{"rendered":"<p>28 REFRESH TOKEN<br \/>\n28 \u5237\u65b0\u4ee4\u724c<\/p>\n<p>In this chapter, we are going to learn about refresh tokens and their use in modern web application development.\u200c<br \/>\n\u5728\u672c\u7ae0\u4e2d\uff0c\u6211\u4eec\u5c06\u4e86\u89e3\u5237\u65b0\u4ee4\u724c\u53ca\u5176\u5728\u73b0\u4ee3 Web \u5e94\u7528\u7a0b\u5e8f\u5f00\u53d1\u4e2d\u7684\u4f7f\u7528\u3002<\/p>\n<p>In the previous chapter, we have created a flow where a user logs in, gets an access token to be able to access protected resources, and after the token expires, the user has to log in again to obtain a new valid token:<br \/>\n\u5728\u4e0a\u4e00\u7ae0\u4e2d\uff0c\u6211\u4eec\u521b\u5efa\u4e86\u4e00\u4e2a\u6d41\u7a0b\uff0c\u7528\u6237\u767b\u5f55\u540e\uff0c\u83b7\u53d6\u8bbf\u95ee\u4ee4\u724c\u624d\u80fd\u8bbf\u95ee\u53d7\u4fdd\u62a4\u7684\u8d44\u6e90\uff0c\u4ee4\u724c\u8fc7\u671f\u540e\uff0c\u7528\u6237\u5fc5\u987b\u518d\u6b21\u767b\u5f55\u624d\u80fd\u83b7\u53d6\u65b0\u7684\u6709\u6548\u4ee4\u724c\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/2801.jpg\" alt=\"alt text\" \/><\/p>\n<p>This flow is great and is used by many enterprise applications.<br \/>\n\u6b64\u6d41\u7a0b\u975e\u5e38\u68d2\uff0c\u5e76\u88ab\u8bb8\u591a\u4f01\u4e1a\u5e94\u7528\u7a0b\u5e8f\u4f7f\u7528\u3002<\/p>\n<p>But sometimes we have a requirement not to force our users to log in every single time the token expires. For that, we can use a refresh token.<br \/>\n\u4f46\u6709\u65f6\u6211\u4eec\u8981\u6c42\u4e0d\u8981\u5728\u6bcf\u6b21\u4ee4\u724c\u8fc7\u671f\u65f6\u90fd\u5f3a\u5236\u6211\u4eec\u7684\u7528\u6237\u767b\u5f55\u3002\u4e3a\u6b64\uff0c\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528 refresh token\u3002<\/p>\n<p>Refresh tokens are credentials that can be used to acquire new access tokens. When an access token expires, we can use a refresh token to get a new access token from the authentication component. The lifetime of a refresh token is usually set much longer compared to the lifetime of an access token.<br \/>\n\u5237\u65b0\u4ee4\u724c\u662f\u53ef\u7528\u4e8e\u83b7\u53d6\u65b0\u8bbf\u95ee\u4ee4\u724c\u7684\u51ed\u8bc1\u3002\u5f53 access token \u8fc7\u671f\u65f6\uff0c\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528 refresh token \u4ece\u8eab\u4efd\u9a8c\u8bc1\u7ec4\u4ef6\u83b7\u53d6\u65b0\u7684\u8bbf\u95ee Token\u3002\u4e0e\u8bbf\u95ee\u4ee4\u724c\u7684\u751f\u547d\u5468\u671f\u76f8\u6bd4\uff0c\u5237\u65b0\u4ee4\u724c\u7684\u751f\u547d\u5468\u671f\u901a\u5e38\u8981\u957f\u5f97\u591a\u3002<\/p>\n<p>Let\u2019s introduce the refresh token to our authentication workflow:<br \/>\n\u8ba9\u6211\u4eec\u5c06\u5237\u65b0\u4ee4\u724c\u5f15\u5165\u6211\u4eec\u7684\u8eab\u4efd\u9a8c\u8bc1\u5de5\u4f5c\u6d41\u7a0b\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/2802.jpg\" alt=\"alt text\" \/><\/p>\n<ol>\n<li>\n<p>First, the client authenticates with the authentication component by providing the credentials.<br \/>\n\u9996\u5148\uff0c\u5ba2\u6237\u7aef\u901a\u8fc7\u63d0\u4f9b\u51ed\u8bc1\u6765\u4f7f\u7528\u8eab\u4efd\u9a8c\u8bc1\u7ec4\u4ef6\u8fdb\u884c\u8eab\u4efd\u9a8c\u8bc1\u3002<\/p>\n<\/li>\n<li>\n<p>Then, the authentication component issues the access token and the refresh token.<br \/>\n\u7136\u540e\uff0c\u8eab\u4efd\u9a8c\u8bc1\u7ec4\u4ef6\u9881\u53d1\u8bbf\u95ee\u4ee4\u724c\u548c\u5237\u65b0\u4ee4\u724c\u3002<\/p>\n<\/li>\n<li>\n<p>After that, the client requests the resource endpoints for a protected resource by providing the access token.<br \/>\n\u4e4b\u540e\uff0c\u5ba2\u6237\u7aef\u901a\u8fc7\u63d0\u4f9b\u8bbf\u95ee\u4ee4\u724c\u6765\u8bf7\u6c42\u53d7\u4fdd\u62a4\u8d44\u6e90\u7684\u8d44\u6e90\u7ec8\u7aef\u8282\u70b9\u3002<\/p>\n<\/li>\n<li>\n<p>The resource endpoint validates the access token and provides a protected resource.<br \/>\n\u8d44\u6e90\u7ec8\u7aef\u8282\u70b9\u9a8c\u8bc1\u8bbf\u95ee\u4ee4\u724c\u5e76\u63d0\u4f9b\u53d7\u4fdd\u62a4\u7684\u8d44\u6e90\u3002<\/p>\n<\/li>\n<li>\n<p>Steps 3 &amp; 4 keep on repeating until the access token expires.<br \/>\n\u6b65\u9aa43\u548c4\u4e0d\u65ad\u91cd\u590d\uff0c\u76f4\u5230\u8bbf\u95ee\u4ee4\u724c\u8fc7\u671f\u3002<\/p>\n<\/li>\n<li>\n<p>Once the access token expires, the client requests a new access token by providing the refresh token.<br \/>\n\u8bbf\u95ee\u4ee4\u724c\u8fc7\u671f\u540e\uff0c\u5ba2\u6237\u7aef\u5c06\u901a\u8fc7\u63d0\u4f9b\u5237\u65b0\u4ee4\u724c\u6765\u8bf7\u6c42\u65b0\u7684\u8bbf\u95ee\u4ee4\u724c\u3002<\/p>\n<\/li>\n<li>\n<p>The authentication component issues a new access token and refresh token.<br \/>\n\u8eab\u4efd\u9a8c\u8bc1\u7ec4\u4ef6\u9881\u53d1\u65b0\u7684\u8bbf\u95ee\u4ee4\u724c\u548c\u5237\u65b0\u4ee4\u724c\u3002<\/p>\n<\/li>\n<li>\n<p>Steps 3 through 7 keep on repeating until the refresh token expires.<br \/>\n\u6b65\u9aa4 3 \u5230 7 \u4f1a\u4e0d\u65ad\u91cd\u590d\uff0c\u76f4\u5230\u5237\u65b0\u4ee4\u724c\u8fc7\u671f\u3002<\/p>\n<\/li>\n<li>\n<p>Once the refresh token expires, the client needs to authenticate with the authentication server once again and the flow repeats from step 1.<br \/>\n\u5237\u65b0\u4ee4\u724c\u8fc7\u671f\u540e\uff0c\u5ba2\u6237\u7aef\u9700\u8981\u518d\u6b21\u5411\u8eab\u4efd\u9a8c\u8bc1\u670d\u52a1\u5668\u8fdb\u884c\u8eab\u4efd\u9a8c\u8bc1\uff0c\u5e76\u4e14\u4ece\u6b65\u9aa4 1 \u5f00\u59cb\u91cd\u590d\u8be5\u6d41\u7a0b\u3002<\/p>\n<\/li>\n<\/ol>\n<h2>28.1 Why Do We Need a Refresh Token<\/h2>\n<p>\u4e3a\u4ec0\u4e48\u9700\u8981\u5237\u65b0\u4ee4\u724c<\/p>\n<p>So, why do we need both access tokens and refresh tokens? Why don\u2019t we just set a long expiration date, like a month or a year for the access tokens? Because, if we do that and someone manages to get hold of our access token they can use it for a long period, even if we change our password!\u200c<br \/>\n\u90a3\u4e48\uff0c\u4e3a\u4ec0\u4e48\u6211\u4eec\u9700\u8981\u8bbf\u95ee\u4ee4\u724c\u548c\u5237\u65b0\u4ee4\u724c\u5462\uff1f\u4e3a\u4ec0\u4e48\u6211\u4eec\u4e0d\u76f4\u63a5\u8bbe\u7f6e\u4e00\u4e2a\u8f83\u957f\u7684\u5230\u671f\u65e5\u671f\uff0c\u4f8b\u5982\u8bbf\u95ee\u4ee4\u724c\u7684\u4e00\u4e2a\u6708\u6216\u4e00\u5e74\u5462\uff1f\u56e0\u4e3a\uff0c\u5982\u679c\u6211\u4eec\u8fd9\u6837\u505a\u5e76\u4e14\u6709\u4eba\u8bbe\u6cd5\u83b7\u5f97\u4e86\u6211\u4eec\u7684\u8bbf\u95ee\u4ee4\u724c\uff0c\u5373\u4f7f\u6211\u4eec\u66f4\u6539\u4e86\u5bc6\u7801\uff0c\u4ed6\u4eec\u4e5f\u53ef\u4ee5\u957f\u65f6\u95f4\u4f7f\u7528\u5b83\uff01<\/p>\n<p>The idea of refresh tokens is that we can make the access token short- lived so that, even if it is compromised, the attacker gets access only for a shorter period. With refresh token-based flow, the authentication server issues a one-time use refresh token along with the access token. The app stores the refresh token safely.<br \/>\n\u5237\u65b0\u4ee4\u724c\u7684\u7406\u5ff5\u662f\uff0c\u6211\u4eec\u53ef\u4ee5\u4f7f\u8bbf\u95ee\u4ee4\u724c\u7684\u751f\u5b58\u671f\u8f83\u77ed\uff0c\u8fd9\u6837\uff0c\u5373\u4f7f\u5b83\u88ab\u6cc4\u9732\uff0c\u653b\u51fb\u8005\u4e5f\u53ea\u80fd\u5728\u8f83\u77ed\u7684\u65f6\u95f4\u5185\u83b7\u5f97\u8bbf\u95ee\u6743\u9650\u3002\u4f7f\u7528\u57fa\u4e8e\u5237\u65b0\u4ee4\u724c\u7684\u6d41\u7a0b\uff0c\u8eab\u4efd\u9a8c\u8bc1\u670d\u52a1\u5668\u4f1a\u9881\u53d1\u4e00\u6b21\u6027\u4f7f\u7528\u7684\u5237\u65b0\u4ee4\u724c\u4ee5\u53ca\u8bbf\u95ee\u4ee4\u724c\u3002\u5e94\u7528\u7a0b\u5e8f\u5b89\u5168\u5730\u5b58\u50a8\u5237\u65b0\u4ee4\u724c\u3002<\/p>\n<p>Every time the app sends a request to the server it sends the access token in the Authorization header and the server can identify the app using it. Once the access token expires, the server will send a token expired response. Once the app receives the token expired response, it sends the expired access token and the refresh token to obtain a new access token and a refresh token.<br \/>\n\u6bcf\u6b21\u5e94\u7528\u7a0b\u5e8f\u5411\u670d\u52a1\u5668\u53d1\u9001\u8bf7\u6c42\u65f6\uff0c\u5b83\u90fd\u4f1a\u5728 Authorization \u6807\u5934\u4e2d\u53d1\u9001\u8bbf\u95ee\u4ee4\u724c\uff0c\u670d\u52a1\u5668\u53ef\u4ee5\u4f7f\u7528\u5b83\u6765\u8bc6\u522b\u5e94\u7528\u7a0b\u5e8f\u3002\u4e00\u65e6\u8bbf\u95ee\u4ee4\u724c\u8fc7\u671f\uff0c\u670d\u52a1\u5668\u5c06\u53d1\u9001\u4ee4\u724c\u8fc7\u671f\u54cd\u5e94\u3002\u5e94\u7528\u7a0b\u5e8f\u6536\u5230\u4ee4\u724c\u8fc7\u671f\u54cd\u5e94\u540e\uff0c\u5b83\u4f1a\u53d1\u9001\u8fc7\u671f\u7684\u8bbf\u95ee\u4ee4\u724c\u548c\u5237\u65b0\u4ee4\u724c\uff0c\u4ee5\u83b7\u53d6\u65b0\u7684\u8bbf\u95ee\u4ee4\u724c\u548c\u5237\u65b0\u4ee4\u724c\u3002<\/p>\n<p>If something goes wrong, the refresh token can be revoked which means that when the app tries to use it to get a new access token, that request will be rejected and the user will have to enter credentials once again and authenticate.<br \/>\n\u5982\u679c\u51fa\u73b0\u95ee\u9898\uff0c\u53ef\u4ee5\u64a4\u9500\u5237\u65b0\u4ee4\u724c\uff0c\u8fd9\u610f\u5473\u7740\u5f53\u5e94\u7528\u7a0b\u5e8f\u5c1d\u8bd5\u4f7f\u7528\u5b83\u6765\u83b7\u53d6\u65b0\u7684\u8bbf\u95ee\u4ee4\u724c\u65f6\uff0c\u8be5\u8bf7\u6c42\u5c06\u88ab\u62d2\u7edd\uff0c\u7528\u6237\u5c06\u4e0d\u5f97\u4e0d\u518d\u6b21\u8f93\u5165\u51ed\u636e\u5e76\u8fdb\u884c\u8eab\u4efd\u9a8c\u8bc1\u3002<\/p>\n<p>Thus, refresh tokens help in a smooth authentication workflow without the need for users to submit their credentials frequently, and at the same time, without compromising the security of the app.<br \/>\n\u56e0\u6b64\uff0c\u5237\u65b0\u4ee4\u724c\u6709\u52a9\u4e8e\u5b9e\u73b0\u987a\u7545\u7684\u8eab\u4efd\u9a8c\u8bc1\u5de5\u4f5c\u6d41\u7a0b\uff0c\u800c\u65e0\u9700\u7528\u6237\u9891\u7e41\u63d0\u4ea4\u5176\u51ed\u8bc1\uff0c\u540c\u65f6\u4e0d\u4f1a\u5f71\u54cd\u5e94\u7528\u7a0b\u5e8f\u7684\u5b89\u5168\u6027\u3002<\/p>\n<h2>28.2 Refresh Token Implementation<\/h2>\n<p>28.2 \u5237\u65b0\u4ee4\u724c\u5b9e\u73b0<\/p>\n<p>So far we have learned the concept of refresh tokens. Now, let\u2019s dig into\u200c the implementation part.<br \/>\n\u5230\u76ee\u524d\u4e3a\u6b62\uff0c\u6211\u4eec\u5df2\u7ecf\u4e86\u89e3\u4e86\u5237\u65b0\u4ee4\u724c\u7684\u6982\u5ff5\u3002\u73b0\u5728\uff0c\u8ba9\u6211\u4eec\u6df1\u5165\u7814\u7a76\u5b9e\u73b0\u90e8\u5206\u3002<\/p>\n<p>The first thing we have to do is to modify the User class:<br \/>\n\u6211\u4eec\u8981\u505a\u7684\u7b2c\u4e00\u4ef6\u4e8b\u662f\u4fee\u6539 User \u7c7b\uff1a<\/p>\n<pre><code>public class User : IdentityUser { public string? FirstName { get; set; } public string? LastName { get; set; } public string? RefreshToken { get; set; } public DateTime RefreshTokenExpiryTime { get; set; } }<\/code><\/pre>\n<p>Here we add two additional properties, which we are going to add to the AspNetUsers table.<br \/>\n\u5728\u8fd9\u91cc\uff0c\u6211\u4eec\u6dfb\u52a0\u4e86\u4e24\u4e2a\u9644\u52a0\u5c5e\u6027\uff0c\u6211\u4eec\u5c06\u5c06\u5b83\u4eec\u6dfb\u52a0\u5230 AspNetUsers \u8868\u4e2d\u3002<\/p>\n<p>To do that, we have to create and execute another migration:<br \/>\n\u4e3a\u6b64\uff0c\u6211\u4eec\u5fc5\u987b\u521b\u5efa\u5e76\u6267\u884c\u53e6\u4e00\u4e2a\u8fc1\u79fb\uff1a<\/p>\n<pre><code>Add-Migration AdditionalUserFiledsForRefreshToken<\/code><\/pre>\n<p>If for some reason you get the message that you need to review your migration due to possible data loss, you should inspect the migration file and leave only the code that adds and removes our additional columns:<br \/>\n\u5982\u679c\u51fa\u4e8e\u67d0\u79cd\u539f\u56e0\uff0c\u60a8\u6536\u5230\u4e00\u6761\u6d88\u606f\uff0c\u6307\u51fa\u7531\u4e8e\u53ef\u80fd\u7684\u6570\u636e\u4e22\u5931\u800c\u9700\u8981\u68c0\u67e5\u8fc1\u79fb\uff0c\u5219\u5e94\u68c0\u67e5\u8fc1\u79fb\u6587\u4ef6\uff0c\u5e76\u4ec5\u4fdd\u7559\u6dfb\u52a0\u548c\u5220\u9664\u5176\u4ed6\u5217\u7684\u4ee3\u7801\uff1a<\/p>\n<pre><code>protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn&lt;string&gt;( name: &quot;RefreshToken&quot;, table: &quot;AspNetUsers&quot;, type: &quot;nvarchar(max)&quot;, nullable: true); migrationBuilder.AddColumn&lt;DateTime&gt;( name: &quot;RefreshTokenExpiryTime&quot;, table: &quot;AspNetUsers&quot;, type: &quot;datetime2&quot;, nullable: false, defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( name: &quot;RefreshToken&quot;, table: &quot;AspNetUsers&quot;); migrationBuilder.DropColumn( name: &quot;RefreshTokenExpiryTime&quot;, table: &quot;AspNetUsers&quot;); }<\/code><\/pre>\n<p>Also, you should open the RepositoryContextModelSnapshot file, find the AspNetRoles part and revert the Ids of both roles to the previous values:<br \/>\n\u6b64\u5916\uff0c\u8fd8\u5e94\u6253\u5f00 RepositoryContextModelSnapshot \u6587\u4ef6\uff0c\u627e\u5230 AspNetRoles \u90e8\u5206\uff0c\u5e76\u5c06\u4e24\u4e2a\u89d2\u8272\u7684 ID \u8fd8\u539f\u4e3a\u4ee5\u524d\u7684\u503c\uff1a<\/p>\n<pre><code>b.ToTable(&quot;AspNetRoles&quot;, (string)null); b.HasData( new { Id = &quot;4ac8240a-8498-4869-bc86-60e5dc982d27&quot;, ConcurrencyStamp = &quot;ec511bd4-4853-426a-a2fc-751886560c9a&quot;, Name = &quot;Manager&quot;, NormalizedName = &quot;MANAGER&quot; }, new { Id = &quot;562419f5-eed1-473b-bcc1-9f2dbab182b4&quot;, ConcurrencyStamp = &quot;937e9988-9f49-4bab-a545-b422dde85016&quot;, Name = &quot;Administrator&quot;, NormalizedName = &quot;ADMINISTRATOR&quot; });<\/code><\/pre>\n<p>After that is done, we can execute our migration with the Update- Database command. This will add two additional columns in the AspNetUsers table.<br \/>\n\u5b8c\u6210\u540e\uff0c\u6211\u4eec\u53ef\u4ee5\u4f7f\u7528 Update- Database \u547d\u4ee4\u6267\u884c\u8fc1\u79fb\u3002\u8fd9\u5c06\u5728 AspNetUsers \u8868\u4e2d\u6dfb\u52a0\u4e24\u4e2a\u9644\u52a0\u5217\u3002<\/p>\n<p>To continue, let\u2019s create a new record in the Shared\/DataTransferObjects folder:<br \/>\n\u8981\u7ee7\u7eed\uff0c\u8ba9\u6211\u4eec\u5728 Shared\/DataTransferObjects \u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u8bb0\u5f55\uff1a<\/p>\n<pre><code>public record TokenDto(string AccessToken, string RefreshToken);<\/code><\/pre>\n<p>Next, we are going to modify the IAuthenticationService interface:<br \/>\n\u63a5\u4e0b\u6765\uff0c\u6211\u4eec\u5c06\u4fee\u6539 IAuthenticationService \u63a5\u53e3\uff1a<\/p>\n<pre><code>public interface IAuthenticationService { Task&lt;IdentityResult&gt; RegisterUser(UserForRegistrationDto userForRegistration); Task&lt;bool&gt; ValidateUser(UserForAuthenticationDto userForAuth); Task&lt;TokenDto&gt; CreateToken(bool populateExp); }<\/code><\/pre>\n<p>Then, we have to implement two new methods in the AuthenticationService class:<br \/>\n\u7136\u540e\uff0c\u6211\u4eec\u5fc5\u987b\u5728 AuthenticationService \u7c7b\u4e2d\u5b9e\u73b0\u4e24\u4e2a\u65b0\u65b9\u6cd5\uff1a<\/p>\n<pre><code>private string GenerateRefreshToken() { var randomNumber = new byte[32]; using (var rng = RandomNumberGenerator.Create()) { rng.GetBytes(randomNumber); return Convert.ToBase64String(randomNumber);} } private ClaimsPrincipal GetPrincipalFromExpiredToken(string token) { var jwtSettings = _configuration.GetSection(&quot;JwtSettings&quot;); var tokenValidationParameters = new TokenValidationParameters { ValidateAudience = true, ValidateIssuer = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable(&quot;SECRET&quot;))), ValidateLifetime = true, ValidIssuer = jwtSettings[&quot;validIssuer&quot;], ValidAudience = jwtSettings[&quot;validAudience&quot;] }; var tokenHandler = new JwtSecurityTokenHandler(); SecurityToken securityToken; var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken); var jwtSecurityToken = securityToken as JwtSecurityToken; if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) { throw new SecurityTokenException(&quot;Invalid token&quot;); } return principal; }<\/code><\/pre>\n<p>GenerateRefreshToken contains the logic to generate the refresh token. We use the RandomNumberGenerator class to generate a cryptographic random number for this purpose.<br \/>\nGenerateRefreshToken \u5305\u542b\u751f\u6210\u5237\u65b0\u4ee4\u724c\u7684\u903b\u8f91\u3002\u4e3a\u6b64\uff0c\u6211\u4eec\u4f7f\u7528 RandomNumberGenerator \u7c7b\u751f\u6210\u52a0\u5bc6\u968f\u673a\u6570\u3002<\/p>\n<p>GetPrincipalFromExpiredToken is used to get the user principal from the expired access token. We make use of the ValidateToken method from the JwtSecurityTokenHandler class for this purpose. This method validates the token and returns the ClaimsPrincipal object.<br \/>\nGetPrincipalFromExpiredToken \u7528\u4e8e\u4ece\u8fc7\u671f\u7684\u8bbf\u95ee\u4ee4\u724c\u4e2d\u83b7\u53d6\u7528\u6237\u4e3b\u4f53\u3002\u4e3a\u6b64\uff0c\u6211\u4eec\u4f7f\u7528 JwtSecurityTokenHandler \u7c7b\u4e2d\u7684 ValidateToken \u65b9\u6cd5\u3002\u6b64\u65b9\u6cd5\u9a8c\u8bc1\u4ee4\u724c\u5e76\u8fd4\u56de ClaimsPrincipal \u5bf9\u8c61\u3002<\/p>\n<p>After that, to generate a refresh token and the expiry date for the logged- in user, and to return both the access token and refresh token to the caller, we have to modify the CreateToken method in the same class:<br \/>\n\u4e4b\u540e\uff0c\u8981\u4e3a\u5df2\u767b\u5f55\u7528\u6237\u751f\u6210\u5237\u65b0\u4ee4\u724c\u548c\u5230\u671f\u65e5\u671f\uff0c\u5e76\u5c06\u8bbf\u95ee\u4ee4\u724c\u548c\u5237\u65b0\u4ee4\u724c\u8fd4\u56de\u7ed9\u8c03\u7528\u65b9\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539\u540c\u4e00\u7c7b\u4e2d\u7684 CreateToken \u65b9\u6cd5\uff1a<\/p>\n<pre><code>public async Task&lt;TokenDto&gt; CreateToken(bool populateExp) { var signingCredentials = GetSigningCredentials();var claims = await GetClaims(); var tokenOptions = GenerateTokenOptions(signingCredentials, claims); var refreshToken = GenerateRefreshToken(); _user.RefreshToken = refreshToken; if(populateExp) _user.RefreshTokenExpiryTime = DateTime.Now.AddDays(7); await _userManager.UpdateAsync(_user); var accessToken = new JwtSecurityTokenHandler().WriteToken(tokenOptions); return new TokenDto(accessToken, refreshToken); }<\/code><\/pre>\n<p>Finally, we have to modify the Authenticate action:<br \/>\n\u6700\u540e\uff0c\u6211\u4eec\u5fc5\u987b\u4fee\u6539 Authenticate\u4f5c\uff1a<\/p>\n<pre><code>[HttpPost(&quot;login&quot;)] [ServiceFilter(typeof(ValidationFilterAttribute))] public async Task&lt;IActionResult&gt; Authenticate([FromBody] UserForAuthenticationDto user) { if (!await _service.AuthenticationService.ValidateUser(user)) return Unauthorized(); var tokenDto = await _service.AuthenticationService .CreateToken(populateExp: true); return Ok(tokenDto); }<\/code><\/pre>\n<p>That\u2019s it regarding the action modification.<br \/>\n\u8fd9\u5c31\u662f\u5173\u4e8e\u52a8\u4f5c\u4fee\u6539\u7684\u5185\u5bb9\u3002<\/p>\n<p>Now, we can test this by sending the POST request from Postman:<br \/>\n\u73b0\u5728\uff0c\u6211\u4eec\u53ef\u4ee5\u901a\u8fc7\u4ece Postman \u53d1\u9001 POST \u8bf7\u6c42\u6765\u6d4b\u8bd5\u8fd9\u4e00\u70b9\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/authentication\/login\">https:\/\/localhost:5001\/api\/authentication\/login<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/2803.jpg\" alt=\"alt text\" \/><\/p>\n<p>We can see the successful authentication and both our tokens. Additionally, if we inspect the database, we are going to find populated RefreshToken and Expiry columns for JDoe:<br \/>\n\u6211\u4eec\u53ef\u4ee5\u770b\u5230\u6210\u529f\u7684\u8eab\u4efd\u9a8c\u8bc1\u548c\u6211\u4eec\u7684\u4ee4\u724c\u3002\u6b64\u5916\uff0c\u5982\u679c\u6211\u4eec\u68c0\u67e5\u6570\u636e\u5e93\uff0c\u6211\u4eec\u5c06\u627e\u5230\u586b\u5145\u7684 JDoe \u7684 RefreshToken \u548c Expiry \u5217\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/2804.jpg\" alt=\"alt text\" \/><\/p>\n<p>It is a good practice to have a separate endpoint for the refresh token\u200c action, and that\u2019s exactly what we are going to do now.<br \/>\n\u6700\u597d\u4e3a\u5237\u65b0\u4ee4\u724c\u4f5c\u8bbe\u7f6e\u4e00\u4e2a\u5355\u72ec\u7684\u7ec8\u7aef\u8282\u70b9\uff0c\u8fd9\u6b63\u662f\u6211\u4eec\u73b0\u5728\u8981\u505a\u7684\u4e8b\u60c5\u3002<\/p>\n<p>Let\u2019s start by creating a new TokenController in the Presentation project:<br \/>\n\u8ba9\u6211\u4eec\u9996\u5148\u5728 Presentation \u9879\u76ee\u4e2d\u521b\u5efa\u4e00\u4e2a\u65b0\u7684 TokenController\uff1a<\/p>\n<pre><code>[Route(&quot;api\/token&quot;)] [ApiController] public class TokenController : ControllerBase { private readonly IServiceManager _service; public TokenController(IServiceManager service) =&gt; _service = service; }<\/code><\/pre>\n<p>Before we continue with the controller modification, we are going to modify the IAuthenticationService interface:<br \/>\n\u5728\u7ee7\u7eed\u4fee\u6539\u63a7\u5236\u5668\u4e4b\u524d\uff0c\u6211\u4eec\u5c06\u4fee\u6539 IAuthenticationService \u63a5\u53e3\uff1a<\/p>\n<pre><code>public interface IAuthenticationService { Task&lt;IdentityResult&gt; RegisterUser(UserForRegistrationDto userForRegistration); Task&lt;bool&gt; ValidateUser(UserForAuthenticationDto userForAuth); Task&lt;TokenDto&gt; CreateToken(bool populateExp); Task&lt;TokenDto&gt; RefreshToken(TokenDto tokenDto); }<\/code><\/pre>\n<p>And to implement this method:<br \/>\n\u8981\u5b9e\u73b0\u6b64\u65b9\u6cd5\uff1a<\/p>\n<pre><code>public async Task&lt;TokenDto&gt; RefreshToken(TokenDto tokenDto) { var principal = GetPrincipalFromExpiredToken(tokenDto.AccessToken); var user = await _userManager.FindByNameAsync(principal.Identity.Name); if (user == null || user.RefreshToken != tokenDto.RefreshToken || user.RefreshTokenExpiryTime &lt;= DateTime.Now) throw new RefreshTokenBadRequest(); _user = user; return await CreateToken(populateExp: false); }<\/code><\/pre>\n<p>We first extract the principal from the expired token and use the Identity.Name property, which is the username of the user, to fetch that user from the database. If the user doesn\u2019t exist, or the refresh tokens are not equal, or the refresh token has expired, we stop the flow returning the BadRequest response to the user. Then we just populate the _user variable and call the CreateToken method to generate new Access and Refresh tokens. This time, we don\u2019t want to update the expiry time of the refresh token thus sending false as a parameter.<br \/>\n\u6211\u4eec\u9996\u5148\u4ece\u8fc7\u671f\u7684\u4ee4\u724c\u4e2d\u63d0\u53d6\u4e3b\u4f53\uff0c\u5e76\u4f7f\u7528 Identity.Name \u5c5e\u6027\uff08\u5373\u7528\u6237\u7684\u7528\u6237\u540d\uff09\u4ece\u6570\u636e\u5e93\u4e2d\u83b7\u53d6\u8be5\u7528\u6237\u3002\u5982\u679c\u7528\u6237\u4e0d\u5b58\u5728\uff0c\u6216\u8005\u5237\u65b0\u4ee4\u724c\u4e0d\u76f8\u7b49\uff0c\u6216\u8005\u5237\u65b0\u4ee4\u724c\u5df2\u8fc7\u671f\uff0c\u6211\u4eec\u5c06\u505c\u6b62\u5411\u7528\u6237\u8fd4\u56de BadRequest \u54cd\u5e94\u7684\u6d41\u3002\u7136\u540e\uff0c\u6211\u4eec\u53ea\u9700\u586b\u5145 _user \u53d8\u91cf\u5e76\u8c03\u7528 CreateToken \u65b9\u6cd5\u4ee5\u751f\u6210\u65b0\u7684 Access \u548c Refresh \u4ee4\u724c\u3002\u8fd9\u4e00\u6b21\uff0c\u6211\u4eec\u4e0d\u60f3\u66f4\u65b0\u5237\u65b0\u4ee4\u724c\u7684\u5230\u671f\u65f6\u95f4\uff0c\u56e0\u6b64\u53d1\u9001 false \u4f5c\u4e3a\u53c2\u6570\u3002<\/p>\n<p>Since we don\u2019t have the RefreshTokenBadRequest class, let\u2019s create it in the Entities\\Exceptions folder:<br \/>\n\u7531\u4e8e\u6211\u4eec\u6ca1\u6709 RefreshTokenBadRequest \u7c7b\uff0c\u56e0\u6b64\u8ba9\u6211\u4eec\u5728 Entities\\Exceptions \u6587\u4ef6\u5939\u4e2d\u521b\u5efa\u5b83\uff1a<\/p>\n<pre><code>public sealed class RefreshTokenBadRequest : BadRequestException { public RefreshTokenBadRequest() : base(&quot;Invalid client request. The tokenDto has some invalid values.&quot;) { } }<\/code><\/pre>\n<p>And add a required using directive in the AuthenticationService class to remove the present error.<br \/>\n\u5e76\u5728 AuthenticationService \u7c7b\u4e2d\u6dfb\u52a0\u5fc5\u9700\u7684 using \u6307\u4ee4\u4ee5\u5220\u9664\u5f53\u524d\u9519\u8bef\u3002<\/p>\n<p>Finally, let\u2019s add one more action in the TokenController:<br \/>\n\u6700\u540e\uff0c\u8ba9\u6211\u4eec\u5728 TokenController \u4e2d\u518d\u6dfb\u52a0\u4e00\u4e2a\u4f5c\uff1a<\/p>\n<pre><code>[HttpPost(&quot;refresh&quot;)] [ServiceFilter(typeof(ValidationFilterAttribute))] public async Task&lt;IActionResult&gt; Refresh([FromBody]TokenDto tokenDto) { var tokenDtoToReturn = await _service.AuthenticationService.RefreshToken(tokenDto); return Ok(tokenDtoToReturn); }<\/code><\/pre>\n<p>That\u2019s it.<br \/>\n\u5c31\u662f\u8fd9\u6837\u3002<\/p>\n<p>Our refresh token logic is prepared and ready for testing.<br \/>\n\u6211\u4eec\u7684\u5237\u65b0\u4ee4\u724c\u903b\u8f91\u5df2\u51c6\u5907\u5c31\u7eea\uff0c\u53ef\u4ee5\u8fdb\u884c\u6d4b\u8bd5\u3002<\/p>\n<p>Let\u2019s first send the POST authentication request:<br \/>\n\u8ba9\u6211\u4eec\u9996\u5148\u53d1\u9001 POST \u8eab\u4efd\u9a8c\u8bc1\u8bf7\u6c42\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/authentication\/login\">https:\/\/localhost:5001\/api\/authentication\/login<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/2805.jpg\" alt=\"alt text\" \/><\/p>\n<p>As before, we have both tokens in the response body.<br \/>\n\u548c\u4ee5\u524d\u4e00\u6837\uff0c\u6211\u4eec\u5728\u54cd\u5e94\u6b63\u6587\u4e2d\u6709\u4e24\u4e2a\u6807\u8bb0\u3002<\/p>\n<p>Now, let\u2019s send the POST refresh request with these tokens as the request body:<br \/>\n\u73b0\u5728\uff0c\u8ba9\u6211\u4eec\u53d1\u9001 POST \u5237\u65b0\u8bf7\u6c42\uff0c\u5e76\u5c06\u8fd9\u4e9b\u4ee4\u724c\u4f5c\u4e3a\u8bf7\u6c42\u6b63\u6587\uff1a<\/p>\n<p><a href=\"https:\/\/localhost:5001\/api\/token\/refresh\">https:\/\/localhost:5001\/api\/token\/refresh<\/a><\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/2806.jpg\" alt=\"alt text\" \/><\/p>\n<p>And we can see new tokens in the response body. Additionally, if we inspect the database, we will find the same refresh token value:<br \/>\n\u6211\u4eec\u53ef\u4ee5\u5728\u54cd\u5e94\u6b63\u6587\u4e2d\u770b\u5230\u65b0\u6807\u8bb0\u3002\u6b64\u5916\uff0c\u5982\u679c\u6211\u4eec\u68c0\u67e5\u6570\u636e\u5e93\uff0c\u6211\u4eec\u5c06\u53d1\u73b0\u76f8\u540c\u7684\u5237\u65b0\u4ee4\u724c\u503c\uff1a<\/p>\n<p><img decoding=\"async\" src=\"\/images\/ultimateaspnetcorewebapi6\/2807.jpg\" alt=\"alt text\" \/><\/p>\n<p>Usually, in your client application, you would inspect the exp claim of the access token and if it is about to expire, your client app would send the request to the api\/token endpoint and get a new set of valid tokens.<br \/>\n\u901a\u5e38\uff0c\u5728\u60a8\u7684\u5ba2\u6237\u7aef\u5e94\u7528\u7a0b\u5e8f\u4e2d\uff0c\u60a8\u5c06\u68c0\u67e5\u8bbf\u95ee\u4ee4\u724c\u7684 exp \u58f0\u660e\uff0c\u5982\u679c\u5b83\u5373\u5c06\u8fc7\u671f\uff0c\u60a8\u7684\u5ba2\u6237\u7aef\u5e94\u7528\u7a0b\u5e8f\u4f1a\u5c06\u8bf7\u6c42\u53d1\u9001\u5230 api\/token \u7ec8\u7aef\u8282\u70b9\u5e76\u83b7\u53d6\u4e00\u7ec4\u65b0\u7684\u6709\u6548\u4ee4\u724c\u3002<\/p>\n","protected":false},"excerpt":{"rendered":"<p>28 REFRESH TOKEN 28 \u5237\u65b0\u4ee4\u724c In this chapter, we are going to learn about refresh tokens and their use in modern web application development.\u200c \u5728\u672c\u7ae0\u4e2d\uff0c\u6211\u4eec\u5c06\u4e86\u89e3\u5237\u65b0\u4ee4\u724c\u53ca\u5176\u5728\u73b0\u4ee3 Web \u5e94\u7528\u7a0b\u5e8f\u5f00\u53d1\u4e2d\u7684\u4f7f\u7528\u3002 In the previous chapter, we have created a flow where a user logs in, gets an access token to be able to access protected resources, and after the [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-1154","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1154","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1154"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/1154\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1154"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=1154"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=1154"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}