.Net Core Web Api_筆記33_採用JWT的授權

 


用戶首次登入時,透過傳輸賬號密碼驗證身份,驗證成功後,Server產生的 Token 回應使用者。使用者後續請求只需要傳送 Token,服務器只需對 Token 進行校驗來確認身份。




在此一樣新增一個新專案來做JWT 實作





這邊在此模板的Controller上加上這個修飾[Authorize]
主要代表針對當前這個控制器下所有Action預設都是被套用權限授權控管的
都需要登入後判定有無權限才可存取

跟上一篇一樣操作



那一樣至 微軟github範本去搜查




Program.cs範本配置程式碼
可以將範例程式碼抄過來做測試微調

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Ticketer.Services;
using Ticketer;

JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler();
SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray());

string GenerateJwtToken(string name)
{
    if (string.IsNullOrEmpty(name))
    {
        throw new InvalidOperationException("Name is not specified.");
    }

    var claims = new[] { new Claim(ClaimTypes.Name, name) };
    var credentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);
    var token = new JwtSecurityToken("ExampleServer", "ExampleClients", claims, expires: DateTime.Now.AddSeconds(60), signingCredentials: credentials);
    return JwtTokenHandler.WriteToken(token);
}

var builder = WebApplication.CreateBuilder(args);

// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682

// Add services to the container.
builder.Services.AddGrpc();
builder.Services.AddSingleton<TicketRepository>();
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
    {
        policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
        policy.RequireClaim(ClaimTypes.Name);
    });
});
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters =
            new TokenValidationParameters
            {
                ValidateAudience = false,
                ValidateIssuer = false,
                ValidateActor = false,
                ValidateLifetime = true,
                IssuerSigningKey = SecurityKey
            };
});

var app = builder.Build();

IWebHostEnvironment env = app.Environment;

if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();


// Configure the HTTP request pipeline.
app.MapGrpcService<TicketerService>();

app.MapGet("/generateJwtToken", context =>
{
    return context.Response.WriteAsync(GenerateJwtToken(context.Request.Query["name"]));
});


app.Run();

那配置好大致設定後的Program.cs (認證中間件必須放在使用Controller中間件之前)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

JwtSecurityTokenHandler JwtTokenHandler = new JwtSecurityTokenHandler();
SymmetricSecurityKey SecurityKey = new SymmetricSecurityKey(Guid.NewGuid().ToByteArray());

string GenerateJwtToken(string name)
{
    if (string.IsNullOrEmpty(name))
    {
        throw new InvalidOperationException("Name is not specified.");
    }

    var claims = new[] { new Claim(ClaimTypes.Name, name) };
    var credentials = new SigningCredentials(SecurityKey, SecurityAlgorithms.HmacSha256);
    var token = new JwtSecurityToken("ExampleServer", "ExampleClients", claims, expires: DateTime.Now.AddSeconds(60), signingCredentials: credentials);
    return JwtTokenHandler.WriteToken(token);
}
// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy(JwtBearerDefaults.AuthenticationScheme, policy =>
    {
        policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
        policy.RequireClaim(ClaimTypes.Name);
    });
});
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters =
            new TokenValidationParameters
            {
                ValidateAudience = false,
                ValidateIssuer = false,
                ValidateActor = false,
                ValidateLifetime = true,
                IssuerSigningKey = SecurityKey
            };
    });
var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}



app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.MapGet("/generateJwtToken", context =>
{
    return context.Response.WriteAsync(GenerateJwtToken(context.Request.Query["name"]));
});

app.Run();

官方範本中
可以看到Policy採用JwtBearerDefaults.AuthenticationScheme
而在Authentication中針對JwtBearer Token驗證配置
主要只有針對Lifetime 以及 SigningKey開啟驗證機制
而每一次重新運行 token都會重新產生



那在swag進行測試API路由訪問
會回傳401無授權的訊息


我們可藉由cmd curl做測試
curl -v http://localhost:5153/WeatherForecast




我們可以去呼叫產生token的路由,記得補上query get參數在路由網址後頭
curl -v http://localhost:5153/generateJwtToken?name=sam

就可拿到授權的token



再次訪問並指定Bearer token
token複製到*之前
curl -H後面雙引號要包覆"Authorization: Bearer {token字串}"

curl -v http://localhost:5153/WeatherForecast -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoic2FtIiwiZXhwIjoxNjc0ODg1NzQ5LCJpc3MiOiJFeGFtcGxlU2VydmVyIiwiYXVkIjoiRXhhbXBsZUNsaWVudHMifQ.Tw1Djo5hhkGoYJffcZEqdA4ijwUlOO40Lh-7Ccwz9j4"


(備註: -H/--header 設定 request 裡所攜帶的 header)



若想去解析這串token對應到底捨麼內容
可以到jwt.io

粘貼到encode可對應解析不同part資料



JWT由三部分組成

Base64編碼的Header
Base64編碼的Payload
簽名

三部分用點間隔。

第一部分以Base64編碼的Header主要包括Token的類型和所使用的算法

{
   "alg": "HS265",
   "typ": "JWT"
}

第二部分以Base64編碼的Payload主要包含的是聲明、宣告(Claims)
可以說就是存放溝通訊息的地方,在定義上有 3 種聲明 (Claims)

Reserved (註冊聲明)
Public (公開聲明)
Private (私有聲明)


Claims有點類似你身分證欄位有幾個用於識別用途的
如下:
{
    "exp": 1674885749,
    "iss": "ExampleServer",
    "aud": "ExampleClients"
}

像exp就代表有效時間,在此是用unix 時間戳
https://www.unixtimestamp.com/
可用這個來轉換對應西元年月日

iss跟aud則分別代表issuer以及audience

issuer代表頒發Token的Web應用程式
audience是Token的受理者
如果是依賴第三方來創建的Token,這兩個參數肯定必須要指定,因為
第三方本來就不被受信任,如此設置這兩個參數後,我們可驗證這兩個參數。


註冊聲明參數 (建議但無強制性)

iss (Issuer) - jwt簽發者
sub (Subject) - jwt所面向的用戶
aud (Audience) - 接收jwt的一方
exp (Expiration Time) - jwt的過期時間,這個過期時間必須要大於簽發時間
nbf (Not Before) - 定義在什麼時間之前,該jwt都是不可用的
iat (Issued At) - jwt的簽發時間
jti (JWT ID) - jwt的唯一身份標識,主要用來作為一次性token,從而迴避重放攻擊

(PS:Exp和Nbf是使用Unix時間戳字串)



第三部分則是將Key通過對應的加密算法生成簽名(Signature)
由三個部分組成
header (base64後的)
payload (base64後的)
secret

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), 'secret')
secret 需存留在 server-side,jwt 的 簽發驗證都必須使用這個 secret,
當其他人得知這個 secret,那就意味著client-side是可以自我簽發 jwt ,因此
在任何場景都不應該外流




對應配置會相應影響生成的token



JwtSecurityToken
代表一個jwt token,可以直接用此物件產生token字串,也可以使用token字串創建此物件


簽名算法

HS256 (HMAC with SHA-256)
HS256 Signing Algorithm 使用密鑰生成固定的簽名,
簡單地說,HS256必須與任何想要驗證 JWT的 客戶端或 API共享秘密。
與任何其他對稱算法(symmetric keyed hashing algorithm)一樣,相同的秘密用於簽名和驗證 JWT。
HS256 is a symmetric algorithm that shares one secret key between the identity provider and your application. The same key is used to sign a JWT and allow verification that signature.



RS256 (RSA Signature with SHA-256)
RS256 Signing Algorithm
RS256使用成非對稱進行(asymmetric algorithm)簽名。
使用私鑰來簽名 JWT,並且必須使用對應的公鑰來驗證簽名。

RS256 algorithm is an asymmetric algorithm that uses a private key to sign a JWT and a public key to verification that signature.




當然就安全性考量會比較推薦採用RS256這類非對稱式加密算法

