AspNet Core: Jwt 身份認證

AspNet Core: Jwt 身份認證

資源服務器

創建項目

新建一個“AspNetCore WebApi” 項目,名爲:DotNet.WebApi.Jwt.ApiResources

依賴包

添加依賴包:

<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.3" />

添加API

新建控制器 Controllers/StudentController.cs:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace DotNet.WebApi.Jwt.ApiResources.Controllers
{
    //[Authorize(Policy = "OnlyRead")]
    [Authorize]
    [Route("api/[controller]")]
    [ApiController]
    public class StudentController : ControllerBase
    {
        [Authorize(Policy = "ReadWrite")]
        [HttpGet("GetStudents")]
        public ActionResult<dynamic> GetStudents()
        {
            return new List<dynamic>()
            {
                new {Id=1,Name="張三",Age=21 },
                new {Id=2,Name="李四",Age=22 },
                new {Id=3,Name="王五",Age=23 },
            };
        }

        [Authorize(Policy = "OnlyRead")]
        [HttpGet("GetStudent")]
        public ActionResult<dynamic> GetStudent()
        {
            return new List<dynamic>()
            {
            new { Id = 10, Name = "錢六", Age = 19 }
            };
        }
    }
}

Program

將 Program.cs 修改爲:


using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace DotNet.WebApi.Jwt.ApiResources
{
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.Title = "API資源服務器";

            var builder = WebApplication.CreateBuilder(args);

            //設置跨域
            builder.Services.AddCors(options =>
            {
                options.AddDefaultPolicy(
                    builder =>
                    {
                        //允許任何來源訪問。
                        builder.AllowAnyOrigin().AllowAnyHeader();
                        //將isexpired頭添加到策略中。
                        builder.WithExposedHeaders(new string[] { "isexpired" });
                    });
            });

            //配置策略授權
            builder.Services.AddAuthorization(options => {
                options.AddPolicy("OnlyRead", policy => policy.RequireRole("Read").Build());
            });

            //配置JWT。
            builder.Services.AddAuthentication(a =>
            {
                a.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                a.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(j =>
            {
                j.RequireHttpsMetadata = false;
                j.SaveToken = true;
                j.TokenValidationParameters = new TokenValidationParameters
                {
                    //是否調用對簽名securityToken的SecurityKey進行驗證
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("798654167464654646")),//簽名祕鑰
                    ValidateIssuer = true,//是否驗證頒發者
                    ValidIssuer = "dotnet-jwt",//頒發者
                    ValidateAudience = true, //是否驗證接收者
                    ValidAudience = "StudentAPI",//接收者
                    ValidateLifetime = true,//是否驗證失效時間
                };
                //捕獲Token過期事件
                j.Events = new JwtBearerEvents
                {
                    OnAuthenticationFailed = context =>
                    {
                        //出現此類異常。
                        if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                        {
                            //在響應頭中添加isexpired:true鍵值對。
                            context.Response.Headers.Add("isexpired", "true");
                        }
                        return Task.CompletedTask;
                    }
                };
            });

            builder.Services.AddAuthorization();
            builder.Services.AddControllers();
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            var app = builder.Build();

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

            if (app.Environment.IsProduction())
            {
                //生產環境端口號
                app.Urls.Add("https://*:6002");
            }

            app.UseHttpsRedirection();
            app.UseCors();   //啓用跨域
            app.UseAuthentication(); //身份驗證
            app.UseAuthorization();  //授權
            app.MapControllers();

            app.Run();
        }
    }
}

