HTTP Bearer認證及JWT的使用


一、概述

1、理解Http的無狀態特性

HTTP是一個無狀態的協議,WEB服務器在處理所有傳入HTTP請求時,根本就不知道某個請求是否是一個用戶的第一次請求與后續請求,或者是另一個用戶的請求。 WEB服務器每次在處理請求時,都會按照用戶所訪問的資源所對應的處理代碼,從頭到尾執行一遍,然后輸出響應內容,WEB服務器根本不會記住已處理了哪些用戶的請求,因此,我們通常說HTTP協議是無狀態的。

2、為什么需要認證

雖然HTTP協議與WEB服務器是無狀態,但我們的業務需求卻要求有狀態,典型的就是用戶登錄, 在這種業務需求中,要求WEB服務器端能區分某個請求是不是一個已登錄用戶發起的,或者當前請求是哪個用戶發出的。 在開發WEB應用程序時,我們通常會使用Cookie來保存一些簡單的數據供服務端維持必要的狀態。總的來說,加入認證的根本原因就是確保請求的合法性以及資源的安全性,如下圖:

二、HTTP Bearer認證

http認證根據憑證協議的不同,划分為不同的方式。常用的方式有:

  • HTTP基本認證
  • HTTP摘要認證
  • HTTP Bearer認證

本篇文章介紹HTTP Bearer認證。

1、原理解析

下面通過圖詳細的了解下HTTP Bearer認證過程:

Bearer認證也是http協議中的標准認證方式,在Bearer認證中的憑證稱為Bearer_token 或者Access_token。該種方式的優點就是靈活方便,因為憑證的生成和驗證完全由開發人員設計和實現。一般憑證的設計盡量能保證以下幾點:
  • 用戶信息安全性,即保證重要信息不被泄露和惡意破解

  • 避免重放攻擊

  • 更方便的適應更多的應用場景,主要體現在在服務端不需要憑證狀態保存,分布式場景中,不需要狀態共享

  • 少查庫,減少服務端負擔。

目前最流行的token編碼協議就是JWT(JSON WEB TOKEN),下面通過jwt協議說明。

二、JWT協議

1、什么是JWT

Json web token (JWT),是一種基於JSON的開放標准((RFC 7519)。

2、JWT協議優缺點

優點:
1、可以避免用戶信息泄露。
2、payload中可以攜帶一些必要的非敏感信息,比如用戶名、用戶郵箱,供前端使用
3、服務端不用存放jwt的狀態信息,減輕服務端壓力,在分布式場景中更方便,不需要狀態共享
4、通過時間戳的方式避免重放攻擊,token的時效性盡量短
缺點:
1、可以使用jwt 提供的jti,即jwt的id,來避免重放攻擊,但是jti的值需要存儲,不管是存儲在redis還是mysql,都是要損耗性能的。

3、JWT組成

   

 

JWT是由三段信息構成的,將這三段信息文本連接起來就構成了Jwt字符串。第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload),第三部分是簽證(signature),下面是個JWT實例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySWQiOiJBZG1pbiIsIkV4cGlyZSI6IjIwMjAtMDctMTEgMTY6NDc6MTYifQ.9ev6IGc1K3xvYaEfmMYeyFz5oHCM57fRGOvSZ-jvArw

(1)Header

 

 

 在這里,我們說明了這是一個JWT,並且我們所用的簽名算法是HS256算法。對它也要進行Base64編碼,之后的字符串就成了JWT的Header(頭部):

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

(2)Payload

 

 

 將上面的JSON對象進行[base64編碼]可以得到下面的字符串。這個字符串我們將它稱作JWT的Payload(載荷):

eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9

(3)signature

jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:

  • header (base64后的)
  • payload (base64后的)
  • secret

這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行secret組合加密,然后就構成了jwt的第三部分。

var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); 
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

將這三部分用.連接成一個完整的字符串,構成了最終的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。

4、JWT簽名的目的

最后一步簽名的過程,實際上是對頭部以及載荷內容進行簽名。一般而言,加密算法對於不同的輸入產生的輸出總是不一樣的。所以,如果有人對頭部以及載荷的內容解碼之后進行修改,再進行編碼的話,那么新的頭部和載荷的簽名和之前的簽名就將是不一樣的。而且,如果不知道服務器加密的時候用的密鑰的話,得出來的簽名也一定會是不一樣的。

服務器應用在接受到JWT后,會首先對頭部和載荷的內容用同一算法再次簽名。那么服務器應用是怎么知道我們用的是哪一種算法呢?別忘了,我們在JWT的頭部中已經用alg字段指明了我們的加密算法了。如果服務器應用對頭部和載荷再次以同樣方法簽名之后發現,自己計算出來的簽名和接受到的簽名不一樣,那么就說明這個Token的內容被別人動過的,我們應該拒絕這個Token,返回一個HTTP 401 Unauthorized響應。

5、JWT安全性

使用JWT會暴露信息嗎?是的。所以,在JWT中,不應該在載荷里面加入任何敏感的數據。在上面的例子中,我們傳輸的是用戶的User ID。這個值實際上不是什么敏感內容,一般情況下被知道也是安全的。但是像密碼這樣的內容就不能被放在JWT中了。如果將用戶的密碼放在了JWT中,那么懷有惡意的第三方通過Base64解碼就能很快地知道你的密碼了。總結如下:

  • 不應該在jwt的payload部分存放敏感信息,因為該部分是客戶端可解密的部分。
  • 保護好secret私鑰,該私鑰非常重要。
  • 如果可以,請使用https協議

6、JWT的適用場景

JWT適合用於向Web應用傳遞一些非敏感信息。其實JWT還經常用於設計用戶認證和授權系統,甚至實現Web應用的單點登錄。

三、基於JWT協議的Bearer認證示例

1、自定義CheckJWTAttribute特性方式

之前使用的是這種方式,根據jwt原理自定義生成JWT、驗證jwt,感覺挺好。原理就是自定義一個攔截器(特性),攔截器對每個請求都優先進行處理,認證成功的進行下一步操作。

(1)新建netcore webapi項目

 

 

(2)定義JWTPayload類

using System;

namespace JwtAuthenticationOne
{
    public class JWTPayload
    {
        public string UserName { get; set; }
        public string Email { get; set; }
        public string UserId { get; set; }
        public DateTime Expire { get; set; }
    }
}

(3)定義CheckJWTAttribute特性

using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Threading.Tasks;

namespace JwtAuthenticationOne
{
    /// <summary>
    /// JWT校檢
    /// </summary>
    public class CheckJWTAttribute : BaseActionFilterAsync
    {
        private static readonly int _errorCode = 401;
        public override async Task OnActionExecuting(ActionExecutingContext context)
        {
            if (context.ContainsFilter<NoCheckJWTAttribute>()) return;
            try
            {
                var req = context.HttpContext.Request;
                string token = req.GetToken();
                if (token.IsNullOrEmpty())
                {
                    context.Result = Error("缺少token", _errorCode);
                    return;
                }
                if (!JWTHelper.CheckToken(token, JWTHelper.JWTSecret))
                {
                    context.Result = Error("token校檢失敗!", _errorCode);
                    return;
                }

                var payload = JWTHelper.GetPayload<JWTPayload>(token);
                if (payload.Expire < DateTime.Now)
                {
                    context.Result = Error("token過期!", _errorCode);
                    return;
                }
            }
            catch (Exception ex)
            {
                context.Result = Error(ex.Message, _errorCode);
            }
            await Task.CompletedTask;
        }
    }
}

(4)定義NoCheckJWTAttribute類

namespace JwtAuthenticationOne
{
    /// <summary>
    /// 忽略JWT校驗
    /// </summary>
    public class NoCheckJWTAttribute : BaseActionFilterAsync
    {
    }
}

(5)定義BaseActionFilterAsync類

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Threading.Tasks;

namespace JwtAuthenticationOne
{
    public class BaseActionFilterAsync : Attribute, IAsyncActionFilter
    {
        /// <summary>
        /// action執行之前執行
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public async virtual Task OnActionExecuting(ActionExecutingContext context)
        {
            await Task.CompletedTask;
        }
        /// <summary>
        /// action執行之后執行
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public async virtual Task OnActionExecuted(ActionExecutedContext context)
        {
            await Task.CompletedTask;
        }
        /// <summary>
        /// 在模型綁定完成后,在操作之前異步調用。
        /// </summary>
        /// <param name="context"></param>
        /// <param name="next"></param>
        /// <returns></returns>
        public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
        {
            await OnActionExecuting(context);
            if (context.Result == null)
            {
                var nextContext = await next();
                await OnActionExecuted(nextContext);
            }
        }

        /// <summary>
        /// 返回JSON
        /// </summary>
        /// <param name="json">json字符串</param>
        /// <returns></returns>
        public ContentResult JsonContent(string json)
        {
            return new ContentResult { Content = json, StatusCode = 200, ContentType = "application/json; charset=utf-8" };
        }

        /// <summary>
        /// 返回成功
        /// </summary>
        /// <returns></returns>
        public ContentResult Success()
        {
            AjaxResult res = new AjaxResult
            {
                Success = true,
                Msg = "請求成功!"
            };

            return JsonContent(res.ToJson());
        }

        /// <summary>
        /// 返回成功
        /// </summary>
        /// <param name="msg">消息</param>
        /// <returns></returns>
        public ContentResult Success(string msg)
        {
            AjaxResult res = new AjaxResult
            {
                Success = true,
                Msg = msg
            };

            return JsonContent(res.ToJson());
        }

        /// <summary>
        /// 返回成功
        /// </summary>
        /// <param name="data">返回的數據</param>
        /// <returns></returns>
        public ContentResult Success<T>(T data)
        {
            AjaxResult<T> res = new AjaxResult<T>
            {
                Success = true,
                Msg = "請求成功!",
                Data = data
            };

            return JsonContent(res.ToJson());
        }

        /// <summary>
        /// 返回錯誤
        /// </summary>
        /// <returns></returns>
        public ContentResult Error()
        {
            AjaxResult res = new AjaxResult
            {
                Success = false,
                Msg = "請求失敗!"
            };

            return JsonContent(res.ToJson());
        }

        /// <summary>
        /// 返回錯誤
        /// </summary>
        /// <param name="msg">錯誤提示</param>
        /// <returns></returns>
        public ContentResult Error(string msg)
        {
            AjaxResult res = new AjaxResult
            {
                Success = false,
                Msg = msg,
            };

            return JsonContent(res.ToJson());
        }

        /// <summary>
        /// 返回錯誤
        /// </summary>
        /// <param name="msg">錯誤提示</param>
        /// <param name="errorCode">錯誤代碼</param>
        /// <returns></returns>
        public ContentResult Error(string msg, int errorCode)
        {
            AjaxResult res = new AjaxResult
            {
                Success = false,
                Msg = msg,
                ErrorCode = errorCode
            };

            return JsonContent(res.ToJson());
        }
    }
}

(6)定義AjaxResult類

namespace JwtAuthenticationOne
{
    /// <summary>
    /// Ajax請求結果
    /// </summary>
    public class AjaxResult
    {
        /// <summary>
        /// 是否成功
        /// </summary>
        public bool Success { get; set; } = true;

        /// <summary>
        /// 錯誤代碼
        /// </summary>
        public int ErrorCode { get; set; }

        /// <summary>
        /// 返回消息
        /// </summary>
        public string Msg { get; set; }
    }
    /// <summary>
    /// Ajax請求結果
    /// </summary>
    public class AjaxResult<T> : AjaxResult
    {
        /// <summary>
        /// 返回數據
        /// </summary>
        public T Data { get; set; }
    }
}

(7)定義擴展類Extention

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

namespace JwtAuthenticationOne
{
    public static partial class Extention
    {
        /// <summary>
        /// 構造函數
        /// </summary>
        static Extention()
        {
            JsonSerializerSettings setting = new JsonSerializerSettings();
            JsonConvert.DefaultSettings = new Func<JsonSerializerSettings>(() =>
            {
                //日期類型默認格式化處理
                setting.DateFormatHandling = DateFormatHandling.MicrosoftDateFormat;
                setting.DateFormatString = "yyyy-MM-dd HH:mm:ss";
                return setting;
            });
        }
        /// <summary>
        /// 將對象序列化成Json字符串
        /// </summary>
        /// <param name="obj">需要序列化的對象</param>
        /// <returns></returns>
        public static string ToJson(this object obj)
        {
            return JsonConvert.SerializeObject(obj);
        }
        /// <summary>
        /// 是否擁有某過濾器
        /// </summary>
        /// <typeparam name="T">過濾器類型</typeparam>
        /// <param name="actionExecutingContext">上下文</param>
        /// <returns></returns>
        public static bool ContainsFilter<T>(this FilterContext actionExecutingContext)
        {
            return actionExecutingContext.Filters.Any(x => x.GetType() == typeof(T));
        }
        /// <summary>
        /// 獲取Token
        /// </summary>
        /// <param name="req">請求</param>
        /// <returns></returns>
        public static string GetToken(this HttpRequest req)
        {
            string tokenHeader = req.Headers["Authorization"].ToString();
            if (tokenHeader.IsNullOrEmpty()) return null;
            string pattern = "^Bearer (.*?)$";
            if (!Regex.IsMatch(tokenHeader, pattern)) throw new Exception("token格式不對!格式為:Bearer {token}");
            string token = Regex.Match(tokenHeader, pattern).Groups[1]?.ToString();
            if (token.IsNullOrEmpty()) throw new Exception("token不能為空!");
            return token;
        }
        /// <summary>
        /// 判斷是否為Null或者空
        /// </summary>
        /// <param name="obj">對象</param>
        /// <returns></returns>
        public static bool IsNullOrEmpty(this object obj)
        {
            if (obj == null)
                return true;
            else
            {
                string objStr = obj.ToString();
                return string.IsNullOrEmpty(objStr);
            }
        }
        /// <summary>
        /// Base64Url編碼
        /// </summary>
        /// <param name="text">待編碼的文本字符串</param>
        /// <returns>編碼的文本字符串</returns>
        public static string Base64UrlEncode(this string text)
        {
            var plainTextBytes = Encoding.UTF8.GetBytes(text);
            var base64 = Convert.ToBase64String(plainTextBytes).Replace('+', '-').Replace('/', '_').TrimEnd('=');

            return base64;
        }
        /// <summary>
        /// Base64Url解碼
        /// </summary>
        /// <param name="base64UrlStr">使用Base64Url編碼后的字符串</param>
        /// <returns>解碼后的內容</returns>
        public static string Base64UrlDecode(this string base64UrlStr)
        {
            base64UrlStr = base64UrlStr.Replace('-', '+').Replace('_', '/');
            switch (base64UrlStr.Length % 4)
            {
                case 2:
                    base64UrlStr += "==";
                    break;
                case 3:
                    base64UrlStr += "=";
                    break;
            }
            var bytes = Convert.FromBase64String(base64UrlStr);

            return Encoding.UTF8.GetString(bytes);
        }
        /// <summary>
        /// 將Json字符串轉為JObject
        /// </summary>
        /// <param name="jsonStr">Json字符串</param>
        /// <returns></returns>
        public static JObject ToJObject(this string jsonStr)
        {
            return jsonStr == null ? JObject.Parse("{}") : JObject.Parse(jsonStr.Replace("&nbsp;", ""));
        }
        /// <summary>
        /// 將Json字符串反序列化為對象
        /// </summary>
        /// <typeparam name="T">對象類型</typeparam>
        /// <param name="jsonStr">Json字符串</param>
        /// <returns></returns>
        public static T ToObject<T>(this string jsonStr)
        {
            return JsonConvert.DeserializeObject<T>(jsonStr);
        }
        /// <summary>
        /// HMACSHA256算法
        /// </summary>
        /// <param name="text">內容</param>
        /// <param name="secret">密鑰</param>
        /// <returns></returns>
        public static string ToHMACSHA256String(this string text, string secret)
        {
            secret = secret ?? "";
            byte[] keyByte = Encoding.UTF8.GetBytes(secret);
            byte[] messageBytes = Encoding.UTF8.GetBytes(text);
            using (var hmacsha256 = new HMACSHA256(keyByte))
            {
                byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
                return Convert.ToBase64String(hashmessage).Replace('+', '-').Replace('/', '_').TrimEnd('=');
            }
        }
    }
}

(8)定義JWTHelper類

using Newtonsoft.Json.Linq;

namespace JwtAuthenticationOne
{
    /// <summary>
    /// JWT幫助類
    /// </summary>
    public class JWTHelper
    {
        private static readonly string _headerBase64Url = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}".Base64UrlEncode();
        public static readonly string JWTSecret = AppsettingsHelper.GetValue("JWTSecret");

        /// <summary>
        /// 生成Token
        /// </summary>
        /// <param name="payloadJsonStr">載荷,數據JSON字符串</param>
        /// <param name="secret">秘鑰</param>
        /// <returns></returns>
        public static string GetToken(string payloadJsonStr, string secret)
        {
            string payloadBase64Url = payloadJsonStr.Base64UrlEncode();
            string sign = $"{_headerBase64Url}.{payloadBase64Url}".ToHMACSHA256String(secret);
            return $"{_headerBase64Url}.{payloadBase64Url}.{sign}";
        }

        /// <summary>
        /// 獲取Token中的數據
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        public static JObject GetPayload(string token)
        {
            return token.Split('.')[1].Base64UrlDecode().ToJObject();
        }

        /// <summary>
        /// 獲取Token中的數據
        /// </summary>
        /// <typeparam name="T">泛型</typeparam>
        /// <param name="token">token</param>
        /// <returns></returns>
        public static T GetPayload<T>(string token)
        {
            if (token.IsNullOrEmpty())
                return default;

            return token.Split('.')[1].Base64UrlDecode().ToObject<T>();
        }

        /// <summary>
        /// 校驗Token
        /// </summary>
        /// <param name="token">token</param>
        /// <param name="secret">密鑰</param>
        /// <returns></returns>
        public static bool CheckToken(string token, string secret)
        {
            var items = token.Split('.');
            var oldSign = items[2];
            string newSign = $"{items[0]}.{items[1]}".ToHMACSHA256String(secret);
            return oldSign == newSign;
        }
    }
}

(9)定義AppsettingsHelper類

using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace JwtAuthenticationOne
{
    public class AppsettingsHelper
    {
        private static IConfiguration _config;
        private static object _lock = new object();
        public static IConfiguration Configuration
        {
            get
            {
                if (_config == null)
                {
                    lock (_lock)
                    {
                        if (_config == null)
                        {
                            var builder = new ConfigurationBuilder().SetBasePath(AppContext.BaseDirectory).AddJsonFile("appsettings.json");
                            _config = builder.Build();
                        }
                    }
                }
                return _config;
            }
            set
            {
                _config = value;
            }
        }
        /// <summary>
        /// 從AppSettings獲取key的值
        /// </summary>
        /// <param name="key">key</param>
        /// <returns></returns>
        public static string GetValue(string key)
        {
            return Configuration[key];
        }

        /// <summary>
        /// 獲取連接字符串
        /// </summary>
        /// <param name="nameOfCon">連接字符串名</param>
        /// <returns></returns>
        public static string GetConnectionString(string nameOfCon)
        {
            return Configuration.GetConnectionString(nameOfCon);
        }
        /// <summary>
        /// 封裝要操作的字符
        /// </summary>
        /// <param name="sections">節點配置</param>
        /// <returns></returns>
        public static string GetApp(params string[] sections)
        {
            try
            {
                if (sections.Any())
                {
                    return Configuration[string.Join(":", sections)];
                }
            }
            catch (Exception) { }
            return "";
        }

        /// <summary>
        /// 遞歸獲取配置信息數組
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sections"></param>
        /// <returns></returns>
        public static List<T> GetApp<T>(params string[] sections)
        {
            List<T> list = new List<T>();
            // 引用 Microsoft.Extensions.Configuration.Binder 包
            Configuration.Bind(string.Join(":", sections), list);
            return list;
        }
    }
}

