From a97a7c58c6d7b998432cbe3b09cedee463c46653 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sat, 20 Jul 2019 17:52:30 +0800 Subject: Add version column in user database. --- Timeline/Services/JwtService.cs | 1 - 1 file changed, 1 deletion(-) (limited to 'Timeline/Services/JwtService.cs') diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs index f5df59a5..2139ba56 100644 --- a/Timeline/Services/JwtService.cs +++ b/Timeline/Services/JwtService.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Security.Claims; using System.Text; using Timeline.Configs; -using Timeline.Entities; namespace Timeline.Services { -- cgit v1.2.3 From 615ffca61fcc90b11b04c8d115018a26a4a63a33 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sat, 20 Jul 2019 23:47:41 +0800 Subject: WIP: Change the JwtService. --- Timeline/Authenticate/AuthHandler.cs | 4 +-- Timeline/Configs/JwtConfig.cs | 6 ++++ Timeline/Services/JwtService.cs | 62 ++++++++++++++++++++++++------------ 3 files changed, 49 insertions(+), 23 deletions(-) (limited to 'Timeline/Services/JwtService.cs') diff --git a/Timeline/Authenticate/AuthHandler.cs b/Timeline/Authenticate/AuthHandler.cs index 71d8aeaa..63442481 100644 --- a/Timeline/Authenticate/AuthHandler.cs +++ b/Timeline/Authenticate/AuthHandler.cs @@ -23,9 +23,7 @@ namespace Timeline.Authenticate /// public string TokenQueryParamKey { get; set; } = "token"; - public TokenValidationParameters TokenValidationParameters { get; - set; } - = new TokenValidationParameters(); + public TokenValidationParameters TokenValidationParameters { get; set; } = new TokenValidationParameters(); } class AuthHandler : AuthenticationHandler diff --git a/Timeline/Configs/JwtConfig.cs b/Timeline/Configs/JwtConfig.cs index 9550424e..1b395650 100644 --- a/Timeline/Configs/JwtConfig.cs +++ b/Timeline/Configs/JwtConfig.cs @@ -5,5 +5,11 @@ namespace Timeline.Configs public string Issuer { get; set; } public string Audience { get; set; } public string SigningKey { get; set; } + + /// + /// Set the default value of expire offset of jwt token. + /// Unit is second. Default is 3600 seconds, aka 1 hour. + /// + public long DefaultExpireOffset { get; set; } = 3600; } } diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs index 2139ba56..e7f5690d 100644 --- a/Timeline/Services/JwtService.cs +++ b/Timeline/Services/JwtService.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using System; using System.IdentityModel.Tokens.Jwt; -using System.Linq; using System.Security.Claims; using System.Text; using Timeline.Configs; @@ -13,30 +12,45 @@ namespace Timeline.Services public class TokenInfo { public string Name { get; set; } - public string[] Roles { get; set; } + public long Version { get; set; } + } + + [Serializable] + public class JwtTokenVerifyException : Exception + { + public JwtTokenVerifyException() { } + public JwtTokenVerifyException(string message) : base(message) { } + public JwtTokenVerifyException(string message, Exception inner) : base(message, inner) { } + protected JwtTokenVerifyException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } } public interface IJwtService { /// - /// Create a JWT token for a given user info. + /// Create a JWT token for a given token info. /// /// The info to generate token. + /// The expire time. If null then use current time with offset in config. /// Return the generated token. - string GenerateJwtToken(TokenInfo tokenInfo); + string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null); /// /// Verify a JWT token. /// Return null is is null. /// /// The token string to verify. - /// Return null if is null or token is invalid. Return the saved info otherwise. + /// Return null if is null. Return the saved info otherwise. + /// Thrown when the token is invalid. TokenInfo VerifyJwtToken(string token); } public class JwtService : IJwtService { + private const string VersionClaimType = "timeline_version"; + private readonly IOptionsMonitor _jwtConfig; private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); private readonly ILogger _logger; @@ -47,30 +61,28 @@ namespace Timeline.Services _logger = logger; } - public string GenerateJwtToken(TokenInfo tokenInfo) + public string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null) { if (tokenInfo == null) throw new ArgumentNullException(nameof(tokenInfo)); if (tokenInfo.Name == null) - throw new ArgumentException("Name is null.", nameof(tokenInfo)); - if (tokenInfo.Roles == null) - throw new ArgumentException("Roles is null.", nameof(tokenInfo)); + throw new ArgumentException("Name of token info is null.", nameof(tokenInfo)); - var jwtConfig = _jwtConfig.CurrentValue; + var config = _jwtConfig.CurrentValue; var identity = new ClaimsIdentity(); identity.AddClaim(new Claim(identity.NameClaimType, tokenInfo.Name)); - identity.AddClaims(tokenInfo.Roles.Select(role => new Claim(identity.RoleClaimType, role))); + identity.AddClaim(new Claim(VersionClaimType, tokenInfo.Version.ToString(), ClaimValueTypes.Integer64)); var tokenDescriptor = new SecurityTokenDescriptor() { Subject = identity, - Issuer = jwtConfig.Issuer, - Audience = jwtConfig.Audience, + Issuer = config.Issuer, + Audience = config.Audience, SigningCredentials = new SigningCredentials( - new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.SigningKey)), SecurityAlgorithms.HmacSha384), + new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)), SecurityAlgorithms.HmacSha384), IssuedAt = DateTime.Now, - Expires = DateTime.Now.AddDays(1) + Expires = expires.GetValueOrDefault(DateTime.Now.AddSeconds(config.DefaultExpireOffset)) }; var token = _tokenHandler.CreateToken(tokenDescriptor); @@ -97,18 +109,28 @@ namespace Timeline.Services ValidIssuer = config.Issuer, ValidAudience = config.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)) - }, out SecurityToken validatedToken); + }, out _); + + var versionClaim = principal.FindFirstValue(VersionClaimType); + if (versionClaim == null) + throw new JwtTokenVerifyException("Version claim does not exist."); + if (!long.TryParse(versionClaim, out var version)) + throw new JwtTokenVerifyException("Can't convert version claim into a integer number."); return new TokenInfo { Name = principal.Identity.Name, - Roles = principal.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray() + Version = version }; } - catch (Exception e) + catch (SecurityTokenException e) { - _logger.LogInformation(e, "Token validation failed! Token is {} .", token); - return null; + 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; } } } -- cgit v1.2.3 From 79fcf66b157f38199771d30c0fd0cedbfbc786f2 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sun, 21 Jul 2019 22:58:27 +0800 Subject: WIP: change UserService. --- Timeline/Controllers/TokenController.cs | 2 +- Timeline/Entities/UserInfo.cs | 4 - Timeline/Entities/UserUtility.cs | 14 ++- Timeline/Services/JwtService.cs | 29 +++--- Timeline/Services/UserService.cs | 174 ++++++++++++++++++++++++++------ Timeline/Startup.cs | 3 +- 6 files changed, 172 insertions(+), 54 deletions(-) (limited to 'Timeline/Services/JwtService.cs') 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 /// The info to generate token. /// The expire time. If null then use current time with offset in config. /// Return the generated token. + /// Thrown when is null. string GenerateJwtToken(TokenInfo tokenInfo, DateTime? expires = null); /// @@ -41,7 +42,8 @@ namespace Timeline.Services /// Return null is is null. /// /// The token string to verify. - /// Return null if is null. Return the saved info otherwise. + /// Return the saved info in token. + /// Thrown when is null. /// Thrown when the token is invalid. TokenInfo VerifyJwtToken(string token); @@ -53,25 +55,21 @@ namespace Timeline.Services private readonly IOptionsMonitor _jwtConfig; private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); - private readonly ILogger _logger; - public JwtService(IOptionsMonitor jwtConfig, ILogger logger) + public JwtService(IOptionsMonitor 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. /// - /// The username of the user to be anthenticated. - /// The password of the user to be anthenticated. - /// Return null if anthentication failed. An containing the created token and user info if anthentication succeeded. + /// The username of the user to anthenticate. + /// The password of the user to anthenticate. + /// An containing the created token and user info. + /// Thrown when or is null. + /// Thrown when the user with given username does not exist. + /// Thrown when password is wrong. Task CreateToken(string username, string password); /// @@ -95,7 +133,11 @@ namespace Timeline.Services /// If success, return the user info. /// /// The token to verify. - /// Return null if verification failed. The user info if verification succeeded. + /// The user info specified by the token. + /// Thrown when is null. + /// Thrown when the token is of bad format. Thrown by . + /// Thrown when the user specified by the token does not exist. Usually it has been deleted after the token was issued. + /// Thrown when the version in the token is expired. User needs to recreate the token. Task VerifyToken(string token); /// @@ -173,67 +215,122 @@ namespace Timeline.Services Task 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 _logger; + + private readonly IMemoryCache _memoryCache; private readonly DatabaseContext _databaseContext; + private readonly IJwtService _jwtService; private readonly IPasswordService _passwordService; private readonly IQCloudCosService _cosService; - public UserService(ILogger logger, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService, IQCloudCosService cosService) + public UserService(ILogger 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 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 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(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 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(Configuration.GetSection(nameof(QCloudCosConfig))); services.AddSingleton(); + + services.AddMemoryCache(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. -- cgit v1.2.3 From d088a03986713a71e274fc16144ca42b7f020e3f Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Tue, 23 Jul 2019 16:47:53 +0800 Subject: WIP: Develop UserService. Remove unused components. --- Timeline/Configs/JwtConfig.cs | 4 +- Timeline/Entities/PutResult.cs | 17 +++ Timeline/Entities/UserUtility.cs | 1 - Timeline/Formatters/StringInputFormatter.cs | 29 ---- Timeline/Services/JwtService.cs | 3 +- Timeline/Services/QCloudCosService.cs | 1 - Timeline/Services/UserService.cs | 204 +++++++--------------------- Timeline/Startup.cs | 12 +- 8 files changed, 72 insertions(+), 199 deletions(-) create mode 100644 Timeline/Entities/PutResult.cs delete mode 100644 Timeline/Formatters/StringInputFormatter.cs (limited to 'Timeline/Services/JwtService.cs') diff --git a/Timeline/Configs/JwtConfig.cs b/Timeline/Configs/JwtConfig.cs index 1b395650..4d5ef97f 100644 --- a/Timeline/Configs/JwtConfig.cs +++ b/Timeline/Configs/JwtConfig.cs @@ -8,8 +8,8 @@ namespace Timeline.Configs /// /// Set the default value of expire offset of jwt token. - /// Unit is second. Default is 3600 seconds, aka 1 hour. + /// Unit is second. Default is 3600 * 24 seconds, aka 1 day. /// - public long DefaultExpireOffset { get; set; } = 3600; + public long DefaultExpireOffset { get; set; } = 3600 * 24; } } diff --git a/Timeline/Entities/PutResult.cs b/Timeline/Entities/PutResult.cs new file mode 100644 index 00000000..4ed48572 --- /dev/null +++ b/Timeline/Entities/PutResult.cs @@ -0,0 +1,17 @@ +namespace Timeline.Entities +{ + /// + /// Represents the result of a "put" operation. + /// + public enum PutResult + { + /// + /// Indicates the item did not exist and now is created. + /// + Created, + /// + /// Indicates the item exists already and is modified. + /// + Modified + } +} diff --git a/Timeline/Entities/UserUtility.cs b/Timeline/Entities/UserUtility.cs index 1de7ac7d..c8e82fba 100644 --- a/Timeline/Entities/UserUtility.cs +++ b/Timeline/Entities/UserUtility.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Timeline.Entities; using Timeline.Models; using Timeline.Services; diff --git a/Timeline/Formatters/StringInputFormatter.cs b/Timeline/Formatters/StringInputFormatter.cs deleted file mode 100644 index ca9216d7..00000000 --- a/Timeline/Formatters/StringInputFormatter.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.Net.Http.Headers; -using System.IO; -using System.Text; -using System.Threading.Tasks; - -namespace Timeline.Formatters -{ - public class StringInputFormatter : TextInputFormatter - { - public StringInputFormatter() - { - SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/plain")); - - SupportedEncodings.Add(Encoding.UTF8); - SupportedEncodings.Add(Encoding.Unicode); - } - - public override Task ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding) - { - var request = context.HttpContext.Request; - using (var reader = new StreamReader(request.Body, effectiveEncoding)) - { - var stringContent = reader.ReadToEnd(); - return InputFormatterResult.SuccessAsync(stringContent); - } - } - } -} diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs index b070ad62..f721971b 100644 --- a/Timeline/Services/JwtService.cs +++ b/Timeline/Services/JwtService.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using System; using System.IdentityModel.Tokens.Jwt; diff --git a/Timeline/Services/QCloudCosService.cs b/Timeline/Services/QCloudCosService.cs index b37631e5..748173c4 100644 --- a/Timeline/Services/QCloudCosService.cs +++ b/Timeline/Services/QCloudCosService.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 49c9747d..ec8e5091 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -50,70 +50,6 @@ namespace Timeline.Services System.Runtime.Serialization.StreamingContext context) : base(info, context) { } } - public enum PutUserResult - { - /// - /// A new user is created. - /// - Created, - /// - /// A existing user is modified. - /// - Modified - } - - public enum PatchUserResult - { - /// - /// Succeed to modify user. - /// - Success, - /// - /// A user of given username does not exist. - /// - NotExists - } - - public enum DeleteUserResult - { - /// - /// A existing user is deleted. - /// - Deleted, - /// - /// A user of given username does not exist. - /// - NotExists - } - - public enum ChangePasswordResult - { - /// - /// Success to change password. - /// - Success, - /// - /// The user does not exists. - /// - NotExists, - /// - /// Old password is wrong. - /// - BadOldPassword - } - - public enum PutAvatarResult - { - /// - /// Success to upload avatar. - /// - Success, - /// - /// The user does not exists. - /// - UserNotExists - } - public interface IUserService { /// @@ -160,31 +96,29 @@ namespace Timeline.Services /// /// Username of user. /// Password of user. - /// Array of roles of user. - /// Return if a new user is created. - /// Return if a existing user is modified. - Task PutUser(string username, string password, bool isAdmin); + /// Whether the user is administrator. + /// Return if a new user is created. + /// Return if a existing user is modified. + /// Thrown when or is null. + Task PutUser(string username, string password, bool isAdmin); /// - /// Partially modify a use of given username. + /// Partially modify a user of given username. /// - /// Username of the user to modify. - /// New password. If not modify, then null. - /// New roles. If not modify, then null. - /// Return if modification succeeds. - /// Return if the user of given username doesn't exist. - Task PatchUser(string username, string password, bool? isAdmin); + /// Username of the user to modify. Can't be null. + /// New password. Null if not modify. + /// Whether the user is administrator. Null if not modify. + /// Thrown if is null. + /// Thrown if the user with given username does not exist. + Task PatchUser(string username, string password, bool? isAdmin); /// /// Delete a user of given username. - /// Return if the user is deleted. - /// Return if the user of given username - /// does not exist. /// - /// Username of thet user to delete. - /// if the user is deleted. - /// if the user doesn't exist. - Task DeleteUser(string username); + /// Username of thet user to delete. Can't be null. + /// Thrown if is null. + /// Thrown if the user with given username does not exist. + Task DeleteUser(string username); /// /// Try to change a user's password with old password. @@ -192,27 +126,10 @@ namespace Timeline.Services /// The name of user to change password of. /// The user's old password. /// The user's new password. - /// if success. - /// if user does not exist. - /// if old password is wrong. - Task ChangePassword(string username, string oldPassword, string newPassword); - - /// - /// Get the true avatar url of a user. - /// - /// The name of user. - /// The url if user exists. Null if user does not exist. - Task GetAvatarUrl(string username); - - /// - /// Put a avatar of a user. - /// - /// The name of user. - /// The data of avatar image. - /// The mime type of the image. - /// Return if success. - /// Return if user does not exist. - Task PutAvatar(string username, byte[] data, string mimeType); + /// Thrown if or or is null. + /// Thrown if the user with given username does not exist. + /// Thrown if the old password is wrong. + Task ChangePassword(string username, string oldPassword, string newPassword); } internal class UserCache @@ -348,8 +265,13 @@ namespace Timeline.Services .ToArrayAsync(); } - public async Task PutUser(string username, string password, bool isAdmin) + public async Task PutUser(string username, string password, bool isAdmin) { + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (password == null) + throw new ArgumentNullException(nameof(password)); + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); if (user == null) @@ -362,7 +284,7 @@ namespace Timeline.Services Version = 0 }); await _databaseContext.SaveChangesAsync(); - return PutUserResult.Created; + return PutResult.Created; } user.EncryptedPassword = _passwordService.HashPassword(password); @@ -373,15 +295,17 @@ namespace Timeline.Services //clear cache RemoveCache(user.Id); - return PutUserResult.Modified; + return PutResult.Modified; } - public async Task PatchUser(string username, string password, bool? isAdmin) + public async Task PatchUser(string username, string password, bool? isAdmin) { - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + if (username == null) + throw new ArgumentNullException(nameof(username)); + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); if (user == null) - return PatchUserResult.NotExists; + throw new UserNotExistException(); bool modified = false; @@ -399,78 +323,50 @@ namespace Timeline.Services if (modified) { + user.Version += 1; await _databaseContext.SaveChangesAsync(); //clear cache RemoveCache(user.Id); } - - return PatchUserResult.Success; } - public async Task DeleteUser(string username) + public async Task DeleteUser(string username) { - var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + if (username == null) + throw new ArgumentNullException(nameof(username)); + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); if (user == null) - { - return DeleteUserResult.NotExists; - } + throw new UserNotExistException(); _databaseContext.Users.Remove(user); await _databaseContext.SaveChangesAsync(); //clear cache RemoveCache(user.Id); - - return DeleteUserResult.Deleted; } - public async Task ChangePassword(string username, string oldPassword, string newPassword) + public async Task ChangePassword(string username, string oldPassword, string newPassword) { + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (oldPassword == null) + throw new ArgumentNullException(nameof(oldPassword)); + if (newPassword == null) + throw new ArgumentNullException(nameof(newPassword)); + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); if (user == null) - return ChangePasswordResult.NotExists; + throw new UserNotExistException(); var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, oldPassword); if (!verifyResult) - return ChangePasswordResult.BadOldPassword; + throw new BadPasswordException(); user.EncryptedPassword = _passwordService.HashPassword(newPassword); + user.Version += 1; await _databaseContext.SaveChangesAsync(); //clear cache RemoveCache(user.Id); - - return ChangePasswordResult.Success; - } - - public async Task GetAvatarUrl(string username) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - - if ((await GetUser(username)) == null) - return null; - - var exists = await _cosService.IsObjectExists("avatar", username); - if (exists) - return _cosService.GenerateObjectGetUrl("avatar", username); - else - return _cosService.GenerateObjectGetUrl("avatar", "__default"); - } - - public async Task PutAvatar(string username, byte[] data, string mimeType) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (data == null) - throw new ArgumentNullException(nameof(data)); - if (mimeType == null) - throw new ArgumentNullException(nameof(mimeType)); - - if ((await GetUser(username)) == null) - return PutAvatarResult.UserNotExists; - - await _cosService.PutObject("avatar", username, data, mimeType); - return PutAvatarResult.Success; } } } diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 374b918a..a6965190 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -1,4 +1,3 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpOverrides; @@ -9,7 +8,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Timeline.Authenticate; using Timeline.Configs; -using Timeline.Formatters; using Timeline.Models; using Timeline.Services; @@ -31,10 +29,7 @@ namespace Timeline // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddMvc(options => - { - options.InputFormatters.Add(new StringInputFormatter()); - }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2); + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddCors(options => { @@ -50,7 +45,7 @@ namespace Timeline services.Configure(Configuration.GetSection(nameof(JwtConfig))); var jwtConfig = Configuration.GetSection(nameof(JwtConfig)).Get(); - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + services.AddAuthentication(AuthConstants.Scheme) .AddScheme(AuthConstants.Scheme, AuthConstants.DisplayName, o => { }); services.AddScoped(); @@ -73,9 +68,6 @@ namespace Timeline services.AddHttpClient(); - services.Configure(Configuration.GetSection(nameof(QCloudCosConfig))); - services.AddSingleton(); - services.AddMemoryCache(); } -- cgit v1.2.3 From cccaf4cfdc1b932882768975f409763c22ed1ee1 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Tue, 23 Jul 2019 19:03:25 +0800 Subject: Fix bugs and write unit tests. --- Timeline.Tests/AuthorizationUnitTest.cs | 18 +++++++++--------- .../Authentication/AuthenticationExtensions.cs | 9 +-------- Timeline.Tests/JwtTokenUnitTest.cs | 22 ++++++---------------- Timeline/Authenticate/AuthHandler.cs | 2 +- Timeline/Controllers/UserTestController.cs | 11 ++++++----- Timeline/Services/JwtService.cs | 2 +- Timeline/Services/PasswordService.cs | 2 ++ Timeline/Services/UserService.cs | 4 +--- 8 files changed, 27 insertions(+), 43 deletions(-) (limited to 'Timeline/Services/JwtService.cs') diff --git a/Timeline.Tests/AuthorizationUnitTest.cs b/Timeline.Tests/AuthorizationUnitTest.cs index 28715ada..ee3deac8 100644 --- a/Timeline.Tests/AuthorizationUnitTest.cs +++ b/Timeline.Tests/AuthorizationUnitTest.cs @@ -10,9 +10,9 @@ namespace Timeline.Tests { public class AuthorizationUnitTest : IClassFixture> { - private const string NeedAuthorizeUrl = "Test/User/NeedAuthorize"; - private const string BothUserAndAdminUrl = "Test/User/BothUserAndAdmin"; - private const string OnlyAdminUrl = "Test/User/OnlyAdmin"; + private const string AuthorizeUrl = "Test/User/Authorize"; + private const string UserUrl = "Test/User/User"; + private const string AdminUrl = "Test/User/Admin"; private readonly WebApplicationFactory _factory; @@ -26,7 +26,7 @@ namespace Timeline.Tests { using (var client = _factory.CreateDefaultClient()) { - var response = await client.GetAsync(NeedAuthorizeUrl); + var response = await client.GetAsync(AuthorizeUrl); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } } @@ -36,7 +36,7 @@ namespace Timeline.Tests { using (var client = await _factory.CreateClientWithUser("user", "user")) { - var response = await client.GetAsync(NeedAuthorizeUrl); + var response = await client.GetAsync(AuthorizeUrl); Assert.Equal(HttpStatusCode.OK, response.StatusCode); } } @@ -47,9 +47,9 @@ namespace Timeline.Tests using (var client = _factory.CreateDefaultClient()) { var token = (await client.CreateUserTokenAsync("user", "user")).Token; - var response1 = await client.SendWithAuthenticationAsync(token, BothUserAndAdminUrl); + var response1 = await client.SendWithAuthenticationAsync(token, UserUrl); Assert.Equal(HttpStatusCode.OK, response1.StatusCode); - var response2 = await client.SendWithAuthenticationAsync(token, OnlyAdminUrl); + var response2 = await client.SendWithAuthenticationAsync(token, AdminUrl); Assert.Equal(HttpStatusCode.Forbidden, response2.StatusCode); } } @@ -59,9 +59,9 @@ namespace Timeline.Tests { using (var client = await _factory.CreateClientWithUser("admin", "admin")) { - var response1 = await client.GetAsync(BothUserAndAdminUrl); + var response1 = await client.GetAsync(UserUrl); Assert.Equal(HttpStatusCode.OK, response1.StatusCode); - var response2 = await client.GetAsync(OnlyAdminUrl); + var response2 = await client.GetAsync(AdminUrl); Assert.Equal(HttpStatusCode.OK, response2.StatusCode); } } diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs index cda9fe99..f4e2e45a 100644 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs +++ b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs @@ -1,11 +1,9 @@ using Microsoft.AspNetCore.Mvc.Testing; using Newtonsoft.Json; using System; -using System.Net; using System.Net.Http; using System.Threading.Tasks; using Timeline.Entities.Http; -using Xunit; namespace Timeline.Tests.Helpers.Authentication { @@ -13,15 +11,10 @@ namespace Timeline.Tests.Helpers.Authentication { private const string CreateTokenUrl = "/token/create"; - public static async Task CreateUserTokenAsync(this HttpClient client, string username, string password, bool assertSuccess = true) + public static async Task CreateUserTokenAsync(this HttpClient client, string username, string password) { var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password }); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - if (assertSuccess) - Assert.True(result.Success); - return result; } diff --git a/Timeline.Tests/JwtTokenUnitTest.cs b/Timeline.Tests/JwtTokenUnitTest.cs index a4e5432f..6ab4e8a6 100644 --- a/Timeline.Tests/JwtTokenUnitTest.cs +++ b/Timeline.Tests/JwtTokenUnitTest.cs @@ -28,11 +28,7 @@ namespace Timeline.Tests using (var client = _factory.CreateDefaultClient()) { var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "???", Password = "???" }); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - Assert.False(result.Success); - Assert.Null(result.Token); - Assert.Null(result.UserInfo); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } } @@ -44,9 +40,8 @@ namespace Timeline.Tests var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = "user", Password = "user" }); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - Assert.True(result.Success); Assert.NotNull(result.Token); - Assert.NotNull(result.UserInfo); + Assert.NotNull(result.User); } } @@ -56,11 +51,7 @@ namespace Timeline.Tests using (var client = _factory.CreateDefaultClient()) { var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = "bad token hahaha" }); - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - - var validationInfo = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - Assert.False(validationInfo.IsValid); - Assert.Null(validationInfo.UserInfo); + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } } @@ -75,10 +66,9 @@ namespace Timeline.Tests Assert.Equal(HttpStatusCode.OK, response.StatusCode); var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - Assert.True(result.IsValid); - Assert.NotNull(result.UserInfo); - Assert.Equal(createTokenResult.UserInfo.Username, result.UserInfo.Username); - Assert.Equal(createTokenResult.UserInfo.IsAdmin, result.UserInfo.IsAdmin); + Assert.NotNull(result.User); + Assert.Equal(createTokenResult.User.Username, result.User.Username); + Assert.Equal(createTokenResult.User.IsAdmin, result.User.IsAdmin); } } } diff --git a/Timeline/Authenticate/AuthHandler.cs b/Timeline/Authenticate/AuthHandler.cs index 80bbaf14..80860edf 100644 --- a/Timeline/Authenticate/AuthHandler.cs +++ b/Timeline/Authenticate/AuthHandler.cs @@ -78,7 +78,7 @@ namespace Timeline.Authenticate { var userInfo = await _userService.VerifyToken(token); - var identity = new ClaimsIdentity(); + var identity = new ClaimsIdentity(AuthConstants.Scheme); identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); identity.AddClaims(Entities.UserUtility.IsAdminToRoleArray(userInfo.IsAdmin).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); diff --git a/Timeline/Controllers/UserTestController.cs b/Timeline/Controllers/UserTestController.cs index f1edb0d5..21686b81 100644 --- a/Timeline/Controllers/UserTestController.cs +++ b/Timeline/Controllers/UserTestController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Timeline.Authenticate; namespace Timeline.Controllers { @@ -8,21 +9,21 @@ namespace Timeline.Controllers { [HttpGet("[action]")] [Authorize] - public ActionResult NeedAuthorize() + public ActionResult Authorize() { return Ok(); } [HttpGet("[action]")] - [Authorize(Roles = "user,admin")] - public ActionResult BothUserAndAdmin() + [UserAuthorize] + public new ActionResult User() { return Ok(); } [HttpGet("[action]")] - [Authorize(Roles = "admin")] - public ActionResult OnlyAdmin() + [AdminAuthorize] + public ActionResult Admin() { return Ok(); } diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs index f721971b..e970bbd4 100644 --- a/Timeline/Services/JwtService.cs +++ b/Timeline/Services/JwtService.cs @@ -126,7 +126,7 @@ namespace Timeline.Services Version = version }; } - catch (SecurityTokenException e) + catch (Exception e) { throw new JwtTokenVerifyException("Validate token failed caused by a SecurityTokenException. See inner exception.", e); } diff --git a/Timeline/Services/PasswordService.cs b/Timeline/Services/PasswordService.cs index 8eab526e..106080f1 100644 --- a/Timeline/Services/PasswordService.cs +++ b/Timeline/Services/PasswordService.cs @@ -24,6 +24,8 @@ namespace Timeline.Services bool VerifyPassword(string hashedPassword, string providedPassword); } + //TODO! Use exceptions!!! + /// /// Copied from https://github.com/aspnet/AspNetCore/blob/master/src/Identity/Extensions.Core/src/PasswordHasher.cs /// Remove V2 format and unnecessary format version check. diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index ec8e5091..01d05903 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -153,16 +153,14 @@ namespace Timeline.Services private readonly IJwtService _jwtService; private readonly IPasswordService _passwordService; - private readonly IQCloudCosService _cosService; - public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService, IQCloudCosService cosService) + public UserService(ILogger logger, IMemoryCache memoryCache, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService) { _logger = logger; _memoryCache = memoryCache; _databaseContext = databaseContext; _jwtService = jwtService; _passwordService = passwordService; - _cosService = cosService; } private string GenerateCacheKeyByUserId(long id) => $"user:{id}"; -- cgit v1.2.3