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中以下图获取和使用用户信息:
项目完成后,以下图: