diff --git a/projects/Infrastructure/Extensions/HttpContextExtensions.cs b/projects/Infrastructure/Extensions/HttpContextExtensions.cs index 40cd41b9..28d8e61d 100644 --- a/projects/Infrastructure/Extensions/HttpContextExtensions.cs +++ b/projects/Infrastructure/Extensions/HttpContextExtensions.cs @@ -1,14 +1,14 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Text; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; -using Microsoft.IdentityModel.Tokens; namespace Infrastructure.Extensions { @@ -22,23 +22,33 @@ namespace Infrastructure.Extensions httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, new AuthenticationProperties { IsPersistent = rememberMe }); } - public static void SignIn(this HttpContext httpContext, string userName, bool rememberMe, IConfiguration cfg) + public static void SignIn(this HttpContext httpContext, string userName, bool rememberMe, IConfiguration cfg, DateTime expires) + { + var token = httpContext.GetToken(userName, rememberMe, cfg, expires); + httpContext.Response.Cookies.Delete("jwt"); + httpContext.Response.Cookies.Append("jwt", token); + } + + public static string GetToken(this HttpContext httpContext, string userName, bool rememberMe, IConfiguration cfg, DateTime expires) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(cfg["jwt:key"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var claims = new List { new Claim(ClaimTypes.Name, userName) }; - //claims.AddRange(roles.Select(o => new Claim(ClaimTypes.Role, o)).ToList()); var token = new JwtSecurityToken( issuer: cfg["jwt:issuer"], audience: cfg["jwt:audience"], claims: claims, - expires: DateTime.Now.AddMinutes(rememberMe ? 3600 : 3), + expires: expires, signingCredentials: creds); var tokenText = new JwtSecurityTokenHandler().WriteToken(token); - httpContext.Response.Cookies.Delete("jwt"); - httpContext.Response.Cookies.Append("jwt", tokenText); + return tokenText; + } + + public static JwtSecurityToken ReadToken(this HttpContext httpContext, string token) + { + return new JwtSecurityTokenHandler().ReadJwtToken(token); } } } \ No newline at end of file diff --git a/projects/Infrastructure/Infrastructure.csproj b/projects/Infrastructure/Infrastructure.csproj index 73fecc90..eceb11b1 100644 --- a/projects/Infrastructure/Infrastructure.csproj +++ b/projects/Infrastructure/Infrastructure.csproj @@ -10,6 +10,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/projects/Infrastructure/Web/BaseStartup.cs b/projects/Infrastructure/Web/BaseStartup.cs index c1263eb5..4fde99c6 100644 --- a/projects/Infrastructure/Web/BaseStartup.cs +++ b/projects/Infrastructure/Web/BaseStartup.cs @@ -19,6 +19,7 @@ using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Razor; +using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.SignalR; @@ -31,14 +32,11 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using System; using System.Diagnostics; using System.Globalization; -using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Reflection; -using System.Security.Claims; using System.Text; using System.Text.Encodings.Web; using System.Text.Unicode; @@ -117,11 +115,30 @@ namespace Infrastructure.Web return localizer; }; }); - services.AddControllers().AddNewtonsoftJson(o => + services.AddApiVersioning(o => { - o.SerializerSettings.ContractResolver = new DefaultContractResolver(); - o.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + o.ReportApiVersions = true; + o.AssumeDefaultVersionWhenUnspecified = true; + o.DefaultApiVersion = new ApiVersion(1, 0); + o.ApiVersionReader = ApiVersionReader.Combine( + new UrlSegmentApiVersionReader() + //,new QueryStringApiVersionReader() + //,new HeaderApiVersionReader() + ); }); + services.AddControllers() + .ConfigureApiBehaviorOptions(options => + { + options.SuppressConsumesConstraintForFormFileParameters = true; + options.SuppressInferBindingSourcesForParameters = true; + options.SuppressModelStateInvalidFilter = true; + options.SuppressMapClientErrors = true; + //options.ClientErrorMapping[404].Link = "https://httpstatuses.com/404"; + }) + .AddNewtonsoftJson(o => + { + o.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + }); services.Configure(o => { var supportedCultures = new[] @@ -223,7 +240,7 @@ namespace Infrastructure.Web }, OnMessageReceived = context => { - if(!context.Request.IsStatic()) + if (!context.Request.IsStatic()) { Debug.WriteLine(context.Request.Path); if (context.Request.Query.ContainsKey("access_token")) diff --git a/projects/UserCenter/Controllers/AccountController.cs b/projects/UserCenter/Controllers/AccountController.cs index 7c08b15d..90af0709 100644 --- a/projects/UserCenter/Controllers/AccountController.cs +++ b/projects/UserCenter/Controllers/AccountController.cs @@ -205,16 +205,10 @@ namespace UserCenter.Controllers } else { - //var userPermissions = this._userRepo.ReadOnlyTable().Where(o => o.UserName == userName) - // .SelectMany(o => o.UserRoles) - // .Select(o => o.Role) - // .SelectMany(o => o.RolePermissions) - // .Select(o => o.Permission.Number) - // .ToList(); - HttpContext.SignIn(model.UserName, model.RememberMe, _cfg); + HttpContext.SignIn(model.UserName, model.RememberMe, _cfg, DateTime.Now.AddDays(1)); if (string.IsNullOrEmpty(returnUrl)) { - returnUrl = Url.Action("Index","Home"); + returnUrl = Url.Action("Index", "Home"); } ViewBag.Url = returnUrl; var urls = new List(); diff --git a/projects/UserCenter/Controllers/TokenController.cs b/projects/UserCenter/Controllers/TokenController.cs new file mode 100644 index 00000000..a8399802 --- /dev/null +++ b/projects/UserCenter/Controllers/TokenController.cs @@ -0,0 +1,143 @@ +using Application.Domain.Entities; +using Application.Models; +using Infrastructure.Data; +using Infrastructure.Extensions; +using Infrastructure.Security; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using System; +using System.Linq; +using System.Net.Mime; +using System.Security.Claims; + +namespace UserCenter.Controllers +{ + [ApiVersion("1.0")] + [Route("api/[controller]/[action]")] + [Route("api/v{version:apiVersion}/[controller]/[action]")] + [ApiController] + [Produces(MediaTypeNames.Application.Json)] + public class TokenController : ControllerBase + { + private readonly IConfiguration _cfg; + private readonly IRepository _userRepo; + private readonly IEncryptionService _encryptionService; + + public TokenController(IConfiguration cfg, + IRepository userRepo, + IEncryptionService encryptionService) + { + this._cfg = cfg; + this._userRepo = userRepo; + this._encryptionService = encryptionService; + } + + [HttpPost] + public ActionResult GetToken([FromBody]LoginModel model) + { + var success = false; + try + { + var user = this._userRepo.Table().FirstOrDefault(o => o.UserName == model.UserName); + if (user == null) + { + ModelState.AddModelError("", "用户名或密码错误"); + } + else + { + var maxAccessFailedCount = this._cfg.GetValue("MaxFailedAccessAttemptsBeforeLockout"); + var lockoutEndMinutes = this._cfg.GetValue("DefaultAccountLockoutMinutes"); + + if (user.LockoutEnabled)//对已启用登录锁定的用户,如果当前登录时间超出锁定时间,先解除锁定状态 + { + if (user.LockoutEnd.HasValue && DateTime.UtcNow > user.LockoutEnd) + { + user.LockoutEnd = null; + user.AccessFailedCount = 0; + this._userRepo.SaveChanges(); + } + } + if (user.LockoutEnabled)//对启用登录锁定的用户进行验证 + { + if (user.LockoutEnd.HasValue == false) + { + if (user.PasswordHash == this._encryptionService.CreatePasswordHash(model.Password, user.SecurityStamp)) + { + user.LockoutEnd = null; + user.AccessFailedCount = 0; + success = true; + } + else + { + user.AccessFailedCount += 1; + if (user.AccessFailedCount >= maxAccessFailedCount) + { + user.LockoutEnd = DateTime.UtcNow.AddMinutes(lockoutEndMinutes); + ModelState.AddModelError(nameof(model.UserName), $"用户被锁定,请于{user.LockoutEnd.Value.ToLocalTime().ToString("HH:mm")}后重试"); + } + else + { + ModelState.AddModelError(nameof(model.UserName), $"密码错误,再错误{maxAccessFailedCount - user.AccessFailedCount}次后将锁定用户{lockoutEndMinutes}分钟"); + } + } + this._userRepo.SaveChanges(); + } + } + else//对未启用登录锁定的用户进行验证 + { + if (user.PasswordHash == this._encryptionService.CreatePasswordHash(model.Password, user.SecurityStamp)) + { + success = true; + } + else + { + ModelState.AddModelError("", "用户名或密码错误"); + } + } + } + if (success) + { + return Ok(new + { + AccessToken = Request.HttpContext.GetToken(model.UserName, false, _cfg, DateTime.Now.AddHours(_cfg.GetValue("AccessTokenHours", 0.5))), + RefreshToken = Request.HttpContext.GetToken(model.UserName, false, _cfg, DateTime.Now.AddHours(_cfg.GetValue("AccessTokenHours", 720))), + }); + } + else + { + return BadRequest(ModelState); + } + } + catch (Exception ex) + { + ex.PrintStack(); + return Problem(ex.Message); + } + } + + [HttpPost] + public ActionResult RefreshToken([FromBody]string refreshToken) + { + try + { + var token = Request.HttpContext.ReadToken(refreshToken); + if (DateTime.UtcNow > token.ValidTo) + { + ModelState.AddModelError("", "已过期"); + return BadRequest(ModelState); + } + var userName = token.Claims.FirstOrDefault(o => o.Type == ClaimTypes.Name).Value; + return Ok(new + { + AccessToken = Request.HttpContext.GetToken(userName, false, _cfg, DateTime.Now.AddHours(_cfg.GetValue("AccessTokenHours", 0.5))), + RefreshToken = Request.HttpContext.GetToken(userName, false, _cfg, DateTime.Now.AddHours(_cfg.GetValue("AccessTokenHours", 720))), + }); + } + catch (Exception ex) + { + ex.PrintStack(); + return Problem(ex.Message); + } + } + } +} \ No newline at end of file diff --git a/projects/UserCenter/Program.cs b/projects/UserCenter/Program.cs index 15f79678..6a9d0243 100644 --- a/projects/UserCenter/Program.cs +++ b/projects/UserCenter/Program.cs @@ -12,6 +12,8 @@ namespace UserCenter { WebHost.CreateDefaultBuilder(args) .Run(new List { + new EFConfigurationValue { Id = "AccessTokenHours", Value= "0.5"}, + new EFConfigurationValue { Id = "RefreshToken", Value= "720"}, new EFConfigurationValue { Id = "MaxFailedAccessAttemptsBeforeLockout", Value= "5"}, new EFConfigurationValue { Id = "DefaultAccountLockoutMinutes", Value= "10"}, new EFConfigurationValue { Id = "CaptchaSeconds", Value= "60"}, diff --git a/projects/gateway/src/main/resources/static/favicon.ico b/projects/gateway/src/main/resources/static/favicon.ico deleted file mode 100644 index 20e937b8..00000000 Binary files a/projects/gateway/src/main/resources/static/favicon.ico and /dev/null differ diff --git a/projects/gateway/src/main/resources/static/index.html b/projects/gateway/src/main/resources/static/index.html deleted file mode 100644 index deac907b..00000000 --- a/projects/gateway/src/main/resources/static/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - 网关 - - -

网关

- - \ No newline at end of file