1、创建 WebApi 项目


使用 C# 构建 ASP.NET Core Web API 的全过程。内容包括项目初始化、Minimal APIs 与 Controller 两种开发模式对比、中间件配置、模型绑定(路由、查询、Header、Body)、数据注解与自定义验证、过滤器(Action & Exception)的应用。此外,还涵盖了基于 Entity Framework Core 连接 SQL Server 数据库进行数据持久化操作,以及使用 JWT 实现身份认证与授权的安全机制。



Program.cs 代码保留如下:
var builder = WebApplication.CreateBuilder(args); // Add services to the container.
var app = builder.Build(); // Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.Run();
在 Program.cs 中进行最小 API 使用:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseHttpsRedirection();
//获取所有衬衫数据列表
app.MapGet("/shirts", () => { return "获取所有衬衫数据列表"; });
//获取指定 ID 的衬衫数据
app.MapGet("/shirts/{id}", (int id) => { return $"获取 ID 为 {id} 的衬衫数据"; });
//创建一件新的衬衫
app.MapPost("/shirts", () => { return "创建一件新的衬衫"; });
//更新指定 ID 的衬衫数据
app.MapPut("/shirts/{id}", (int id) => { return $"更新 ID 为 {id} 的衬衫数据"; });
//删除指定 ID 的衬衫数据
app.MapDelete("/shirts/{id}", (int id) => { return $"删除 ID 为 {id} 的衬衫数据"; });
app.Run();


app 中的 Use 开头的方法都是中间件组件使用方法。

方法 1:将路由写在方法前面并指定操作动词
using Microsoft.AspNetCore.Mvc;
namespace WebApiTest.Controllers
{
[ApiController]
public class ShirtsController : ControllerBase
{
//获取所有衬衫数据列表
[HttpGet]
[Route("api/shirts")]
public string GetShirts() { return "获取所有衬衫数据列表"; }
//获取指定 ID 的衬衫数据
[HttpGet]
[Route("api/shirts/{id}")]
public string GetShirtById(int id) { return $"获取 ID 为 {id} 的衬衫数据"; }
//创建一件新的衬衫
[HttpPost]
[Route("api/shirts")]
public string CreateShirt() { return "创建一件新的衬衫"; }
//更新指定 ID 的衬衫数据
[HttpPut]
[Route("api/shirts/{id}")]
public string UpdateShirt(int id) { return $"更新 ID 为 {id} 的衬衫数据"; }
//删除指定 ID 的衬衫数据
[HttpDelete]
[Route("api/shirts/{id}")]
public string DeleteShirt(int id) { return $"删除 ID 为 {id} 的衬衫数据"; }
}
}
方法 2:将路由写在类前面,再在方法前面指定操作动词
using Microsoft.AspNetCore.Mvc;
namespace WebApiTest.Controllers
{
[ApiController]
[Route("/api/[controller]")]
public class ShirtsController : ControllerBase
{
//获取所有衬衫数据列表
[HttpGet]
public string GetShirts() { return "获取所有衬衫数据列表"; }
//获取指定 ID 的衬衫数据
[HttpGet("{id}")]
public string GetShirtById(int id) { return $"获取 ID 为 {id} 的衬衫数据"; }
//创建一件新的衬衫
[HttpPost]
public string CreateShirt() { return "创建一件新的衬衫"; }
//更新指定 ID 的衬衫数据
[HttpPut("{id}")]
public string UpdateShirt(int id) { return $"更新 ID 为 {id} 的衬衫数据"; }
//删除指定 ID 的衬衫数据
[HttpDelete("{id}")]
public string DeleteShirt(int id) { return $"删除 ID 为 {id} 的衬衫数据"; }
}
}
使用注解 [Route("/shirts")] 设置 Web API 的 URL,可以在方法前使用,也可以在类前使用。
方法前

类前

