首頁 > 軟體

《Asp.Net Core3 + Vue3入坑教學》

2021-03-07 16:00:27

簡介

《Asp.Net Core3 + Vue3入坑教學》 此教學適合新手入門或者前後端分離嘗試者。可以根據圖文一步一步進操作編碼也可以選擇直接檢視原始碼。每一篇文章都有對應的原始碼

本文將 .Net Core 3升級成 .Net 5

目錄

《Asp.Net Core3 + Vue3入坑教學》系列教學目錄

Asp.Net Core後端專案

  1. 後端專案搭建與Swagger設定步驟
  2. 設定CROS策略解決跨域問題
  3. AutoMapper & Restful API & DI
  4. EF Core & Postgresql
  5. (本文).Net Core 3升級成 .Net 5 & JWT
  6. (暫未發表敬請期待...)

Vue3 前端專案

暫未發表敬請期待...

本文簡介

本文為《Asp.Net Core3 + Vue3入坑教學》系列教學的後端第五篇 - .Net Core 3升級成 .Net 5 & JWT。上文已經為Simple專案增加了EF Core與Postgresql資料庫的連線,本文繼續為Simple專案增加JWT(JSON Web Token)的應用,目標是讓除了使用者請求認證介面之外的其餘請求都需要帶著JWT!在使用之前先將SKD的版本升級成 .Net 5。

JWT詳解參考 https://jwt.io/introduction

把Simple專案從 .Net Core 3升級成 .Net 5 & 使用JWT

.Net Core 3升級成 .Net 5

上官網下載 .Net 5 SDK

https://dotnet.microsoft.com/download/visual-studio-sdks?utm_source=getdotnetsdk&utm_medium=referral

確保VS版本支援 .net 5 SDK(如果是2019的話,升級成最新的即可)

開啟專案,右鍵解決方案,開啟專案屬性

將目標框架調整成 .NET 5.0

更新Nuget包

專案應用JWT

在ServiceProvider資料夾下新建JWT擴充套件類

程式碼如下:


using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Simple_Asp.Net_Core.ServiceProvider
{
    public static class JWT
    {
        public static void AddJWT(this IServiceCollection services)
        {
            services.AddAuthentication(x =>
            {
                x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
           .AddJwtBearer(o =>
           {
               o.RequireHttpsMetadata = false;
               o.Events = new JwtBearerEvents()
               {
                   OnAuthenticationFailed = context =>
                   {
                       //Token expired
                       if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                           context.Response.Headers.Add("Token-Expired", "true");

                       return Task.CompletedTask;
                   },
               };

               o.TokenValidationParameters = new TokenValidationParameters
               {
                   // 是否驗證失效時間
                   ValidateLifetime = true,
                   ClockSkew = TimeSpan.FromSeconds(30),
                   ValidateAudience = true,
                   // 這裡採用動態驗證的方式,在重新登陸時,重新整理token,舊token就強制失效了
                   AudienceValidator = AudienceValidator,
                   // 是否驗證Issuer
                   ValidateIssuer = false,
                   // 是否驗證SecurityKey
                   ValidateIssuerSigningKey = true,
                   IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Const.SecurityKey))
               };
           });
        }

        private static bool AudienceValidator(IEnumerable<string> audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
        {
            return Audiences.IsNewestAudience(audiences.FirstOrDefault());
        }
    }
}

在ServiceProvider資料夾下新建新建類Audiences.cs和Const.cs

程式碼如下:

using System;
using System.Collections.Generic;

namespace Simple_Asp.Net_Core.ServiceProvider
{
    public static class Audiences
    {
        private static IDictionary<string, string> _audiences = new Dictionary<string, string>();

        public static string UpdateAudience(string userNo)
        {
            if (string.IsNullOrWhiteSpace(userNo))
                return string.Empty;

            var audience = $"{userNo}_{DateTime.Now}";
            _audiences[userNo] = audience;

            return audience;
        }

        public static bool IsNewestAudience(string audience)
        {
            if (string.IsNullOrWhiteSpace(audience))
                return false;

            var userName = audience.Split('_')[0];

            if (!_audiences.ContainsKey(userName))
                return false;
            else
                return _audiences[userName] == audience;
        }
    }
}
namespace Simple_Asp.Net_Core.ServiceProvider
{
    internal class Const
    {
        public const string Domain = "http://localhost:81";
        public const string SecurityKey = "Simple_Asp.Net_Core";
    }
}

在類中直接引入nuget包,或者使用Nuget包管理工具引入Microsoft.AspNetCore.Authentication.JwtBearer

Startup.cs 設定調整,增加認證與授權設定

程式碼如下:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json.Serialization;
using Simple_Asp.Net_Core.Data;
using Simple_Asp.Net_Core.ServiceProvider;
using System;

namespace Simple_Asp.Net_Core
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddJWT();

            services.AddDbContext<CommanderContext>(options =>
                options.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=123456"));

            services.AddCORS();
            services.AddMvc();
            services.AddSwagger();

            services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());

            services.AddScoped<ICommanderRepo, SqlCommanderRepo>();

            services.AddControllers().AddNewtonsoftJson(s =>
            {
                s.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c =>
                {
                    c.SwaggerEndpoint("/swagger/v1/swagger.json", "ApiHelp V1");
                });
            }
            app.UseCors("CorsTest");
            app.UseAuthentication();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
        }
    }
}

