diff options
author | 杨宇千 <crupest@outlook.com> | 2019-07-21 22:58:27 +0800 |
---|---|---|
committer | 杨宇千 <crupest@outlook.com> | 2019-07-21 22:58:27 +0800 |
commit | 79fcf66b157f38199771d30c0fd0cedbfbc786f2 (patch) | |
tree | 402aa9c3f5fae0c1825cbeecf20c5a94c371f77d | |
parent | 615ffca61fcc90b11b04c8d115018a26a4a63a33 (diff) | |
download | timeline-79fcf66b157f38199771d30c0fd0cedbfbc786f2.tar.gz timeline-79fcf66b157f38199771d30c0fd0cedbfbc786f2.tar.bz2 timeline-79fcf66b157f38199771d30c0fd0cedbfbc786f2.zip |
WIP: change UserService.
-rw-r--r-- | Timeline/Controllers/TokenController.cs | 2 | ||||
-rw-r--r-- | Timeline/Entities/UserInfo.cs | 4 | ||||
-rw-r--r-- | Timeline/Entities/UserUtility.cs | 14 | ||||
-rw-r--r-- | Timeline/Services/JwtService.cs | 29 | ||||
-rw-r--r-- | Timeline/Services/UserService.cs | 174 | ||||
-rw-r--r-- | Timeline/Startup.cs | 3 |
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. |