學習ASP.NET Core(05)-使用Swagger與Jwt授權

Jscroop發表於2020-05-14

上一篇我們使用IOC容器解決了依賴問題,同時簡單配置了WebApi環境,本章我們使用一下Swagger,並通過Jwt完成授權


一、Swagger的使用

1、什麼是Swagger

前後端分離專案中,後端人員開發完成後通常會編寫API介面文件,說明方法對應的功能、引數等資訊,也就是說前後端唯一的聯絡就是API介面,書寫良好規範的API介面能極大的減緩前後端人員之間扯皮的頻率。swagger則是能夠讓後端開發人員更加便捷的書寫規範API文件的一款框架。

2、Swagger相關配置

  1. 使用NuGet搜尋Swashbuckle.AspNetCore,安裝在BlogSystem.Core專案中,如下:

  1. 開啟Startup類,在ConfigureServices 方法中新增服務,如下:

                //註冊Swagger服務
                services.AddSwaggerGen(options =>
                {
                    options.SwaggerDoc("V1",new OpenApiInfo
                    {
                        Version = "V1",
                        Title = "BlogSystem API Doc-V1",
                        Description = "BlogSystem API介面文件-V1版",
                        Contact = new OpenApiContact { Name = "BlogSystem", Email = "xxx@xx.com" },
                    });
                    options.OrderActionsBy(x=>x.RelativePath);
                });
    
  2. 在開啟Startup類的Configure方法中新增中介軟體,這裡可以配置在開發環境,如下:

  3. 執行專案,成功顯示Swagger頁面,但是報了一個Not Found /swagger/v1/swagger.json錯誤,檢查發現是版本號大小寫不一致導致,將中介軟體配置中的v1改成大寫後解決

3、Swagger註釋功能

上面操作完成後,由於沒有備註資訊,不熟悉的人無法知道每個介面對應的功能,下面我們加上備註資訊功能;

1、右擊專案名稱,選擇屬性—生成,勾選XML文件檔案

2、儲存後發現多了很多成員註釋的警告,身為強迫症堅決不能忍,再次開啟生成頁面,在取消顯示警告中新增1591,儲存,解決...這裡的註釋需要在方法名上使用三個左斜槓進行標註,我們可以在之前新增的註冊方法上新增備註

3、這個時候還沒完,在ConfigureServices 方法中註冊服務指向剛剛新增的生成的xml檔案路徑,如下圖

4、這個時候執行一下,發現ViewModel的註釋沒有顯示,直接對Model層做上述相同操作,但是輸出目錄要選到當前BlogSystem.Core專案對應的路徑,服務註冊時不用帶上第二個引數。完成後執行,終於OK

二、授權驗證與Jwt

1、授權與認證

1.1、授權與驗證的概念

首先,要區分授權與認證的概念。簡單來說,授權:通過一些資訊確認其身份;認證:確認該身份對應的許可權。

舉個例子:一棟辦公樓,只有工作人員才能進入,那麼工作人員可以憑藉員工卡證明自己的身份,進入大樓,即為授權;如果員工張三是辦公樓內A公司的員工,那麼他只能進出A公司,而李四是辦公樓物管處的員工,那麼他可以進出整棟辦公樓的所有公司,即為驗證。

1.2、實現的幾種方法

Web應用程式是基於Http請求的,但是Http請求是無狀態的,無法記住我們的登入狀態,怎麼辦呢?

1、Cookie:常用的處理辦法是將使用者的登入資訊以key-value的形式存在客戶端瀏覽器的Cookie中,客戶端每次傳送請求時伺服器端會判斷並驗證客戶端有無對應的Cookie,存在則表示你是授權過的使用者,可以進行授權後的操作;

2、Session方法:它是儲存於伺服器記憶體中key-value集合;客戶端向伺服器傳送請求,如果請求頭中沒有SessionId,伺服器會分配一個給客戶端,並存放在客戶端的Cookie中;如果請求頭中有SessionId,則會帶著該值一起傳送到伺服器,伺服器根據Session找到授權資訊,進行授權判斷操作。其實現也需要基於客戶端的Cookie

3、令牌驗證:即在Http請求資訊中加入令牌的資訊,將使用者的資訊和令牌設定通過演算法加密後存入Http請求資訊中,客戶端傳送請求時也帶上令牌資訊來表明自己的身份,服務端進行許可權的驗證

這三種方法各有利弊,可以根據業務需求進行選擇使用,這裡我使用jwt僅僅是練手...

2、JWT介紹及問題

2.1、Jwt簡介

