前言

上一篇博客介绍了使用Nancy框架内部的方法来创建了一个简单到不能再简单的Document。但是还有许许多多的不足。

为了能稍微完善一下这个Document,这篇引用了当前流行的Swagger,以及另一个开源的Nancy.Swagger项目来完成今天的任务!

注:Swagger是已经相对成熟的了,但Nancy(2.0.0-clinteastwood)和Nancy.Swagger(2.2.6-alpha)是基于目前的最新版本,但目前的都是没有发布正式版,所以后续API可能会有些许变化。

下面先来简单看看什么是Swagger

何为Swagger

The World's Most Popular Framework for APIs.这是Swagger官方的描述。能说出是世界上最流行的,也是要有一定资本的!

光看这个描述就知道Swagger不会差!毕竟人家敢这样说。当然个人也认为Swagger确实很不错。

通过官方文档,我们都知道要想生成Swagger文档,可以使用YAML或JSON两种方式来书写,由于我们平常写程序用的比较多的是JSON!

所以本文主要是使用了JSON,顺带说一下YAML的语法也是属于易懂易学的。

既然是用JSON书写,那么要怎么写呢?这个其实是有一套规定、约束,我们只要遵守这些来写就可以了。详细内容可以参见OpenAPI Specification

本文后面的内容将默认园友们对Swagger有过了解。

Swagger主要有下面几个东西,要引用基本的样式和脚本就不在多说了。

当然,引用样式和脚本只是最基本的前提,下面这段js(来自swagger-ui项目)才是最为主要的!

<script>
window.onload = function() {
// Build a system
const ui = SwaggerUIBundle({
url: "your url",//返回json数据的url地址
dom_id: '#swagger-ui',//在这个div展示内容
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
}) window.ui = ui
}
</script>

就是在上面加上注释的两个属性:url指定了我们要展示数据(JSON格式)的来源,dom_id指定了在id为swagger-ui的容器中展示我们的文档。

在加载的时候创建了Swagger相关的内容,主要的有下面的两个,其余的用默认的就可以了。

简单来说,我们请求了这个url拿到了这些json数据,再根据这些数据在dom_id中构造出我们所看到的页面。有那么点数据驱动的意思。

当然这些JSON数据是有格式要求的。可以看看下面的简单示例

{
"swagger": "2.0",
"info": {
"title": "Simple API overview",
"version": "v2"
},
"paths": {
"/": {
"get": {
"operationId": "listVersionsv2",
"summary": "List API versions",
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "200 300 response",
"examples": {
"application/json": "一串json"
}
}
}
}
}
},
"consumes": [
"application/json"
]
}

这也就意味着我们只需要严格按照Swagger的定义,就可以生成一个即美观,又可执行的API文档了。

更多相关JSON示例可参见

https://github.com/OAI/OpenAPI-Specification/tree/master/examples/v2.0/json

Nancy.Swagger说明

Nancy.Swagger是我们今天的主角,是一个基于MIT协议的开源项目。Github地址:Nancy.Swagger

当然通过上面关于Swagger的说明,也已经大概明白了这个项目主要为我们做了什么。就是构造Swgger所需要的JSON格式的数据!

它并没有像Swashbuckle.AspNetCore一样集成了SwaggerUI的内容到项目中去,只是一个提供数据的项目。

其官方的示例Demo是用跳转到petstore.swagger.io方式来完成的。但是经常性是要等待很长时间的,应该是网络的问题。

为了避免这一情况,可以通过下面的操作避免:

  • 手动下载swagger-ui相关的内容并添加到我们的新项目中。同时我还将这些设置成嵌入式的资源。

  • 添加一个用于显示的页面,示例为doc.html,内容可以照搬swagger-ui目录下面的index.html

  • 在Bootstrapper中添加静态资源的引用

protected override void ConfigureConventions(NancyConventions nancyConventions)
{
base.ConfigureConventions(nancyConventions);
nancyConventions.StaticContentsConventions.Add(StaticContentConventionBuilder.AddDirectory("swagger-ui"));
}
  • 在访问我们API时,将其重定向到doc.html页面
public class HomeModule : NancyModule
{
public HomeModule()
{
Get("/", _ =>
{
return Response.AsRedirect("/swagger-ui");
}); Get("/swagger-ui",_=>
{
var url = $"{Request.Url.BasePath}/api-docs";
return View["doc", url];
});
}
}
  • 修改doc.html的内容,将上述的url,替换成@Model
window.onload = function() {
// Build a system
const ui = SwaggerUIBundle({
url: "@Model",
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
window.ui = ui
}

完成上面的内容后,就开始构造我们的文档了。

构造文档的基本信息

这里主要是设置这个API文档的概要信息,比如文档的标题,此api的版本等

需要通过SwaggerMetadataProvider的SetInfo方法来设置这些信息

下面是具体的示例代码,写在Bootstrapper中:

protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
SwaggerMetadataProvider.SetInfo("Nancy Swagger Example", "v1.0", "Some open api", new Contact()
{
EmailAddress = "catcher_hwq@outlook.com",
Name = "Catcher Wong",
Url = "http://www.cnblogs.com/catcher1994"
}, "http://www.cnblogs.com/catcher1994"); base.ApplicationStartup(container, pipelines);
}

此时对应的大致效果(这个时候是不能正常运行的,只是显示了这部分的效果)如下:

上面代码生成的JSON数据是符合规范的,如下所示:

下面要做的就是构造路由相关的信息

不带任何请求参数

先在Module中定义一个简单的路由,这个路由不带任何参数。

Get("/", _ =>
{
var list = new List<Product>
{
new Product{ Name="p1", Price=199 , IsActive = true },
new Product{ Name="p2", Price=299 , IsActive= true }
}; return Negotiate.WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), list);
}, null, "GetProductList");

然后在MetadataModule中添加相应的描述,这里的MetadataModule与上一篇是相似的,这也是为什么我会在上一篇先介绍不使用

第三方组件的来构造的原因,因为这种写法下面,两者没有本质的区别!

 Describe["GetProductList"] = desc => desc.AsSwagger(
with => with.Operation(
op => op.OperationId("GetProductList")
.Tag("Products")
.Summary("Get all products")
.Response(r=>r.Schema<IEnumerable<Product>>().Description("OK"))
.Description("This returns a list of products")
));

下面是部分Nancy.Swagger里面的核心内容,也是上一篇所没有的特殊之处。

AsSwagger是RouteDescription一个扩展方法,这个方法是返回我们需要的PathItem。

OperationId是这个路由的一个友好名称,源码里面的字段定义表明它要唯一。对更加详尽的描述可能去看Swagger中对这些参数的说明!

Tag可以理解为这个路由属于那个分组,起分隔符的作用,举个例子,现在有A,B两个模块的API,我们肯定不能把它们交叉排列下去

而是A的放到一个地方,B的一个地方,便于我们的的区分。

Summary是当前路由的精简描述,要小于120个字符。

Description是当前路由的详细描述。

Response是期望的运行结果的相关内容,可以有多个,这里没有标明状态码,而是直接写处理的内容,此时说明这里用的是默认的状态码。

Response里面又是一个委托,里面又有部分定义:

Schema表明当前响应应该返回的类型是什么

Description是这个响应对应的描述信息

这个时候是会出错的,因为我们在Respoonse的时候指定了Schema,但是我们并没有指定它的定义。

我们需要先在MetadataModule中引用ISwaggerModelCatalog这个接口并调用它的AddModel方法把相关的类型添加进去,这样才能正常运行!

public ProductsMetadataModule(ISwaggerModelCatalog modelCatalog)
{
//添加相应的类型
modelCatalog.AddModels(typeof(Product), typeof(IEnumerable<Product>)); Describe["GetProductList"] = desc => desc.AsSwagger(
with => with.Operation(
op =>
op.OperationId("GetProductList")
.Tag("Products")
.Summary("Get all products")
//在Schema中使用modelCatalog
.Response(r => r.Schema<IEnumerable<Product>>(modelCatalog).Description("OK"))
.Description("This returns a list of products")
));
}

示例结果如下:

先来看看上面设置对应的内容:

点击Try it out运行的结果

可以看到使用curl 去访问我们的实际接口拿到服务器的响应信息(结果和头部)

在终端执行一下这个命令,也是这个结果。

带Path参数和Query参数

同样的,先在Module中定义一个路由,这个路由包含了一个Path参数和一个Query参数

Get("/{productid}", _ =>
{ var productId = _.productid; if (string.IsNullOrWhiteSpace(productId))
{
return HttpStatusCode.NotFound;
} var isActive = Request.Query.isActive ?? true; var product = new Product
{
Name = "apple",
Price = 100,
IsActive = isActive
}; return Negotiate.WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), product);
}, null, "GetProductByProductId");

这里作了多一点操作,为的是演示尽可能多的用法。如果传递的产品id为空,则直接返回404。如果没有输入isActive这个Query参数

返回Productr的IsActive就为false。

然后在MetadataModule中添加相应的描述

Describe["GetProductByProductId"] = desc => desc.AsSwagger(
with => with.Operation(
op => op.OperationId("GetProductByProductId")
.Tag("Products2")
.Summary("Get a product by product's id")
.Description("This returns a product's infomation by the special id")
.Parameter(new Parameter
{
Name = "productid",
In = ParameterIn.Path,//指明该参数是对应路由上面的同名参数
Required = true,//必填
Description = "id of a product"
})
.Parameter(new Parameter
{
Name = "isactive",
In = ParameterIn.Query,//指明该参数是对应QueryString上面的参数
Description = "get the actived product",
Required = false//非必填
})
.Response(r => r.Schema<Product>(modelCatalog).Description("Here is the product"))
.Response(404, r => r.Description("Can't find the product"))
));

这里多了一个Parameter是上面没有提到的,这个就是我们的请求参数,这里的请求参数包含下面五种:

  • Path
  • Query
  • Body
  • Header
  • Form

下面是运行的效果图,分别演示了下面几种情况

  • 不填productid,不能执行,输入框会变红
  • 填了productid,能执行,但是服务器端返回的isactive是false
  • 填了productid和isactive,能执行,服务器返回的isactive是true

当然现在在MetadataModule的参数还有其他的写法

 Describe["GetProductByProductId"] = desc => desc.AsSwagger(
with => with.Operation(
op => op.OperationId("GetProductByProductId")
.Tag("Products2")
.Summary("Get a product by product's id")
.Description("This returns a product's infomation by the special id")
.Parameters(new List<Parameter>
{
new Parameter{Name = "productid",In = ParameterIn.Path,Required = true,Description = "id of a product"},
new Parameter{Name = "isactive",In = ParameterIn.Query,Description = "get the actived product",Required = false}
})
.ProduceMimeType("application/json")
.Response(r => r.Schema<Product>(modelCatalog).Description("Here is the product"))
.Response(404, r => r.Description("Can't find the product"))
));

可以用Parameters直接将所有的参数,组合成一个集合来进行处理。

此时的效果和上面是一样的。

请求头参数和请求体参数

在Module中添加一个新增商品的方法,这个方法包含两种请求参数,一种是正常POST的json格式的数据,一种是请求头,对于请求头,只是判断了一下客户端发起的请求有没有包含相应的请求头就是了,并没有做严格的判断。同时为了演示多种MIME类型的返回结果,这里兼容了json和xml格式的返回结果。

Post("/", _ =>
{
var product = this.Bind<Product>(); if(!Request.Headers.Any(x=>x.Key=="test"))
{
return HttpStatusCode.BadRequest;
} return Negotiate
.WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), product)
.WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/xml"), product)
;
}, null, "AddProduct");

同样的,MetadataModule中添加如下的描述:

Describe["AddProduct"] = desc => desc.AsSwagger(
with => with.Operation(
op => op.OperationId("AddProduct")
.Tag("Products")
.Summary("Add a new product to database")
.Description("This returns the added product's infomation")
.BodyParameter(para=>para.Name("para").Schema<Product>().Description("the infomation of the adding product").Build())//Request body
.Parameter(new Parameter()
{
Name = "test",
In = ParameterIn.Header,//http请求头
Description = "must be not null",
Required = true,
})
.ConsumeMimeType("application/json") //post的参数只允许是json格式
.ProduceMimeTypes(new List<string>{ "application/json","application/xml" })//结果支持json和xml
.Response(r => r.Schema<Product>(modelCatalog).Description("Here is the added product"))
.Response(400, r => r.Description("Some errors occur during the processing"))
));

BodyParameter是我们在POST等操作时用的,它需要指定我们POST的数据格式(Schema那里的类型),为了演示添加请求头信息,所以这里也加了一个必填的请求头信息。

ConsumeMimeType表示我们发起请求的数据格式必须是json格式的,当然也可以支持多种不同的数据格式。

ProduceMimeTypes表示服务端响应时支持的数据格式,这里指定了json和Xml也是为了和我们Module中的内容相对应。

演示效果:

标注过时API和一个API属于多个分组

有时候,API的界限分的不是很清晰或者有交集的时候,可能会出现这样的情况:一个api会属于多个分组。

前面我们都是直接指定了一个tag,也就表示上面的只是对应一个tag。

先来定义一个方法,用于演示多分组和过时、废弃的API

Head("/",_=>
{
return HttpStatusCode.OK;
},null,"HeadOfProduct");

Metadata内容

 Describe["HeadOfProduct"] = desc => desc.AsSwagger(
with => with.Operation(
op => op.OperationId("HeadOfProduct")
.Tags(new List<string>() { "Products", "Products2" })//同时属于两个分组
.Summary("Something is deprecated")
.Description("This returns only http header")
.IsDeprecated()//过时的,相当于常用的Obsolete,但是还可以用
.Response(r => r.Description("Nothing will return but http headers"))
));

效果如下:

虽说已经标记为过时了,但是本质这个方法还是存在,所以也是能正常调用的。

安全认证问题

Swagger支持3种安全认证折方式:APIKEY、Basic、OAuth2.0,同样的Nancy.Swagger也支持,不过有点坑就是了。

使用的话有两个步骤(这里用最简单的APIKEY演示):

Step 1: 引用定义,在Bootstrapper中添加验证相关的内容

protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
SwaggerMetadataProvider.SetInfo("Nancy Swagger Example", "v1.0", "Some open api", new Contact()
{
EmailAddress = "catcher_hwq@outlook.com",
Name = "Catcher Wong",
Url = "http://www.cnblogs.com/catcher1994",
}, "http://www.cnblogs.com/catcher1994"); var securitySchemeBuilder = new ApiKeySecuritySchemeBuilder();
securitySchemeBuilder.Description("Authentication with apikey");
securitySchemeBuilder.IsInQuery();
securitySchemeBuilder.Name("Item1");
SwaggerMetadataProvider.AddSecuritySchemeBuilder(securitySchemeBuilder, "Item1"); base.ApplicationStartup(container, pipelines);
}

Step 2 : 在MetadataModule中添加描述

Describe["Head"] = description => description.AsSwagger(
with => with.Operation(
op => op.OperationId("Head")
.Tag("Head method")
.SecurityRequirement(SecuritySchemes.ApiKey)
.Summary("an example head method")
.Response(r => r.Description("OK"))));

当然,目前是没有办法正常运行的!此时运行效果如下:

单独打开/api-docs这个路径时提示如下错误:

这个十有八九是Nancy.Swagger的安全验证存在bug的,这个项目没有足够多的单元测试可能也是导致问题的一部分原因。

发现的主要bug是在MetadataModule中使用SecurityRequirement(SecuritySchemes.ApiKey)时一直在报错,报错内容如下:

Nancy.RequestExecutionException: Oh noes! ---< System.InvalidCastException: Unable to cast object of type 'Swagger.ObjectModel.SecuritySchemes' to type 'System.String'.

at Swagger.ObjectModel.SwaggerModel.SwaggerSerializerStrategy.ToObject(IDictionary source)

于是调试源码,发现在Swagger.ObjectModel项目下的ToObject方法有问题

private static dynamic ToObject(IDictionary source)
{
var expando = new ExpandoObject();
var expandoCollection = (ICollection<KeyValuePair<string, object>>)expando; foreach (string key in source.Keys)
{
expandoCollection.Add(new KeyValuePair<string, object>(key, source[key]));
} return expando;
}

从上面的出错内容也能清楚的看到,SecuritySchemes不能转成string的,其中SecuritySchemes是一个枚举类型。

为了能正常运行,肯定要修改验证一下!!于是修改成如下 :

private static dynamic ToObject(IDictionary source)
{
var expando = new ExpandoObject();
var expandoCollection = (ICollection<KeyValuePair<string, object>>)expando;
//用了var,在使用的时候强制ToString一下将其转成string
foreach (var key in source.Keys)
{
expandoCollection.Add(new KeyValuePair<string, object>(key.ToString(), source[key]));
} return expando;
}

由于在Mac上无法打开这个项目,所以上面的修改是切换回windows完成的。

进行上面的修改后,项目是已经能正常运行了!但是却少了一个很重要的东西!

在这个方法里面加了APIKEY验证的,但是小锁的标记却没有出来!

之后对比了Swagger的官方示例http://petstore.swagger.io/

居然有这么坑爹的事情!security是一个数组啊,不是一个对象啊~~

后面就修改了Nancy.Swagger里面的许多代码(瞎改的,只为了能正常运行),涉及了好几个类文件,就不一一说明了。

第一个问题已经提了PR到这个项目了,第二个问题还没找到比较满意的方案,暂时没提。

直接上最后的效果图,分别演示了,没有验证,验证成功和验证失败这三种情况!

注:本文只演示了其中Nancy.Swagger的其中一种用法,而且还有部分内容是没有涉及到的。还有两种其他用法有时间会拿出来和大家分享。

注意事项

在过程中还有一个需要十分注意的地方(本来这个应该是在上一篇提及的):就是XXModule和XXMetadataModule相对应的位置关系。

Nancy在这里限制的比较死,强制了下面三种情况:

Module所在的位置 MetadtaModule应该在的位置
./BlahModule ./BlahMetadataModule
./BlahModule ./Metadata/BlahMetadataModule
./Modules/BlahModule ../Metadata/BlahMetadataModule

这是文件分布所要注意的问题。

还有一个命名应该注意的问题:当我们对一个Module起名为ProductsModule时,它对应的MetadataModule一定要是ProductsMetadataModule。

而不能是其它,有一次由于粗心,忘记把s字母带上,花了不少时间去找原因~~

上述两个问题的答案在Nancy.Metadata.Modules项目的DefaultMetadataModuleConventions类中。

简单总结

Nancy.Swagger给我们API文档化的道路上带来了不少的便利之处,除了安全验证这一块的问题有点坑,其他的算是比较正常,用起来也还算简单。

对于Swagger来说,通用性很好,只要提供的指定格式的数据就能很好的渲染出让人舒适的界面,或许这就是它这么流行的一个关键点吧。

下面是一张脑图简单的概括相关的内容 :

本文已同步到Catcher写的Nancy汇总博客:Nancy之大杂烩

浅析如何在Nancy中使用Swagger生成API文档的更多相关文章

  1. ASP.NET Core 3.0 WebApi中使用Swagger生成API文档简介

    参考地址,官网:https://docs.microsoft.com/zh-cn/aspnet/core/tutorials/getting-started-with-swashbuckle?view ...

  2. Web Api 2.0中使用Swagger生成Api文档的2个小Tips

    当Web Api 2.0使用OAuth2授权时,如何在Swagger中添加Authorization请求头? Swagger说明文档支持手动调用Api, 但是当Api使用OAuth2授权时,由于没有地 ...

  3. Spring MVC中使用Swagger生成API文档和完整项目示例Demo,swagger-server-api(二十)

    一:Swagger介绍 Swagger是当前最好用的Restful API文档生成的开源项目,通过swagger-spring项目 实现了与SpingMVC框架的无缝集成功能,方便生成spring r ...

  4. Spring MVC中使用Swagger生成API文档和完整项目示例Demo,swagger-server-api

    本文作者:小雷FansUnion-一个有创业和投资经验的资深程序员-全球最大中文IT社区CSDN知名博主-排名第119   实际项目中非常需要写文档,提高Java服务端和Web前端以及移动端的对接效率 ...

  5. NetCore 3.0 中使用Swagger生成Api说明文档及升级报错原因

    认识Swagger Swagger 是一个规范和完整的框架,用于生成.描述.调用和可视化 RESTful 风格的 Web 服务.总体目标是使客户端和文件系统作为服务器以同样的速度来更新.文件的方法,参 ...

  6. .Net Core 3.1 WebApi使用Swagger生成Api文档

    用swagger生成Api文档 1.安装Swashbuckle.AspNetCore 右键单击"解决方案资源管理器" > "管理 NuGet 包"中的项目 ...

  7. 12 Django Rest Swagger生成api文档

    01-简介 Swagger:是一个规范和完整的框架,用于生成.描述.调用和可视化RESTful风格的Web服务.总体目标是使客户端和文件系统源代码作为服务器以同样的速度来更新.当接口有变动时,对应的接 ...

  8. Laravel(PHP)使用Swagger生成API文档不完全指南 - 基本概念和环境搭建 - 简书

    在PHPer中,很多人听说过Swagger,部分人知道Swagger是用来做API文档的,然而只有少数人真正知道怎么正确使用Swagger,因为PHP界和Swagger相关的资料实在是太少了.所以鄙人 ...

  9. 使用swagger生成API文档

    有时候一份清晰明了的接口文档能够极大地提高前后端双方的沟通效率和开发效率.本文将介绍如何使用swagger生成接口文档. swagger介绍 Swagger本质上是一种用于描述使用JSON表示的RES ...

随机推荐

  1. SQL Server中的Merge关键字 更新表数据

    简介 Merge关键字是一个神奇的DML关键字.它在SQL Server 2008被引入,它能将Insert,Update,Delete简单的并为一句.MSDN对于Merge的解释非常的短小精悍:”根 ...

  2. vue组件递归

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  3. 运用google-protobuf的IM消息应用开发(前端篇)

    前言: 公司原本使用了第三方提供的IM消息系统,随着业务发展需要,三方的服务有限,并且出现问题也很难处理和排查,所以这次新版本迭代,我们的server同事呕心沥血做了一个新的IM消息系统,我们也因此配 ...

  4. css常用居中

    对一个已知大小的元素上下左右居中(已知大小了,直接margin也就行了): css如下:.parent{height:100px;width:100px;background:grey;positio ...

  5. PyQt通过resize改变窗体大小时ListWidget显示异常

    前几天开始的pygame音乐播放器Doco,做的差不多了,上午做到了歌词显示和搜索页面.遇到bug,即通过resize改变ui大小时ListWidget显示异常 #目的: 增加一部分窗口用来显示歌词和 ...

  6. 使用cocapods报错 [!] Your Podfile has had smart quotes sanitised. To avoid issues in the future, you should not use TextEdit for editing it. If you are not using TextEdit, you should turn off smart quotes

    从github上下载的工程大部分都使用了cocapods,在install的时候可能会报错. 报错原因: 1.不要使用文本编辑去编辑Podfile文件,使用Xcode编辑,或者使用终端敲命令去编辑. ...

  7. CSS如何实现圆角的outline效果?

    一.首先,outline是个很牛逼的东西 温故而知鑫,10年的时候写过一篇可用性方面的文章:“页面可用性之outline轮廓外框的一些研究”,还算挺有用的:3年之后,也就是13年,介绍了个没什么使用价 ...

  8. 深入tornado中的协程

    tornado使用了单进程(当然也可以多进程) + 协程 + I/O多路复用的机制,解决了C10K中因为过多的线程(进程)的上下文切换 而导致的cpu资源的浪费. tornado中的I/O多路复用前面 ...

  9. Spring Boot 学习笔记--整合Redis

    1.新建Spring Boot项目 添加spring-boot-starter-data-redis依赖 <dependency> <groupId>org.springfra ...

  10. 关于DCL的使用

    DCL1 创建用户语法:CREATE USER 用户名@地址 IDENTIFIED BY '密码';CREATE USER user1@localhost IDENTIFIED BY '123'; C ...