ASP.NET Core Web Api of JWT Refresh Token (c)

Foreword

As stated, this section we enter the final section JWT, the JWT essence is to get an access token from the authentication server, and then follow the user can access a protected resource, but the key question is: access token life cycle in the end to set how long it? Seen some children's shoes will use JWT JWT is set to expire very long time, some for hours, and some day, some even a month, doing so of course there is a problem, if it is malicious to obtain access token, you can throughout the life cycle using the access token, which means that there is impersonate the user identity, authentication server at this time of course, is to always trust the counterfeit access token, to make counterfeit access token is invalid, the only solution is to modify the key but if we do, it will make the granted access token will be invalid, so the best solution is not to change the key, we should try to control the problem at the source, rather than wait until a problem presents road again want to solve it, refresh token debut.

 

RefreshToken

What is the refresh token it? Refresh access token is used to obtain a new access token from the authentication server exchange, with a refresh token can reacquire new access token refresh token after token expires without access by the client credentials to log in again , this way, both to ensure the user's access token expires after a good experience, but also to ensure greater system security, at the same time, if get a new access token by refreshing invalid token validation that can be included respondents blacklist restrict access, then access token and refresh token life cycle is set to how long it appropriate? It depends on the security requirements of the system, in general, the access token life cycle will not be too long, such as five minutes, another example of micro-channel acquisition AccessToken expiration time of 2 hours. Next, I will use two tables to demonstrate the whole process to achieve refresh token, there may be a better solution, welcome in the comments, study, study. We create a new http: // localhost: 5000's WebApi for authentication, and then create a http: // localhost: 5001 client, first click [simulation] Sign in to get Toen gain access tokens and refresh tokens, and then click [call the client to get the current time], as follows:

Next, we create a user table (User) and user refresh token table (UserRefreshToken), structured as follows:

    public class User
    {
        public string Id { get; set; }
        public string Email { get; set; }
        public string UserName { get; set; }

        private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();

        public IEnumerable<UserRefreshToken> UserRefreshTokens => _userRefreshTokens;

        /// <summary>
        /// 验证刷新token是否存在或过期
        /// </summary>
        /// <param name="refreshToken"></param>
        /// <returns></returns>
        public bool IsValidRefreshToken(string refreshToken)
        {
            return _userRefreshTokens.Any(d => d.Token.Equals(refreshToken) && d.Active);
        }

        /// <summary>
        /// 创建刷新Token
        /// </summary>
        /// <param name="token"></param>
        /// <param name="userId"></param>
        /// <param name="minutes"></param>
        public void CreateRefreshToken(string token, string userId, double minutes = 1)
        {
            _userRefreshTokens.Add(new UserRefreshToken() { Token = token, UserId = userId, Expires = DateTime.Now.AddMinutes(minutes) });
        }

        /// <summary>
        /// 移除刷新token
        /// </summary>
        /// <param name="refreshToken"></param>
        public void RemoveRefreshToken(string refreshToken)
        {
            _userRefreshTokens.Remove(_userRefreshTokens.FirstOrDefault(t => t.Token == refreshToken));
        }
    public class UserRefreshToken
    {
        public string Id { get; private set; } = Guid.NewGuid().ToString();
        public string Token { get; set; }
        public DateTime Expires { get; set; }
        public string UserId { get; set; }
        public bool Active => DateTime.Now <= Expires;
    }

As can be seen for the operation of the refresh token we put it in the user entity, that is in use Back Fields EF Core without outside exposure. Next, we will generate an access token, token refresh, the access token verification, acquiring the package to the corresponding user identity as follows:

        /// <summary>
        /// 生成访问令牌
        /// </summary>
        /// <param name="claims"></param>
        /// <returns></returns>
        public string GenerateAccessToken(Claim[] claims)
        {
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));

            var token = new JwtSecurityToken(
                issuer: "http://localhost:5000",
                audience: "http://localhost:5001",
                claims: claims,
                notBefore: DateTime.Now,
                expires: DateTime.Now.AddMinutes(1),
                signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
            );

            return new JwtSecurityTokenHandler().WriteToken(token);
        }

        /// <summary>
        /// 生成刷新Token
        /// </summary>
        /// <returns></returns>
        public string GenerateRefreshToken()
        {
            var randomNumber = new byte[32];
            using (var rng = RandomNumberGenerator.Create())
            {
                rng.GetBytes(randomNumber);
                return Convert.ToBase64String(randomNumber);
            }
        }

        /// <summary>
        /// 从Token中获取用户身份
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public ClaimsPrincipal GetPrincipalFromAccessToken(string token)
        {
            var handler = new JwtSecurityTokenHandler();

            try
            {
                return handler.ValidateToken(token, new TokenValidationParameters
                {
                    ValidateAudience = false,
                    ValidateIssuer = false,
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)),
                    ValidateLifetime = false
                }, out SecurityToken validatedToken);
            }
            catch (Exception)
            {
                return null;
            }
        }