代碼解析:
(1)添加JWT 身份認證中間件

            //配置JWT。
            builder.Services.AddAuthentication(a =>
            {
                a.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                a.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(j =>
            {
                j.RequireHttpsMetadata = false;
                j.SaveToken = true;
                j.TokenValidationParameters = new TokenValidationParameters
                {
                    //是否調用對簽名securityToken的SecurityKey進行驗證
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("798654167464654646")),//簽名祕鑰
                    ValidateIssuer = true,//是否驗證頒發者
                    ValidIssuer = "dotnet-jwt",//頒發者
                    ValidateAudience = true, //是否驗證接收者
                    ValidAudience = "StudentAPI",//接收者
                    ValidateLifetime = true,//是否驗證失效時間
                };
                //捕獲Token過期事件
                j.Events = new JwtBearerEvents
                {
                    OnAuthenticationFailed = context =>
                    {
                        //出現此類異常。
                        if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                        {
                            //在響應頭中添加isexpired:true鍵值對。
                            context.Response.Headers.Add("isexpired", "true");
                        }
                        return Task.CompletedTask;
                    }
                };
            });

(2)捕獲 Token 事件,處理refreshToken:

                //捕獲Token過期事件
                j.Events = new JwtBearerEvents
                {
                    OnAuthenticationFailed = context =>
                    {
                        //出現此類異常。
                        if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                        {
                            //在響應頭中添加isexpired:true鍵值對。
                            context.Response.Headers.Add("isexpired", "true");
                        }
                        return Task.CompletedTask;
                    }
                };

認證服務器

創建項目

新建一個“AspNetCore 空” 項目,名爲:DotNet.WebApi.Jwt.Authentication

依賴包

添加依賴包:

<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.3" />

數據庫

創建一個數據庫,用於保存登錄用戶

JWTUser

namespace DotNet.WebApi.Jwt.Authentication.Data
{
    public class JWTUser
    {
        //用戶Id。
        [Key]
        public int UserId { get; set; }
        //用戶名。
        public string? UserName { get; set; }
        //用戶密碼。
        public string? UserPwd { get; set; }
        //用戶郵箱。
        public string? UserEmail { get; set; }
    }
}

JWTDbContext

using Microsoft.EntityFrameworkCore;

namespace DotNet.WebApi.Jwt.Authentication.Data
{
    /// <summary>
    /// 數據庫上下文。
    /// </summary>
    public class JWTDbContext : DbContext
    {
        public JWTDbContext(DbContextOptions<JWTDbContext> options) : base(options)
        {
        }

        public DbSet<JWTUser>? JWTUsers { get; set; }
    }
}

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "JwtDbConnection": "Server=localhost;Database=JWTDb;Uid=sa;Pwd=123456;Encrypt=True;TrustServerCertificate=True;"
  }
}

用戶註冊

Controllers/AccountController.cs

using DotNet.WebApi.Jwt.Authentication.Data;
using Microsoft.AspNetCore.Mvc;

namespace DotNet.WebApi.Jwt.Authentication.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AccountController : ControllerBase
    {
        private readonly JWTDbContext _context;

        public AccountController(JWTDbContext context)
        {
            _context = context;
        }

        /// <summary>
        /// 添加用戶。
        /// </summary>
        /// <param name="user"></param>
        /// <returns></returns>
        [HttpPost("Register")]
        public async Task<ActionResult<int>> RegisterUser(JWTUser user)
        {
            //如果user參數爲空,則返回404錯誤。
            if (user == null) return NotFound();
            //添加用戶
            _context.JWTUsers?.Add(user);
            //執行操作。
            var count = await _context.SaveChangesAsync();
            return count;
        }
    }
}

Token 控制器

Controllers/TokenController.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using DotNet.WebApi.Jwt.Authentication.Data;

namespace DotNet.WebApi.Jwt.Authentication.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TokenController : ControllerBase
    {
        private readonly JWTDbContext _context;

        private const string signingKey = "798654167464654646";
        private const string issuer = "dotnet-jwt";
        private const string audience = "StudentAPI";

        public TokenController(JWTDbContext context)
        {
            _context = context;
        }

        /// <summary>
        /// 生成Token
        /// </summary>
        /// <returns></returns>
        [HttpGet("Get")]
        public async Task<ActionResult> BuildAccessToken(string userName, string userPwd)
        {
            //判斷用戶信息是否爲空
            if (string.IsNullOrWhiteSpace(userName) || string.IsNullOrWhiteSpace(userPwd))
            {
                return NotFound();
            }
                
            //根據用戶名和密碼找到用戶實體
            var user = await _context.JWTUsers!.AsNoTracking()
                .FirstOrDefaultAsync( u =>
                   u.UserName!.Equals(userName) && u.UserPwd!.Equals(userPwd)
                 );
            if (user == null) 
            {
                return BadRequest("用戶名或密碼錯誤。");
            }
                
            //聲明
            var claims = new[]
            {
                new Claim(ClaimTypes.Sid,user.UserId.ToString()),
                new Claim(ClaimTypes.Name, userName),
                new Claim(ClaimTypes.Role,"Read")
            };
            //設置密鑰
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));
            //設置憑據
            var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            //生成token
            var jwtToken = new JwtSecurityToken(issuer, 
                                                audience, 
                                                claims, 
                                                expires: DateTime.UtcNow.AddMinutes(30), 
                                                signingCredentials: credentials);
            var token = new JwtSecurityTokenHandler().WriteToken(jwtToken);

            return Ok(token);
        }

        /// <summary>
        /// 根據Token獲取身份聲明。
        /// </summary>
        /// <param name="token">token</param>
        /// <returns></returns>
        private ClaimsPrincipal GetPrincipalFromAccessToken(string token)
        {
            var jwtSecurityToken = new JwtSecurityTokenHandler();
            var claimsPrincipal = jwtSecurityToken.ValidateToken(token, new TokenValidationParameters
            {
                ValidateAudience = false,
                ValidateIssuer = false,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey)),
                ValidateLifetime = false
            }, out SecurityToken validatedToken);

            return claimsPrincipal;
        }

        /// <summary>
        /// 根據舊Token換取新Token
        /// </summary>
        /// <param name="accessToken"></param>
        /// <returns></returns>
        [HttpGet("Refresh")]
        public ActionResult BuildRefreshToken(string accessToken)
        {
            if (string.IsNullOrWhiteSpace(accessToken)) return NotFound();
            var userClaims = GetPrincipalFromAccessToken(accessToken);
            if (userClaims == null) return NotFound();
            
            //獲取舊Token中的聲明
            var claims = new[]
            {
                //用戶ID
                new Claim(ClaimTypes.Sid,userClaims.FindFirst(u=>u.Type.Equals(ClaimTypes.Sid))!.Value),
                //用戶名
                new Claim(ClaimTypes.Name,userClaims.FindFirst(u=>u.Type.Equals(ClaimTypes.Name))!.Value),
                //角色
                new Claim(ClaimTypes.Role,userClaims.FindFirst(u=>u.Type.Equals(ClaimTypes.Role))!.Value)
            };
            //設置密鑰
            var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));
            //設置憑據
            var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
            //生成token
            var jwtToken = new JwtSecurityToken(issuer, audience, claims, expires: DateTime.UtcNow.AddMinutes(30), signingCredentials: credentials);
            var token = new JwtSecurityTokenHandler().WriteToken(jwtToken);

            return Ok(token);
        }
    }
}

代碼分析:
(1)生成token:BuildAccessToken()
(2)刷新token:BuildRefreshToken(accessToken),使用過期的token,從中調用GetPrincipalFromAccessToken(accessToken)解析出 userClaims,用於生成新的token。

Program

Program.cs


using DotNet.WebApi.Jwt.Authentication.Data;
using Microsoft.EntityFrameworkCore;

namespace DotNet.WebApi.Jwt.Authentication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            //註冊數據庫上下文服務
            //UseSqlServer表示使用SQLServer數據庫。
            builder.Services.AddDbContext<JWTDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("JwtDbConnection")
            ));

            //設置跨域
            builder.Services.AddCors(options =>
            {
                options.AddDefaultPolicy(
                    builder =>
                    {
                        //允許任何來源訪問。
                        builder.AllowAnyOrigin().AllowAnyHeader();
                    });
            });

            builder.Services.AddAuthorization();
            builder.Services.AddControllers();
            // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
            builder.Services.AddEndpointsApiExplorer();
            builder.Services.AddSwaggerGen();

            var app = builder.Build();
            //生成數據庫和表結構
            var scope = app.Services.CreateScope();
            var context = scope.ServiceProvider.GetRequiredService<JWTDbContext>();
            //如果數據庫不存在,則生成表結構。
            context.Database.EnsureCreated();
           
            if (app.Environment.IsDevelopment())
            {
                app.UseSwagger();
                app.UseSwaggerUI();
            }

            app.Urls.Add("https://*:6001"); // 修改端口
            app.UseHttpsRedirection();
            app.UseCors();     //啓用跨域
            app.UseAuthorization();
            app.MapControllers();

            app.Run();
        }
    }
}

客戶端

創建項目

創建一個 “AspNet Core 空項目”,名爲:DotNet.WebApi.Jwt.WebClient。這個項目沒用到 AspNet Core 的任何功能,僅僅只是作爲一個靜態文件站點,即:一個純前端項目。

添加 JS 庫

創建"wwwroot"文件夾,然後選擇該文件夾,右鍵【添加/客戶端庫】,添加bootract.min.css、jquery.min.js 文件。

用戶註冊

新建Html頁面:wwwroot/Users/Register.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>用戶註冊</title>
    <link href="../bootstrap/css/bootstrap.min.css" rel="stylesheet" />
    <script src="../jquery/jquery.min.js"></script>
</head>
<body>
    <div style="padding:20px;width:600px;margin:30px;">
        <h3>用戶註冊</h3>
        <hr />
        <div class="form-floating">
            <div class="mb-3">
                <label class="form-label">用戶名:</label>
                <input type="text" id="userName" class="form-control">
            </div>
            <div class="mb-3">
                <label class="form-label">密  碼:</label>
                <input type="password" id="userPwd" class="form-control">
            </div>
            <div class="mb-3">
                <input type="submit" id="btn" value="註冊" class="btn btn-primary" />
            </div>
            <div>
                <span id="msg" style="color:red"></span>
            </div>
        </div>
    </div>
    <script>
        $("#btn").click(function () {
            $.ajax({
                //請求類型
                type: "post",
                //請求路徑
                url: "https://localhost:6001/api/Account/Register",
                //預期服務器返回的數據類型
                dataType: "text",
                data: JSON.stringify({ UserName: $("#userName").val(), UserPwd: $("#userPwd").val() }),
                contentType: "application/json",
                //請求成功時的回調函數
                success: function (result) {
                    if (result == "1") {
                        $("#msg").text("用戶註冊成功。");
                    }
                }
            });
        });
    </script>
