http编解码和grpc拦截器

master
hzy 1 year ago
parent 6996112349
commit 6b48932683
  1. 32
      README.md
  2. 4
      go.mod
  3. 8
      go.sum
  4. 116
      interceptor/v1/README.md
  5. 55
      interceptor/v1/grpc/interceptor.go
  6. 76
      transport/v1/README.md
  7. 89
      transport/v1/http/request.go
  8. 154
      transport/v1/http/response.go
  9. 19
      transport/v1/http/transport.go

@ -5,10 +5,11 @@
* [安装](#安装)
* [通用包](#通用包)
* [pagination](#pagination)
* [transport:通用http ResponseEncoder](#transport通用http-responseencoder)
* [meta:跨服务meta信息设置和获取](#meta跨服务meta信息设置和获取)
* [encrypt:对称加密](#encrypt对称加密)
<!-- TOC -->
* [transport-通用httpEncoder](#transport)
* [interceptor-通用grpcInterceptor](#interceptor)
* [meta-跨服务meta信息设置和获取](#meta)
* [encrypt-对称加密](#encrypt)
<!-- TOC -->
## 安装
@ -38,21 +39,34 @@ go get gitea.drugeyes.vip/pharnexbase/utils@v1.0.0
> 说明
### transport:通用http Encoder
### transport
* v1
> 请求和响应封装
> - ResponseEncoder
> http请求和响应封装
> - request
> - 验证前端参数签名
> - response
> - ResponseEncoderWithEncrypt 正式服加密后端响应参数
> - bff原样返回server错误及自身业务错误,正式服其它错误统一返回InternalServerError
### interceptor
* v1
### meta:跨服务meta信息设置和获取
> grpc请求和响应拦截器
> - server
> - server原样返回业务错误,正式服其它错误统一返回InternalServerError
> - client
> - client默认转换server的所有error为kratos的error,方便bff原样返回
### meta
* v1
> 跨服务meta信息设置和获取
> - user(基础用户信息)
### encrypt:对称加密
### encrypt
* v1

@ -3,8 +3,10 @@ module gitea.drugeyes.vip/pharnexbase/utils
go 1.18
require (
gitea.drugeyes.vip/pharnexbase/tools v1.0.0
github.com/aliyun/aliyun-log-go-sdk v0.1.43
github.com/go-kratos/kratos/v2 v2.5.4
github.com/tidwall/gjson v1.14.4
google.golang.org/protobuf v1.28.0
)
@ -21,6 +23,8 @@ require (
github.com/gorilla/mux v1.8.0 // indirect
github.com/pierrec/lz4 v2.6.0+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
go.opentelemetry.io/otel v1.7.0 // indirect
go.opentelemetry.io/otel/trace v1.7.0 // indirect
go.uber.org/atomic v1.5.0 // indirect

@ -1,5 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
gitea.drugeyes.vip/pharnexbase/tools v1.0.0 h1:u7hULwCArUgHbFWqJYN7HdJ3cAiAB2Du2RXW/Ivo3SE=
gitea.drugeyes.vip/pharnexbase/tools v1.0.0/go.mod h1:PhGz0SSvvuPSiNmRK9jT0vv/PUU0PaPfi8FwtOEEcZU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
@ -302,6 +304,12 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tklauser/go-sysconf v0.3.9/go.mod h1:11DU/5sG7UexIrp/O6g35hrWzu0JxlwQ3LSFUzyeuhs=
github.com/tklauser/numcpus v0.3.0/go.mod h1:yFGUr7TUHQRAhyqBcEg0Ge34zDBAsIvJJcyE6boqnA8=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=

@ -0,0 +1,116 @@
# interceptor-通用grpcInterceptor
## 说明
> grpc请求和响应拦截器
> - server
> - server原样返回业务错误,正式服其它错误统一返回InternalServerError
> - client
> - client默认转换server的所有error为kratos的error,方便bff原样返回
## 使用示例
### client
```go
package data
import (
"context"
grpc2 "gitea.drugeyes.vip/pharnexbase/utils/interceptor/v1/grpc"
"github.com/go-kratos/kratos/v2/middleware/circuitbreaker"
"github.com/go-kratos/kratos/v2/middleware/tracing"
"github.com/go-kratos/kratos/v2/transport/grpc"
"github.com/google/wire"
user "user-center-front/api/user/v2"
"user-center-front/internal/conf"
)
// ProviderSet is data providers.
var ProviderSet = wire.NewSet(
NewData,
NewUserService,
)
// Data .
type Data struct {
User user.UserCenterClient
}
// NewData .
func NewData(userServer user.UserCenterClient) *Data {
return &Data{
User: userServer,
}
}
func NewUserService(c *conf.Bootstrap) user.UserCenterClient {
grpcInterceptor := grpc2.NewInterceptor(c.Env, nil)
conn, err := grpc.DialInsecure(
context.Background(),
grpc.WithEndpoint(c.Service.GetUser()),
grpc.WithTimeout(0),
grpc.WithMiddleware(
tracing.Client(), // 链路追踪
circuitbreaker.Client(), // 熔断器
),
// 拦截器
grpc.WithUnaryInterceptor(grpcInterceptor.UnaryClientInterceptor()),
)
if err != nil {
panic(err)
}
return user.NewUserCenterClient(conn)
}
```
### server
```go
package server
import (
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware/logging"
"github.com/go-kratos/kratos/v2/middleware/ratelimit"
"github.com/go-kratos/kratos/v2/middleware/recovery"
"github.com/go-kratos/kratos/v2/middleware/validate"
"github.com/go-kratos/kratos/v2/transport/grpc"
grpc2 "gitea.drugeyes.vip/pharnexbase/utils/interceptor/v1/grpc"
v2 "user-center-v2/api/user/v2"
"user-center-v2/internal/conf"
pkgLog "user-center-v2/internal/pkg/log"
"user-center-v2/internal/service"
)
// NewGRPCServer new a gRPC server.
func NewGRPCServer(c *conf.Bootstrap, userService *service.UserCenterService, logger log.Logger) *grpc.Server {
grpcInterceptor := grpc2.NewInterceptor(c.Env, v2.ErrorReason_value)
var opts = []grpc.ServerOption{
grpc.Middleware(
recovery.Recovery(),
pkgLog.Trace(),
ratelimit.Server(),
logging.Server(logger),
validate.Validator(),
),
// 拦截器
grpc.UnaryInterceptor(grpcInterceptor.UnaryServerInterceptor()),
}
if c.Server.Grpc.Network != "" {
opts = append(opts, grpc.Network(c.Server.Grpc.Network))
}
if c.Server.Grpc.Addr != "" {
opts = append(opts, grpc.Address(c.Server.Grpc.Addr))
}
if c.Server.Grpc.Timeout != nil {
opts = append(opts, grpc.Timeout(c.Server.Grpc.Timeout.AsDuration()))
}
srv := grpc.NewServer(opts...)
v2.RegisterUserCenterServer(srv, userService)
return srv
}
```

@ -0,0 +1,55 @@
package grpc
import (
"context"
"gitea.drugeyes.vip/pharnexbase/utils/enum"
"github.com/go-kratos/kratos/v2/errors"
"google.golang.org/grpc"
)
var ErrInternalServer = errors.InternalServer("InternalServerError", "服务错误")
type Interceptor struct {
env enum.Env // 环境
reason map[string]int32 // 业务错误枚举
}
func NewInterceptor(env enum.Env, reason map[string]int32) *Interceptor {
return &Interceptor{
env: env,
reason: reason,
}
}
func (i *Interceptor) UnaryClientInterceptor() grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
h := func(ctx context.Context, req interface{}) (interface{}, error) {
return reply, invoker(ctx, method, req, reply, cc, opts...)
}
_, err := h(ctx, req)
if err != nil {
err = errors.FromError(err)
}
return err
}
}
func (i *Interceptor) UnaryServerInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
resp, err = handler(ctx, req)
if err == nil {
return
}
if i.env == enum.Env_Production {
se := errors.FromError(err)
if _, ok := i.reason[se.Reason]; ok {
// 业务错误,原样返回
return
}
// 正式服,统一返回“服务错误”
err = ErrInternalServer
}
return
}
}

@ -0,0 +1,76 @@
# interceptor-通用grpcInterceptor
## 说明
> http请求和响应封装
> - request
> - 验证前端参数签名
> - response
> - ResponseEncoderWithEncrypt 正式服加密后端响应参数
> - bff原样返回server错误及自身业务错误,正式服其它错误统一返回InternalServerError
## 使用示例
### http
```go
package server
import (
userV1 "gitea.drugeyes.vip/ebm/ebm-bff/api/v1/user"
"gitea.drugeyes.vip/ebm/ebm-bff/internal/conf"
"gitea.drugeyes.vip/ebm/ebm-bff/internal/service"
trace "gitea.drugeyes.vip/ebm/ebm-bff/middleware/trace"
http1 "gitea.drugeyes.vip/pharnexbase/utils/transport/v1/http"
"github.com/go-kratos/kratos/v2/log"
"github.com/go-kratos/kratos/v2/middleware/logging"
"github.com/go-kratos/kratos/v2/middleware/recovery"
"github.com/go-kratos/kratos/v2/middleware/tracing"
"github.com/go-kratos/kratos/v2/middleware/validate"
"github.com/go-kratos/kratos/v2/transport/http"
"github.com/gorilla/handlers"
)
// NewHTTPServer new HTTP server.
func NewHTTPServer(
c *conf.Bootstrap,
user *service.UserService,
logger log.Logger) *http.Server {
headersOk := handlers.AllowedHeaders([]string{
"Content-Type", "X-Requested-With",
"Authorization", "User-Agent", "Accept", "Referer",
"Client-Version"})
originsOk := handlers.AllowedOrigins([]string{"*"})
methodsOk := handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"})
httpTrans := http1.NewTransport(c.Env, "sm4Key", "apiKey")
var opts = []http.ServerOption{
http.Filter(handlers.CORS(headersOk, originsOk, methodsOk)),
http.RequestDecoder(httpTrans.RequestDecoder()), // body解码
http.RequestQueryDecoder(httpTrans.RequestQueryDecoder()), // query解码
http.ResponseEncoder(httpTrans.ResponseEncoder()), // 正常响应
http.ErrorEncoder(httpTrans.ErrorEncoder()), // 错误响应
http.Middleware(
recovery.Recovery(),
tracing.Server(),
logging.Server(logger),
validate.Validator(),
trace.SetTraceIdToHeader,
),
}
if c.Server.Http.Network != "" {
opts = append(opts, http.Network(c.Server.Http.Network))
}
if c.Server.Http.Addr != "" {
opts = append(opts, http.Address(c.Server.Http.Addr))
}
if c.Server.Http.Timeout != nil {
opts = append(opts, http.Timeout(c.Server.Http.Timeout.AsDuration()))
}
srv := http.NewServer(opts...)
userV1.RegisterUserCenterHTTPServer(srv, user)
return srv
}
```

@ -0,0 +1,89 @@
package http
import (
"bytes"
"encoding/json"
"gitea.drugeyes.vip/pharnexbase/tools/request"
"github.com/go-kratos/kratos/v2/errors"
"github.com/go-kratos/kratos/v2/transport/http"
"github.com/go-kratos/kratos/v2/transport/http/binding"
"github.com/tidwall/gjson"
"io"
"net/url"
"strings"
)
var SignErr = errors.Forbidden("SignError", "签名错误")
func (t *Transport) RequestQueryDecoder() http.DecodeRequestFunc {
return func(r *http.Request, v interface{}) error {
// 将post和query参数合并
params := make(map[string]string)
for _, val := range strings.Split(r.URL.RawQuery, "&") {
vals := strings.Split(val, "=")
if len(vals) < 2 {
continue
}
params[vals[0]], _ = url.PathUnescape(vals[1])
}
signature := request.NewSignature(t.apiKey, request.NewSHA1HashAlg())
signStr := signature.GenSignature(params)
if signStr != r.Header.Get("sm5") {
return SignErr
}
if err := binding.BindQuery(r.URL.Query(), v); err != nil {
return errors.BadRequest("CODEC", err.Error())
}
return nil
}
}
// RequestDecoder 请求拦截
func (t *Transport) RequestDecoder() http.DecodeRequestFunc {
return func(r *http.Request, v interface{}) error {
// 从Request Header的Content-Type中提取出对应的解码器
codec, ok := http.CodecForRequest(r, "Content-Type")
// 如果找不到对应的解码器此时会报错
if !ok {
return errors.BadRequest("CODEC", r.Header.Get("Content-Type"))
}
data, err := io.ReadAll(r.Body)
if err != nil {
return errors.BadRequest("CODEC", err.Error())
}
// 将post和query参数合并
params := make(map[string]string)
gjson.ParseBytes(data).ForEach(func(key, value gjson.Result) bool {
switch value.Type {
case gjson.JSON:
var buf bytes.Buffer
json.Compact(&buf, []byte(value.String()))
params[key.String()] = buf.String()
default:
params[key.String()] = value.String()
}
return true
})
for _, val := range strings.Split(r.URL.RawQuery, "&") {
vals := strings.Split(val, "=")
if len(vals) < 2 {
continue
}
params[vals[0]], _ = url.PathUnescape(vals[1])
}
signature := request.NewSignature(t.apiKey, request.NewSHA1HashAlg())
signStr := signature.GenSignature(params)
if signStr != r.Header.Get("sm5") {
return SignErr
}
if err = codec.Unmarshal(data, v); err != nil {
return errors.BadRequest("CODEC", err.Error())
}
return nil
}
}

@ -1,10 +1,13 @@
package http
import (
"encoding/hex"
"fmt"
"gitea.drugeyes.vip/pharnexbase/utils/enum"
transPbV1 "gitea.drugeyes.vip/pharnexbase/utils/transport/v1"
"github.com/go-kratos/kratos/v2/errors"
"github.com/go-kratos/kratos/v2/transport/http"
"github.com/tjfoc/gmsm/sm4"
stdhttp "net/http"
"strings"
)
@ -13,6 +16,8 @@ const (
baseContentType = "application"
)
var ErrInternalServer = errors.InternalServer("InternalServerError", "服务错误")
type Response struct {
Code int `json:"code" form:"code"`
Data interface{} `json:"data" form:"data"`
@ -20,65 +25,126 @@ type Response struct {
Message string `json:"message" form:"message"`
}
func ResponseEncoder(w stdhttp.ResponseWriter, r *stdhttp.Request, v interface{}) error {
if v == nil {
return nil
}
func (t *Transport) ResponseEncoder() http.EncodeResponseFunc {
return func(w stdhttp.ResponseWriter, r *stdhttp.Request, v interface{}) error {
if v == nil {
return nil
}
// 重定向
if rd, ok := v.(http.Redirector); ok {
url, code := rd.Redirect()
stdhttp.Redirect(w, r, url, code)
return nil
}
// 重定向
if rd, ok := v.(http.Redirector); ok {
url, code := rd.Redirect()
stdhttp.Redirect(w, r, url, code)
return nil
}
// 文件下载
if data, ok := v.(*transPbV1.FileResponse); ok {
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", data.Filename))
if data.Filetype != "" {
w.Header().Set("Content-Type", data.Filetype)
}
w.Header().Set("Access-Control-Expose-Headers", "*")
w.WriteHeader(stdhttp.StatusOK)
w.Write(data.Data)
// 文件下载
if data, ok := v.(*transPbV1.FileResponse); ok {
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", data.Filename))
if data.Filetype != "" {
w.Header().Set("Content-Type", data.Filetype)
return nil
}
w.Header().Set("Access-Control-Expose-Headers", "*")
w.WriteHeader(stdhttp.StatusOK)
w.Write(data.Data)
codec, _ := http.CodecForRequest(r, "Accept")
data, err := codec.Marshal(v)
if err != nil {
return err
}
w.Header().Set("Content-Type", ContentType(codec.Name()))
w.Header().Set("Access-Control-Expose-Headers", "Traceid, En ,Content-Disposition, Authorization")
d := fmt.Sprintf(`{"code": 200,"data": %s,"reason": "","message": ""}`, string(data))
_, err = w.Write([]byte(d))
if err != nil {
return err
}
return nil
}
}
codec, _ := http.CodecForRequest(r, "Accept")
data, err := codec.Marshal(v)
if err != nil {
return err
}
func (t *Transport) ResponseEncoderWithEncrypt() http.EncodeResponseFunc {
return func(w stdhttp.ResponseWriter, r *stdhttp.Request, v interface{}) error {
if v == nil {
return nil
}
// 重定向
if rd, ok := v.(http.Redirector); ok {
url, code := rd.Redirect()
stdhttp.Redirect(w, r, url, code)
return nil
}
// 文件下载
if data, ok := v.(*transPbV1.FileResponse); ok {
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", data.Filename))
if data.Filetype != "" {
w.Header().Set("Content-Type", data.Filetype)
}
w.Header().Set("Access-Control-Expose-Headers", "*")
w.WriteHeader(stdhttp.StatusOK)
w.Write(data.Data)
w.Header().Set("Content-Type", ContentType(codec.Name()))
return nil
}
codec, _ := http.CodecForRequest(r, "Accept")
data, err := codec.Marshal(v)
if err != nil {
return err
}
d := fmt.Sprintf(`{"code": 200,"data": %s,"reason": "","message": ""}`, string(data))
dataRes := string(data)
// 正式服加密
if t.env == enum.Env_Production {
ecbMsg, err := sm4.Sm4Ecb([]byte(t.sm4Key), data, true)
if err == nil {
dataRes = hex.EncodeToString(ecbMsg)
w.Header().Set("en", "4")
}
}
_, err = w.Write([]byte(d))
if err != nil {
return err
w.Header().Set("Content-Type", ContentType(codec.Name()))
w.Header().Set("Access-Control-Expose-Headers", "Traceid, En ,Content-Disposition, Authorization")
d := fmt.Sprintf(`{"code": 200,"data": %s,"reason": "","message": ""}`, dataRes)
_, err = w.Write([]byte(d))
if err != nil {
return err
}
return nil
}
return nil
}
func ErrorEncoderPro(w stdhttp.ResponseWriter, r *stdhttp.Request, err error) {
se := errors.FromError(err)
if _, ok := err.(*errors.Error); !ok {
se.Reason = "InternalServerError"
se.Code = stdhttp.StatusInternalServerError
se.Message = "internal server error"
}
func (t *Transport) ErrorEncoder() http.EncodeErrorFunc {
return func(w stdhttp.ResponseWriter, r *stdhttp.Request, err error) {
se := errors.FromError(err)
if t.env == enum.Env_Production {
if _, ok := err.(*errors.Error); !ok {
se = ErrInternalServer
}
}
codec, _ := http.CodecForRequest(r, "Accept")
body, err := codec.Marshal(se)
if err != nil {
w.WriteHeader(stdhttp.StatusInternalServerError)
return
codec, _ := http.CodecForRequest(r, "Accept")
body, err := codec.Marshal(se)
if err != nil {
w.WriteHeader(stdhttp.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", ContentType(codec.Name()))
w.Header().Set("Access-Control-Expose-Headers", "TraceID, En ,Content-Disposition, Authorization")
w.WriteHeader(int(se.Code))
_, _ = w.Write(body)
}
w.Header().Set("Content-Type", ContentType(codec.Name()))
w.WriteHeader(int(se.Code))
_, _ = w.Write(body)
}
// ContentType returns the content-type with base prefix.

@ -0,0 +1,19 @@
package http
import (
"gitea.drugeyes.vip/pharnexbase/utils/enum"
)
type Transport struct {
env enum.Env // 环境
sm4Key string // sm4Key
apiKey string // apiKey
}
func NewTransport(env enum.Env, sm4Key, apiKey string) *Transport {
return &Transport{
env: env,
sm4Key: sm4Key,
apiKey: apiKey,
}
}
Loading…
Cancel
Save