问题描述
我在Web API实施方面还很陌生,我创建了一个Web API服务以使其与ASP.net web form
应用程序以及使用HttpClient
对象的一些独立应用程序(C#Console/Windows应用程序)一起使用.
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.
我在Google上搜索了很多,但是找不到刷新令牌逻辑的正确实现.如果有人有正确的方法来处理过期的访问令牌,请帮助我.
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.
-
在ASP.net Web表单登录页面中,我将Web API称为"TokenController",该控制器使用两个参数loginID和password并返回我存储在会话对象中的JWT令牌.
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());
}
}
TokenController .cs
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.cs
PremiumCalculatorController : ApiController
{
[HttpPost]
public IHttpActionResult CalculatAnnualPremium(PremiumFactorInfo premiumFactDetails)
{
PremiumInfo result;
result = AnnualPremium.GetPremium(premiumFactDetails);
return Ok(result);
}
}
Web表单应用程序
Login.aspx.cs
Login.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.
推荐答案
我有几点评论:
-
访问令牌应由客户端保存,而不是在服务器上的会话中保存.刷新令牌的计数相同.这样做的原因是通常没有会话.智能客户端无需会话即可处理令牌,MVC网站可以使用Cookie,而API不知道会话.并非禁止这样做,但是再次,您将需要担心会话到期,并且在重新启动服务器时所有用户都必须再次登录.
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 =密码
- Content-Type必须为"application/x-www-form-urlencoded"
- 仅当通过安全线路(https)发送请求时,才应处理该请求.
在获得access_token且仅在请求refresh_token的情况下,您应包括刷新令牌.
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
- Content-Type必须为"application/x-www-form-urlencoded"
刷新令牌有几种方案,您也可以将它们组合在一起:
There are several scenarios for the refresh token, which you can also combine:
- 将刷新令牌保存在数据库中.每次使用刷新令牌时,您都可以将其从数据库中删除,然后保存新的刷新令牌,新的刷新令牌也将在新的access_token中返回.
- 将刷新令牌设置为更长的生命周期,并且在刷新访问令牌时不要刷新它.在这种情况下,返回的access_token不包含新的刷新令牌.这样,您将需要在refresh_token过期后再次登录.
请注意,永不过期且无法撤消的刷新令牌为用户提供了无限的访问权限,因此请谨慎实施.
Please note, a refresh token that never expires and cannot be revoked gives a user unlimited access, so be carefull with your implementation.
在我的答案中这里,您将看到如何使用Identity 2处理刷新令牌.可以考虑切换到Identity 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令牌的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!