對稱式加密(加密跟解密使用的是同一把):又經常被稱作私鑰加密,意思是指訊息發送方和接收方使用同一把密鑰去進行資料的加解密,特點是演算法都是公開的,加解密的速度也快。適用於針對大量資料的情況做使用。

加密過程:明文+加密演算法+私鑰->密文
解密過程:密文+解密算法+私鑰->明文

非對稱式加密(加密跟解密使用的是不同把):又常被稱作公鑰加密,相對於對稱式加密是較為安全的。因為對稱式加密溝通傳輸過程當中雙方使用同一把密鑰,若其中一方的密鑰遭洩漏那整個傳輸通訊過程就會被破解。
至於非對式加密則是使用一對密鑰而非只有一把,分為公鑰與私鑰,且兩者成對出現。
私鑰是自己留存不可對外洩漏,而公鑰則是公開的密鑰,任何人都能獲取。
用公鑰or私鑰鍾任一個來作加密用另一個進行解密。

若是採用公鑰加密的密文則只能用私鑰解密
加密過程:
明文+加密算法+公鑰 -> 密文
解密過程:
密文+解密算法+私鑰->明文

若是採用私鑰加密過的密文則只能用公鑰解密
加密過程:
明文+加密算法+私鑰 -> 密文
解密過程:
密文+解密算法+公鑰 ->明文


這裡我們可以安置一個api action
用上一篇範例來直接判定目前存取者是誰

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Text;

namespace JwtAuth.Controllers
{
    [Authorize]
    [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;
        }

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

        [HttpGet]
        [Route("/get1")]
        public string GetInfo()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine(User.Identity!.Name);
            sb.AppendLine(User.Identity!.AuthenticationType);
            sb.AppendLine(User.Identity!.IsAuthenticated.ToString());
            sb.AppendLine(User.Identity!.ToString());

            var items = Request.HttpContext.AuthenticateAsync().Result;
            foreach (var item in items.Properties!.Items)
            {
                sb.AppendLine(item.Key + "," + item.Value);
            }

            return sb.ToString();
        }
    }
}

這裡若我再次重新啟動web api並cmd重新curl一次同樣token會報

The signature key was not found

原因就在於token會失效而且每次都重新生成

* Mark bundle as not supporting multiuse
< HTTP/1.1 401 Unauthorized
< Content-Length: 0
< Date: Sat, 28 Jan 2023 07:15:02 GMT
< Server: Kestrel
< WWW-Authenticate: Bearer error="invalid_token", error_description="The signature key was not found"
<
* Connection #0 to host localhost left intact

在此我們重新去要一個token
curl -v http://localhost:5153/generateJwtToken?name=sam


再重新攜帶token來請求資源就可訪問的到



我們可再次請求/get1來判定身分 打上來是誰

curl -v http://localhost:5153/get1  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoic2FtIiwiZXhwIjoxNjc0ODkxMDM3LCJpc3MiOiJFeGFtcGxlU2VydmVyIiwiYXVkIjoiRXhhbXBsZUNsaWVudHMifQ.NbFqzMkMoZ4KdDpullFNTIigKowlMkxetQQLOyTgMCI"






sam
AuthenticationTypes.Federation
True
System.Security.Claims.ClaimsIdentity
.expires,Sat, 28 Jan 2023 07:30:37 GMT
.Token.access_token,eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoic2FtIiwiZXhwIjoxNjc0ODkxMDM3LCJpc3MiOiJFeGFtcGxlU2VydmVyIiwiYXVkIjoiRXhhbXBsZUNsaWVudHMifQ.NbFqzMkMoZ4KdDpullFNTIigKowlMkxetQQLOyTgMCI
.TokenNames,access_token


列印資訊可以看的到 認證型別以及失效日







Ref:










留言

這個網誌中的熱門文章

何謂淨重(Net Weight)、皮重(Tare Weight)與毛重(Gross Weight)

Architecture(架構) 和 Framework(框架) 有何不同?_軟體設計前的事前規劃的藍圖概念

經得起原始碼資安弱點掃描的程式設計習慣培養(五)_Missing HSTS Header