When the user clicks login, access authentication server, the login is successful we create an access token and refresh token and returns, as follows:

        [HttpPost("login")]
        public async Task<IActionResult> Login()
        {
            var user = new User()
            {
                Id = "D21D099B-B49B-4604-A247-71B0518A0B1C",
                UserName = "Jeffcky",
                Email = "[email protected]"
            };

            await context.Users.AddAsync(user);

            var refreshToken = GenerateRefreshToken();

            user.CreateRefreshToken(refreshToken, user.Id);

            await context.SaveChangesAsync();

            var claims = new Claim[]
            {
                new Claim(ClaimTypes.Name, user.UserName),
                new Claim(JwtRegisteredClaimNames.Email, user.Email),
                new Claim(JwtRegisteredClaimNames.Sub, user.Id),
            };

            return Ok(new Response() { AccessToken = GenerateAccessToken(claims), RefreshToken = refreshToken });
        }

At this point we return to chart given above, we get Token click [Login] simulation, this time an Ajax request, and then return to the access token and refresh token stored locally localStorage as follows:

<the INPUT of the type = " the Button " the above mentioned id = " btn " value = " Analog Sign in to get Token " /> 

<the INPUT of the type = " the Button " the above mentioned id = " btn-currentTime " value = " call the client to get the current time " />
       // diagrammatic拟登Landfall 
        $ ( ' #Btn ' ) .click (function () { 
            GetTokenAndRefreshToken (); 
        }); 

        // 获取Token 
        function GetTokenAndRefreshToken () {     
         $ .post ( ' http: // localhost: 5000 / api / account / login ' ) .done (function (data) { 
                SaveAccessToken (Data.AccessToken); 
                SaveRefreshToken (Data.RefreshToken); 
            }); 
        }
        // Get AccessToken from localStorage 
        function getAccessToken () {
             return localStorage.getItem ( ' accessToken ' ); 
        } 

        // Get refreshtoken from localStorage 
        function getRefreshToken () {
             return localStorage.getItem ( ' refreshtoken ' ); 
        } 

        // save AccessToken to localStorage 
        saveAccessToken function (token) { 
            localStorage.setItem ( ' accessToken ' , token); 
        } 

        // save RefreshToken to localStorage
        function saveRefreshToken(refreshToken) {
            localStorage.setItem('refreshToken', refreshToken);
        }

At this point we call the client again and click [] to get the current time, while login access token is set to return to the request header, the code is as follows:

       $('#btn-currentTime').click(function () {
            GetCurrentTime();
        });

        //调用客户端获取当前时间
        function GetCurrentTime() {
            $.ajax({
                type: 'get',
                contentType: 'application/json',
                url: 'http://localhost:5001/api/home',
                beforeSend: function (xhr) {
                    xhr.setRequestHeader('Authorization', 'Bearer ' + getAccessToken());
                },
                success: function (data) {
                    alert(data);
                },
                error: function (xhr) {
                   
                }
            });
        }

The client request interface is very simple, step by step in order to let everyone understand, I have to come out, as follows:

        [Authorize]
        [HttpGet("api/[controller]")]
        public string GetCurrentTime()
        {
            return DateTime.Now.ToString("yyyy-MM-dd");
        }