接下來就是應用了

首先我們在 CommandsController 控制器上增加特性 [Authorize]

然後啟動專案,可以發現,swagger上發出的請求出現了401錯誤

401的錯誤就是沒有許可權

HTTP401狀態詳解 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status/401

現在需要為swagger的請求增加Token

首先增加JWT獲取介面,新建控制器OAuthController.cs

當前模擬使用者資訊獲取與使用者資訊校驗使用者校驗

Action增加[AllowAnonymous]特性,讓此介面可以接收任何的請求

程式碼如下:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using Simple_Asp.Net_Core.Dtos;
using Simple_Asp.Net_Core.Extensions;
using Simple_Asp.Net_Core.ServiceProvider;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Text;

namespace Simple_Asp.Net_Core.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class OAuthController : ControllerBase
    {
        [HttpPost]
        [AllowAnonymous]
        public IActionResult Authenticate(string name, string password)
        {
            // 此處需補充使用者校驗與使用者具體資訊獲取...
            
            var user = new UserProviderDto(name, password);
            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Encoding.ASCII.GetBytes(Const.SecurityKey);
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Audience = Audiences.UpdateAudience(user.Name),
                Subject = user.GetClaimsIdentity(),
                Expires = DateTime.UtcNow.AddDays(0.5),
                SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
            };
            var token = tokenHandler.CreateToken(tokenDescriptor);
            var tokenString = tokenHandler.WriteToken(token);
            return Ok(new
            {
                access_token = tokenString
            });
        }
    }
}

在Dto資料夾下增加使用者Dto UserProviderDto.cs

程式碼如下:

namespace Simple_Asp.Net_Core.Dtos
{
    public class UserProviderDto
    {
        public UserProviderDto(string name, string password)
        {
            Name = name;
            Password = password;
        }

        public string ID { get; set; }

        /// <summary>
        /// 使用者名稱
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 手機號
        /// </summary>
        public string Phone { get; set; }

        /// <summary>
        /// 電子郵箱
        /// </summary>
        public string Mail { get; set; }

        public string Password { get; set; }
    }
}

新建 Extensions 資料夾 、 新建ClaimLoginUserExtensions 擴充套件類

程式碼如下:


using Newtonsoft.Json;
using Simple_Asp.Net_Core.Dtos;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;

namespace Simple_Asp.Net_Core.Extensions
{
    public static class ClaimLoginUserExtensions
    {
        private const string USER = "User";

