Webapi 2.0 如何在访问令牌过期时实现刷新 JWT 令牌 [英] Webapi 2.0 how to implement refresh JWT token when access token expired

查看:32
本文介绍了Webapi 2.0 如何在访问令牌过期时实现刷新 JWT 令牌的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在 Web API 实现方面很新,我创建了一个 Web API 服务,将它与 ASP.net Web 表单 应用程序以及一些独立应用程序(C# 控制台/Windows 应用程序)一起使用使用 HttpClient 对象.

I am quite new in web API implementation, I have created a web API service to use it with ASP.net web form applications as well as some stand alone applications(C# Console/Windows application) using HttpClient object.

我已经在 web api 中实现了一个基本的 JWT 访问令牌身份验证和过期时间限制,这种身份验证技术工作正常,直到令牌未过期,当令牌过期时 web api 不接受请求,因为令牌已过期!根据身份验证实现,这很好,但我想在 web api 中实现刷新令牌逻辑,以便令牌可以更新/引用,并且客户端应该能够使用 web api 资源.

I have implemented a basic JWT access token authentication with expiration time limit in web api, this authentication technique is working fine until token not expired, when token get expired web api does not accept request as token has expired! which is fine as per authentication implementation, but I want to implement refresh token logic in web api so token can renew/refersh and client should be able to use the web api resource.

我在谷歌上搜索了很多,但无法找到刷新令牌逻辑的正确实现.如果有人有正确的方法来处理过期的访问令牌,请帮助我.

I googled a lot but unable to find the proper implementation of refresh token logic. Please help me if any one has right approach to handle the expired access token.

以下是我在 asp.net 应用程序中使用 web api 所遵循的步骤.

Following are the steps that I have followed to use the web api in asp.net application.

  1. 在 ASP.net Web 表单登录页面中,我将 Web API 称为TokenController",该控制器采用两个参数 loginID 和密码,并返回我存储在会话对象中的 JWT 令牌.

  1. In ASP.net web form login page I called the web API "TokenController" this controller take two arguments loginID and password and return the JWT token that I stored in session object.

现在,每当我的客户端应用程序也需要使用 web api 资源时,必须在请求标头中发送访问令牌,同时使用 httpclient 调用 web api.

Now whenever my client application need too use the web api resource has to send the access token in request header while making call to web api using httpclient.

但是当令牌过期时,客户端无法使用 web api 资源,他必须再次登录并更新令牌!这是我不想要的,用户不应提示再次登录,因为应用程序会话超时时间尚未结束.

But when token get expired client unable use the web api resource he has to login again and renew the token! this I don't want, user should not prompt to be login again as application session out time not elapsed yet.

如何在不强制用户再次登录的情况下刷新令牌.

How do I refresh the token without forcing user to login again.

如果我下面给出的JWT访问令牌实现逻辑不合适或不正确,请告诉我正确的方法.

If my given below JWT access token implementation logic is not suitable or it is incorrect, please let me know the correct way.

以下是代码.

WebAPI

AuthHandler.cs

AuthHandler.cs

  public class AuthHandler : DelegatingHandler
    {

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
                CancellationToken cancellationToken)
    {
        HttpResponseMessage errorResponse = null;           
        try
        {
            IEnumerable<string> authHeaderValues;
            request.Headers.TryGetValues("Authorization", out authHeaderValues);

            if (authHeaderValues == null)
                return base.SendAsync(request, cancellationToken);

            var requestToken = authHeaderValues.ElementAt(0);

            var token = "";

            if (requestToken.StartsWith("Bearer ", StringComparison.CurrentCultureIgnoreCase))
            {
                token = requestToken.Substring("Bearer ".Length);
            }

            var secret = "w$e$#*az";

            ClaimsPrincipal cp = ValidateToken(token, secret, true);


            Thread.CurrentPrincipal = cp;

            if (HttpContext.Current != null)
            {
                Thread.CurrentPrincipal = cp;
                HttpContext.Current.User = cp;
            }
        }
        catch (SignatureVerificationException ex)
        {
            errorResponse = request.CreateErrorResponse(HttpStatusCode.Unauthorized, ex.Message);
        }
        catch (Exception ex)
        {
            errorResponse = request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex.Message);
        }


        return errorResponse != null
            ? Task.FromResult(errorResponse)
            : base.SendAsync(request, cancellationToken);
    }

    private static ClaimsPrincipal ValidateToken(string token, string secret, bool checkExpiration)
    {
        var jsonSerializer = new JavaScriptSerializer();
        string payloadJson = string.Empty;

        try
        {
            payloadJson = JsonWebToken.Decode(token, secret);
        }
        catch (Exception)
        {
            throw new SignatureVerificationException("Unauthorized access!");
        }

        var payloadData = jsonSerializer.Deserialize<Dictionary<string, object>>(payloadJson);


        object exp;
        if (payloadData != null && (checkExpiration && payloadData.TryGetValue("exp", out exp)))
        {
            var validTo = AuthFactory.FromUnixTime(long.Parse(exp.ToString()));
            if (DateTime.Compare(validTo, DateTime.UtcNow) <= 0)
            {
                throw new SignatureVerificationException("Token is expired!");
            }
        }

        var clmsIdentity = new ClaimsIdentity("Federation", ClaimTypes.Name, ClaimTypes.Role);

        var claims = new List<Claim>();

        if (payloadData != null)
            foreach (var pair in payloadData)
            {
                var claimType = pair.Key;

                var source = pair.Value as ArrayList;

                if (source != null)
                {
                    claims.AddRange(from object item in source
                                    select new Claim(claimType, item.ToString(), ClaimValueTypes.String));

                    continue;
                }

                switch (pair.Key.ToUpper())
                {
                    case "USERNAME":
                        claims.Add(new Claim(ClaimTypes.Name, pair.Value.ToString(), ClaimValueTypes.String));
                        break;
                    case "EMAILID":
                        claims.Add(new Claim(ClaimTypes.Email, pair.Value.ToString(), ClaimValueTypes.Email));
                        break;
                    case "USERID":
                        claims.Add(new Claim(ClaimTypes.UserData, pair.Value.ToString(), ClaimValueTypes.Integer));
                        break;
                    default:
                        claims.Add(new Claim(claimType, pair.Value.ToString(), ClaimValueTypes.String));
                        break;
                }
            }

        clmsIdentity.AddClaims(claims);

        ClaimsPrincipal cp = new ClaimsPrincipal(clmsIdentity);

        return cp;
    }


}

