# README
webapi
这是SlimWebApi的 Go 版。 SlimAPI 通信协议详见godoc-SlimAPI通信协议。
同时,基于 SlimAPI ,提供一套带有签名校验逻辑的扩展 SlimAuth。
快速使用
安装:
go get -u github.com/cmstar/go-webapi@latest
上代码:
package main
import (
"fmt"
"net/http"
"time"
"github.com/cmstar/go-errx"
"github.com/cmstar/go-logx"
"github.com/cmstar/go-webapi"
"github.com/cmstar/go-webapi/slimapi"
)
func main() {
// 初始化 API 容器。
slim := slimapi.NewSlimApiHandler("demo")
// 注册 WebAPI 方法。
slim.RegisterMethods(Methods{})
// 初始化日志。 https://github.com/cmstar/go-logx
logger := logx.NewStdLogger(nil)
logFinder := logx.NewSingleLoggerLogFinder(logger)
// 初始化引擎。
e := webapi.NewEngine()
// 注册路由,使用 chi 库的语法。 https://github.com/go-chi/chi
e.Handle("/api/{~method}", slim, logFinder)
// 启动。
err := http.ListenAndServe(":15001", e)
if err != nil {
logger.Log(logx.LevelFatal, err.Error())
}
}
// 用于承载 WebAPI 方法。
type Methods struct{}
// 方法必须是 exported ,即大写字母开头的。
func (Methods) Plus(req struct {
A int // 参数首字母也必须是大写的。
B int
}) int {
return req.A + req.B
}
func (Methods) Time() string {
return time.Now().Format("2006-01-02 15:04")
}
// 参数也可以是 *webapi.ApiState ,可通过其访问到当前请求的上下文。
// 也可以同时搭配 struct 型的参数。
func (Methods) Headers(state *webapi.ApiState, req struct{ NoMeaning bool }) map[string][]string {
// RawRequest 是标准库中,当前请求的 *http.Request 。
return state.RawRequest.Header
}
// 支持至多两个返回值,第一个返回值对应输出的 Data 字段;第二个返回值必须是 error 。详见《错误处理》节。
func (Methods) Err(req struct {
BizErr bool
Value string
}) (string, error) {
if req.BizErr {
return req.Value, errx.NewBizError(12345, "your message", nil)
}
return "", fmt.Errorf("not a biz-error: %v", req.Value)
}
跑起来,现在可以调用 Methods
上的方法了。方法名称和参数都是大小写不敏感的。
GET http://localhost:15001/api/plus?a=11&b=22
=> {
"Code": 0,
"Message": "",
"Data": 33
}
---
# 以 JSON 格式请求。
POST http://localhost:15001/api/plus
Content-Type: application/json
{"a":11, "b":22}
=> {
"Code": 0,
"Message": "",
"Data": 33
}
---
GET http://localhost:15001/api/time
=> {
"Code": 0,
"Message": "",
"Data": "2022-03-06 23:16"
}
---
GET http://localhost:15001/api/headers
=> {
"Code": 0,
"Message": "",
"Data": {
"Accept": ["text/html,application/xhtml+xml"],
"Accept-Encoding": ["gzip, deflate, br"],
"Connection": ["keep-alive"],
"User-Agent": ["Mozilla/5.0 ..."],
...
}
}
---
# BizError 会以 Code + Message 的方式体现在输出上。
GET http://localhost:15001/api/err?bizErr=1&value=my-value
=> {
"Code": 12345,
"Message": "your message",
"Data": "my-value"
}
---
# 非 BizError 均表现为 internal error 。
GET http://localhost:15001/api/err?bizErr=false&value=my-value
=> {
"Code": 500,
"Message": "internal error",
"Data": ""
}
如果需要接收上传的文件,参考 WIKI 页 上传文件 。
错误处理
表示 WebAPI 的方法支持0-2个返回值(详见GoDoc)。
当方法返回:
- 没有
error
返回值或返回的error
为nil
:表示调用成功,输出的Code=0
。 - 返回
errx.BizError
:输出Code=BizError.Code(), Message=BizError.Message()
。 - 返回不是
errx.BizError
的error
:统一输出Code=500, Message=internal error
。 - 方法
panic
:统一输出Code=500, Message=internal error
。
BizError
的详细说明,参考go-errx库。
# Packages
Package logsetup 提供一组预定义的 [webapi.LogSetup] ,以便快速实现 [webapi.ApiLogger] 。.
Package slimapi 基于 webapi 包,实现基于 SlimAPI 协议的开发框架。
SlimAPI 是一个基于 HTTP WebAPI 的通信契约。旨在将代码与 HTTP 通信解耦,使编码者可以更多的关注业务逻辑而不是通信方式。
# SlimAPI 请求
可以通过 HTTP 的 Content-Type 头指定使用何种格式请求,目前支持的类型如下:
- GET 不读取 Content-Type 头。
- POST FORM 表单格式, Content-Type 可以是 application/x-www-form-urlencoded 或 multipart/form-data 。
- POST JSON 以 JSON 作为数据,值为 application/json 。
也可以不指定 Content-Type 头,而通过`~format`参数指定格式,详见下文。
# URL 形式1
http://domain/ApiEntry?~method=METHOD&~format=FORMAT&~callback=CALLBACK
以“~”标记的参数为 API 框架的元参数:
- ~method:必填;表示被调用的方法的名称。
- ~format:可选;请求所使用的数据格式,支持get/post/json;此参数在可以在不方面指定`Content-Type`时提供相同的功能。
- ~callback:可选;JSONP回调函数的名称,一旦制定此参数,返回一个 JSONP 结果,Content-Type: text/javascript 。
参数名称都是大小写不敏感的。`~format`参数优先级高于`Content-Type`,若指定了`~format`,则`Content-Type`的值被忽略。
`~format`的可选值:
- get 默认值。使用 GET 方式处理。
- post 效果等同于给定 Content-Type: application/x-www-form-urlencoded
- json 效果等同于给定 Content-Type: application/json
# URL 形式2
http://domain/ApiEntry?METHOD.FORMAT(CALLBACK)
不需要再写参数名字,直接将需要的元参数值追加在URL后面。
同形式1,“.FORMAT”和“(CALLBACK)”是可选的,
省略“.FORMAT”后形如: http://domain/ApiEntry?METHOD(CALLBACK) ;
省略“(CALLBACK)”后形如: http://domain/ApiEntry?METHOD.FORMAT 。
# URL 形式3
通过路由规则,将元参数编排到 URL 路径里。这是最常见的方案: http://domain/ApiEntry/METHOD
这里 METHOD 就是元参数 ~method 。
# SlimAPI 请求参数的格式
请求参数
- GET 参数体现在 URL 上,形如 data=1&name=abc&time=2014-4-8 。
- 表单 以 POST 方式放在 HTTP BODY 中。
- JSON 只能使用 POST 方式上送。
可在 GET/表单参数中传递简单的数组,数组元素间使用 ~ 分割,如 1~2~3~4~5 可表示数组 [1, 2, 3, 4, 5]。
在GET/表单格式下:
data=1&name=abc&time=2014-4-8&array=1~2~3~4
与JSON格式下的下面内容等价:
{ "data":1, "name": "abc", "time": "2014-4-8", "array": [1, 2, 3, 4] }
日期格式使用字符串的 yyyy-MM-dd HH:mm:ss 格式,默认为 UTC 时间。也支持 RFC3339 ,这种格式自带时区。
# 使用 multipart/form-data 传递复杂参数
在 multipart/form-data 的 body 上,通常每个 part 表示一个 text/plain 类型的简单值。 SlimAPI 支持以 JSON 形式传递复杂的参数:
- part 的 Content-Type 必须是 application/json 。
- part 的 Content-Disposition 必须带有 filename ,具有任意非空值。
下面的请求等同于用 JSON 传递了 {"A":"123", "B":{"B1":"v1", "B2":"v2"}} :
POST your_url
Content-Type: multipart/form-data; boundary=----xyz
----xyz
Content-Disposition: form-data; name="A"
123
----xyz
Content-Disposition: form-data; name="B"; filename="blob"
Content-Type: application/json
{"B1":"v1","B2":"v2"}
----xyz--
# SlimAPI 回执格式
若指定了 ~callback 参数,则返回结果为 JSONP 格式: Content-Type: text/javascript ;否则为 JSON 格式: Content-Type: application/json 。
状态码总是200,具体异常码需要从Code字段判定。数据装在一个基本的信封中,信封格式如下:
{ Code: 0, Message: "", Data: {} }
- Code 0为API调用成功未见异常,非0值为异常:
- 1-999 API请求、通信及运行时异常,尽可能与 HTTP 状态码一致:
- 500 服务端内部异常。
- 400 请求参数或报文错误。
- 403 客户端无访问权限。
- 其他约定:
- -1 未明确定义的错误。
- 1000-9999 预留
- 大于等于10000为业务预定义异常,由具体API自行定义,但建议至少分为用户可见和不可见两个区间:
- 10000-19999 建议保留为不提示给用户的业务异常,对接 API 的客户端对于这些异常码,对用户提示一个统一的如“网络异常”的错误。
- 20000-29999 对接 API 的客户端可直接将 Message 展示给用户,用于展示的错误消息需要由服务端控制的场景。
- Message 附加信息, Code 不为0时记录错误描述,可为空字符串。
- Data 返回的主数据,不同API各不相同,其所有可能形式如下:
- 若API没有返回值,则为 null 。
- 对于返回布尔型结果的 API ,Data 为 true 或 false 。
- 对于返回数值结果的 API ,Data即为数值,如:123.654 。
- 对于返回字符串结果的 API , Data 为字符串,如:"string value" 。
- 日期也作为字符串,参照前面提到的日期格式。
- 对于返回集合的 API , Data 为数组,如: ["result1", "result2"] 。
- 对于返回复杂对象的 API ,Data 为 JSON object ,如:{ "Field1": "Value1", "Field2": "Value2" } 。
*/.
slimauth 实现 SlimAuth 协议,它是带有签名校验逻辑的 SlimAPI 的扩展。
但是和 SlimAPI 相比,当前不支持 multipart/form-data 类型的请求。
# 签名校验
每个 API 调用者会被分配到一组配对的 key-secret , key 用于标识调用者的身份, secret 用于生成签名。
在发起 HTTP 请求时,签名信息放在 Authorization 头,格式为:
Authorization: SLIM-AUTH Key={key}, Sign={sign}, Timestamp={timestamp}, Version=1
花括号内是可变的参数值。除开头的 scheme 部分外,其余各参数由逗号隔开,顺序不做要求,参数名称前的空白字符会被忽略。各参数定义为:
- Authorization scheme 固定为 SLIM-AUTH 。
- Key 是请求者的 key 。
- Sign 是基于请求内容和 secret 生成的签名。详见签名算法节。
- Timestamp 是生成签名时的 UNIX 时间戳,单位是秒。
- Version 表示签名算法的版本,当前固定值为 1 。可省略,省略时默认为 1 。
API 服务器将根据签名算法,校验 Sign 的值是否正确,并要求 Timestamp 在允许的误差范围内(默认为 300 秒)。
特别的,当不方便定制请求头时,也可以将 Authorization 头的值,放在 URL 的 ~auth 参数上(记得 urlEncode )。
~auth 参数不参与签名计算。如果同时提供参数和请求头,则只读取请求头。
此功能特别适用于 JSONP 请求( SlimAPI 的功能之一),因为其不能定制 HTTP 头。
# 签名算法
字符集统一使用 UTF-8 。签名使用 HMAC-SHA256 算法,通过 secret 对待签名串进行哈希计算得到。待签名串根据请求的内容生成,格式为:
TIMESTAMP
METHOD
PATH
QUERY_VALUES
BODY_VALUES (optional)
END (constant)
每个部分间用换行符(\n)分割,各部分的值为:
1.
webapitest 包提供用于测试 webapi 包的辅助方法。.
# Functions
AllRouteParams 获取请求中所有的路由参数。若没有路由参数,返回 nil 。.
BadRequestResponse 返回一个表示不合规的请求的 ApiResponse 。.
CreateApiError 创建一个 ApiError 。 message 和 args 指定描述信息,使用 fmt.Sprintf() 格式化。 cause 是引起此错误的错误,可以为 nil 。 message 会体现在 ApiError.Error() ,格式为:
message:: cause.Error().
CreateBadRequestError 创建一个 BadRequestError 。 message 和 args 指定其消息,使用 fmt.Sprintf() 格式化。 描述信息可能作为 WebAPI 的返回值,被请求者看到,故可能不应当过多暴露程序细节。更具体的错误可以放在 cause 上。.
CreateHandlerFunc 返回一个封装了给定的 ApiHandler 的 http.HandlerFunc 。
logFinder 用于获取 Logger ,该 Logger 会赋值给 ApiState.Logger 。可为 nil 表示不记录日志。 对于每个请求,其日志名称基于响应该请求的方法,由两部分构成,格式为“{ApiHandler.Name()}.{ApiMethod.Provider}.{ApiMethod.Name}”。 如果未能检索到对应的方法,则日志名称为 ApiHandler.Name() 。.
DescribeError 根据给定的错误,返回错误的日志级别、名称和错误描述。 如果 err 为 nil ,返回 logx.LevelInfo 和空字符串。 此方法可用于搭配 ApiLogger.Log() 输出带有错误描述的日志。
描述信息使用 common.Errors.Describe() 获取。.
GetRouteParam 从给定的请求中获取指定名称的路由参数。参数不存在时,返回空字符串。.
InternalErrorResponse 返回一个表示不合规的请求的 ApiResponse 。.
NewArgumentDecoderPipeline 返回一个 [ArgumentDecoderPipeline] 。 其第一个元素是预定义的 [ApiStateDecodeFunc] ,用于赋值 [*ApiState] ; decodeFuncs 会追加在后面。.
NewBasicApiMethodCaller 返回一个预定义的 ApiMethodCaller 的标准实现。 当实现一个 ApiHandler 时,可基于此实例实现 ApiMethodCaller 。.
NewBasicApiMethodRegister 返回一个预定义的 ApiMethodRegister 的标准实现。 当实现一个 ApiHandler 时,可基于此实例实现 ApiMethodRegister 。.
NewBasicApiResponseBuilder 返回一个预定义的 ApiResponseBuilder 的标准实现。 当实现一个 ApiHandler 时,可基于此实例实现 ApiResponseBuilder 。.
NewBasicApiUserHostResolver 返回一个预定义的 ApiUserHostResolver 的标准实现。 当实现一个 ApiHandler 时,可基于此实例实现 ApiUserHostResolver 。.
NewEngine 创建一个 ApiEngine 实例,并完成初始化设置。.
NewLogSetupPipeline 返回一个 [LogSetupPipeline] 。.
NewState 创建一个新的 ApiState ,每个请求应使用一个新的 ApiState 。.
PanicApiError 使用 CreateApiError 创建 ApiError ,并直接直接 panic 。 当 ApiHandler 遇见不应该发生(如编码 bug)的异常情况时,可使用此方法中断处理过程。.
ParseQueryString 模拟 .net Framework 的 HttpRequest.QueryString 的解析方式。 给定的 queryString 可以以“?”开头,也可以不带。
在传统ASP.net中,“?a&b”解析为一个名称为 null,值为“a,b”的参数;而 Go 的框架则将其等同于 “?a=&b=” 处理,变成 两个名称分别为 a 、 b 而值为空的参数。这与预定义的 API 协议如 SlimAPI 不符。
此方法用于获取与 .net Framework 相同的解析结果。 如果一个参数出现多次,会被以逗号拼接起来,如“?a=1&a=2”结果为“a=1,2”; 特别的,单一的“?”会得到一个没有参数名称的参数,值为空字符串。.
SetRouteParams 向当前请求中添加一组路由参数,返回追加参数后的请求。 若给定参数表为 nil 或不包含元素,则返回原始请求。.
SuccessResponse 返回一个表示成功的 ApiResponse 。.
Wrap 将一个 ApiHandler 包装为 *ApiHandlerWrapper ,用于“重写”其中的方法。.
# Constants
ContentTypeBinary 对应 Content-Type: application/octet-stream 的值。.
ContentTypeForm 对应 Content-Type: application/x-www-form-urlencoded 的值。.
ContentTypeJavascript 对应 Content-Type: text/javascript 的值。.
ContentTypeJson 对应 Content-Type: application/json 的值。.
ContentTypeMultipartForm 对应 Content-Type: multipart/form-data 的值。.
ContentTypeNone 未指定类型。.
ContentTypePlainText 对应 Content-Type: text/javascript 的值。.
错误码。表示不合规的请求数据。.
错误码。表示发生内部错误。.
HttpHeaderContentDisposition 对应 HTTP 头中的 Content-Disposition 字段。.
HttpHeaderContentType 对应 HTTP 头中的 Content-Type 字段。.
# Variables
ApiStateDecodeFunc 是一个 [ArgumentDecoder] ,它用于解析并赋值 [*ApiState] 。
这是一个单例。.
# Structs
ApiEngine 是一个 [http.Handler] 。表示一个抽象的 HTTP 服务器,基于 [ApiHandler] 注册和管理 WebAPI 。.
ApiError 用于表示 ApiHandler 处理过程中的内部错误,这些错误通常表示代码存在问题(如编码 bug)。 这些问题不能在程序生命周期中自动解决,通常使用 panic 中断程序。.
ApiHandlerWrapper 用于组装各个接口,以实现 ApiHandler 。 各种 ApiHandler 的实现中,可使用此类型作为脚手架,组装各个内嵌接口。.
ApiMethod 表示一个通过 ApiMethodRegister 注册的方法。.
ApiResponse 用于表示返回的数据。.
ApiSetup 用于向 ApiHandler 注册 API 方法。.
ApiState 用于记录一个请求的处理流程中的数据。每个请求使用一个新的 ApiState 。 处理过程采用管道模式,每个步骤从 ApiState 获取所需数据,并将处理结果写回 ApiState 。 当处理过程结束后,以 Response 开头的字段应被填充。.
BadRequestError 记录一个不正确的请求,例如请求的参数不符合要求,请求的 API 方法不存在等。 这些错误是外部请求而不是编码导致(假设没 bug )的, WebAPI 流程应能够正确处理这些错误并返回对应结果。 可以为 BadRequestError 指定一个描述信息,此信息可能作为 WebAPI 的返回值,被请求者看到。.
QueryString 模拟 .net Framework 的 HttpRequest.QueryString 。.
No description provided by the author
# Interfaces
ApiDecoder 用于构建调用方法的参数表。.
ApiHandler 定义了 WebAPI 处理过程中的抽象环节。 CreateHandlerFunc() 返回一个函数,基于 ApiHandler 实现完整的处理过程。
在响应请求时,各接口的执行顺序为: - ApiUserHostResolver - ApiNameResolver - ApiDecoder - ApiMethodCaller - ApiResponseBuilder - ApiResponseWriter - ApiLogger
ApiMethodRegister 仅在注册阶段使用,在响应请求的过程中不会被调用。.
ApiLogger 在 ApiResponseWriter.WriteResponse 被调用后,生成日志。.
ApiMethodCaller 用于调用特定的方法。.
ApiMethodRegister 用于向 ApiHandler 中注册 WebAPI 方法。 此过程用于初始化 ApiHandler ,初始化过程应在接收第一个请求前完成,并以单线程方式进行。 注册方法时,应对方法的输入输出类型做合法性校验。.
ApiNameResolver 用于从当前 HTTP 请求中,解析得到目标 API 方法的名称。.
ApiResponseBuilder 处理 ApiDecoder 和 ApiMethodCaller 执行过程中产生的错误。.
ApiResponseWriter 处理 [ApiResponseBuilder] 的处理结果,获得实际需要返回的数据,填入 Response* (以 Response 开头)字段。.
ApiUserHostResolver 用于获取发起 HTTP 请求的客户端 IP 地址。 一个请求可能经过多次代理转发,原始地址通常需要从特定 HTTP 头获取,比如 X-Forwarded-For 。.
ArgumentDecoder 定义一个过程,此过程用于从 [ApiState] 中解析得到 API 方法的特定参数的值。 一组实例形成一个解析 API 方法中每个参数的管道 [ArgumentDecoderPipeline] 。.
LogSetup 定义一个过程,此过程用于向 [ApiState] 填充日志信息。.
# Type aliases
ApiDecoderFunc 用于将函数适配到 [ApiDecoder] 。.
ApiLoggerFunc 用于将函数适配到 [ApiLogger] 。.
ApiMethodCallerFunc 用于将函数适配到 [ApiMethodCaller] 。.
ApiNameResolverFunc 用于将函数适配到 [ApiNameResolver] 。.
ApiResponseBuilderFunc 用于将函数适配到 [ApiResponseBuilder] 。.
ApiResponseWriterFunc 用于将函数适配到 [ApiResponseWriter] 。.
ApiUserHostResolverFunc 用于将函数适配到 [ApiUserHostResolver] 。.
ArgumentDecodeFunc 用于将函数适配到 [ArgumentDecoder.DecodeArg] 。.
ArgumentDecoderPipeline 是 [ArgumentDecoder] 组成的管道。 实现 [ApiDecoder] ,此实现要求被调用的每个方法,其参数表中的参数类型是不重复的。
在 [ApiDecoder.Decode] 时,将依次执行管道内的每个 [ArgumentDecoder.DecodeArg] 。 可以通过增减和调整元素的顺序定制执行的过程。.
LogSetupFunc 用于将函数 [LogSetup.Setup] 。.
LogSetupPipeline 是 [LogSetup] 组成的管道,实现 [ApiLogger] 。
在 [ApiLogger.Log] 时,依次执行每个 [LogSetup.Setup] ,并将得到的 [ApiState.LogLevel] 和 [ApiState.LogMessage] 输出到日志。 若 [LogLevel] 未被设置,默认使用 [logx.LevelInfo] 级别。.