Jwt是基於令牌的方法,網上資料很多,這裡簡單說明下,它由Header、Payload、Signature三部分組成。

  • Header包含了加密演算法和加密的物件型別,它會經過BASE64編碼後存入token中;

  • Payload用來存放一些宣告資訊,資料格式為鍵值對形式,官方定義了部分欄位如簽發人,簽發時間,生效時間,過期時間等,當然我們也可以自定義新增,它同樣會經過BASE64編碼後存入token中;

  • Signature為金鑰,這部分需要絕對保密,系統預設會使用Header中宣告的演算法將三者結合起來產生一串字元

2.2、使用Jwt的流程

  1. 使用者成功登入後,伺服器後端根據設定的令牌資訊和使用者資訊進行加密生成一串加密的字元,返回給前端;

  2. 前端將後端返回的資訊進行儲存,一般會選擇客戶端瀏覽器中的Cookie或localStorage進行存放;

  3. 客戶端傳送請求前,會驗證本地Cookie或localStorage中是否存在Jwt的資訊,存在則加入Http請求頭中;

  4. 伺服器接受Http請求時,對Http請求頭中的Jwt資訊進行驗證,驗證通過後則授權成功

2.3、JWT存在的問題

1、安全性:有的朋友會說,將資訊加入請求頭,萬一別人攔截複製我請求頭中的資訊使用怎麼辦呢?

  • 任何方案都是有利有弊的,傳統的session+cookie 方案,如果洩露了 sessionId同樣會存在此類問題。其實只要做到以下幾點就可以極大程度的避免此類情況:①使用https 加密Web應用;②將jwt存入cookie中;③返回 jwt 給客戶端時設定 httpOnly=true,能有效阻止XSS 攻擊和 CSRF 攻擊

2、登出和修改密碼問題:傳統的 session+cookie 方案使用者點選登出,服務端清空 session 即可;但是Jwt是無狀態的,且服務端沒有儲存,即使客戶端刪除了JWT,它仍然是在有效期內的,相應的解決辦法如下

  • ①刪除客戶端的Cookie,但如果使用者通過某種手段記住且在請求頭中新增了JWT,在有效期內仍然是可以訪問的,即使是在這段時間內修改了密碼的情況下;②將JWT令牌中的Secret設定為和使用者相關的動態數值,使用者登出後改變Secret的值,但這樣JWT是不變的,使用原先的JWT會無法登入;③藉助第三方,如NoSql資料庫儲存JWT的狀態,但這違背了JWT無狀態的特性

3、續簽問題:payload中會儲存一個有效期時間,時間一到就無法訪問了;傳統的 session+cookie 是會自動續簽的,所有沒有這個問題。對應的幾個解決方案如下:

  • ①快要過期的時候重新整理 jwt,這個只有快到期時使用者訪問了網站才有機會觸發;②第三方記錄過期時間,每次訪問重新整理過期時間;③每次請求重新整理過期時間,有點暴力...會有效能影響;④將第一、第三條方案中和一下,每次訪問都判斷過期時間是否在預設的一個時間段內,在就重新整理

三、配置使用JWT

這裡我們先完成授權部分的功能,登出、續簽、以及許可權驗證功能等後續再進一步完善

1、配置檔案

首先我們在appsettings.json檔案中加上Payload需要用到的欄位資訊,如下:

2、註冊Jwt

ASP.NET Core中已經封裝了授權方法,在Action方法上方新增[Authorize],即表示需要授權才能訪問;如何進行授權的驗證呢?那就需要我們來定義了,使用NuGet包安裝下方外掛,在StartUp類中進行服務的註冊,如下Microsoft.AspNetCore.Authentication.JwtBearer

3、啟用中介軟體

同時我們需要在Startup另一個方法中啟用授權中介軟體,這裡把驗證中介軟體一併加上,需要注意其順序

4、JWT方法封裝

接下來的問題是使用者資訊如何封裝為Jwt令牌,我們在BlogSystem.Core專案下建一個Helpers資料夾,再新建一個JwtHelper類,新增對應的封裝方法和解析方法。功能如下:

using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace BlogSystem.Mvc.Helpers
{
    public static class JwtHelper
    {
        private static IConfiguration _configuration;
        //獲取Startup建構函式中的Configuration物件
        public static void GetConfiguration(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        /// <summary>
        /// Jwt加密
        /// </summary>
        /// <param name="tokenModel"></param>
        /// <returns></returns>
        public static string JwtEncrypt(TokenModelJwt tokenModel)
        {
            //獲取配置檔案中的資訊
            var iss = _configuration["JwtTokenManagement:issuer"];
            var aud = _configuration["JwtTokenManagement:audience"];
            var secret = _configuration["JwtTokenManagement:secret"];

                      //設定宣告資訊
            var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Jti, tokenModel.UserId.ToString()),//Jwt唯一標識Id
                new Claim(JwtRegisteredClaimNames.Iat, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"),//令牌簽發時間
                new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,//不早於的時間宣告
                new Claim(JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddHours(24)).ToUnixTimeSeconds()}"),//令牌過期時間
                new Claim(ClaimTypes.Expiration, DateTime.Now.AddHours(24).ToString(CultureInfo.CurrentCulture)),//令牌截至時間
                new Claim(JwtRegisteredClaimNames.Iss,iss),//發行人
                new Claim(JwtRegisteredClaimNames.Aud,aud),//訂閱人
                new Claim(ClaimTypes.Role,tokenModel.Level)//許可權——目前只支援單許可權
            };

            //金鑰處理,key和加密演算法
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
            var cred = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

            //封裝成jwt物件
            var jwt = new JwtSecurityToken(
                claims: claims,
                signingCredentials: cred
            );

            //生成返回jwt令牌
            return new JwtSecurityTokenHandler().WriteToken(jwt);
        }

        /// <summary>
        /// Jwt解密
        /// </summary>
        /// <param name="jwtStr"></param>
        /// <returns></returns>
        public static TokenModelJwt JwtDecrypt(string jwtStr)
        {
            if (string.IsNullOrEmpty(jwtStr)||string.IsNullOrWhiteSpace(jwtStr))
            {
                return new TokenModelJwt();
            }
            jwtStr = jwtStr.Substring(7);//擷取前面的Bearer和空格
            var jwtHandler = new JwtSecurityTokenHandler();
            JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);

            jwtToken.Payload.TryGetValue(ClaimTypes.Role, out object level);

            var model = new TokenModelJwt
            {
                UserId = Guid.Parse(jwtToken.Id),
                Level = level == null ? "" : level.ToString()
            };
            return model;
        }
    }

    /// <summary>
    /// 令牌包含的資訊
    /// </summary>
    public class TokenModelJwt
    {
        public Guid UserId { get; set; }

        public string Level { get; set; }
    }
}

其中我們把Configuration物件在startup建構函式中傳遞了過來

5、Jwt加密功能測試

1、修改註冊功能和新增登入功能,如下:

        /// <summary>
        /// 使用者註冊
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        [HttpPost(nameof(Register))]
        public async Task<IActionResult> Register(RegisterViewModel model)
        {
            if (!await _userService.Register(model))
            {
                return Ok("使用者已存在");
            }
            return Ok("建立成功");
        }

        /// <summary>
        /// 使用者登入
        /// </summary>
        /// <param name="model"></param>
        /// <returns></returns>
        [HttpPost("Login", Name = nameof(Login))]
        public async Task<IActionResult> Login(LoginViewModel model)
        {
            //判斷賬號密碼是否正確
            var userId = await _userService.Login(model);
            if (userId == Guid.Empty) return Ok("賬號或密碼錯誤!");

            //登入成功進行jwt加密
            var user = await _userService.GetOneByIdAsync(userId);
            TokenModelJwt tokenModel = new TokenModelJwt { UserId = user.Id, Level = user.Level.ToString() };
            var jwtStr = JwtHelper.JwtEncrypt(tokenModel);
            return Ok(jwtStr);
        }

2、這裡我們使用Swagger介面進行測試,輸入賬號密碼,成功拿到加密字元,如下:

6、授權測試

1、為了測試授權,我們新增一個方法,在上方標註【Authorize】,如下:

        [Authorize]
        [HttpPost("Test")]
        public ActionResult Test()
        {
            return Ok("測試");
        }

2、測試如下,401錯誤未授權,無法訪問

3、我們使用PostMan在請求頭中插入Token,注意前面加了Bearer和一個空格,返回200成功執行

7、Swagger配置驗證功能

上面可以看到,需要測試授權時,我們只能通過PostMan在請求頭插入Token資訊,Swagger其實也是可以新增授權功能的,下面我們來配置一下環境

1、使用NuGet安裝如下外掛,如下:

2、在StartUp類的ConfigureService方法的Swagger註冊服務中新增如下資訊,其中方法名一定要為oauth2,不知道為什麼

3、執行一下,右上角出現了鎖,使用登入得到加密字元後,點選右上角Authorize輸入Bearer+空格+加密字元,表示已授權後,選擇上面建的測試方法,點選執行,成功返回200狀態碼

本章完~

本人知識點有限,若文中有錯誤的地方請及時指正,方便大家更好的學習和交流。

本文部分內容參考了網路上的視訊內容和文章,僅為學習和交流,地址如下:

老張的哲學,系列教程一目錄:.netcore+vue 前後端分離

徐靖峰,深入理解JWT的使用場景和優劣

原創文章宣告

相關文章