</body>
</html>

用戶登錄

新建Html頁面:wwwroot/Users/Login.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>用戶登錄</title>
    <link href="../bootstrap/css/bootstrap.min.css" rel="stylesheet" />
    <script src="../jquery/jquery.min.js"></script>
</head>
<body>
    <div style="padding:20px;width:600px;margin:30px;">
        <h3>用戶登錄</h3>
        <hr />
        <div class="form-floating">
            <div class="mb-3">
                <label class="form-label">用戶名:</label>
                <input type="text" id="userName" class="form-control">
            </div>
            <div class="mb-3">
                <label class="form-label">密  碼:</label>
                <input type="password" id="userPwd" class="form-control">
            </div>
            <div class="mb-3">
                <input type="submit" id="btn" value="登錄" class="btn btn-primary" />
            </div>
            <div class="mb-3">
                <span id="msg" style="color:red"></span>
            </div>
        </div>
    </div>
    <script>
        $("#btn").click(function () {
            $.ajax({
                //請求類型
                type: "get",
                //請求路徑
                url: "https://localhost:6001/api/Token/Get",
                //預期服務器返回的數據類型
                dataType: "text",
                data: { UserName: $("#userName").val(), UserPwd: $("#userPwd").val() },
                contentType: "application/json",
                //請求成功時的回調函數
                success: function (token) {
                    localStorage.setItem("token", token);
                    console.log(token);
                    location.href = 'GetData.html';
                }
            });
        });
    </script>
</body>
</html>

代碼解析:
(1) 獲取token:調用 Get請求:https://localhost:6001/api/Token/Get,獲取token。
(2)將token保存到本地存儲: localStorage.setItem("token", token);

獲取API數據

新建Html頁面:wwwroot/Users/GetData.html,內部調用 認證服務器【DotNet.WebApi.Jwt.Authentication】獲取 token,然後使用token調用資源服務器【DotNet.WebApi.Jwt.ApiResources】的API

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>獲取數據</title>
    <link href="../bootstrap/css/bootstrap.min.css" rel="stylesheet" />
    <script src="../jquery/jquery.min.js"></script>
</head>
<body style="margin:20px;">
    <table id="showTable" class="table table-bordered">
        <thead>
            <tr>
                <td>ID</td>
                <td>姓名</td>
                <td>年齡</td>
            </tr>
        </thead>
        <tbody></tbody>
    </table>
    <script>
        var tbody = $("#showTable tbody")
        //請求資源
        $.ajax({
            type: 'get',
            contentType: 'application/json',
            url: 'https://localhost:6002/api/Student/GetStudent',
            beforeSend: function (xhr) {
                //獲取Token
                var accessToken = localStorage.getItem("token");
                //使用Token請求資源
                xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
            },
            //獲取的數據
            success: function (data) {
                $.each(data, function (n, value) {
                    var trs = "";
                    trs += "<tr>" +
                        "<td>" + value.id + "</td>" +
                        "<td>" + value.name + "</td>" +
                        "<td>" + value.age + "</td>" +
                        "</tr>";
                    tbody += trs;
                });
                $("#showTable").append(tbody);
            },
            error: function (xhr) {
                if (xhr.status === 401 && xhr.getResponseHeader('isexpired') === 'true') {
                    //Token已過期了。
                    getRefreshAccessToken();
                }
            }
        })
        //獲取刷新後的新Token。
        function getRefreshAccessToken() {
            $.ajax({
                type: 'get',
                contentType: 'application/json',
                url: 'https://localhost:6001/api/Token/Refresh',
                data: { accessToken: localStorage.getItem("token") },
                success: function (token) {
                    //將獲取的新Token存儲起來
                    localStorage.setItem("token", token);
                }
            })
        }
    </script>
</body>
</html>

代碼解析:
(1) 從本地存儲中獲取token:var accessToken = localStorage.getItem("token");
(2) 刷新token: 當返回401並且響應頭中有‘xhr.getResponseHeader('isexpired') === 'true'’,調用getRefreshAccessToken()從認證服務的
https://localhost:6001/api/Token/Refresh,參數爲當前過期的 token,獲取新的token。

Program

修改 Program.cs 爲:

namespace DotNet.WebApi.Jwt.WebClient
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            var app = builder.Build();

            app.UseStaticFiles(); //啓用靜態文件

            app.Run();
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章