在路由 Route("/shirts/{id}") 或在操作动词 HttpGet("{id}") 设置,在方法参数前设置 [FromRoute] 也可以省略。
[HttpGet("{id}")]
public string GetShirtById([FromRoute]int id) { return $"获取 ID 为 {id} 的衬衫数据"; }
方法参数前使用注解 [FromQuery]。
http://localhost:5186/api/shirts/1?color=红色
[HttpGet("{id}")]
public string GetShirtById([FromRoute]int id, [FromQuery]string color) { return $"获取 ID 为 {id} 的衬衫数据"; }
方法参数前使用注解 [FromHeader]。
[HttpGet("{id}")]
public string GetShirtById([FromRoute]int id, [FromQuery]string color, [FromHeader]int size) { return $"获取 ID 为 {id} 的衬衫数据,衬衫颜色为{color},大小为{size}"; }
方法参数前使用注解 [FromBody]。
[HttpPost]
public string CreateShirt([FromBody]Shirt shirt) { return $"创建一件新的衬衫,{shirt.Id}, {shirt.Name}, {shirt.Color}, {shirt.Gender}, {shirt.Price}"; }
方法参数前使用注解 [FromForm]。
[HttpPost]
public string CreateShirt([FromForm]Shirt shirt) { return $"创建一件新的衬衫,{shirt.Id}, {shirt.Name}, {shirt.Color}, {shirt.Gender}, {shirt.Price}"; }

创建脚本 Shirt_EnsureCorrectSizingAttribute.cs。
using System.ComponentModel.DataAnnotations;
using WebApiDemo.Models;
namespace WebApiTest.Models.Validations
{
public class Shirt_EnsureCorrectSizingAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
var shirt = validationContext.ObjectInstance as Shirt;
if(shirt != null && !string.IsNullOrEmpty(shirt.Gender))
{
if(shirt.Gender.Equals("men", StringComparison.OrdinalIgnoreCase) && shirt.Size < 8)
{
return new ValidationResult("男性衬衫的尺码必须大于或等于 8。");
}
else if (shirt.Gender.Equals("women", StringComparison.OrdinalIgnoreCase) && shirt.Size < 6)
{
return new ValidationResult("女性衬衫的尺码必须大于或等于 6。");
}
}
return ValidationResult.Success;
}
}
}
在属性前添加注解

返回类型使用 IActionResult。
正确返回使用 Ok(返回数据)。
未找到使用 NotFound()。
错误响应使用 BadRequest()。
[HttpGet("{id}")]
public IActionResult GetShirtById([FromRoute]int id, [FromQuery]string color, [FromHeader]int size)
{
if(id <= 0)
{
//错误响应
return BadRequest();
}
else if(id > 10)
{
//未找到
return NotFound();
}
//正确响应
return Ok($"获取 ID 为 {id} 的衬衫数据,衬衫颜色为{color},大小为{size}");
}
用户输入传递的 id 可能不符合规范,可以通过操作过滤器进行模型验证对 id 进行验证。
创建脚本 Shirt_ValidateShirtIdFilterAttribute。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace WebApiTest.Filters.ActionFilters
{
public class Shirt_ValidateShirtIdFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
var id = context.ActionArguments["id"] as int?;
if (id.HasValue)
{
if(id.Value <= 0)
{
context.ModelState.AddModelError("Id", "衬衫 ID 必须大于 0。");
var problemDetails = new Microsoft.AspNetCore.Mvc.ValidationProblemDetails(context.ModelState)
{
Status = 400,
Title = "请求参数错误",
};
context.Result = new BadRequestObjectResult(problemDetails);
}
}
}
}
}
需要使用验证的方法前添加注解

更新衬衫之前可能其他请求把该衬衫已经删掉,更新时可能会报错,模拟该情景。
创建脚本 Shirt_HandleUpdateExceptionFilterAttribute。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace WebApiTest.Filters.ExceptionFilters
{
public class Shirt_HandleUpdateExceptionFilterAttribute : ExceptionFilterAttribute
{
public int?[] shirtsId = new int?[] { 1, 2, 3, 4, 5 };
public override void OnException(ExceptionContext context)
{
base.OnException(context);
//判断衬衫 ID 是否存在 暂时假设 shirtsId 为数据库中已有的衬衫 ID 列表
var strShirtId = context.RouteData.Values["id"] as string;
if (int.TryParse(strShirtId, out int shirtId))
{
if (shirtsId.FirstOrDefault(x => x == shirtId) == null)
{
context.ModelState.AddModelError("Id", $"衬衫已经不存在");
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Status = StatusCodes.Status404NotFound,
};
context.Result = new NotFoundObjectResult(problemDetails);
}
}
}
}
}
在需要异常处理的方法前使用

案例使用 SqlServer 数据库。

打开 SQL Server Management Studio 连接本地数据库

1)打开 管理 NuGet 程序包

2)搜索安装指定的包

3)双击查看是否安装成功


1)创建脚本 ApplicationDbContext。
using Microsoft.EntityFrameworkCore;
using WebApiDemo.Models;
namespace WebApiTest.Data
{
public class ApplicationDbContext : DbContext
{
public DbSet<Shirt> Shirts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//数据播种
modelBuilder.Entity<Shirt>().HasData(
new Shirt { Id = 1, Name = "衬衫 1", Color = "red", Gender = "men", Size = 10, Price = 100 },
new Shirt { Id = 2, Name = "衬衫 2", Color = "blue", Gender = "men", Size = 12, Price = 140 },
new Shirt { Id = 3, Name = "衬衫 3", Color = "黑色", Gender = "women", Size = 11, Price = 132 },
new Shirt { Id = 4, Name = "衬衫 4", Color = "白色", Gender = "women", Size = 7, Price = 151 }
);
}
}
}
1)添加连接字符串


2)获取连接字符串,视图->服务器资源管理器->数据连接





//需要将 master 替换为需要创建的数据库名称
Data Source=(local);Initial Catalog=master;Integrated Security=True;Trust Server Certificate=True
3)数据库上下文中创建构造函数指定连接数据库位置
using Microsoft.EntityFrameworkCore;
using WebApiDemo.Models;
namespace WebApiTest.Data
{
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions options) : base(options) { }
public DbSet<Shirt> Shirts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
//数据播种
modelBuilder.Entity<Shirt>().HasData(
new Shirt { Id = 1, Name = "衬衫 1", Color = "red", Gender = "men", Size = 10, Price = 100 },
new Shirt { Id = 2, Name = "衬衫 2", Color = "blue", Gender = "men", Size = 12, Price = 140 },
new Shirt { Id = 3, Name = "衬衫 3", Color = "黑色", Gender = "women", Size = 11, Price = 132 },
new Shirt { Id = 4, Name = "衬衫 4", Color = "白色", Gender = "women", Size = 7, Price = 151 }
);
}
}
}
4)在 Program.cs 添加运行 EntityFrameworkCore 所需的服务

builder.Services.AddDbContext<ApplicationDbContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("ShirtStoreManagement")); });
5)创建迁移代码并进行迁移

控制台运行命令 Add-Migration Init(Init 可以替换为该次进行操作说明)

运行成功会创建在迁移文件夹下创建迁移代码

在控制台运行迁移代码,Update-Database Init(Init 为迁移代码名称,不指定会执行最新的代码)

执行成功可以在查看数据库创建成功

在 ShirtsController 构造函数中依赖注入 Db 上下文

//获取所有衬衫数据列表
[HttpGet]
public IActionResult GetShirts()
{
return Ok(db.Shirts.ToList());
}
修改 Shirt_ValidateShirtIdFilterAttribute,实现验证 ID 在数据库是否存在。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApiTest.Data;
namespace WebApiTest.Filters.ActionFilters
{
public class Shirt_ValidateShirtIdFilterAttribute : ActionFilterAttribute
{
private readonly ApplicationDbContext db;
public Shirt_ValidateShirtIdFilterAttribute(ApplicationDbContext db) { this.db = db; }
public override void OnActionExecuting(ActionExecutingContext context)
{
base.OnActionExecuting(context);
var id = context.ActionArguments["id"] as int?;
if (id.HasValue)
{
if(id.Value <= 0)
{
context.ModelState.AddModelError("Id", "衬衫 ID 必须大于 0。");
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Status = 400,
Title = "请求参数错误",
};
context.Result = new BadRequestObjectResult(problemDetails);
}
else
{
var shirt = db.Shirts.Find(id.Value);
if(shirt == null)
{
context.ModelState.AddModelError("Id", $"ID 为{id.Value}的衬衫在数据库不存在");
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Status = 400,
Title = "衬衫 ID 不存在",
};
context.Result = new NotFoundObjectResult(problemDetails);
}
else
{
context.HttpContext.Items["shirt"] = shirt;
}
}
}
}
}
}
在操作过滤器中查询数据库存在 ID 衬衫在 context.HttpContext.Items 中进行存在,然后在控制器中获取,避免重复进行数据库查询减少性能开销。
控制器查询代码
//获取指定 ID 的衬衫数据
[HttpGet("{id}")]
[TypeFilter(typeof(Shirt_ValidateShirtIdFilterAttribute))]
public IActionResult GetShirtById([FromRoute]int id)
{
//正确响应
return Ok(HttpContext.Items["shirt"]);
}
//创建一件新的衬衫
[HttpPost]
public IActionResult CreateShirt([FromBody]Shirt shirt)
{
this.db.Shirts.Add(shirt);
this.db.SaveChanges();
return CreatedAtAction(nameof(GetShirtById), new { id = shirt.Id}, shirt);
}
修改代码 Shirt_HandleUpdateExceptionFilterAttribute,实现验证当前 ID 数据是否还存在,可能存在当前操作之前数据被删除的可能。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApiTest.Data;
namespace WebApiTest.Filters.ExceptionFilters
{
public class Shirt_HandleUpdateExceptionFilterAttribute : ExceptionFilterAttribute
{
private readonly ApplicationDbContext db;
public Shirt_HandleUpdateExceptionFilterAttribute(ApplicationDbContext db) { this.db = db; }
public override void OnException(ExceptionContext context)
{
base.OnException(context);
//判断衬衫 ID 是否存在 暂时假设 shirtsId 为数据库中已有的衬衫 ID 列表
var strShirtId = context.RouteData.Values["id"] as string;
if (int.TryParse(strShirtId, out int shirtId))
{
if (this.db.Shirts.FirstOrDefault(x => x.Id == shirtId) == null)
{
context.ModelState.AddModelError("Id", $"衬衫已经在数据库不存在,可能在此之前被删除了");
var problemDetails = new ValidationProblemDetails(context.ModelState)
{
Status = StatusCodes.Status404NotFound,
};
context.Result = new NotFoundObjectResult(problemDetails);
}
}
}
}
}
控制器更新代码
//更新指定 ID 的衬衫数据
[HttpPut("{id}")]
[TypeFilter(typeof(Shirt_ValidateShirtIdFilterAttribute))]
[TypeFilter(typeof(Shirt_HandleUpdateExceptionFilterAttribute))]
public IActionResult UpdateShirt(int id, [FromBody]Shirt shirt)
{
var shirtToUpdate = HttpContext.Items["shirt"] as Shirt;
shirtToUpdate.Name = shirt.Name;
shirtToUpdate.Size = shirt.Size;
shirtToUpdate.Color = shirt.Color;
shirtToUpdate.Gender = shirt.Gender;
shirtToUpdate.Price = shirt.Price;
this.db.SaveChanges();
return Ok($"ID 为{id}衬衫更新成功");
}
//删除指定 ID 的衬衫数据
[HttpDelete("{id}")]
[TypeFilter(typeof(Shirt_ValidateShirtIdFilterAttribute))]
public IActionResult DeleteShirt(int id)
{
var shirtToDelete = HttpContext.Items["shirt"] as Shirt;
this.db.Shirts.Remove(shirtToDelete);
this.db.SaveChanges();
return Ok(shirtToDelete);
}

1)创建应用程序信息类,Application.cs
namespace WebApiDemo.Authority
{
public class Application
{
public int ApplicationId { get; set; }
public string? ApplicationName { get; set; }
public string? ClientId { get; set; }
public string? Secret { get; set; }
public string? Scopes { get; set; }
}
}
2)创建应用内容存储,AppRepository.cs,替代已存储数据库的应用程序注册信息
namespace WebApiDemo.Authority
{
public static class AppRepository
{
private static List<Application> _applications = new List<Application>
{
new Application
{
ApplicationId = 1,
ApplicationName = "MVCWebApp",
ClientId = "53D3C1E6-4587-4AD5-8C6E-A8E4BD59940E",
Secret = "0673FC70-0514-4011-B4A3-DF9BC03201BC",
Scopes = "read,write,delete"
}
};
public static Application? GetApplicationByClientId(string clientId)
{
return _applications.FirstOrDefault(app => app.ClientId == clientId);
}
}
}
3)创建应用凭证类,AppCredential.cs,用于接受认证请求参数
namespace WebApiDemo.Authority
{
public class AppCredential
{
public string? ClientId { get; set; } = string.Empty;
public string? Secret { get; set; } = string.Empty;
}
}
4)创建应用认证控制器接口,AuthorityController.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using WebApiDemo.Authority;
namespace WebApiDemo.Controllers
{
[ApiController]
public class AuthorityController : ControllerBase
{
private readonly IConfiguration configuration;
public AuthorityController(IConfiguration configuration) { this.configuration = configuration; }
[HttpPost("auth")]
public IActionResult Authenticate([FromBody] AppCredential credential)
{
if (Authenticator.Authenticate(credential.ClientId, credential.Secret))
{
var expiresAt = DateTime.UtcNow.AddMinutes(10);
return Ok(new { access_token = Authenticator.CreateToken(credential.ClientId, expiresAt, configuration["SecurityKey"] ?? string.Empty), expires_at = expiresAt });
}
else
{
ModelState.AddModelError("Unauthorized", "未被授权");
var problemDetails = new ValidationProblemDetails(ModelState)
{
Status = StatusCodes.Status401Unauthorized,
};
return new UnauthorizedObjectResult(problemDetails);
}
}
}
}
5)设置密钥

6)创建应用认证身份验证逻辑类,Authenticator.cs
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
namespace WebApiDemo.Authority
{
public static class Authenticator
{
public static bool Authenticate(string clientId, string secret)
{
var app = AppRepository.GetApplicationByClientId(clientId);
if (app == null) return false;
return (app.ClientId == clientId && app.Secret == secret);
}
public static string CreateToken(string clientId, DateTime expiresAt, string strSecretKey)
{
//安全算法
//签名密钥
//负载(声明)
//生成签名
//算法
var signingCredentials = new SigningCredentials(
new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(strSecretKey)),
SecurityAlgorithms.HmacSha256Signature);
//负载(声明)
var app = AppRepository.GetApplicationByClientId(clientId);
var claimsDictionary = new Dictionary<string, object>
{
{ "AppName", app?.ApplicationName??string.Empty },
//{ "Read", (app?.Scopes??string.Empty).Contains("read") ? "true" : "false" },
//{ "Write", (app?.Scopes??string.Empty).Contains("write") ? "true" : "false" }
};
var scopes = app?.Scopes?.Split(',') ?? Array.Empty<string>();
if(scopes.Length > 0)
{
foreach(var scope in scopes)
{
claimsDictionary.Add(scope.Trim().ToLower(), "true");
}
}
var tokenDescriptor = new SecurityTokenDescriptor
{
SigningCredentials = signingCredentials,
Claims = claimsDictionary,
Expires = expiresAt,
NotBefore = DateTime.UtcNow,
};
var tokenHandler = new JsonWebTokenHandler();
return tokenHandler.CreateToken(tokenDescriptor);
}
public static async Task<IEnumerable<Claim>?> VerifyTokenAsync(string tokenString, string securityKey)
{
if(string.IsNullOrWhiteSpace(tokenString) || string.IsNullOrWhiteSpace(securityKey))
{
return null;
}
var keyBytes = System.Text.Encoding.UTF8.GetBytes(securityKey);
var tokenHandle = new JsonWebTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
IssuerSigningKey = new SymmetricSecurityKey(keyBytes),
ValidateIssuerSigningKey = false,
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
};
try
{
var result = await tokenHandle.ValidateTokenAsync(tokenString, validationParameters);
if(result.SecurityToken != null)
{
var tokenObject = tokenHandle.ReadJsonWebToken(tokenString);
return tokenObject.Claims ?? Enumerable.Empty<Claim>();
}
else
{
return null;
}
}
catch (SecurityTokenMalformedException)
{
return null;
}
catch (SecurityTokenExpiredException)
{
return null;
}
catch (SecurityTokenInvalidSignatureException)
{
return null;
}
catch (Exception)
{
throw;
}
}
}
}
7)通过过滤器方式实现 JWT 令牌验证
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApiDemo.Attributes;
using WebApiDemo.Authority;
namespace WebApiDemo.Filters.AuthFilters
{
public class JwtTokenAuthFilterAttribute : Attribute, IAsyncAuthorizationFilter
{
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
//1. 从请求头中获取授权标识
if (!context.HttpContext.Request.Headers.TryGetValue("Authorization", out var authHeader))
{
context.Result = new UnauthorizedResult();
return;
}
string tokenString = authHeader.ToString();
//2. 去掉 Bearer 前缀
if (tokenString.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
tokenString = tokenString.Substring("Bearer ".Length).Trim();
}
else
{
context.Result = new UnauthorizedResult();
return;
}
//3. 获取配置和安全密钥
var configuration = context.HttpContext.RequestServices.GetService<IConfiguration>();
var securityKey = configuration?["SecurityKey"] ?? string.Empty;
//4. 验证令牌 并且 提取声明
//if(!await Authenticator.VerifyTokenAsync(tokenString, securityKey))
//{
// context.Result = new UnauthorizedResult();
//}
var claims = await Authenticator.VerifyTokenAsync(tokenString, securityKey);
if (claims == null)
{
context.Result = new UnauthorizedResult(); // 无效令牌 401
}
else
{
//5. 获取声明需求
var requiredClaism = context.ActionDescriptor.EndpointMetadata
.OfType<RequiredClaimAttribute>()
.ToList();
if(requiredClaism != null && !requiredClaism.All(rc => claims.Any(c => c.Type.Equals(rc.ClaimType, StringComparison.OrdinalIgnoreCase) && c.Value.Equals(rc.ClaimValue, StringComparison.OrdinalIgnoreCase))))
{
context.Result = new StatusCodeResult(403); // 权限不足 403
}
}
}
}
}
8)授权、权限作用域校验类,RequiredClaimAttribute.cs
namespace WebApiDemo.Attributes
{
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class,AllowMultiple = true)]
public class RequiredClaimAttribute : Attribute
{
public string ClaimType { get; }
public string ClaimValue { get; }
public RequiredClaimAttribute(string claimType, string claimValue)
{
this.ClaimType = claimType;
this.ClaimValue = claimValue;
}
}
}

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online