(10)使用CheckJWTAttribute

定義一個BaseApiController,所有的controller都繼承該類,BaseApiController類使用CheckJWTAttribute特性

using Microsoft.AspNetCore.Mvc;

namespace JwtAuthenticationOne.Controllers
{
    /// <summary>
    /// Mvc對外接口基控制器
    /// </summary>
    [CheckJWT]
    public class BaseApiController : ControllerBase
    {
    }
}

不需要認證的,使用NoCheckJWT限制,如下:

[HttpGet]
[NoCheckJWT]
public IEnumerable<WeatherForecast> Get()
{
  var rng = new Random();
   return Enumerable.Range(1, 5).Select(index => new WeatherForecast
   {
       Date = DateTime.Now.AddDays(index),
       TemperatureC = rng.Next(-20, 55),
        Summary = Summaries[rng.Next(Summaries.Length)]
   }).ToArray();
}

(11)最終項目結構及效果

 

 運行項目,查看效果:

 

 

2、使用aspnetcore提供的組件

(1)新建netcore webapi項目

(2)安裝組件

通過nugut搜索安裝Microsoft.AspNetCore.Authentication.JwtBearer 

(3)jwtconfig配置

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "Issuer": "Issuer",
    "Audience": "Audience",
    "SigningKey": "EF1DA5B4-C7FA-4240-B997-7D1701BF9BE2"
  }
}

(4)定義jwtconfig對應的實體

namespace JwtAuthenticationTwo
{
    public class JwtConfig
    {
        public string Issuer { get; set; }
        public string Audience { get; set; }
        public string SigningKey { get; set; }
    }
}

(5)Startup.cs 配置

ConfigureServices 中需要進行添加的信息

