用NetCore + ReactJS 實現一個前後端分離的網站 (4) 使用者登入與授權
阿新 • • 發佈:2022-12-02
用NetCore + ReactJS 實現一個前後端分離的網站 (4) 使用者登入與授權
1. 前言
這幾天學了一些前端的知識,用Ant Design Pro
的腳手架搭建了一個前端專案->這裡。
登入介面是現成的,所以回到後端來完成相應的API。
2. 登入與授權
2.1. 首先利用EFCore的Migration功能建立資料表,並新增種子資料。
User.cs
using Microsoft.EntityFrameworkCore.Metadata.Internal; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; namespace NovelTogether.Core.Model.Models { public class User { [Key] public int ID { get; set; } [Required] [Column(TypeName = "varchar")] [MaxLength(128)] [JsonPropertyName("username")] public string? UserName { get; set; } [Required] public string? Password { get; set; } [Required] public string? PasswordSalt { get; set; } [Required] [Column(TypeName = "nvarchar")] [MaxLength(128)] public string? NickName { get; set; } [Column(TypeName = "nvarchar")] [MaxLength(128)] public string? FirstName { get; set; } [Column(TypeName = "nvarchar")] [MaxLength(128)] public string? LastName { get; set; } public string? IDNumber { get; set; } public string? Email { get; set; } public string? PhoneNumner { get; set; } public string? Address { get; set; } [Required] public DateTime Birthday { get; set; } [Required] public DateTime CreatedDate { get; set; } public DateTime? ModifiedTime { get; set; } } }
ModelBuilderExtension.cs
public static void AddUserSeed(this ModelBuilder modelBuilder) { modelBuilder.Entity<User>().HasData( new User() { ID = 1, UserName = "SuperAdmin", Password = string.Empty, PasswordSalt = string.Empty, NickName = "我是超級管理員", Birthday = new DateTime(1970, 1, 1), CreatedDate = DateTime.Now }, new User() { ID = 2, UserName = "Wright", Password = "ddcddb33bd354212e2c2404fbf079a84", PasswordSalt = "qwe123!@#", NickName = "我是普通管理員", Birthday = new DateTime(1970, 1, 1), CreatedDate = DateTime.Now } ); }
2.2. 依葫蘆畫瓢新增IUserRepository, UserRepository, IUserService, UserService
,為後面的API提供服務。
2.3. 使用者身份驗證和Jwt Token授權。
先說一下登入以及API授權訪問的原理:
- 前端對使用者輸入的密碼和從後端獲取的
密碼鹽
一起用MD5
加密,然後傳到後端驗證。
為什麼要加鹽:因為常見的密碼的MD5值是固定的,資料庫要是洩露了,很容易被別人猜出來,所以資料的密碼是MD5(密碼+鹽)。
為什麼不在後端生成隨機數保護密碼的傳輸:前後端分離,所有資訊都在網路上傳輸,隨機數也不安全,只有用Https
來保護資料,所以不需要加額外的資料安全措施。
- 後端密碼驗證通過之後,生成一個Jwt Token,並在Payload中存放一個userid通過SetCookie的方式在前端寫入
HttpOnly
的Cookie,確保這個Cookie前端JS的無法訪問和修改
的。 - 前端後續的API請求會自動帶上這個Cookie,後端接收到請求後,通過中介軟體取出Jwt Token,驗證通過後取出其中存放的userid放入
上下文
中。 - 所有的API就可以從上下文中得到當前使用者的id。
下面是相應的程式碼:
2.3.1. 在ViewModels目錄下建立登入介面用的request以及response的實體類,以及一般response的實體類。
LoginModel.cs
using System.Text.Json.Serialization;
namespace NovelTogether.Core.Model.ViewModels
{
public class LoginModel
{
[JsonPropertyName("username")]
public string? UserName { get; set; }
public string? Password { get; set; }
public bool AutoLogin { get; set; }
}
}
LoginResultModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NovelTogether.Core.Model.ViewModels
{
public class LoginResultModel
{
public string? Status { get; set; }
public string? Type { get; set; }
public string? CurrentAuthority { get;set; }
public LoginResultModel Success(LoginType loginType)
{
Status = "success";
Type = loginType.ToString().ToLower();
return this;
}
public LoginResultModel Error(LoginType loginType)
{
Status = "error";
Type = loginType.ToString().ToLower();
return this;
}
}
public enum LoginType
{
Account,
Mobile
}
}
ResponseModel.cs
using System.Text.Json;
using System.Text.Json.Serialization;
namespace NovelTogether.Core.Model.ViewModels
{
public class ResponseModel<TEntity> where TEntity : class
{
public bool Success { get; set; }
public TEntity Data { get; set; }
public int ErrorCode { get; set; }
public string ErrorMessage { get; set; }
public ErrorShowType ErrorShowType { get; set; }
public ResponseModel<TEntity> Ok(TEntity entity)
{
Success = true;
Data = entity;
return this;
}
public ResponseModel<TEntity> Failed(int statusCode)
{
Success = false;
ErrorCode = statusCode;
ErrorMessage = "error";
return this;
}
public ResponseModel<TEntity> Failed(int statusCode, TEntity entity)
{
Success = false;
ErrorCode = statusCode;
ErrorMessage = JsonSerializer.Serialize(entity);
return this;
}
}
public enum ErrorShowType
{
SILENT = 0,
WARN_MESSAGE = 1,
ERROR_MESSAGE = 2,
NOTIFICATION = 3,
REDIRECT = 9,
}
}
2.3.2. 在Common中建立工具類JwtHelper.cs,用來生成Jwt Token。
JwtHelper.cs
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace NovelTogether.Core.API.Helpers.Jwt
{
public class JwtHelper
{
public static string CreateToken(string secret, string issuer, string audience, int expiredHours, List<Claim> claims)
{
//祕鑰
byte[] secretBytes = Encoding.UTF8.GetBytes(secret);
//生成祕鑰
var key = new SymmetricSecurityKey(secretBytes);
//生成數字簽名的簽名金鑰、簽名金鑰識別符號和安全演算法
var credential = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
//構建JwtSecurityToken類例項
var token = new JwtSecurityToken(
//新增頒發者
issuer: issuer,
//新增受眾
audience: audience,
//新增其他需要加密的資訊
claims,
//自定義過期時間
expires: DateTime.UtcNow.AddHours(expiredHours),
signingCredentials: credential);
//簽發token
var jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
return jwtToken;
}
}
}
2.3.3. 在API中新增Token驗證的中介軟體
。
AuthMiddleware.cs
using Microsoft.IdentityModel.Tokens;
using NovelTogether.Core.API.Utils;
using System.IdentityModel.Tokens.Jwt;
using System.Text;
namespace NovelTogether.Core.API.Middlewares
{
public class AuthMiddleware
{
private readonly RequestDelegate _next;
private readonly JwtOption _jwtOption;
public AuthMiddleware(RequestDelegate next, JwtOption jwtOption)
{
_next = next;
_jwtOption = jwtOption;
}
public async Task Invoke(HttpContext context)
{
//Get the upload token, which can be customized and extended
var token = context.Request.Cookies[Consts.TOKEN_NAME]?.Split(" ").Last();
if (token != null)
AttachTokenToContext(context, token);
await _next(context);
}
private void AttachTokenToContext(HttpContext context, string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidAudience = _jwtOption.Audience,
ValidIssuer = _jwtOption.Issuer,
ValidateIssuer = true,
ValidateAudience = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOption.Secret)),
ClockSkew = new System.TimeSpan(0, 0, 30)
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
// attach user id to context on successful jwt validation
context.Items[Consts.USER_ID] = jwtToken.Claims.First(x => x.Type == Consts.USER_ID).Value;
}
catch
{
}
}
}
}
2.3.4 在API中新增自定義屬性用替換預設的[Authorize]
屬性。
ApiAuthorizeAttribute.cs
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using NovelTogether.Core.Model.ViewModels;
using NovelTogether.Core.API.Utils;
namespace NovelTogether.Core.API.Attributes
{
public class ApiAuthorizeAttribute : Attribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
var accountId = context.HttpContext.Items[Consts.USER_ID];
if (accountId == null)
{
//context.HttpContext.Response.StatusCode = HttpStatusCode.Unauthorized.GetHashCode();
context.Result = new UnauthorizedObjectResult(new ResponseModel<string>().Failed(HttpStatusCode.Unauthorized.GetHashCode()));
}
}
}
}
2.3.5. 建立UserController,下面的程式碼中實現了三個方法:登入、登出、獲取當前使用者資訊。
獲取使用者資訊的介面受
[ApiAuthorize]
屬性保護。
UserController.cs
using Microsoft.AspNetCore.Mvc;
using NovelTogether.Core.API.Attributes;
using NovelTogether.Core.API.Helpers.Jwt;
using NovelTogether.Core.API.Utils;
using NovelTogether.Core.IService;
using NovelTogether.Core.Model.ViewModels;
using System.Net;
using System.Security.Claims;
namespace NovelTogether.Core.API.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class UserController : Controller
{
private readonly IUserService _userService;
private readonly IConfiguration _configuration;
public UserController(IUserService userService, IConfiguration configuration)
{
_userService = userService;
_configuration = configuration;
}
[HttpPost("Login")]
public async Task<LoginResultModel> Login(LoginModel loginModel)
{
// 驗證使用者名稱、密碼
var userName = loginModel.UserName;
var password = loginModel.Password;
if (string.IsNullOrEmpty(userName) || string.IsNullOrWhiteSpace(password))
{
return new LoginResultModel().Error(LoginType.Account);
}
var user = await _userService.SelectAsync(x => x.UserName != null && x.UserName.ToLower() == userName.ToLower() && x.Password == password);
if (user == null)
{
return new LoginResultModel().Error(LoginType.Account);
}
var token = JwtHelper.CreateToken(
_configuration.GetValue<string>("Config:JWT:Secret"),
_configuration.GetValue<string>("Config:JWT:Issuer"),
_configuration.GetValue<string>("Config:JWT:Audience"),
_configuration.GetValue<int>("Config:JWT:ExpiredHours"),
new List<Claim>()
{
new Claim(ClaimTypes.NameIdentifier, userName.ToLower()),
new Claim(Consts.USER_ID, user.ID.ToString())
});
// 返回JWT Token
Response.Cookies.Append(Consts.TOKEN_NAME, token, new CookieOptions
{
HttpOnly = true
});
return new LoginResultModel().Success(LoginType.Account);
}
[HttpPost("Logout")]
public ResponseModel<string> Logout()
{
Response.Cookies.Delete(Consts.TOKEN_NAME);
return new ResponseModel<string>().Ok("Logout");
}
[HttpGet]
[ApiAuthorize]
public async Task<ResponseModel<UserModel>> Get()
{
var userId = int.Parse(HttpContext.Items[Consts.USER_ID].ToString());
var user = await _userService.SelectAsync(x => x.ID == userId);
if (user == null)
{
return new ResponseModel<UserModel>().Failed(HttpStatusCode.NotFound.GetHashCode());
}
// 返回使用者資訊
return new ResponseModel<UserModel>().Ok(new UserModel() { UserName = user.UserName, NickName = user.NickName });
}
}
}