AuthFactory.cs

AuthFactory.cs

public static class AuthFactory
{
internal static DateTime FromUnixTime(double unixTime)
{
    var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    return epoch.AddSeconds(unixTime);
}


internal static string CreateToken(User user, string loginID, out double issuedAt, out double expiryAt)
{

    var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
    expiryAt = Math.Round((DateTime.UtcNow.AddMinutes(TokenLifeDuration) - unixEpoch).TotalSeconds);
    issuedAt = Math.Round((DateTime.UtcNow - unixEpoch).TotalSeconds);

    var payload = new Dictionary<string, object>
        {
            {enmUserIdentity.UserName.ToString(), user.Name},
            {enmUserIdentity.EmailID.ToString(), user.Email},
            {enmUserIdentity.UserID.ToString(), user.UserID},
            {enmUserIdentity.LoginID.ToString(), loginID}
            ,{"iat", issuedAt}
            ,{"exp", expiryAt}
        };

    var secret = "w$e$#*az";

    var token = JsonWebToken.Encode(payload, secret, JwtHashAlgorithm.HS256);

    return token;
}

public static int TokenLifeDuration
{
    get
    {
        int tokenLifeDuration = 20; // in minuets
        return tokenLifeDuration;
    }
}

internal static string CreateMasterToken(int userID, string loginID)
{

    var payload = new Dictionary<string, object>
        {
            {enmUserIdentity.LoginID.ToString(), loginID},
            {enmUserIdentity.UserID.ToString(), userID},
            {"instanceid", DateTime.Now.ToFileTime()}
        };

    var secret = "w$e$#*az";

    var token = JsonWebToken.Encode(payload, secret, JwtHashAlgorithm.HS256);

    return token;
}

}

WebApiConfig.cs

WebApiConfig.cs

public static class WebApiConfig
{

    public static void Register(HttpConfiguration config)
    {
        var cors = new EnableCorsAttribute("*", "*", "*");
        config.EnableCors(cors);

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Formatters.Remove(config.Formatters.XmlFormatter);

        config.MessageHandlers.Add(new AuthHandler());
    }
}

令牌控制器.cs

