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

上一篇我們使用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 = "[email protected]" },
                    });
                    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的使用場景和優劣

原創文章聲明
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章