public void ConfigureServices(IServiceCollection services)
{
  services.AddControllers();
   #region   Token驗證信息  JWT
   //讀取JWT的配置信息
   var jwtconfig = Configuration.GetSection("Jwt").Get<JwtConfig>();
   //注冊JWT認證所需要的服務
   services.AddAuthentication(option =>
   {
     //設置驗證時使用的默認方案 option.DefaultAuthenticateScheme
= JwtBearerDefaults.AuthenticationScheme;
     //設置挑戰時使用的默認方案 option.DefaultChallengeScheme
= JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(option => { option.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = jwtconfig.Issuer, ValidAudience = jwtconfig.Audience, ValidateIssuer = true, ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtconfig.SigningKey)), // 緩沖過期時間,總的有效時間等於這個時間加上jwt的過期時間,如果不配置,默認是5分鍾 ClockSkew = TimeSpan.FromSeconds(5) };        option.Events = new JwtBearerEvents { //此處為權限驗證失敗后觸發的事件 OnChallenge = context => { //此處代碼為終止.Net Core默認的返回類型和數據結果,這個很重要哦,必須 context.HandleResponse(); //自定義自己想要返回的數據結果,我這里要返回的是Json對象,通過引用Newtonsoft.Json庫進行轉換 var payload = JsonConvert.SerializeObject(new { message = "授權未通過,Token無效", status = false, code = 401 }); //自定義返回的數據類型 context.Response.ContentType = "application/json"; //自定義返回狀態碼,默認為401 我這里改成 200 context.Response.StatusCode = StatusCodes.Status200OK; //輸出Json數據結果 context.Response.WriteAsync(payload); return Task.FromResult(0); } }; }); services.AddOptions().Configure<JwtConfig>(Configuration.GetSection("Jwt")); #endregion services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "JwtAuthenticationTwo", Version = "v1" }); }); }

Configure需要添加的信息

// JWT身份認證
app.UseAuthentication();
app.UseAuthorization();

以上配置好了 就基本上可以使用JWT驗證了,下面介紹如何生成jwt token

(6)定義JwtHelper.cs

using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;

namespace JwtAuthenticationTwo
{
    /// <summary>
    /// JWT幫助類信息
    /// </summary>
    public class JwtHelper
    {
        /// <summary>
        /// 頒發JWT字符串
        /// </summary>
        /// <param name="tokenModel"></param>
        /// <returns></returns>
        public static string IssueJwt(Claim[] claim)
        {
            // 讀取對應的配置信息
            string iss = AppsettingsHelper.GetApp("Jwt:Issuer");
            string aud = AppsettingsHelper.GetApp("Jwt:Audience");
            string secret = AppsettingsHelper.GetApp("Jwt:SigningKey");
            //加密關鍵字
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
            //編碼格式
            var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            //token存儲相關信息
            var token = new JwtSecurityToken(
                issuer: iss,
                audience: aud,
                claims: claim,
                notBefore: DateTime.Now,
                expires: DateTime.Now.AddSeconds(300),
                signingCredentials: creds);
            var jwtHandler = new JwtSecurityTokenHandler();
            //生成對應的編碼信息
            var encodedJwt = jwtHandler.WriteToken(token);
            return encodedJwt;
        }
        /// <summary>
        /// 解析
        /// </summary>
        /// <param name="jwtStr"></param>
        /// <returns></returns>
        public static List<Claim> SerializeJwt(string jwtStr)
        {
            var jwtHandler = new JwtSecurityTokenHandler();
            JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
            object role;
            try
            {
                jwtToken.Payload.TryGetValue(ClaimTypes.Role, out role);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
            var list = jwtToken.Claims.ToList();
            return list;
        }
    }
}

(7)定義AppsettingsHelper.cs

using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;

namespace JwtAuthenticationTwo
{
    public class AppsettingsHelper
    {
        private static IConfiguration _config;
        private static object _lock = new object();
        public static IConfiguration Configuration
        {
            get
            {
                if (_config == null)
                {
                    lock (_lock)
                    {
                        if (_config == null)
                        {
                            var builder = new ConfigurationBuilder().SetBasePath(AppContext.BaseDirectory).AddJsonFile("appsettings.json");
                            _config = builder.Build();
                        }
                    }
                }
                return _config;
            }
            set
            {
                _config = value;
            }
        }
        /// <summary>
        /// 從AppSettings獲取key的值
        /// </summary>
        /// <param name="key">key</param>
        /// <returns></returns>
        public static string GetValue(string key)
        {
            return Configuration[key];
        }

        /// <summary>
        /// 獲取連接字符串
        /// </summary>
        /// <param name="nameOfCon">連接字符串名</param>
        /// <returns></returns>
        public static string GetConnectionString(string nameOfCon)
        {
            return Configuration.GetConnectionString(nameOfCon);
        }
        /// <summary>
        /// 封裝要操作的字符
        /// </summary>
        /// <param name="sections">節點配置</param>
        /// <returns></returns>
        public static string GetApp(params string[] sections)
        {
            try
            {
                if (sections.Any())
                {
                    return Configuration[string.Join(":", sections)];
                }
            }
            catch (Exception) { }
            return "";
        }

        /// <summary>
        /// 遞歸獲取配置信息數組
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sections"></param>
        /// <returns></returns>
        public static List<T> GetApp<T>(params string[] sections)
        {
            List<T> list = new List<T>();
            // 引用 Microsoft.Extensions.Configuration.Binder 包
            Configuration.Bind(string.Join(":", sections), list);
            return list;
        }
    }
}

(8)控制器層使用示例

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace JwtAuthenticationTwo.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;

        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }
        [Authorize]
        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
        /// <summary>
        /// 測試獲取token信息
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        public ActionResult<string> GetToken()
        {
            //參數存儲的信息
            var claim = new Claim[]{
                new Claim("UserName", "測試"),
                new Claim("UserId", "10086")
            };
            //生成證書
            var token = JwtHelper.IssueJwt(claim);
            //解析證書
            var data = JwtHelper.SerializeJwt(token);
            return Ok(new { token = token, data = data });
        }
    }
}

(9)項目結構和效果

 

 項目運行效果:

然后整體就基本上結束了。雖然說這種方式沒有自定義的方式代碼容易讀,但是也方便了好多

四、擴展

第三部分介紹了使用Microsoft.AspNetCore.Authentication.JwtBearer 來實現jwt認證,微軟官網提供了對該dll的解析。這里簡單的描述下。先來熟悉幾個概念:

  • 身份驗證:身份驗證基本上意味着使用給定的信息並嘗試用該信息對用戶進行身份驗證。因此,這將嘗試創建一個用戶標識,並使其可用於框架。例如,cookie認證方案使用cookie數據恢復用戶身份。或者JWT bearing認證方案將使用在請求中作為Authorization頭的一部分提供的令牌來創建用戶身份。

  • 挑戰:當認證方案受到挑戰時,該方案應提示用戶對自己進行認證。例如,這可能意味着用戶被重定向到一個登錄表單,或者將被重定向到外部身份驗證提供者。

  • Forbid:當一個認證方案被禁止時,該方案基本上只是響應一些東西,告訴用戶他們可能不做任何他們試圖做的事情。這通常是一個HTTP 403錯誤,可能是重定向到某些錯誤頁面。 

  • 登錄:當一個身份驗證方案正在被登錄時,該方案將被告知獲取一個現有用戶(claimsprinprincipal)並以某種方式持久化該用戶。例如,在cookie認證方案上注冊用戶基本上會創建一個包含該用戶身份的cookie。 

  • 退出:這與登錄相反,它將告訴身份驗證方案刪除持久性。在cookie方案上注銷將有效地使cookie過期。

現在來具體說下JwtBearer中涉及的概念,先來說下AuthenticationOptions涉及到的屬性

  • Defaultauthenticatscheme:設置認證時使用的默認方案。

  • Defaultchallengesscheme:設置挑戰時使用的默認方案。

  • DefaultForbidScheme:設置禁止訪問時使用的默認方案。

  • DefaultSignInScheme:設置要登錄的默認方案。

  • DefaultSignOutScheme:設置退出的默認方案。

  • DefaultScheme:設置默認回退方案。

正常情況下,我們沒有必要把這些屬性全部進行配置,如下說明:

  • 身份認證: 配置defaultauthenticatscheme或DefaultScheme
  • 挑戰:配置defaultchallengesscheme或DefaultScheme
  • Forbid:配置 DefaultForbidScheme或defaultchallengesscheme或DefaultScheme
  • Sign-in:配置DefaultSignInScheme或DefaultScheme
  • Sign-out:配置 DefaultSignOutScheme或DefaultScheme

有些時候我們也可以自定義認證或者挑戰方案。下面來說下JwtBear方案中的事件:

  • OnChallenge:發生質詢的時,在將詢問發送回調用方之前調用。
  • OnAuthenticationFailed:認證期間出現異常調用。
  • OnForbidden:因為禁止而導致授權失敗調用。
  • OnTokenValidated:令牌通過認證后調用。
  • OnMessageReceived:首次收到協議消息時調用。

雖然使用微軟提供的JwtBearer方便了很多,但是不是那么好理解了,不太喜歡使用該方案的可以采用自定義的方式實現jwt認證。

五、源碼下載

源碼:https://github.com/qiuxianhu/AuthenticationAndAuthorization

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM