.NET Framework4.7.2 製作 Web API 使用 [NSwag] 套件呈现 Swagger UI +

本文将介绍如何使用 JWT 保持登入状态,配合 Swagger / OpenAPI 呈现。

.NET Framework 4.7.2

开发环境

Visual Studio 2019AspNet.Mvc version="5.2.7"AspNet.WebApi version="5.2.7"EntityFramework version="6.1.3"

Web API 使用 JWT Authentication 进行资料验证

参考资料

ASP. NET Web Api 2 透过 JWT 进行资料验证 (@后端实作主要参考)JSON Web Token 入门教程JSON Web Token (JWT) ProfileOAuth 2.0 笔记 (6) Bearer Token 的使用方法[Web] 以 axios 实践前端 refresh token 机制 (@前端配合实作参考)理解OAuth 2.0注册及 JWT-Token 短期保持登入流程 (@前后端协作流程-本文作者绘製,仅供参考)开发者必备知识 - HTTP认证(HTTP Authentication)[笔记] 认识 OAuth 2.0:一次了解各角色、各类型流程的差异认识Cookie、Session、Token与JWT

NuGet 安装套件

jose-jwt version="3.2.0"

程式码实作

建立一个类别 JwtAuthUtil.cs,负责 token 生成相关功能

using Jose;using System;using System.Collections.Generic;using System.Text;using System.Web.Configuration;using MyWebApiProject.Models;namespace MyWebApiProject.Security{/// <summary>    /// JwtToken 生成功能    /// </summary>    public class JwtAuthUtil    {        private readonly ApplicationDbContext db = new ApplicationDbContext(); // DB 连线        /// <summary>        /// 生成 JwtToken        /// </summary>        /// <param name="id">会员id</param>        /// <returns>JwtToken</returns>        public string GenerateToken(int id)        {// 自订字串,验证用,用来加密送出的 key (放在 Web.config 的 appSettings)            string secretKey = WebConfigurationManager.AppSettings["TokenKey"]; // 从 appSettings 取出            var user = db.User.Find(id); // 进 DB 取出想要夹带的基本资料// payload 需透过 token 传递的资料 (可夹带常用且不重要的资料)            var payload = new Dictionary<string, object>            {                { "Id", user.Id },                { "Account", user.Account },                { "NickName", user.NickName },                { "Image", user.Image },                { "Exp", DateTime.Now.AddMinutes(30).ToString() } // JwtToken 时效设定 30 分            };// 产生 JwtToken            var token = JWT.Encode(payload, Encoding.UTF8.GetBytes(secretKey), JwsAlgorithm.HS512);            return token;        }        /// <summary>        /// 生成只刷新效期的 JwtToken        /// </summary>        /// <returns>JwtToken</returns>        public string ExpRefreshToken(Dictionary<string, object> tokenData)        {            string secretKey = WebConfigurationManager.AppSettings["TokenKey"];// payload 从原本 token 传递的资料沿用,并刷新效期            var payload = new Dictionary<string, object>            {                { "Id", (int)tokenData["Id"] },                { "Account", tokenData["Account"].ToString() },                { "NickName", tokenData["NickName"].ToString() },                { "Image", tokenData["Image"].ToString() },                { "Exp", DateTime.Now.AddMinutes(30).ToString() } // JwtToken 时效刷新设定 30 分            };//产生刷新时效的 JwtToken            var token = JWT.Encode(payload, Encoding.UTF8.GetBytes(secretKey), JwsAlgorithm.HS512);            return token;        }        /// <summary>        /// 生成无效 JwtToken        /// </summary>        /// <returns>JwtToken</returns>        public string RevokeToken()        {            string secretKey = "RevokeToken"; // 故意用不同的 key 生成            var payload = new Dictionary<string, object>            {                { "Id", 0 },                { "Account", "None" },                { "NickName", "None" },                { "Image", "None" },                { "Exp", DateTime.Now.AddDays(-15).ToString() } // 使 JwtToken 过期 失效            };// 产生失效的 JwtToken            var token = JWT.Encode(payload, Encoding.UTF8.GetBytes(secretKey), JwsAlgorithm.HS512);            return token;        }    }}

在登入功能的 API 确认登入成功后,生成 JwtToken 并回传给前端

// GenerateToken() 生成新 JwtToken 用法JwtAuthUtil jwtAuthUtil = new JwtAuthUtil();string jwtToken = jwtAuthUtil.GenerateToken(userQuery.Id); // 登入成功时,回传登入成功顺便夹带 JwtTokenreturn Ok(new { Status = true, JwtToken = jwtToken });

前端将收到的 JWT-Token 字串存入浏览器 localStorage

http://img2.58codes.com/2024/201394877aehPsLye1.jpg

建立一个类别 JwtAuthFilter.cs,负责生成 [JwtAuthFilter] 标籤,可放于需登入的 API 上,用来检核 JWT-Token 是否正确

using Jose;using Newtonsoft.Json;using System;using System.Collections.Generic;using System.Net.Http;using System.Text;using System.Web.Configuration;using System.Web.Http;using System.Web.Http.Controllers;using System.Web.Http.Filters;namespace MyWebApiProject.Security{    /// <summary>    /// JwtAuthFilter 继承 ActionFilterAttribute 可生成 [JwtAuthFilter] 使用    /// </summary>    public class JwtAuthFilter : ActionFilterAttribute    {        // 加解密的 key,如果不一样会无法成功解密        private static readonly string secretKey = WebConfigurationManager.AppSettings["TokenKey"];        /// <summary>        /// 过滤有用标籤 [JwtAuthFilter] 请求的 API 的 JwtToken 状态及内容        /// </summary>        /// <param name="actionContext"></param>        public override void OnActionExecuting(HttpActionContext actionContext)        {            // 取出请求内容并排除不需要验证的 API            var request = actionContext.Request;            if (!WithoutVerifyToken(request.RequestUri.ToString())) {                // 有取到 JwtToken 后,判断授权格式不存在且不正确时                if (request.Headers.Authorization == null || request.Headers.Authorization.Scheme != "Bearer") {                    // 可考虑配合前端专案开发期限,不修改 StatusCode 预设 200,将请求失败搭配 Status: false 供前端判断                    string messageJson = JsonConvert.SerializeObject(new { Status = false, Message = "请重新登入" }); // JwtToken 遗失,需导引重新登入                    var errorMessage = new HttpResponseMessage()                    {                        // StatusCode = System.Net.HttpStatusCode.Unauthorized, // 401                        ReasonPhrase = "JwtToken Lost",                        Content = new StringContent(messageJson,                                    Encoding.UTF8,                                    "application/json")                    };                    throw new HttpResponseException(errorMessage); // Debug 模式会停在此行,点继续执行即可                }                else {                    try {                        // 有 JwtToken 且授权格式正确时执行,用 try 包住,因为如果有篡改可能解密失败                        // 解密后会回传 Json 格式的物件 (即加密前的资料)                        var jwtObject = GetToken(request.Headers.Authorization.Parameter);                        // 检查有效期限是否过期,如 JwtToken 过期,需导引重新登入                        if (IsTokenExpired(jwtObject["Exp"].ToString())) {                            string messageJson = JsonConvert.SerializeObject(new { Status = false, Message = "请重新登入" }); // JwtToken 过期,需导引重新登入                            var errorMessage = new HttpResponseMessage()                            {                                // StatusCode = System.Net.HttpStatusCode.Unauthorized, // 401                                ReasonPhrase = "JwtToken Expired",                                Content = new StringContent(messageJson,                                    Encoding.UTF8,                                    "application/json")                            };                            throw new HttpResponseException(errorMessage); // Debug 模式会停在此行,点继续执行即可                        }                    }                    catch (Exception) {                        // 解密失败                        string messageJson = JsonConvert.SerializeObject(new { Status = false, Message = "请重新登入" }); // JwtToken 不符,需导引重新登入                        var errorMessage = new HttpResponseMessage()                        {                            // StatusCode = System.Net.HttpStatusCode.Unauthorized, // 401                            ReasonPhrase = "JwtToken NotMatch",                            Content = new StringContent(messageJson,                                    Encoding.UTF8,                                    "application/json")                        };                        throw new HttpResponseException(errorMessage); // Debug 模式会停在此行,点继续执行即可                    }                }            }            base.OnActionExecuting(actionContext);        }        /// <summary>        /// 将 Token 解密取得夹带的资料        /// </summary>        /// <param name="token"></param>        /// <returns></returns>        public static Dictionary<string, object> GetToken(string token)        {            return JWT.Decode<Dictionary<string, object>>(token, Encoding.UTF8.GetBytes(secretKey), JwsAlgorithm.HS512);        }        /// <summary>        /// 有在 Global 设定一律检查 JwtToken 时才需设定排除,例如 Login 不需要验证因为还没有 token        /// </summary>        /// <param name="requestUri"></param>        /// <returns></returns>        public bool WithoutVerifyToken(string requestUri)        {            //if (requestUri.EndsWith("/login")) return true;            return false;        }        /// <summary>        /// 验证 token 时效        /// </summary>        /// <param name="dateTime"></param>        /// <returns></returns>        public bool IsTokenExpired(string dateTime)        {            return Convert.ToDateTime(dateTime) < DateTime.Now;        }    }}

使用者用到需登入的 API 时,前端取出浏览器 localStorage 中的 JWT-Token 字串用于请求的 Header 里 Authorization 栏位用 Bearer 规则夹带

将 [JwtAuthFilter] 标籤,放于需登入的 API 上,检核 JWT-Token 是否正确,并刷新效期

// 取出请求内容,解密 JwtToken 取出资料var userToken = JwtAuthFilter.GetToken(Request.Headers.Authorization.Parameter);// ExpRefreshToken() 生成刷新效期 JwtToken 用法JwtAuthUtil jwtAuthUtil = new JwtAuthUtil();string jwtToken = jwtAuthUtil.ExpRefreshToken(userToken);// Do Something ~// 处理完请求内容后,顺便送出刷新效期的 JwtTokenreturn Ok(new { Status = true, JwtToken = jwtToken });

用于强制登出使用者的方式

// RevokeToken() 生成失效的 JwtToken 用法JwtAuthUtil jwtAuthUtil = new JwtAuthUtil();string jwtToken = jwtAuthUtil.RevokeToken();// 用于登出使用者时,刷新为失效的 JwtTokenreturn Ok(new { Status = true, jwtToken = jwtToken });

Web API 使用 [NSwag] 套件呈现 Swagger UI

参考资料

ASP.NET WebAPI 2 整合 NSwag (@后端实作主要参考)如何在 ASP․NET Core 3 完整设定 NSwag 与 OpenAPI v3 文件ASP.NET WebAPI 2 + NSwag - 实作简单 ApiKey Header + IP 权限管控

NuGet 安装套件

NSwag.AspNet.Owin version="13.15.9"NSwag.Annotations version="13.2.5"Microsoft.AspNet.WebApi.Owin version="5.2.7"Microsoft.Owin.Host.SystemWeb version="4.2.0"

程式码实作

在专案新增 OWIN 启动类别 Startup.cs,并加入相关设定

http://img2.58codes.com/2024/201394870JiGbKTEHL.jpg

using Microsoft.Owin;using NSwag;using NSwag.AspNet.Owin;using NSwag.Generation.Processors.Security;using Owin;using System.Web.Http;[assembly: OwinStartup(typeof(MyWebApiProject.Startup))]namespace MyWebApiProject{    /// <summary>    /// OWIN 启动类别    /// </summary>    public class Startup    {        /// <summary>        /// 应用程式配置        /// </summary>        /// <param name="app"></param>        public void Configuration(IAppBuilder app)        {            // 如需如何设定应用程式的详细资讯,请浏览 https://go.microsoft.com/fwlink/?LinkID=316888            var config = new HttpConfiguration();            // 针对 JSON 资料使用 camel (JSON 回应会改 camel,但 Swagger 提示不会)            //config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();            app.UseSwaggerUi3(typeof(Startup).Assembly, settings =>            {                // 针对 WebAPI,指定路由包含 Action 名称                settings.GeneratorSettings.DefaultUrlTemplate =                    "api/{controller}/{action}/{id?}";                // 加入客製化调整逻辑名称版本等                settings.PostProcess = document =>                {                    document.Info.Title = "WebAPI : 专案名称";                };                // 加入 Authorization JWT token 定义                settings.GeneratorSettings.DocumentProcessors.Add(new SecurityDefinitionAppender("Bearer", new OpenApiSecurityScheme()                {                    Type = OpenApiSecuritySchemeType.ApiKey,                    Name = "Authorization",                    Description = "Type into the textbox: Bearer {your JWT token}.",                    In = OpenApiSecurityApiKeyLocation.Header,                    Scheme = "Bearer" // 不填写会影响 Filter 判断错误                }));                // REF: https://github.com/RicoSuter/NSwag/issues/1304 (每支 API 单独呈现认证 UI 图示)                settings.GeneratorSettings.OperationProcessors.Add(new OperationSecurityScopeProcessor("Bearer"));            });            app.UseWebApi(config);            config.MapHttpAttributeRoutes();            config.EnsureInitialized();        }    }}

于专案属性的建置内容勾选输出 XML 文件档案, Swagger UI 才会有方法及参数说明

http://img2.58codes.com/2024/20139487ECH4vCNbkV.jpg

于 Web.config 加入处理器设定,将 URL /swagger/* 导向 NSwag 处理程式

<configuration><system.webServer><handlers><add name="NSwag" path="swagger" verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" /></handlers></system.webServer></configuration>

于 API 及输入栏位加上 /// 撰写 XML 方法及参数说明

使用登入 API 取得回传的 JWT token 于 Swagger UI 的 Authorization JWT token 输入验证时,使用 Bearer + 空格 + JWT token 夹带

http://img2.58codes.com/2024/20139487ge4lNo0f8V.jpg

可于 ApiController 上加入 [OpenApiTag] 描述整个模组功能

[OpenApiTag("Users", Description = "使用者操作功能")]public class UsersController : ApiController{/// <summary>    /// 1-5 联络我们功能 (JWT)    /// </summary>    /// <param name="contactUsVm">留言资料</param>    /// <returns></returns>/// <summary>    [JwtAuthFilter] // 用于检核 JWT-Token     [HttpPost]    [SwaggerResponse(typeof(ApiResult))] // 显示回传资料的注解    [Route("api/users/contact-us")]    public IHttpActionResult SendContactUsMail(ContactUsVm contactUsVm)    {        // Do Sometning        // 取出请求内容,解密 JwtToken 取出资料        var userToken = JwtAuthFilter.GetToken(Request.Headers.Authorization.Parameter);        //单纯刷新效期不新生成,新生成会进资料库        JwtAuthUtil jwtAuthUtil = new JwtAuthUtil();        string jwtToken = jwtAuthUtil.ExpRefreshToken(userToken);        // 送出刷新 JwtToken        return Ok(new ApiResult { Status = true, JwtToken = jwtToken });}}

http://img2.58codes.com/2024/20139487DbSPitvvSq.jpg

跨域处理(CORS)

由于使用 OWIN 启动会改成由 Startup.cs 类别管理,因此需将放行的请求类型及跨域操作加入。

参考资料

API串接常见问题系列文C# Owin初探 概念理解(一)Web API OWIN CORS Handling No Access-Control-Allow-Origin Header (@后端实作主要参考)

NuGet 安装套件

Microsoft.Owin.Cors version="4.2.0"Microsoft.Owin.Security.OAuth version="4.2.0"

程式码实作

删除原 Web API 2 官方建议的作法,于 NuGet 移除 Microsoft.AspNet.WebApi.Cors

在 ASP.NET Web API 2 中启用跨原始来源要求

于 Startup.cs 设定最上方加入启用跨域及验证

using Microsoft.Owin;using Microsoft.Owin.Security.OAuth;using NSwag;using NSwag.AspNet.Owin;using NSwag.Generation.Processors.Security;using Owin;using System.Web.Http;using Thak_tshehWebAPI.Security;[assembly: OwinStartup(typeof(MyWebApiProject.Startup))]namespace MyWebApiProject{    /// <summary>    /// OWIN 启动类别    /// </summary>    public class Startup    {        /// <summary>        /// 应用程式配置        /// </summary>        /// <param name="app"></param>        public void Configuration(IAppBuilder app)        {            // 启用跨域及验证配置            ConfigureAuth(app);            var config = new HttpConfiguration();            // 针对 JSON 资料使用 camel (JSON 回应会改 camel,但 Swagger 提示不会)            //config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();            app.UseSwaggerUi3(typeof(Startup).Assembly, settings =>            {                // 针对 WebAPI,指定路由包含 Action 名称                settings.GeneratorSettings.DefaultUrlTemplate =                    "api/{controller}/{action}/{id?}";                // 加入客製化调整逻辑名称版本等                settings.PostProcess = document =>                {                    document.Info.Title = "WebAPI : 专案名称";                };                // 加入 Authorization JWT token 定义                settings.GeneratorSettings.DocumentProcessors.Add(new SecurityDefinitionAppender("Bearer", new OpenApiSecurityScheme()                {                    Type = OpenApiSecuritySchemeType.ApiKey,                    Name = "Authorization",                    Description = "Type into the textbox: Bearer {your JWT token}.",                    In = OpenApiSecurityApiKeyLocation.Header,                    Scheme = "Bearer" // 不填写会影响 Filter 判断错误                }));                // REF: https://github.com/RicoSuter/NSwag/issues/1304 (每支 API 单独呈现认证 UI 图示)                settings.GeneratorSettings.OperationProcessors.Add(new OperationSecurityScopeProcessor("Bearer"));            });            app.UseWebApi(config);            config.MapHttpAttributeRoutes();            config.EnsureInitialized();        }        /// <summary>        /// 启用跨域及验证配置        /// </summary>        /// <param name="app"></param>        private void ConfigureAuth(IAppBuilder app)        {            // 建立 OAuth 配置            var oAuthOptions = new OAuthAuthorizationServerOptions            {                Provider = new AuthorizationServerProvider()            };            // 启用 OAuth2 bearer tokens 验证并加入配置            app.UseOAuthAuthorizationServer(oAuthOptions);        }    }}

新增 AuthorizationServerProvider.cs 类别档,设定跨域 Request Headers 处理逻辑

using Microsoft.Owin;using Microsoft.Owin.Security.OAuth;using System;using System.Configuration;using System.Linq;using System.Threading.Tasks;namespace MyWebApiProject.Security{    /// <summary>    /// OAuth 配置并继承 OAuthAuthorizationServerProvider    /// </summary>    public class AuthorizationServerProvider : OAuthAuthorizationServerProvider    {        /// <summary>        /// 在验证客户端身分前调用,并依客户端请求来源配置 CORS 允许类型设定        /// </summary>        /// <param name="context"></param>        /// <returns></returns>        public override Task MatchEndpoint(OAuthMatchEndpointContext context)        {            // 依请求来源配置 CORS 允许类型设定            SetCORSPolicy(context.OwinContext);            // 如果请求为预检请求则设为完成直接回传            if (context.Request.Method == "OPTIONS") {                context.RequestCompleted();                return Task.FromResult(0);            }            return base.MatchEndpoint(context);        }        /// <summary>        /// 依请求来源配置 CORS 允许类型设定        /// </summary>        /// <param name="context"></param>        private void SetCORSPolicy(IOwinContext context)        {            // 取出允许跨域的网址 (放在 Web.config 的 appSettings)            string allowedUrls = ConfigurationManager.AppSettings["allowedOrigins"];            // 有填写允许跨域的网址,就分割取出判断请求的来源是否等于允许跨域的网址,并将允许网址加入 Headers            if (!String.IsNullOrWhiteSpace(allowedUrls)) {                var list = allowedUrls.Split(',');                if (list.Length > 0) {                    string origin = context.Request.Headers.Get("Origin");                    var found = list.Where(item => item == origin).Any();                    if (found) {                        context.Response.Headers.Add("Access-Control-Allow-Origin",                                                     new string[] { origin });                    }                }            }            // 配置允许请求的 Headers 内容            context.Response.Headers.Add("Access-Control-Allow-Headers",                                   new string[] { "Authorization", "Content-Type" });            // 配置允许请求的 Headers 方法            context.Response.Headers.Add("Access-Control-Allow-Methods",                                   new string[] { "OPTIONS", "GET", "POST", "PUT", "DELETE"});        }    }}

于 Web.config 加入允许前端跨域请求的网址 (若无需求可不填)

可填测试网域或前端开发时使用的本地端网域,用逗点区隔添加
<configuration>  <appSettings>  <!--Owin CORS-->  <add key="allowedOrigins" value="https://www.myfriendproject.com,https://localhost:44444,http://127.0.0.1:5500" />  </appSettings></configuration>

注意事项

複杂型别 ⇒ 会自动转到 Body ⇒ 所以如果要当 GET 用并夹在 uri ⇒ API输入资料型别前才要加 [FromUri] ⇒ Swagger UI 会变成每个值都是个别输入栏位简单型别 ⇒ 会自动转到 Uri ⇒ 所以如果要当 POST 用并藏到 body ⇒ API输入资料型别前才要加 [FromBody] ⇒ Swagger UI 会变成一个可以输入多行的栏位
[OpenApiTag("Users", Description = "使用者操作功能")]public class UsersController : ApiController{    /// <summary>    /// 1-5 联络我们功能 (JWT)    /// </summary>    /// <param name="contactUsVm">留言资料</param>    /// <returns></returns>    /// <summary>    [JwtAuthFilter] // 用于检核 JWT-Token     [HttpPost]    [SwaggerResponse(typeof(ApiResult))] // 显示回传资料的注解    [Route("api/users/contact-us")]    public IHttpActionResult SendContactUsMail([FromUri] ContactUsVm contactUsVm)    {        // Do Sometning    }}

http://img2.58codes.com/2024/20139487RgGjPa5auE.jpg


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章