接口签名 - 一个实践过的方案(二)

我们签名方案定了,这里要进行具体实现,方案内容参考我的文章接口签名 - 一个实践过的方案(一)-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]
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值