2020/01/31, ASP.NET Core 3.1, VS2019, Microsoft.AspNetCore.Authentication.JwtBearer 3.1.1html
摘要:基於ASP.NET Core 3.1 WebApi搭建後端多層網站架構【10-使用JWT進行受權驗證】
使用JWT給網站作受權驗證前端
文章目錄git
此分支項目代碼github
本章節介紹了使用JWT給網站作受權驗證數據庫
向MS.Component.Jwt
類庫中添加Microsoft.AspNetCore.Authentication.JwtBearer
包引用:json
<ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.1" /> </ItemGroup>
在MS.Component.Jwt
類庫中引用MS.Entities
、MS.WebCore
項目
MS.Models
類庫中確保已引用MS.Component.Jwt
項目後端
在MS.WebApi
應用程序的appsettings.json
中增長JwtSetting節點:api
"JwtSetting": { "Issuer": "MS.WebHost", "Audience": "MS.Audience", "SecurityKey": "MS.WebHost SecurityKey", //more than 16 chars "LifeTime": 1440 //(minutes) token life time default:1440 m=1 day }
在MS.Component.Jwt
類庫中添加JwtSetting.cs
類:安全
namespace MS.Component.Jwt { public class JwtSetting { /// <summary> /// 頒發者 /// </summary> public string Issuer { get; set; } /// <summary> /// 受衆 /// </summary> public string Audience { get; set; } /// <summary> /// 安全密鑰 /// </summary> public string SecurityKey { get; set; } /// <summary> /// 過時時間 /// </summary> public double LifeTime { get; set; } } }
能夠使用選擇性粘貼,將json直接粘貼爲類架構
在MS.Component.Jwt
類庫中新建UserClaim文件夾,在該文件夾中新建UserClaimType.cs
、IClaimsAccessor.cs
、ClaimsAccessor.cs
、UserData.cs
類:
namespace MS.Component.Jwt.UserClaim { public static class UserClaimType { public const string Id = "http://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid"; public const string Account = "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber"; public const string Name = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"; public const string Email = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"; public const string Phone = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone"; public const string RoleName = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"; public const string RoleDisplayName = "http://schemas.xmlsoap.org/ws/2009/09/identity/claims/actor"; } }
這個類是聲明用戶信息的
裏面的值都是從System.Security.Claims.ClaimTypes裏挑選出來的值,也能夠自行定義
IClaimsAccessor接口:
namespace MS.Component.Jwt.UserClaim { public interface IClaimsAccessor { string UserName { get; } long UserId { get; } string UserAccount { get; } string UserRole { get; } string UserRoleDisplayName { get; } } }
ClaimsAccessor實現:
using Microsoft.AspNetCore.Http; using System; using System.Linq; using System.Security.Claims; namespace MS.Component.Jwt.UserClaim { public class ClaimsAccessor : IClaimsAccessor { private readonly IHttpContextAccessor _httpContextAccessor; public ClaimsAccessor(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public ClaimsPrincipal UserPrincipal { get { ClaimsPrincipal user = _httpContextAccessor.HttpContext.User; if (user.Identity.IsAuthenticated) { return user; } else { throw new Exception("用戶未認證"); } } } public string UserName { get { return UserPrincipal.Claims.First(x => x.Type == UserClaimType.Name).Value; } } public long UserId { get { return long.Parse(UserPrincipal.Claims.First(x => x.Type == UserClaimType.Id).Value); } } public string UserAccount { get { return UserPrincipal.Claims.First(x => x.Type == UserClaimType.Account).Value; } } public string UserRole { get { return UserPrincipal.Claims.First(x => x.Type == UserClaimType.RoleName).Value; } } public string UserRoleDisplayName { get { return UserPrincipal.Claims.First(x => x.Type == UserClaimType.RoleDisplayName).Value; } } } }
定義用戶信息訪問接口,開發時經過獲取IClaimsAccessor接口來獲取登陸用戶的信息。
namespace MS.Component.Jwt.UserClaim { public class UserData { public long Id { get; set; } public string Account { get; set; } public string Name { get; set; } public string Email { get; set; } public string Phone { get; set; } public string RoleName { get; set; } public string RoleDisplayName { get; set; } public string Token { get; set; } } }
定義用戶數據類
在MS.Component.Jwt
類庫中新建JwtService.cs
類:
using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using MS.Component.Jwt.UserClaim; using System; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; namespace MS.Component.Jwt { public class JwtService { private readonly JwtSetting _jwtSetting; private readonly TimeSpan _tokenLifeTime; public JwtService(IOptions<JwtSetting> options) { _jwtSetting = options.Value; _tokenLifeTime = TimeSpan.FromMinutes(options.Value.LifeTime); } /* iss (issuer):簽發人 exp (expiration time):過時時間 sub (subject):主題 aud (audience):受衆 nbf (Not Before):生效時間 iat (Issued At):簽發時間 jti (JWT ID):編號 */ /// <summary> /// 生成身份信息 /// </summary> /// <param name="userName">用戶名</param> /// <param name="roleName">登陸時的角色</param> /// <returns></returns> public Claim[] BuildClaims(UserData userData) { // 配置用戶標識 var userClaims = new Claim[] { new Claim(UserClaimType.Id,userData.Id.ToString()),//id new Claim(UserClaimType.Account,userData.Account),//account new Claim(UserClaimType.Name,userData.Name),//name new Claim(UserClaimType.RoleName,userData.RoleName),//rolename new Claim(UserClaimType.RoleDisplayName,userData.RoleDisplayName),//roledisplayname new Claim(JwtRegisteredClaimNames.Jti,userData.Id.ToString()), new Claim(JwtRegisteredClaimNames.Iat, DateTime.Now.ToString()), //new Claim(JwtRegisteredClaimNames.Iss,_jwtSetting.Issuer), //new Claim(JwtRegisteredClaimNames.Aud,_jwtSetting.Audience), //new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") , //這個就是過時時間,可自定義,注意JWT有本身的緩衝過時時間 //new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.Add(_tokenLifeTime)).ToUnixTimeSeconds()}"), }; return userClaims; } /// <summary> /// 生成jwt令牌 /// </summary> /// <param name="claims">自定義的claim</param> /// <returns></returns> public string BuildToken(Claim[] claims) { var nowTime = DateTime.Now; var creds = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSetting.SecurityKey)), SecurityAlgorithms.HmacSha256); JwtSecurityToken tokenkey = new JwtSecurityToken( issuer: _jwtSetting.Issuer, audience: _jwtSetting.Audience, claims: claims, notBefore: nowTime, expires: nowTime.Add(_tokenLifeTime), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(tokenkey); } } }
在MS.Component.Jwt
類庫中新建JwtServiceExtensions.cs
類:
using MS.Component.Jwt.UserClaim; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using System; using System.Text; namespace MS.Component.Jwt { public static class JwtServiceExtensions { public static IServiceCollection AddJwtService(this IServiceCollection services, IConfiguration configuration) { //綁定appsetting中的jwtsetting services.Configure<JwtSetting>(configuration.GetSection(nameof(JwtSetting))); //註冊jwtservice services.AddSingleton<JwtService>(); //註冊IHttpContextAccessor services.AddScoped<IHttpContextAccessor, HttpContextAccessor>(); services.AddScoped<IClaimsAccessor, ClaimsAccessor>(); var jwtConfig = configuration.GetSection("JwtSetting"); services .AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(o => { o.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig["SecurityKey"])), ValidateIssuer = true, ValidIssuer = jwtConfig["Issuer"], ValidateAudience = true, ValidAudience = jwtConfig["Audience"], //總的Token有效時間 = JwtRegisteredClaimNames.Exp + ClockSkew ; RequireExpirationTime = true, ValidateLifetime = true,// 是否驗證Token有效期,使用當前時間與Token的Claims中的NotBefore和Expires對比.同時啓用ClockSkew ClockSkew = TimeSpan.Zero //注意這是緩衝過時時間,總的有效時間等於這個時間加上jwt的過時時間,若是不配置,默認是5分鐘 }; }); return services; } } }
ValidateIssuerSigningKey = true
啓用了密鑰驗證在MS.WebApi
應用程序的Startup.cs
類中,ConfigureServices加上services.AddJwtService(Configuration);
:
在MS.WebApi
應用程序的Startup.cs
類中,中間件配置加上app.UseAuthentication();
以開啓認證中間件:
app.UseAuthentication()
是認證中間件,而app.UseAuthorization()
是受權中間件至此關於開啓jwt受權驗證、開啓認證中間件、jwt服務註冊都已完成
在MS.Models
類庫中,在ViewModel文件夾下新建LoginViewModel.cs
類:
using AutoMapper; using Microsoft.EntityFrameworkCore; using MS.Common.Security; using MS.Component.Jwt.UserClaim; using MS.DbContexts; using MS.Entities; using MS.Entities.Core; using MS.UnitOfWork; using MS.WebCore; using MS.WebCore.Core; using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; namespace MS.Models.ViewModel { public class LoginViewModel { [Display(Name = "用戶名")] [Required(ErrorMessage = "{0}必填")] [StringLength(16, ErrorMessage = "不能超過{0}個字符")] [RegularExpression(@"^[a-zA-Z0-9_]{4,16}$", ErrorMessage = "只能包含字符、數字和下劃線")] public string Account { get; set; } [Display(Name = "密碼")] [Required(ErrorMessage = "{0}必填")] public string Password { get; set; } public async Task<ExecuteResult<UserData>> LoginValidate(IUnitOfWork<MSDbContext> unitOfWork, IMapper mapper, SiteSetting siteSetting) { ExecuteResult<UserData> result = new ExecuteResult<UserData>(); //將登陸用戶查出來 var loginUserInDB = await unitOfWork.GetRepository<UserLogin>().FindAsync(Account); //用戶不存在 if (loginUserInDB is null) { return result.SetFailMessage("用戶不存在"); } //用戶被鎖定 if (loginUserInDB.IsLocked && loginUserInDB.LockedTime.HasValue && (DateTime.Now - loginUserInDB.LockedTime.Value).Minutes < siteSetting.LoginLockedTimeout) { return result.SetFailMessage(string.Format("用戶已被鎖定,請{0}分鐘後再試!", siteSetting.LoginLockedTimeout.ToString())); } //密碼正確 if (Crypto.VerifyHashedPassword(loginUserInDB.HashedPassword, Password)) { //密碼正確後才加載用戶信息、角色信息 var userInDB = await unitOfWork.GetRepository<User>().GetFirstOrDefaultAsync( predicate: a => a.Id == loginUserInDB.UserId, include: source => source .Include(u => u.Role)); //若是用戶已失效 if (userInDB.StatusCode != StatusCode.Enable) { return result.SetFailMessage("用戶已失效,請聯繫管理員!"); } //用戶正常、密碼正確,更新相應字段 loginUserInDB.IsLocked = false; loginUserInDB.AccessFailedCount = 0; loginUserInDB.LastLoginTime = DateTime.Now; //提交到數據庫 await unitOfWork.SaveChangesAsync(); //獲得userdata UserData userData = mapper.Map<UserData>(userInDB); return result.SetData(userData); } //密碼錯誤 else { loginUserInDB.AccessFailedCount++;//失敗次數累加 result.SetFailMessage("用戶名或密碼錯誤!"); //超出失敗次數限制 if (loginUserInDB.AccessFailedCount >= siteSetting.LoginFailedCountLimits) { loginUserInDB.IsLocked = true; loginUserInDB.LockedTime = DateTime.Now; result.SetFailMessage(string.Format("用戶已被鎖定,請{0}分鐘後再試!", siteSetting.LoginLockedTimeout.ToString())); } //提交到數據庫 await unitOfWork.SaveChangesAsync(); return result; } } } }
在LoginViewModel中作了核心的登陸驗證,除了驗證密碼,還會校驗用戶密碼錯誤次數,失敗次數(LoginFailedCountLimits)過多會鎖定帳號,在指定時間(LoginLockedTimeout)後才能繼續登陸,這兩個配置在SiteSetting中
在MS.Models
類庫中,在Automapper文件夾下新建UserProfile.cs
類:
using AutoMapper; using MS.Component.Jwt.UserClaim; using MS.Entities; namespace MS.Models.Automapper { public class UserProfile : Profile { public UserProfile() { CreateMap<User, UserData>() .ForMember(a => a.Id, t => t.MapFrom(b => b.Id)) .ForMember(a => a.RoleName, t => t.MapFrom(b => b.Role.Name)) .ForMember(a => a.RoleDisplayName, t => t.MapFrom(b => b.Role.DisplayName)) ; } } }
創建了User到UserData的映射配置
在MS.Services
類庫下新建Account文件夾,在該文件夾下新建IAccountService.cs
、AccountService.cs
類:
IAccountService.cs:
using MS.Component.Jwt.UserClaim; using MS.Models.ViewModel; using MS.WebCore.Core; using System.Threading.Tasks; namespace MS.Services { public interface IAccountService : IBaseService { Task<ExecuteResult<UserData>> Login(LoginViewModel viewModel); } }
AccountService.cs:
using AutoMapper; using Microsoft.Extensions.Options; using MS.Common.IDCode; using MS.Component.Jwt; using MS.Component.Jwt.UserClaim; using MS.DbContexts; using MS.Models.ViewModel; using MS.UnitOfWork; using MS.WebCore; using MS.WebCore.Core; using System.Threading.Tasks; namespace MS.Services { public class AccountService : BaseService, IAccountService { private readonly JwtService _jwtService; private readonly SiteSetting _siteSetting; public AccountService(JwtService jwtService, IOptions<SiteSetting> options, IUnitOfWork<MSDbContext> unitOfWork, IMapper mapper, IdWorker idWorker) : base(unitOfWork, mapper, idWorker) { _jwtService = jwtService; _siteSetting = options.Value; } public async Task<ExecuteResult<UserData>> Login(LoginViewModel viewModel) { var result = await viewModel.LoginValidate(_unitOfWork, _mapper, _siteSetting); if (result.IsSucceed) { result.Result.Token = _jwtService.BuildToken(_jwtService.BuildClaims(result.Result)); return new ExecuteResult<UserData>(result.Result); } else { return new ExecuteResult<UserData>(result.Message); } } } }
在MS.WebApi
應用程序的Controllers文件夾下新建Base文件夾,在該文件夾下新建AuthorizeController.cs
類:
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace MS.WebApi.Controllers { [Route("[controller]")] [Authorize] public class AuthorizeController : ControllerBase { } }
Controllers文件夾下新建AccountController.cs
類:
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MS.Component.Jwt.UserClaim; using MS.Models.ViewModel; using MS.Services; using MS.WebCore.Core; using System.Threading.Tasks; namespace MS.WebApi.Controllers { [Route("[controller]")] [ApiController] public class AccountController : AuthorizeController { private readonly IAccountService _accountService; public AccountController(IAccountService accountService) { _accountService = accountService; } [HttpPost] [AllowAnonymous] public async Task<ExecuteResult<UserData>> Login(LoginViewModel viewModel) { return await _accountService.Login(viewModel); } } }
將RoleController.cs的基類也修改成AuthorizeController:
至此全部的受權驗證已經完成了,啓動項目,打開Postman,依舊是訪問role接口,會提示401:
在Postman的MSDemo中,新建一個Login請求localhost:5000/account
,json參數爲(這是種子數據中的默認超級管理員帳號):
{ "Account":"admin", "Password":"admin" }
點擊發送,能夠看到登陸成功,返回了用戶信息及token:
咱們複製這段token,右擊MSDemo-Edit-Authorization-TYPE(Bearer Token)-把複製的token粘貼進去:
此時,MSDemo裏全部的接口請求時,都會帶上這段token,就不須要每一個請求單獨添加一次token了
也能夠看到添加上token後,接口訪問又請求成功了
以前作角色增刪改的時候,建立者和修改者都是臨時代碼,不是當前用戶真實Id,這會兒登陸作好了能夠補全了:
BaseService中添加公開類型的IClaimsAccessor成員,AccountService和RoleService的構造函數都要重構一下
在RoleService中以下圖獲取和使用用戶信息:
項目完成後,以下圖: