我们签名方案定了,这里要进行具体实现,方案内容参考我的文章接口签名 - 一个实践过的方案(一)-CSDN博客
一、apisix选取
微服务项目采用kubernetes维护的,用到了ingress作为网关。ingress可以配置不同的控制器,下面文章是 Apache APISIX Ingress 和 Ingress NGINX 两个控制器的对比
为什么 APISIX Ingress 是比 Ingress NGINX 更好的选择? | Apache APISIX® -- Cloud-Native API Gateway
目前Ingress NGINX主要通过 Annotations 和 ConfigMap等方式进行配置,支持的插件功能比较有限。如果想要用JWT、HMAC等鉴权方式,只能自行开发。
而APISIX Ingress得益于APISIX的丰富功能,原生支持APISIX内置的 80+插件,能够覆盖大部分使用场景,还支持 JWT、HMAC、wolf-rbac等多种鉴权方式。
我们选择用 APISIX Ingress,它支持如下扩展:
- 通过Lua进行插件开发
- 通过plugin-runner开发: 这种模式下支持Java/Python/Go等语言进行开发,这可以方便用户利用一些现有的业务逻辑,并且无需学习新语言。
- 通过Wasm进行插件开发
项目用的golang开发,所以可以采用 plugin-runner开发插件。
二、apisix如何写插件
参考文章Docker部署 apisix 并使用golang插件(自定义鉴权方式)_apisix go runner-CSDN博客
从github上面下载代码 GitHub - apache/apisix-go-plugin-runner: Go Plugin Runner for APISIX
所有插件代码在 cmd/go-runner/plugins中,这里已经实现了一些插件,
要写一个插件,只要实现Plugin接口即可
// Plugin represents the Plugin
type Plugin interface {
// Name returns the plguin name
Name() string
// ParseConf is the method to parse given plugin configuration. When the
// configuration can't be parsed, it will be skipped.
ParseConf(in []byte) (conf interface{}, err error)
// RequestFilter is the method to handle request.
// It is like the `http.ServeHTTP`, plus the ctx and the configuration created by
// ParseConf.
//
// When the `w` is written, the execution of plugin chain will be stopped.
// We don't use onion model like Gin/Caddy because we don't serve the whole request lifecycle
// inside the runner. The plugin is only a filter running at one stage.
RequestFilter(conf interface{}, w http.ResponseWriter, r pkgHTTP.Request)
// ResponseFilter is the method to handle response.
// This filter is currently only pre-defined and has not been implemented.
ResponseFilter(conf interface{}, w pkgHTTP.Response)
}
开发好以后,编译代码输出一个可执行文件 go-runner
把这个配置到apisix的配置文件即可。
三、开发插件
下载代码指定版本的代码,基于它写插件apisix-go-plugin-runner-release-0.5.0 · master · intel-connect-iot / service / opencar_plugin · GitLab (gwm.cn)
参照其他插件,我们创建一个文件, api_signature.go
内容如下
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package plugins
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http"
"github.com/apache/apisix-go-plugin-runner/pkg/log"
"github.com/apache/apisix-go-plugin-runner/pkg/plugin"
"mime"
"mime/multipart"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
)
const defaultMultipartMemory = 32 << 20 // 32MB
const (
// 四个要被签名的header头
CaKey = "tw-appkey"
CaNonce = "tw-nonce"
CaSignatureMethod = "tw-signature-method"
CaTimestamp = "tw-timestamp"
SIGNATURE = "tw-signature"
SignatureHeaders = "tw-signature-headers"
// 支持的方法
HmacSHA256 = "HmacSHA256"
HmacSHA1 = "HmacSHA1"
ContentTypeUrlEncoded = "application/x-www-form-urlencoded"
ContentTypeFormData = "multipart/form-data"
ContentTypeApplicationJson = "application/json"
)
type SystemError struct {
Code int
Msg string
HttpCode int
}
func (b *SystemError) Error() string {
return fmt.Sprintf("system error code:%d msg:%s", b.Code, b.Msg)
}
func NewSystemError(code int, msg string, httpCode int) *SystemError {
return &SystemError{
Code: code,
Msg: msg,
HttpCode: httpCode,
}
}
// 所有业务类的错误定义在这里
const (
SErrServiceFailedCode = 2
SErrDBFailedCode = 3
SErrorMustLoginCode = 11
SErrorHeaderSignatureFailCode = 21
SErrorHeaderSignatureMustCode = 22
SErrorHeaderNonceMustCode = 23
SErrorHeaderTimestampMustCode = 24
SErrorHeaderAppKeyMustCode = 25
SErrorReplyPreventionCode = 26
SErrorHeaderAppKeyInvalidCode = 27
)
var (
SErrServiceFailed = NewSystemError(SErrServiceFailedCode, "服务异常", http.StatusOK)
SErrDBFailed = NewSystemError(SErrDBFailedCode, "系统数据错误", http.StatusInternalServerError)
SErrorMustLogin = NewSystemError(SErrorMustLoginCode, "需要重新登录", http.StatusUnauthorized)
SErrorHeaderSignatureFail = NewSystemError(SErrorHeaderSignatureFailCode, "签名错误", http.StatusOK)
SErrorHeaderSignatureMust = NewSystemError(SErrorHeaderSignatureMustCode, "签名不能为空", http.StatusOK)
SErrorHeaderNonceMust = NewSystemError(SErrorHeaderNonceMustCode, "nonce不能为空", http.StatusOK)
SErrorHeaderTimestampMust = NewSystemError(SErrorHeaderTimestampMustCode, "timestamp不正确", http.StatusOK)
SErrorHeaderAppKeyMust = NewSystemError(SErrorHeaderAppKeyMustCode, "key不能为空", http.StatusOK)
SErrorHeaderAppKeyInvalid = NewSystemError(SErrorHeaderAppKeyInvalidCode, "账户无效", http.StatusOK)
SErrorReplyPrevention = NewSystemError(SErrorReplyPreventionCode, "重放识别", http.StatusOK)
)
func init() {
err := plugin.RegisterPlugin(&ApiSignature{})
if err != nil {
log.Fatalf("failed to register plugin %s: %s", plugin_name, err)
}
}
// ApiSignature 接口签名
type ApiSignature struct {
// Embed the default plugin here,
// so that we don't need to reimplement all the methods.
plugin.DefaultPlugin
}
type ApiSignatureConf struct {
Accounts map[string]string `json:"accounts"` // 维护一个存账户密码的map, 比如{"accounts":{"tom":"123"}}
IsDebug bool `json:"is_debug"`
}
func (p *ApiSignature) Name() string {
return "api-signature"
}
func (p *ApiSignature) ParseConf(in []byte) (interface{}, error) {
conf := ApiSignatureConf{}
err := json.Unmarshal(in, &conf)
return conf, err
}
var (
ErrNotMultipart = errors.New("request Content-Type isn't multipart/form-data")
)
// 如果认证通过,直接return即可,不要操作response对象
// 如果认证不通过,即需要拦截,则要在response增加http状态码,返回body等
func (p *ApiSignature) RequestFilter(conf interface{}, w http.ResponseWriter, r pkgHTTP.Request) {
appKey := r.Header().Get(CaKey)
if appKey == "" {
log.Errorf("appkey为空, scrIp:%s", r.SrcIP())
dealError(w, SErrorHeaderAppKeyMust)
return
}
myConf := conf.(ApiSignatureConf)
secret, has := myConf.Accounts[appKey]
if !has {
log.Errorf("appkey不存在, appkey:%s", appKey)
dealError(w, SErrorHeaderAppKeyInvalid)
return
}
sign := r.Header().Get(SIGNATURE)
if sign == "" {
log.Errorf("签名sign不存在, scrIp:%s", r.SrcIP())
dealError(w, SErrorHeaderSignatureMust)
return
}
// 读取body
body, e := r.Body()
if e != nil {
log.Errorf("body读取失败, err:%s", e.Error())
dealError(w, SErrServiceFailed)
return
}
// -------- 准备待签名串 --------
// 1、http method, 结果本身是大写的
method := r.Method()
// 2、url path
httpPath := r.Path()
// 3、headers
headerStr, useHeaderMap, e := GetHeaderString(r)
if e != nil {
log.Errorf("GetHeaderString读取失败, err:%s", e.Error())
dealError(w, e)
return
}
// 4、content md5
bodyMd5 := RequestBodyMd5(r, body)
// 5、query parameters
paramStr := GetParamsString(r, body, r.Args())
// 拼接待签名串
caStr := fmt.Sprintf("%s\n%s", method, httpPath)
if headerStr != "" {
caStr = fmt.Sprintf("%s\n%s", caStr, headerStr)
}
if bodyMd5 != "" {
caStr = fmt.Sprintf("%s\n%s", caStr, bodyMd5)
}
if paramStr != "" {
caStr = fmt.Sprintf("%s\n%s", caStr, paramStr)
}
// 计算签名串
signCals := ""
signatureMethod, _ := useHeaderMap[CaSignatureMethod]
_signatureMethod := signatureMethod.(string)
switch _signatureMethod{
case HmacSHA1:
signCals = HmacSha1(secret, caStr)
default:
signCals = HmacSha256(secret, caStr)
}
// 签名判断, 21代表签名错误
if sign != signCals {
log.Errorf("签名不正确, errSign:%s, rightSign:%s", sign, signCals)
if myConf.IsDebug == true {
dealError(w, SErrorHeaderSignatureFail, caStr, signCals)
} else {
dealError(w, SErrorHeaderSignatureFail)
}
return
}
return
}
func dealError(w http.ResponseWriter, myErr error, strs ...string) {
w.Header().Add("Content-Type", "application/json")
if e, ok := myErr.(*SystemError); ok {
d := map[string]interface{}{
"code" : e.Code,
"message" : e.Error(),
"data" : strs,
}
a, _ := json.Marshal(d)
w.Write(a)
return
}
d := map[string]interface{}{
"code" : 1,
"message" : "server failed",
"data" : strs,
}
a, _ := json.Marshal(d)
w.Write(a)
return
}
// 从header头中拿到关键参数,tw-appkey, tw-nonce, tw-timestamp
// 将header的名字变成全小写
// 将header的值去掉开头和结尾的空白字符串
// 经过上一步之后值header转换为name:value
func GetHeaderString(r pkgHTTP.Request) (string, map[string]interface{}, error) {
m := make(map[string]interface{}, 0)
// key【必须】
key := r.Header().Get(CaKey)
if key == "" {
return "", m, SErrorHeaderAppKeyMust
}
m[CaKey] = key
// signature-method【可选】, 没传或者值不对默认采用HmacSHA256,
method := r.Header().Get(CaSignatureMethod)
if method == "" {
method = HmacSHA256
} else {
switch method {
case HmacSHA256:
case HmacSHA1:
default:
method = HmacSHA256
}
}
m[CaSignatureMethod] = method
// nonce【可选】
nonce := r.Header().Get(CaNonce)
if nonce != "" {
m[CaNonce] = nonce
}
// timestamp【可选】, 如果传了,值必须是毫秒的,即13位
t := time.Unix(0, 0)
timestamp := r.Header().Get(CaTimestamp)
if timestamp != "" {
if len(timestamp) != 13 {
return "", m, SErrorHeaderTimestampMust
}
_timestamp, err := strconv.Atoi(timestamp)
if err != nil {
return "", m, SErrorHeaderTimestampMust
}
t = time.UnixMilli(int64(_timestamp))
m[CaTimestamp] = t
}
// signature-headers
headersList := make(map[string]string, 0)
headers := r.Header().Get(SignatureHeaders)
if headers != "" {
ss := strings.Split(headers, ",")
for _, h := range ss {
switch h {
case CaKey:
headersList[CaKey] = key
break
case CaSignatureMethod:
headersList[CaSignatureMethod] = method
break
case CaNonce:
headersList[CaNonce] = nonce
break
case CaTimestamp:
headersList[CaTimestamp] = timestamp
break
}
}
}
return HeaderSignString(headersList), m, nil
}
// headers签名串
// 有4个内容可能需要加入签名串,并按照key的字典顺序排序
// 1、tw-appkey
// 2、tw-nonce
// 3、tw-signature-method
// 4、tw-timestamp
func HeaderSignString(m map[string]string) string {
str := ""
if key, ok := m[CaKey]; ok {
str += fmt.Sprintf("%s:%s", CaKey, key)
}
if nonce, ok := m[CaNonce]; ok {
str += fmt.Sprintf("\n%s:%s", CaNonce, nonce)
}
if method, ok := m[CaSignatureMethod]; ok {
str += fmt.Sprintf("\n%s:%s", CaSignatureMethod, method)
}
if time, ok := m[CaTimestamp]; ok {
str += fmt.Sprintf("\n%s:%s", CaTimestamp, time)
}
return str
}
// 请求体md5值,表单除外
func RequestBodyMd5(r pkgHTTP.Request, body []byte) string {
contentType := getContentType(r)
if strings.Contains(contentType, ContentTypeUrlEncoded) ||
strings.Contains(contentType, ContentTypeFormData) {
return ""
}
if len(body) == 0 {
return ""
}
s := Md5(string(body))
return s
}
func getContentType(r pkgHTTP.Request) string {
content := r.Header().Get("Content-Type")
for i, char := range content {
if char == ' ' || char == ';' {
return content[:i]
}
}
return content
}
// GetParamsString
// 获取post表单的所有参数、query的参数,进行拼接
func GetParamsString(r pkgHTTP.Request, body []byte, queryMap url.Values) string {
contentType := r.Header().Get("Content-Type")
form := make(url.Values, 0)
// 读到才处理
formA, err := getBodyMultipartUrlEncoded(contentType, body)
if err == nil {
for k, v := range formA {
form[k] = append(form[k], v...)
}
}
// query参数也写到form中
for k, v := range queryMap {
form[k] = append(form[k], v...)
}
// 读到才处理
formB, err := getBodyMultipartForm(contentType, body)
if err == nil {
for k, v := range formB {
form[k] = append(form[k], v...)
}
}
// 三个数据来源都在form中,进行处理
keys := make([]string, 0)
KeysMap := make(map[string]string, 0)
for k, v := range form {
if _, ok := KeysMap[k]; ok == false {
KeysMap[k] = v[0]
keys = append(keys, k)
}
}
// 排序
s := SortByDict(keys)
sort.Sort(s)
str := ""
// 按顺序拼接
for _, k := range s {
v := KeysMap[k]
if v == "" {
str += fmt.Sprintf("%s&", k)
} else {
str += fmt.Sprintf("%s=%s&", k, v)
}
}
str = strings.TrimRight(str, "&")
return str
}
func Md5(str string) string {
hash := md5.New()
hash.Write([]byte(str))
sum := hash.Sum(nil)
sign := hex.EncodeToString(sum)
return sign
}
func HmacSha256(key, str string) string {
hash := hmac.New(sha256.New, []byte(key))
hash.Write([]byte(str))
sum := hash.Sum(nil)
sign := hex.EncodeToString(sum)
return sign
}
func HmacSha1(key, str string) string {
hash := hmac.New(sha1.New, []byte(key))
hash.Write([]byte(str))
sum := hash.Sum(nil)
sign := hex.EncodeToString(sum)
return sign
}
// GetQueryParams
// 读取http请求的query参数,如果一个key对应的值是数组就仅取arr[0]
func getQueryParams(r pkgHTTP.Request) map[string]string {
args := r.Args()
var queryMap = make(map[string]string, len(args))
for k := range args {
queryMap[k] = args[k][0]
}
return queryMap
}
// 当Content-Type是"application/x-www-form-urlencoded",读取表单信息
func getBodyMultipartUrlEncoded(contentType string, body []byte) (vs url.Values, err error) {
vs = make(map[string][]string, 0)
if contentType == ContentTypeUrlEncoded {
maxFormSize := int64(1<<63 - 1)
if int64(len(body)) > maxFormSize {
err = errors.New("http: POST too large")
return
}
vs, err = url.ParseQuery(string(body))
}
return
}
func getQuery(queryStr string) (vs url.Values, err error) {
return url.ParseQuery(queryStr)
}
// 当Content-Type是”multipart/form-data“, 读取表单信息
// 当时一个”multipart/form-data“的请求,请求头可能如下 multipart/form-data; boundary=--------------------------776782700635069718076894
func getBodyMultipartForm(contentType string, body []byte) (url.Values, error) {
vs := make(map[string][]string, 0)
reader := bytes.NewReader(body)
d, params, err := mime.ParseMediaType(contentType)
if err != nil {
return vs, ErrNotMultipart
}
if d == "multipart/form-data" {
boundary, ok := params["boundary"]
if !ok {
return vs, ErrNotMultipart
}
newReader := multipart.NewReader(reader, boundary)
f, err := newReader.ReadForm(defaultMultipartMemory)
if err != nil {
return vs, err
}
return f.Value, nil
}
return vs, nil
}
// 排序
type SortByDict []string // 设置自定义类型
func (a SortByDict) Len() int {
return len(a)
}
func (a SortByDict) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a SortByDict) Less(i, j int) bool {
return a[i] < a[j]
}