.net code 3.1 一个网站可以同时使用网页登录和使用JWT方式访问API
主要用于网站以.Net Core 3.1 Web MVC网站,其中有部分数据需要建立API通过外部Winform程序使用。
主要代码
Startup中的ConfigureServices:
//增加网页登录方式,直接使用自带的Identity services.AddIdentity<ApplicationUser, ApplicationRole>(options => { options.Password.RequireDigit = false; options.Password.RequireLowercase = false; options.Password.RequireNonAlphanumeric = false; options.Password.RequireUppercase = false; options.Password.RequiredLength = 4; options.Password.RequiredUniqueChars = 1; options.User.AllowedUserNameCharacters = ""; }) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); //增加API验证方式 services.AddAuthentication() .AddJwtBearer("JwtBearerLogin", options=> {//用于AccessToken options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { ValidIssuer = JwtSetting.Issuer, ValidAudience = JwtSetting.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSetting.SecretKey)), ValidateLifetime = true }; }).AddJwtBearer("JwtBearerRefreshTokenLogin", options => {//用户RefreshToken options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { ValidIssuer = JwtSetting.Issuer, ValidAudience = JwtSetting.RefreshTokenAudience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSetting.SecretKey)), ValidateLifetime = true }; });
API中的AuthorizeController,用户提供JWT的登录和获取新的Token
//API登录 [HttpPost("/api/Token")] public async Task<IActionResult> TokenAsync([FromBody]Models.AuthorizeViewModels.LoginViewModel viewModel) { if (!ModelState.IsValid)//判断是否合法 { return BadRequest("验证错误!"); } var user = await _userManager.FindByNameAsync(viewModel.UserName); if (user == null) { return BadRequest("输入的用户名或密码错误!"); } if (!await _userManager.CheckPasswordAsync(user, viewModel.Password)) { return BadRequest("输入的用户名或密码错误!"); } _logger.LogInformation("用户" + user.UserName + "通过API Token登录!"); var th = new TokenHepler(_userManager); var res = th.CreateToken(user); res.UserName = user.UserName; return Ok(res); } //使用RefreshToken获取新的AccessToken //[Authorize(AuthenticationSchemes = "JwtBearerRefreshTokenLogin")]代表使用第二种API验证规则,此规则仅用于验证RefreshToken [HttpGet("/api/RefreshToken", Name = nameof(RefreshTokenAsync))] [Authorize(AuthenticationSchemes = "JwtBearerRefreshTokenLogin")] public async Task<IActionResult> RefreshTokenAsync() { var th = new TokenHepler(_userManager); _logger.LogInformation("用户通过API RefreshToken刷新Token!"); return Ok(await th.RefreshTokenAsync(Request.HttpContext.User)); }
相关辅助代码:
TokenHelper:
public class TokenHepler { private readonly UserManager<ApplicationUser> _userManager; public TokenHepler(UserManager<ApplicationUser> options) { _userManager = options; } public TokenViewModel CreateAccessToken(ApplicationUser user) { Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(ClaimTypes.Name, user.UserName) }; return CreateToken(claims, TokenType.AccessToken); } public ComplexTokenViewModel CreateToken(ApplicationUser user) { Claim[] claims = new Claim[] { new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(ClaimTypes.Name, user.UserName) }; return CreateToken(claims); } public ComplexTokenViewModel CreateToken(Claim[] claims) { return new ComplexTokenViewModel { AccessToken = CreateToken(claims, TokenType.AccessToken), RefreshToken = CreateToken(claims, TokenType.RefreshToken) }; } /// <summary> /// 用于创建AccessToken和RefreshToken。 /// 这里AccessToken和RefreshToken只是过期时间不同,【实际项目】中二者的claims内容可能会不同。 /// 因为RefreshToken只是用于刷新AccessToken,其内容可以简单一些。 /// 而AccessToken可能会附加一些其他的Claim。 /// </summary> /// <param name="claims"></param> /// <param name="tokenType"></param> /// <returns></returns> private TokenViewModel CreateToken(Claim[] claims, TokenType tokenType) { var now = DateTime.Now; var expires = now.Add(TimeSpan.FromMinutes(tokenType.Equals(TokenType.AccessToken) ? JwtSetting.ExpiresMinutes : JwtSetting.RefreshTokenExpiresMinutes)); var token = new JwtSecurityToken( issuer: JwtSetting.Issuer, audience: tokenType.Equals(TokenType.AccessToken) ? JwtSetting.Audience : JwtSetting.RefreshTokenAudience, claims: claims, notBefore: now, expires: expires, signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSetting.SecretKey)), SecurityAlgorithms.HmacSha256)); return new TokenViewModel { Token = new JwtSecurityTokenHandler().WriteToken(token), Expires = expires }; } public async Task<TokenViewModel> RefreshTokenAsync(ClaimsPrincipal claimsPrincipal) { var code = claimsPrincipal.Claims.FirstOrDefault(m => m.Type.Equals(ClaimTypes.NameIdentifier)); if (null != code) { return CreateAccessToken(await _userManager.FindByIdAsync(code.Value)); } else { return null; } } } public enum TokenType { AccessToken = 1, RefreshToken = 2 }
TokenModel
public class TokenViewModel { public string Token { get; set; } public DateTime Expires { get; set; } } public class ComplexTokenViewModel { public TokenViewModel AccessToken { get; set; } public TokenViewModel RefreshToken { get; set; } public string UserName { get; set; } }
JwtSetting只是一个简单的用于保存配置文件的静态类
编写需要验证的API时在顶部增加
[ApiController] [Route("/api/DictionaryClasses/{ClassKey}/Dictionarys")] [Authorize(AuthenticationSchemes = "JwtBearerLogin")] public class DictionaryController : ControllerBase
来选择第一种JWT验证方式
需要网页登录的Controller直接使用 [Authorize] 即可。
Winform中的登录方式:
var model = new LoginViewModel() { UserName = userName, Password = password }; var json = JsonConvert.SerializeObject(model); var res = await HttpHelper.DoJsonPostAsync(LoginUrl, json); if (res.IsSuccessStatusCode) { var tokenJson = await res.Content.ReadAsStringAsync(); var complexToken = JsonConvert.DeserializeObject<ComplexTokenViewModel>(tokenJson); var lm = new LoginMessage() { AccessToken = complexToken.AccessToken.Token, AccessTokenExpires = complexToken.AccessToken.Expires, RefreshToken = complexToken.RefreshToken.Token, RefreshTokenExpires = complexToken.RefreshToken.Expires, UserName = complexToken.UserName }; loginMessage = lm; MessageBox.Show("登录成功,欢迎您:" + complexToken.UserName); this.DialogResult = DialogResult.OK; this.Close(); } else if (res.StatusCode == System.Net.HttpStatusCode.NotFound) { MessageBox.Show("无法找到服务器!", "系统错误", MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } else if (res.StatusCode == System.Net.HttpStatusCode.BadRequest) { MessageBox.Show(await res.Content.ReadAsStringAsync());//登录的错误信息API通过BadRequest发送 } else { MessageBox.Show(res.StatusCode.ToString()); }
主要方法是官方文档的《使用 ASP.NET Core 中的特定方案授权》可以配置多种授权方式