aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-07-21 22:58:27 +0800
committer杨宇千 <crupest@outlook.com>2019-07-21 22:58:27 +0800
commit79fcf66b157f38199771d30c0fd0cedbfbc786f2 (patch)
tree402aa9c3f5fae0c1825cbeecf20c5a94c371f77d
parent615ffca61fcc90b11b04c8d115018a26a4a63a33 (diff)
downloadtimeline-79fcf66b157f38199771d30c0fd0cedbfbc786f2.tar.gz
timeline-79fcf66b157f38199771d30c0fd0cedbfbc786f2.tar.bz2
timeline-79fcf66b157f38199771d30c0fd0cedbfbc786f2.zip
WIP: change UserService.
-rw-r--r--Timeline/Controllers/TokenController.cs2
-rw-r--r--Timeline/Entities/UserInfo.cs4
-rw-r--r--Timeline/Entities/UserUtility.cs14
-rw-r--r--Timeline/Services/JwtService.cs29
-rw-r--r--Timeline/Services/UserService.cs174
-rw-r--r--Timeline/Startup.cs3
6 files changed, 172 insertions, 54 deletions
diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs
index 0be5fb2f..cb4408cd 100644
--- a/Timeline/Controllers/TokenController.cs
+++ b/Timeline/Controllers/TokenController.cs
@@ -46,7 +46,7 @@ namespace Timeline.Controllers
{
Success = true,
Token = result.Token,
- UserInfo = result.UserInfo
+ UserInfo = result.User
});
}
diff --git a/Timeline/Entities/UserInfo.cs b/Timeline/Entities/UserInfo.cs
index bb56df9d..9a82c991 100644
--- a/Timeline/Entities/UserInfo.cs
+++ b/Timeline/Entities/UserInfo.cs
@@ -1,7 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
namespace Timeline.Entities
{
public sealed class UserInfo
diff --git a/Timeline/Entities/UserUtility.cs b/Timeline/Entities/UserUtility.cs
index 9a272948..1de7ac7d 100644
--- a/Timeline/Entities/UserUtility.cs
+++ b/Timeline/Entities/UserUtility.cs
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using Timeline.Entities;
using Timeline.Models;
+using Timeline.Services;
namespace Timeline.Entities
{
@@ -38,12 +39,23 @@ namespace Timeline.Entities
return RoleArrayToRoleString(IsAdminToRoleArray(isAdmin));
}
+ public static bool RoleStringToIsAdmin(string roleString)
+ {
+ return RoleArrayToIsAdmin(RoleStringToRoleArray(roleString));
+ }
+
public static UserInfo CreateUserInfo(User user)
{
if (user == null)
throw new ArgumentNullException(nameof(user));
- return new UserInfo(user.Name, RoleArrayToIsAdmin(RoleStringToRoleArray(user.RoleString)));
+ return new UserInfo(user.Name, RoleStringToIsAdmin(user.RoleString));
}
+ internal static UserCache CreateUserCache(User user)
+ {
+ if (user == null)
+ throw new ArgumentNullException(nameof(user));
+ return new UserCache { Username = user.Name, IsAdmin = RoleStringToIsAdmin(user.RoleString), Version = user.Version };
+ }
}
}
diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs
index e7f5690d..b070ad62 100644
--- a/Timeline/Services/JwtService.cs
+++ b/Timeline/Services/JwtService.cs
@@ -11,7 +11,7 @@ namespace Timeline.Services
{
public class TokenInfo
{
- public string Name { get; set; }
+ public long Id { get; set; }
public long Version { get; set; }
}
@@ -34,6 +34,7 @@ namespace Timeline.Services
/// <param name="tokenInfo">The info to generate token.</param>
/// <param name="expires">The expire time. If null then use current time with offset in config.</param>
/// <returns>Return the generated token.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="tokenInfo"/> is null.</exception>
string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null);
/// <summary>
@@ -41,7 +42,8 @@ namespace Timeline.Services
/// Return null is <paramref name="token"/> is null.
/// </summary>
/// <param name="token">The token string to verify.</param>
- /// <returns>Return null if <paramref name="token"/> is null. Return the saved info otherwise.</returns>
+ /// <returns>Return the saved info in token.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
/// <exception cref="JwtTokenVerifyException">Thrown when the token is invalid.</exception>
TokenInfo VerifyJwtToken(string token);
@@ -53,25 +55,21 @@ namespace Timeline.Services
private readonly IOptionsMonitor<JwtConfig> _jwtConfig;
private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler();
- private readonly ILogger<JwtService> _logger;
- public JwtService(IOptionsMonitor<JwtConfig> jwtConfig, ILogger<JwtService> logger)
+ public JwtService(IOptionsMonitor<JwtConfig> jwtConfig)
{
_jwtConfig = jwtConfig;
- _logger = logger;
}
public string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null)
{
if (tokenInfo == null)
throw new ArgumentNullException(nameof(tokenInfo));
- if (tokenInfo.Name == null)
- throw new ArgumentException("Name of token info is null.", nameof(tokenInfo));
var config = _jwtConfig.CurrentValue;
var identity = new ClaimsIdentity();
- identity.AddClaim(new Claim(identity.NameClaimType, tokenInfo.Name));
+ identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, tokenInfo.Id.ToString(), ClaimValueTypes.Integer64));
identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(), ClaimValueTypes.Integer64));
var tokenDescriptor = new SecurityTokenDescriptor()
@@ -95,7 +93,7 @@ namespace Timeline.Services
public TokenInfo VerifyJwtToken(string token)
{
if (token == null)
- return null;
+ throw new ArgumentNullException(nameof(token));
var config = _jwtConfig.CurrentValue;
try
@@ -111,6 +109,12 @@ namespace Timeline.Services
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey))
}, out _);
+ var idClaim = principal.FindFirstValue(ClaimTypes.NameIdentifier);
+ if (idClaim == null)
+ throw new JwtTokenVerifyException("Id claim does not exist.");
+ if (!long.TryParse(idClaim, out var id))
+ throw new JwtTokenVerifyException("Can't convert id claim into a integer number.");
+
var versionClaim = principal.FindFirstValue(VersionClaimType);
if (versionClaim == null)
throw new JwtTokenVerifyException("Version claim does not exist.");
@@ -119,7 +123,7 @@ namespace Timeline.Services
return new TokenInfo
{
- Name = principal.Identity.Name,
+ Id = id,
Version = version
};
}
@@ -127,11 +131,6 @@ namespace Timeline.Services
{
throw new JwtTokenVerifyException("Validate token failed caused by a SecurityTokenException. See inner exception.", e);
}
- catch (ArgumentException e) // This usually means code logic error.
- {
- _logger.LogError(e, "Arguments passed to ValidateToken are bad.");
- throw e;
- }
}
}
}
diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs
index 9fe9e08f..49c9747d 100644
--- a/Timeline/Services/UserService.cs
+++ b/Timeline/Services/UserService.cs
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
@@ -12,7 +13,41 @@ namespace Timeline.Services
public class CreateTokenResult
{
public string Token { get; set; }
- public UserInfo UserInfo { get; set; }
+ public UserInfo User { get; set; }
+ }
+
+ [Serializable]
+ public class UserNotExistException : Exception
+ {
+ public UserNotExistException(): base("The user does not exist.") { }
+ public UserNotExistException(string message) : base(message) { }
+ public UserNotExistException(string message, Exception inner) : base(message, inner) { }
+ protected UserNotExistException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+
+ [Serializable]
+ public class BadPasswordException : Exception
+ {
+ public BadPasswordException(): base("Password is wrong.") { }
+ public BadPasswordException(string message) : base(message) { }
+ public BadPasswordException(string message, Exception inner) : base(message, inner) { }
+ protected BadPasswordException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
+ }
+
+
+ [Serializable]
+ public class BadTokenVersionException : Exception
+ {
+ public BadTokenVersionException(): base("Token version is expired.") { }
+ public BadTokenVersionException(string message) : base(message) { }
+ public BadTokenVersionException(string message, Exception inner) : base(message, inner) { }
+ protected BadTokenVersionException(
+ System.Runtime.Serialization.SerializationInfo info,
+ System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
public enum PutUserResult
@@ -85,9 +120,12 @@ namespace Timeline.Services
/// Try to anthenticate with the given username and password.
/// If success, create a token and return the user info.
/// </summary>
- /// <param name="username">The username of the user to be anthenticated.</param>
- /// <param name="password">The password of the user to be anthenticated.</param>
- /// <returns>Return null if anthentication failed. An <see cref="CreateTokenResult"/> containing the created token and user info if anthentication succeeded.</returns>
+ /// <param name="username">The username of the user to anthenticate.</param>
+ /// <param name="password">The password of the user to anthenticate.</param>
+ /// <returns>An <see cref="CreateTokenResult"/> containing the created token and user info.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="username"/> or <paramref name="password"/> is null.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user with given username does not exist.</exception>
+ /// <exception cref="BadPasswordException">Thrown when password is wrong.</exception>
Task<CreateTokenResult> CreateToken(string username, string password);
/// <summary>
@@ -95,7 +133,11 @@ namespace Timeline.Services
/// If success, return the user info.
/// </summary>
/// <param name="token">The token to verify.</param>
- /// <returns>Return null if verification failed. The user info if verification succeeded.</returns>
+ /// <returns>The user info specified by the token.</returns>
+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="token"/> is null.</exception>
+ /// <exception cref="JwtTokenVerifyException">Thrown when the token is of bad format. Thrown by <see cref="JwtService.VerifyJwtToken(string)"/>.</exception>
+ /// <exception cref="UserNotExistException">Thrown when the user specified by the token does not exist. Usually it has been deleted after the token was issued.</exception>
+ /// <exception cref="BadTokenVersionException">Thrown when the version in the token is expired. User needs to recreate the token.</exception>
Task<UserInfo> VerifyToken(string token);
/// <summary>
@@ -173,67 +215,122 @@ namespace Timeline.Services
Task<PutAvatarResult> PutAvatar(string username, byte[] data, string mimeType);
}
+ internal class UserCache
+ {
+ public string Username { get; set; }
+ public bool IsAdmin { get; set; }
+ public long Version { get; set; }
+
+ public UserInfo ToUserInfo()
+ {
+ return new UserInfo(Username, IsAdmin);
+ }
+ }
+
public class UserService : IUserService
{
private readonly ILogger<UserService> _logger;
+
+ private readonly IMemoryCache _memoryCache;
private readonly DatabaseContext _databaseContext;
+
private readonly IJwtService _jwtService;
private readonly IPasswordService _passwordService;
private readonly IQCloudCosService _cosService;
- public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService, IQCloudCosService cosService)
+ public UserService(ILogger<UserService> logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService, IQCloudCosService cosService)
{
_logger = logger;
+ _memoryCache = memoryCache;
_databaseContext = databaseContext;
_jwtService = jwtService;
_passwordService = passwordService;
_cosService = cosService;
}
+ private string GenerateCacheKeyByUserId(long id) => $"user:{id}";
+
+ private void RemoveCache(long id)
+ {
+ _memoryCache.Remove(GenerateCacheKeyByUserId(id));
+ }
+
public async Task<CreateTokenResult> CreateToken(string username, string password)
{
+ if (username == null)
+ throw new ArgumentNullException(nameof(username));
+ if (password == null)
+ throw new ArgumentNullException(nameof(password));
+
+ // We need password info, so always check the database.
var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync();
if (user == null)
{
- _logger.LogInformation($"Create token failed with invalid username. Username = {username} Password = {password} .");
- return null;
+ var e = new UserNotExistException();
+ _logger.LogInformation(e, $"Create token failed. Reason: invalid username. Username = {username} Password = {password} .");
+ throw e;
}
- var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, password);
-
- if (verifyResult)
+ if (!_passwordService.VerifyPassword(user.EncryptedPassword, password))
{
- var roles = RoleStringToRoleArray(user.RoleString);
- var token = _jwtService.GenerateJwtToken(new TokenInfo
- {
- Name = username,
- Roles = roles
- });
- return new CreateTokenResult
- {
- Token = token,
- UserInfo = new UserInfo(username, RoleArrayToIsAdmin(roles))
- };
+ var e = new BadPasswordException();
+ _logger.LogInformation(e, $"Create token failed. Reason: invalid password. Username = {username} Password = {password} .");
+ throw e;
}
- else
+
+ var token = _jwtService.GenerateJwtToken(new TokenInfo
{
- _logger.LogInformation($"Create token failed with invalid password. Username = {username} Password = {password} .");
- return null;
- }
+ Id = user.Id,
+ Version = user.Version
+ });
+ return new CreateTokenResult
+ {
+ Token = token,
+ User = CreateUserInfo(user)
+ };
}
public async Task<UserInfo> VerifyToken(string token)
{
- var tokenInfo = _jwtService.VerifyJwtToken(token);
+ TokenInfo tokenInfo;
+ try
+ {
+ tokenInfo = _jwtService.VerifyJwtToken(token);
+ }
+ catch (JwtTokenVerifyException e)
+ {
+ _logger.LogInformation(e, $"Verify token falied. Reason: invalid token. Token: {token} .");
+ throw e;
+ }
- if (tokenInfo == null)
+ var id = tokenInfo.Id;
+ var key = GenerateCacheKeyByUserId(id);
+ if (!_memoryCache.TryGetValue<UserCache>(key, out var cache))
{
- _logger.LogInformation($"Verify token falied. Reason: invalid token. Token: {token} .");
- return null;
+ // no cache, check the database
+ var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync();
+
+ if (user == null)
+ {
+ var e = new UserNotExistException();
+ _logger.LogInformation(e, $"Verify token falied. Reason: invalid id. Token: {token} Id: {id}.");
+ throw e;
+ }
+
+ // create cache
+ cache = CreateUserCache(user);
+ _memoryCache.CreateEntry(key).SetValue(cache);
}
- return await Task.FromResult(new UserInfo(tokenInfo.Name, RoleArrayToIsAdmin(tokenInfo.Roles)));
+ if (tokenInfo.Version != cache.Version)
+ {
+ var e = new BadTokenVersionException();
+ _logger.LogInformation(e, $"Verify token falied. Reason: invalid version. Token: {token} Id: {id} Username: {cache.Username} Version: {tokenInfo.Version} Version in cache: {cache.Version}.");
+ throw e;
+ }
+
+ return cache.ToUserInfo();
}
public async Task<UserInfo> GetUser(string username)
@@ -261,7 +358,8 @@ namespace Timeline.Services
{
Name = username,
EncryptedPassword = _passwordService.HashPassword(password),
- RoleString = IsAdminToRoleString(isAdmin)
+ RoleString = IsAdminToRoleString(isAdmin),
+ Version = 0
});
await _databaseContext.SaveChangesAsync();
return PutUserResult.Created;
@@ -269,8 +367,12 @@ namespace Timeline.Services
user.EncryptedPassword = _passwordService.HashPassword(password);
user.RoleString = IsAdminToRoleString(isAdmin);
+ user.Version += 1;
await _databaseContext.SaveChangesAsync();
+ //clear cache
+ RemoveCache(user.Id);
+
return PutUserResult.Modified;
}
@@ -298,6 +400,8 @@ namespace Timeline.Services
if (modified)
{
await _databaseContext.SaveChangesAsync();
+ //clear cache
+ RemoveCache(user.Id);
}
return PatchUserResult.Success;
@@ -314,6 +418,9 @@ namespace Timeline.Services
_databaseContext.Users.Remove(user);
await _databaseContext.SaveChangesAsync();
+ //clear cache
+ RemoveCache(user.Id);
+
return DeleteUserResult.Deleted;
}
@@ -329,6 +436,9 @@ namespace Timeline.Services
user.EncryptedPassword = _passwordService.HashPassword(newPassword);
await _databaseContext.SaveChangesAsync();
+ //clear cache
+ RemoveCache(user.Id);
+
return ChangePasswordResult.Success;
}
diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs
index a865f366..83170c43 100644
--- a/Timeline/Startup.cs
+++ b/Timeline/Startup.cs
@@ -9,7 +9,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.Text;
-using System.Threading.Tasks;
using Timeline.Authenticate;
using Timeline.Configs;
using Timeline.Formatters;
@@ -87,6 +86,8 @@ namespace Timeline
services.Configure<QCloudCosConfig>(Configuration.GetSection(nameof(QCloudCosConfig)));
services.AddSingleton<IQCloudCosService, QCloudCosService>();
+
+ services.AddMemoryCache();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.