        public static ClaimsIdentity GetClaimsIdentity(this UserProviderDto user)
        {
            return new ClaimsIdentity(new Claim[]
            {
                new Claim(USER, JsonConvert.SerializeObject(user))
            });
        }

        public static UserProviderDto GetLoginUser(this IEnumerable<Claim> claims)
        {
            var user = JsonConvert.DeserializeObject<UserProviderDto>(claims.Get(USER));

            return user;
        }

        public static string Get(this IEnumerable<Claim> claims, string claimType)
        {
            return claims.Where(v => v.Type == claimType).First().Value;
        }
    }
}

啟動專案,呼叫api/OAuth介面獲取Token

得到Token

 "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VyIjoie1wiSURcIjpudWxsLFwiTmFtZVwiOlwiYWRtaW5cIixcIlBob25lXCI6bnVsbCxcIk1haWxcIjpudWxsLFwiUGFzc3dvcmRcIjpcIjEyMzQ1NlwifSIsIm5iZiI6MTYxNDIzOTAwNSwiZXhwIjoxNjE0MjgyMjA1LCJpYXQiOjE2MTQyMzkwMDUsImF1ZCI6ImFkbWluXzIwMjEvMi8yNSDmmJ_mnJ_lm5sgMTU6NDM6MjUifQ.yrjK8qX45mNOQ3taecIc-QVaBDlN4QUOdBPRExvpejk"

將Token賦到我們的請求上,在swagger上可以直接設定,點選 Authorize 按鈕

將Token附上,注意開頭為Bearer (Bearer後面接著一個空格!):

範例如下:

Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VyIjoie1wiSURcIjpudWxsLFwiTmFtZVwiOlwiYWRtaW5cIixcIlBob25lXCI6bnVsbCxcIk1haWxcIjpudWxsLFwiUGFzc3dvcmRcIjpcIjEyMzQ1NlwifSIsIm5iZiI6MTYxNDIzOTAwNSwiZXhwIjoxNjE0MjgyMjA1LCJpYXQiOjE2MTQyMzkwMDUsImF1ZCI6ImFkbWluXzIwMjEvMi8yNSDmmJ_mnJ_lm5sgMTU6NDM6MjUifQ.yrjK8qX45mNOQ3taecIc-QVaBDlN4QUOdBPRExvpejk

再點選 Authorize 按鈕

呼叫/api/Commands請求 - 請求成功

程式碼編寫與設定已經全部完成

可以利用JWT線上解析工具(https://jwt.io/#debugger-io) 將前面獲取的JWT的資訊解析出來

總結

本文為Simple專案增加JWT(JSON Web Token)的應用,除了使用者請求認證介面之外的其餘請求都需要帶著JWT,本文還簡單的實現了使用者單一登入,擴充套件類JWT.cs的AudienceValidator方法會判斷使用者使用的Token是否是最新的,如果使用者重複登入則舊的Token會失效!目前只是簡單的使用了靜態變數來儲存Token的資訊,可以結合具體情況將Token儲存至Redis或者資料庫中。
JWT的更多設定可以參考官方API資料 https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer?view=aspnetcore-5.0

GitHub原始碼

注意:原始碼偵錯過程中如果出現xml檔案路徑錯誤,需要參照第一章(後端專案搭建與Swagger設定步驟)Swagger設定「設定XML 檔案檔案」步驟,取消勾選然後再選中 ,將XML路徑設定成與你的電腦路徑匹配!

https://github.com/Impartsoft/Simple_Asp.Net_Core/tree/master/Simple_Asp.Net_Core 5.SDK Update %26 JWT

參考資料

部落格JWT(推薦學習) https://www.cnblogs.com/7tiny/archive/2019/06/13/11012035.html

官方資料 https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.jwtbearer?view=aspnetcore-5.0

jwt官方資料 https://jwt.io


IT145.com E-mail:sddin#qq.com