Well, here we have achieved to simulate Sign in to get access token, and be able to call the client interface to obtain the current time, and we have just returned refresh token and stored in the local localStorage, not used. When the access token expires we need to get a new access token and access token refresh tokens, right. So the question came. We know how the access token has expired it? This is the first, the second is why you want to send the old access token to retrieve a new access token it? Directly through the refresh token in exchange not do it? There is a good question, is not afraid of any thoughts, us one to answer. When we add JWT client middleware, which has an event you can capture the access token has expired (on JWT client configuration first middleware has been talked about, there is no longer long-winded), as follows:

                  options.Events = new JwtBearerEvents
                  {
                      OnAuthenticationFailed = context =>
                      {
                          if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                          {
                              context.Response.Headers.Add("act", "expired");
                          }
                          return Task.CompletedTask;
                      }
                  };

With the above event and capture the access token expires exceptions, where we added a custom key act, is expired in response headers, because only reflect a 401 unauthorized, and do not represent an access token has expired. When we click on the first picture to get the current client call [time] Ajax request, if the access token expires, then error method in the Ajax request to capture, given that we have sent Ajax request in the above error continue to add the following method:

                error: function (XHR) {
                     IF (xhr.status === 401 && xhr.getResponseHeader ( ' ACT ' ) === ' expired The ' ) {
                         // access token has expired certainly 
                    } 
                }

Here we have to solve the problem of how to capture the access token has expired, then we need to do is obtain refresh tokens in exchange for new access tokens directly through the refresh token is also not impossible, but also for safety consideration, we added to the old access token. Next we get an Ajax request refresh tokens, as follows:

        //获取刷新Token
        function GetRefreshToken(func) {
            var model = {
                accessToken: getAccessToken(),
                refreshToken: getRefreshToken()
            };
            $.ajax({
                type: "POST",
                contentType: "application/json; charset=utf-8",
                url: 'http://localhost:5000/api/account/refresh-token',
                dataType: "json",
                data: JSON.stringify(model),
                Success: Function (Data) {
                    an if (!! Data.AccessToken && Data.RefreshToken) {
                         // 跳转ItaruNoboru录 
                    } the else { 
                        SaveAccessToken (Data.AccessToken); 
                        SaveRefreshToken (Data.RefreshToken); 
                        func (); 
                    } 
                } 
            }); 
        }

Methods for Issuing Ajax request a refresh token we pass a function that is requested on a call interface access token expired, click [call the client to get the current time] button Ajax request error method, eventually evolved so as follows:

              error: function (xhr) {
                     IF (xhr.status === 401 && xhr.getResponseHeader ( ' ACT ' ) === ' expired The ' ) { 

                        / * access token certainly has expired, the current request will get passed in order to refresh licensing process 
                         continues after the current request to acquire * refresh tokens exchanged for a new token 
                        * / 
                        GetRefreshToken (GetCurrentTime); 
                    } 
                }

Next is passed through the old access token and refresh token call interface in exchange for a new access token, as follows:

        /// <summary>
        /// 刷新Token
        /// </summary>
        /// <returns></returns>
        [HttpPost("refresh-token")]
        public async Task<IActionResult> RefreshToken([FromBody] Request request)
        {
            //TODO 参数校验

            var principal = GetPrincipalFromAccessToken(request.AccessToken);

            if (principal is null)
            {
                return Ok(false);
            }

            var id = principal.Claims.First(c => c.Type == JwtRegisteredClaimNames.Sub)?.Value;

            if (string.IsNullOrEmpty(id))
            {
                return Ok(false);
            }

            var user = await context.Users.Include(d => d.UserRefreshTokens)
                .FirstOrDefaultAsync(d => d.Id == id);

            if (user is null || user.UserRefreshTokens?.Count() <= 0)
            {
                return Ok(false);
            }

            if (!user.IsValidRefreshToken(request.RefreshToken))
            {
                return Ok(false);
            }

            user.RemoveRefreshToken(request.RefreshToken);

            var refreshToken = GenerateRefreshToken();

            user.CreateRefreshToken(refreshToken, id);

            try
            {
                await context.SaveChangesAsync();
            }
            catch (Exception ex)
            {
                throw ex;
            }

            var claims = new Claim[]
            {
                new Claim(ClaimTypes.Name, user.UserName),
                new Claim(JwtRegisteredClaimNames.Email, user.Email),
                new Claim(JwtRegisteredClaimNames.Sub, user.Id),
            };

            return Ok(new Response()
            {
                AccessToken = GenerateAccessToken(claims),
                RefreshToken = refreshToken
            });
        }

