Webapi 2.0如何在访问令牌过期时实现刷新JWT令牌
Posted
技术标签:
【中文标题】Webapi 2.0如何在访问令牌过期时实现刷新JWT令牌【英文标题】:Webapi 2.0 how to implement refresh JWT token when access token expired 【发布时间】:2018-01-18 11:39:34 【问题描述】:我是 Web API 实现的新手,我创建了一个 Web API 服务以将其与 ASP.net web form
应用程序以及一些使用 HttpClient
对象的独立应用程序(C# 控制台/Windows 应用程序)一起使用。
我已经在 web api 中实现了一个带有过期时间限制的基本 JWT 访问令牌身份验证,这种身份验证技术在令牌未过期之前工作正常,当令牌过期时 web api 不接受请求,因为令牌已过期!根据身份验证实现,这很好,但我想在 web api 中实现刷新令牌逻辑,以便令牌可以更新/引用,并且客户端应该能够使用 web api 资源。
我搜索了很多,但无法找到刷新令牌逻辑的正确实现。如果有人有正确的方法来处理过期的访问令牌,请帮助我。
以下是我在 asp.net 应用程序中使用 web api 所遵循的步骤。
在 ASP.net Web 表单登录页面中,我调用了 Web API“TokenController”,此控制器接受两个参数 loginID 和密码,并返回我存储在会话对象中的 JWT 令牌。
现在,当我的客户端应用程序也需要使用 Web api 资源时,必须在使用 httpclient
调用 Web api 时在请求标头中发送访问令牌。
但是当令牌过期时,客户端无法使用 web api 资源,他必须重新登录并更新令牌!这是我不想要的,用户不应提示再次登录,因为应用程序会话超时时间尚未过去。
如何在不强制用户再次登录的情况下刷新令牌。
如果我下面给出的JWT访问令牌实现逻辑不合适或不正确,请告诉我正确的方法。
以下是代码。
WebAPI
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
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
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());
TokenController .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 : 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
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
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
以上是说明问题的示例代码,可能有一些错字。
【问题讨论】:
谁降级问题至少添加评论你为什么投反对票.. 在 SO 上提出问题时,您应该发布最低问题代码,PS:顺便说一句,我没有投反对票。 @AsifAli72090,我在这里发布了代码来描述我的实现,所以人们可以理解我已经实现的内容,因为这是我的第一个 web api 实现,我不确定我使用的方法是否正确与否。 【参考方案1】:我有几点意见:
访问令牌旨在由客户端保存,而不是在服务器上的会话中。刷新令牌也同样重要。原因是,通常没有会话。智能客户端可以在没有会话的情况下处理令牌,MVC 网站可以使用 cookie,而 API 不知道会话。这不是被禁止的,但是您需要再次担心会话过期,并且在您重新启动服务器时所有用户都必须再次登录。
如果您想实现 OAuth,请阅读specification。在那里,您将找到实现刷新令牌所需的一切。
在 TokenController 中,您处理登录。在那里你也应该检查other conditions。
当获取access_token并且只有请求refresh_token时,你应该在access_token中包含refresh_token。
client applications (grant_type = client_credentials) 不需要刷新令牌,因为它们使用 clientid / secret 来获取访问令牌。扩展 TokenController 以允许 client_credentials 流。请注意:刷新令牌仅适用于用户,并且只有在可以保密的情况下才能使用。刷新令牌非常强大,因此请小心处理。
为了refresh an access token,您需要将刷新令牌发送到端点。在您的情况下,您可以扩展 TokenController 以允许 refresh_token 请求。您需要检查:
-
刷新令牌有多种场景,您也可以组合使用:
请注意,永不过期且无法撤销的刷新令牌为用户提供无限访问权限,因此请谨慎实施。
在我的回答 here 中,您可以看到如何使用身份 2 处理刷新令牌。您可以考虑切换到身份 2。
我想我已经提到了一切。如果我遗漏了什么或者有什么不清楚的地方,请告诉我。
【讨论】:
这个问题与 OAuth 或 Identity 2 无关;)。您所说的所有内容都可以从我链接的教程中阅读。和平相处。 谢谢@Ruard,我理解你上面提到的品脱,但我想了解你的几点,比如你在第一点中添加了“访问令牌的目的是由客户端而不是服务器上的会话”,在我的客户端应用程序(asp.net Web 表单)中,我将令牌存储在会话中,当客户端应用程序使用 HttpClient 类通过服务器端代码调用 Web API 方法时将使用该令牌, 这样对吗? @Ruard你有没有示例代码链接,其中描述了access_token的完整生命周期-> Web API暴露方法-> refresh_token植入。 @NeerajKumarGupta 就像我说的,这不是禁止或不好的,但它不必是你的关注。现在您存储会话 cookie 并链接到服务器上的信息。如果您重新启动服务器或刷新应用程序,则所有会话都可能消失。在没有会话的情况下,您会将信息存储在 cookie 中并使用户对 cookie 负责。 @NeerajKumarGupta 我没有自定义示例代码。我使用 Identity,就像 9 中的链接一样。此外,刷新令牌的实现取决于所选择的策略,涉及生命周期和持久性。但是流量很容易。对于网络表单:获取 access_token(包括 refresh_token)。 access_token 过期(或即将过期),使用 refresh_token 获取新的 access_token。对于客户端应用程序:不要使用 refresh_tokens,而是使用 clientid/secret 来获取(新的)访问令牌。【参考方案2】:这可以通过单独的持久刷新令牌来完成。 http://www.c-sharpcorner.com/article/handle-refresh-token-using-asp-net-core-2-0-and-json-web-token/的一个很好的教程
【讨论】:
问题标记为 C#-4.0、webforms 和 asp.net-web-api2。这个 asp.net core 2.0 教程有什么帮助? 在不做全部工作的情况下给出解决方案的总体思路,从而为他学习。它不是关于语法,而是设计。 感谢@Niko,我浏览了您提供的链接并了解令牌的请求/响应流程。这就是我想了解当我的访问令牌过期时,API 服务器如何发布刷新令牌!该客户端应用程序首先检查请求的响应代码/消息,然后通过将 refresh_token 作为主密钥发送来重新请求 access_token 以获取新的 access_token。 所以我的结论是,当 access_token 过期时,实际请求将分 3 个步骤进行处理,如下所示。 1.发送访问web API资源的请求 2.查看第一个请求响应码/message的响应是访问令牌过期了吗?如果是,则通过发送 refresh token 对 API 进行新的调用以获取新的访问令牌。 3. 使用新的访问令牌向 Web API 请求实际资源(与第一次请求相同)。 @Niko 这是我的正确理解吗? 我想说你现在明白了刷新令牌是如何工作的 :) 图片很容易解释,这里有一个来自salesforce.com的diagram以上是关于Webapi 2.0如何在访问令牌过期时实现刷新JWT令牌的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 jwt 在 Angular 6 和 web api 中刷新令牌?