首页 > 其他分享 >用NetCore + ReactJS 实现一个前后端分离的网站 (4) 用户登录与授权

用NetCore + ReactJS 实现一个前后端分离的网站 (4) 用户登录与授权

时间:2022-12-02 12:24:05浏览次数:69  
标签:set string 登录 NetCore ReactJS new using get public

用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 });
        }
    }
}

标签:set,string,登录,NetCore,ReactJS,new,using,get,public
From: https://www.cnblogs.com/wang-yi-yi/p/16938280.html

相关文章