public class TokenController : ApiController
{
    [AllowAnonymous]
    [Route("signin")]
    [HttpPost]
    public HttpResponseMessage Login(Login model)
    {
        HttpResponseMessage response = null;
        DataTable dtblLogin = null;
        double issuedAt;
        double expiryAt;

        if (ModelState.IsValid)
        {
            dtblLogin = LoginManager.GetUserLoginDetails(model.LoginID, model.Password, true);

            if (dtblLogin == null || dtblLogin.Rows.Count == 0)
            {
                response = Request.CreateResponse(HttpStatusCode.NotFound);
            }
            else
            {
                User loggedInUser = new User();
                loggedInUser.UserID = Convert.ToInt32(dtblLogin.Rows[0]["UserID"]);
                loggedInUser.Email = Convert.ToString(dtblLogin.Rows[0]["UserEmailID"]);
                loggedInUser.Name = Convert.ToString(dtblLogin.Rows[0]["LastName"]) + " " + Convert.ToString(dtblLogin.Rows[0]["FirstName"]);

                string token = AuthFactory.CreateToken(loggedInUser, model.LoginID, out issuedAt, out expiryAt);
                loggedInUser.Token = token;

                response = Request.CreateResponse(loggedInUser);

            }
        }
        else
        {
            response = Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
        }
        return response;
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);
    }

}

PremiumCalculatorController.cs

PremiumCalculatorController.cs

PremiumCalculatorController : ApiController
{
    [HttpPost]
    public IHttpActionResult CalculatAnnualPremium(PremiumFactorInfo premiumFactDetails)
    {
      PremiumInfo result;
      result = AnnualPremium.GetPremium(premiumFactDetails);
      return Ok(result);
    }
}

Web 表单应用程序

登录.aspx.cs

public class Login
{
    protected void imgbtnLogin_Click(object sender, System.EventArgs s)
    {

    UserInfo loggedinUser = LoginManager.ValidateUser(txtUserID.text.trim(), txtPassword.text);

    if (loggedinUser != null)
    {

        byte[] password = LoginManager.EncryptPassword(txtPassword.text);

        APIToken tokenInfo = ApiLoginManager.Login(txtUserID.text.trim(), password);

        loggedinUser.AccessToken = tokenInfo.Token;

        Session.Add("LoggedInUser", loggedinUser);

        Response.Redirect("Home.aspx");

    }
    else
    {
        msg.Show("Logn ID or Password is invalid.");
    }


    }
}

ApiLoginManager.cs

ApiLoginManager.cs

public class ApiLoginManager
{
    public UserDetails Login(string userName, byte[] password)
    {
        APIToken result = null;
        UserLogin objLoginInfo;
        string webAPIBaseURL = "http://localhost/polwebapiService/"
        try
        {
            using (var client = new HttpClient())
            {
                result = new UserDetails();
                client.BaseAddress = new Uri(webAPIBaseURL);
                objLoginInfo = new UserLogin { LoginID = userName, Password = password };

                var response = client.PostAsJsonAsync("api/token/Login", objLoginInfo);

                if (response.Result.IsSuccessStatusCode)
                {
                    string jsonResponce = response.Result.Content.ReadAsStringAsync().Result;
                    result = JsonConvert.DeserializeObject<APIToken>(jsonResponce);
                }

                response = null;
            }

            return result;
        }
        catch (Exception ex)
        {
            throw ex;
        }

    }

}

AnnualPremiumCalculator.aspx.cs

AnnualPremiumCalculator.aspx.cs

public class AnnualPremiumCalculator
{
    protected void imgbtnCalculatePremium_Click(object sender, System.EventArgs s)
    { 
       string token = ((UserInfo)Session["LoggedInUser"]).AccessToken;
       PremiumFactors premiumFacts = CollectUserInputPremiumFactors();
       PremiumInfo premiumDet = CalculatePremium(premiumFacts, token);
       txtAnnulPremium.text = premiumDet.Premium;
       //other details so on 
    }