As passed by the old access token to validate and obtain user identity, and then verify that the refresh token has expired, if not expired then create a new access token, while updating the refresh token. The final client access token expires the moment, get a new access token by token refresh continue to call on a request, as follows:

About JWT had to end here realize refresh Token, feel this realization refresh token stored into a database program still preferable to store the refresh token Redis is also possible to see individual choose. If the above-mentioned refresh token validation is invalid, the visitor can be added to the blacklist, but is nothing more to add an attribute. Do not worry, before the end of this section, there were still eggs.

EntityFramework Core Back Fields depth study

无论是看视频还是看技术博客也好,一定要动手验证,看到这里觉得上述我所演示是不是毫无问题,如果阅读本文的你直接拷贝上述代码你会发现有问题,且听我娓娓道来,让我们来复习下Back Fields。Back Fields命名是有约定dei,上述我是根据约定而命名,所以千万别一意孤行,别乱来,比如如下命名将抛出如下异常:

 private readonly List<UserRefreshToken> _refreshTokens = new List<UserRefreshToken>();

 public IEnumerable<UserRefreshToken> UserRefreshTokens => _refreshTokens;

上述我们配置刷新令牌的Back Fields,代码如下:

  private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
  public IEnumerable<UserRefreshToken> UserRefreshTokens => _userRefreshTokens;

要是我们配置成如下形式,结果又会怎样呢?

 private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
 public IEnumerable<UserRefreshToken> UserRefreshTokens => _userRefreshTokens.AsReadOnly();

此时为了解决这个问题,我们必须将其显式配置成Back Fields,如下:

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<User>(u =>
            {
                var navigation = u.Metadata.FindNavigation(nameof(User.UserRefreshTokens));
                navigation.SetPropertyAccessMode(PropertyAccessMode.Field);
            });
        }

在我个人著作中也讲解到为了性能问题,可将字段进行ToList(),若进行了ToList(),必须显式配置成Back Fields,否则获取不到刷新令牌导航属性,如下:

private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
public IEnumerable<UserRefreshToken> UserRefreshTokens => _userRefreshTokens.ToList();

或者进行如下配置,我想应该也可取,不会存在性能问题,如下:

  private readonly List<UserRefreshToken> _userRefreshTokens = new List<UserRefreshToken>();
  public IReadOnlyCollection<UserRefreshToken> UserRefreshTokens => _userRefreshTokens.AsReadOnly();

这是关于Back Fields问题之一,问题之二则是上述我们请求获取刷新令牌中,我们先在刷新令牌的Back Fields中移除掉旧的刷新令牌,而后再创建新的刷新令牌,但是会抛出如下异常:

我们看到在添加刷新令牌时,用户Id是有值的,对不对,这是为何呢?究其根本问题出在我们移除刷新令牌方法中,如下:

        /// <summary>
        /// 移除刷新token
        /// </summary>
        /// <param name="refreshToken"></param>
        public void RemoveRefreshToken(string refreshToken)
        {
            _userRefreshTokens.Remove(_userRefreshTokens.FirstOrDefault(t => t.Token == refreshToken));
        }

我们将查询出来的导航属性并将其映射到_userRefreshTokens字段中,此时是被上下文所追踪,上述我们查询出存在的刷新令牌并在跟踪的刷新令牌中进行移除,没毛病,没找到原因,于是乎,我将上述方法修改成如下看看是否必须需要主键才能删除旧的刷新令牌:

         /// <summary>
        /// 移除刷新token
        /// </summary>
        /// <param name="refreshToken"></param>
        public void RemoveRefreshToken(string refreshToken)
        {
            var id = _userRefreshTokens.FirstOrDefault(t => t.Token == refreshToken).Id;
            _userRefreshTokens.Remove(new UserRefreshToken() { Id = id });
        }

倒没抛出异常,创建了一个新的刷新令牌,但是旧的刷新令牌却没删除,如下:

至此未找到问题出在哪里,当前版本为2.2,难道不能通过Back Fields移除对象?这个问题待解决。

总结

本节我们重点讲解了如何实现JWT刷新令牌,并也略带讨论了EF Core中Back Fields以及尚未解决的问题,至此关于JWT已结束,下节开始正式进入Docker小白系列,感谢阅读。

Guess you like

Origin www.cnblogs.com/CreateMyself/p/11273732.html
Recommended