    public PremiumInfo CalculatePremium(PremiumFactors premiumFacts, string accessToken)
    {
        PremiumInfo result = null;
        string webAPIBaseURL = "http://localhost/polwebapiService/";
        try
        {
            using (var client = new HttpClient())
            {
                client.BaseAddress = new Uri(webAPIBaseURL);

                StringContent content = new StringContent(JsonConvert.SerializeObject(premiumFacts), Encoding.UTF8, "application/json");

                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

                var response = client.PostAsync("api/calculators/PremiumCalculator", content);

                if (response.Result.IsSuccessStatusCode)
                {
                    string jsonResponce = response.Result.Content.ReadAsStringAsync().Result;
                    result = JsonConvert.DeserializeObject<PremiumInfo>(jsonResponce);
                }

                response = null;

            }

            return result;
        }
        finally
        {

        }

    }

}

以上是说明问题的示例代码,可能有一些拼写错误.

above is a sample code to illustrate the issue, it may have some typo.

推荐答案

我有几点意见:

  1. 访问令牌旨在由客户端保存,而不是在服务器上的会话中.刷新令牌的计数相同.原因是,通常没有会话.智能客户端可以在没有会话的情况下处理令牌,MVC 网站可以使用 cookie 而 API 不知道会话.这不是被禁止的,但是您再次需要担心会话过期,并且在您重新启动服务器时所有用户都必须再次登录.

  1. The access token is meant to be saved by the client and not in a session on the server. The same counts for the refresh token. The reason for that is, that there usually is no session. Smart clients can handle the token without session, MVC websites can use a cookie and the API doesn't know sessions. It is not forbidden, but then again you'll need to worry about session expiration and all users have to login again when you restart your server.

如果您想实施 OAuth,请阅读规范.在那里,您将找到实现刷新令牌所需的一切.

If you want to implement OAuth then read the specification. In there you will find everything you'll need to implement the refresh token.

在 TokenController 中,您处理登录.在那里,您还应该检查其他条件.

In TokenController you handle the login. There you should check other conditions as well.

  • grant_type = 密码
  • 内容类型必须是application/x-www-form-urlencoded"
  • 只有在通过安全线路 (https) 发送时才应处理请求.
    1. 当获取 access_token 并且仅在请求 refresh_token 时,您应该包含 refresh_token 在 access_token 中.

    1. When the access_token is obtained and only if the refresh_token is requested, you should include the refresh_token in the access_token.

    客户端不需要刷新令牌应用程序 (grant_type = client_credentials) 因为那些使用 clientid/secret 来获取访问令牌.扩展 TokenController 以允许 client_credentials 流.请注意:刷新令牌仅供用户使用,只有在可以保密的情况下才应使用.刷新令牌非常强大,请谨慎处理.

    You don't need a refresh token for client applications (grant_type = client_credentials) as those use a clientid / secret to obtain an access token. Extend TokenController to allow the client_credentials flow. Please note: refresh tokens are for users only and should be used only if they can be kept secret. A refresh token is very powerfull, so handle with care.

    为了刷新访问令牌,您需要将刷新令牌发送到端点.在您的情况下,您可以扩展 TokenController 以允许 refresh_token 请求.您需要检查:

    In order to refresh an access token you'll need to send the refresh token to the endpoint. In your case you can extend the TokenController to allow a refresh_token request. You'll need to check:

    • grant_type = refresh_token
    • 内容类型必须是application/x-www-form-urlencoded"
      1. 刷新令牌有多种场景,您也可以组合使用:

      • 将刷新令牌保存在数据库中.每次使用刷新令牌时,您都可以将其从数据库中删除,然后保存新的刷新令牌,该令牌也在新的 access_token 中返回.
      • 将刷新令牌设置为更长的生命周期,并且在刷新访问令牌时不要刷新它.在这种情况下,返回的 access_token 不包括新的刷新令牌.这样你就需要在 refresh_token 过期后重新登录.
        1. 请注意,永不过期且无法撤销的刷新令牌为用户提供了无限制的访问权限,因此请谨慎实施.

        1. Please note, a refresh token that never expires and cannot be revoked gives a user unlimited access, so be carefull with your implementation.

        在我的回答中 此处您可以看到如何使用身份 2 处理刷新令牌.您可以考虑切换到身份 2.

        In my answer here you can see how a refresh token can be handled using Identity 2. You can consider to switch to Identity 2.

        我想我已经提到了一切.如果我遗漏了什么或有什么不清楚的地方,请告诉我.

        I think I've mentioned everything. Please let me know if I missed something or if something isn't clear.

        这篇关于Webapi 2.0 如何在访问令牌过期时实现刷新 JWT 令牌的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