diff --git a/assets/templates/authorized/authorized_api.html b/assets/templates/authorized/authorized_api.html index 31de8f3..a9024f2 100644 --- a/assets/templates/authorized/authorized_api.html +++ b/assets/templates/authorized/authorized_api.html @@ -79,10 +79,10 @@ const hash_id = {{ .HashID }} - $("input#request_api").maxlength({ - warningClass: "badge badge-info", - limitReachedClass: "badge badge-warning" - }); + $("input#request_api").maxlength({ + warningClass: "badge badge-info", + limitReachedClass: "badge badge-warning" + }); // 加载列表页数据 getListData(); @@ -91,12 +91,28 @@ $.get("/api/authorized_list", {id: hash_id}, function (data) { // 成功 if (data.list.length > 0) { + var badgeMethodClass = ""; + $.each(data.list, function (index, value) { + if (value.method === "GET") { + badgeMethodClass = "badge-primary"; + } else if (value.method === "POST") { + badgeMethodClass = "badge-success"; + } else if (value.method === "DELETE") { + badgeMethodClass = "badge-danger"; + } else if (value.method === "PUT") { + badgeMethodClass = "badge-yellow"; + } else if (value.method === "PATCH") { + badgeMethodClass = "badge-cyan"; + } else { + badgeMethodClass = "badge-dark"; + } + const p = '

\n' + '' + - '\n' + + '\n' + '\n' + - '' + value.method + '\n' + value.api + '' + value.method + '\n' + value.api ; $(".apis").append(p); diff --git a/assets/templates/authorized/authorized_demo.html b/assets/templates/authorized/authorized_demo.html new file mode 100644 index 0000000..6a63f70 --- /dev/null +++ b/assets/templates/authorized/authorized_demo.html @@ -0,0 +1,184 @@ + + + + + + + + + + + +

+
+ +
+
+
+
如何给调用方开通 KEY 和 SECRET?
+
+ +
+

1. 新增调用方,输入调用方标识、调用方对接人、备注等信息;

+

2. 授权调用方可调用的接口;

+

3. 查看详情,将调用方的 KEYSECRET 发给调用方;

+
+
+
+ +
+
+
+
调用方如何传递 Token?
+
+ +
+

1. 基于 HTTP Header 中的两个参数 AuthorizationDate 存储签名信息,Authorization 存储签名信息,格式:调用方 KEY + 空格分隔符 + 摘要(加密串),例如:

+
Authorization:blog MjJjMDE1MWFkZjMwOWFmYjFlNzViNDFjYjYwMWFlMmM=
+

2. Date 存储时间信息,格式:GMT 格林尼治标准时间,使用 Asia/Shanghai 时区,例如;

+
Date:Sat, 03 Apr 2021 10:14:43 GMT
+
+
+
+ +
+
+
不同语言生成签名的方法,供参考
+
+ + + +
+
+
+func New(key, secret string, ttl time.Duration) Signature {
+	return &signature{
+		key:    key,
+		secret: secret,
+		ttl:    ttl,
+	}
+}
+
+// Generate
+// path 请求的路径 (不附带 querystring)
+func (s *signature) Generate(path string, method string, params url.Values) (authorization, date string, err error) {
+	if path == "" {
+		err = errors.New("path required")
+		return
+	}
+
+	if method == "" {
+		err = errors.New("method required")
+		return
+	}
+
+	methodName := strings.ToUpper(method)
+	if !methods[methodName] {
+		err = errors.New("method param error")
+		return
+	}
+
+	// Date
+	date = time_parse.GMTLayoutString()
+
+	// Encode() 方法中自带 sorted by key
+	sortParamsEncode := params.Encode()
+
+	// 加密字符串规则
+	buffer := bytes.NewBuffer(nil)
+	buffer.WriteString(path)
+	buffer.WriteString(delimiter)
+	buffer.WriteString(methodName)
+	buffer.WriteString(delimiter)
+	buffer.WriteString(sortParamsEncode)
+	buffer.WriteString(delimiter)
+	buffer.WriteString(date)
+
+	// 对数据进行 hmac 加密,并进行 base64 encode
+	hash := hmac.New(sha256.New, []byte(s.secret))
+	hash.Write(buffer.Bytes())
+	digest := base64.StdEncoding.EncodeToString(hash.Sum(nil))
+
+	authorization = fmt.Sprintf("%s %s", s.key, digest)
+	return
+}
+
+// 模拟数据
+const (
+	key    = "blog"
+	secret = "i1ydX9RtHyuJTrw7frcu"
+	ttl    = time.Minute * 10
+)
+
+func TestSignature_Generate(t *testing.T) {
+	path := "/echo"
+	method := "POST"
+
+	params := url.Values{}
+	params.Add("a", "a1")
+	params.Add("d", "d1")
+	params.Add("c", "c1")
+
+	authorization, date, err := New(key, secret, ttl).Generate(path, method, params)
+	t.Log("authorization:", authorization)
+	t.Log("date:", date)
+	t.Log("err:", err)
+}
+                            
+
+
+
+// 模拟数据
+$key    = "blog";
+$secret = "i1ydX9RtHyuJTrw7frcu";
+
+$path = "/echo";
+$method = "POST";
+
+$params['a'] = "a1";
+$params['d'] = "d1";
+$params['c'] = "c1";
+
+// 对 params key 进行排序
+ksort($params);
+
+// 对 sortParams 进行 Encode
+$sortParamsEncode = http_build_query($params);
+
+// GMT 格林尼治标准时间,使用 Asia/Shanghai 时区
+$date = gmdate('D, d M Y H:i:s T',time() + 8*3600);
+
+// 加密字符串规则
+$encryptStr = $path."|".strtoupper($method)."|".$sortParamsEncode."|".$date;
+
+// 对数据进行 sha256 加密,并进行 base64 encode
+$digest = base64_encode(hash_hmac("sha256", $encryptStr, $secret, true));
+
+$authorization = $key." ".$digest;
+
+echo "authorization:{$authorization}";
+echo "---";
+echo "date:{$date}";
+                            
+
+
+ +
+
+
+ +
+
+ + + + + + diff --git a/assets/templates/index/index.html b/assets/templates/index/index.html index 024af46..4357fbe 100644 --- a/assets/templates/index/index.html +++ b/assets/templates/index/index.html @@ -48,6 +48,15 @@ 授权调用方 + + + @@ -55,13 +64,6 @@ - - diff --git a/assets/templates/tool/tool_logs.html b/assets/templates/tool/tool_logs.html new file mode 100644 index 0000000..75e9387 --- /dev/null +++ b/assets/templates/tool/tool_logs.html @@ -0,0 +1,105 @@ + + + + + + + + + + + +
+ + + +
+
+
日志列表 仅展示最新的 100 条日志。
+
    +
  • + +
  • +
+
+
+
+ {{range $key, $value := .Logs}} + {{$badgeLevelClass := ""}} + {{$badgeMethodClass := ""}} + {{$badgeCodeClass := ""}} +
+
+
+ {{if eq $value.Level "info"}} + {{$badgeLevelClass = "badge-info"}} + {{else if eq $value.Level "error"}} + {{$badgeLevelClass = "badge-danger"}} + {{else if eq $value.Level "warn"}} + {{$badgeLevelClass = "badge-warning"}} + {{else}} + {{$badgeLevelClass = "badge-dark"}} + {{end}} + + {{if eq $value.Method "GET"}} + {{$badgeMethodClass = "badge-primary"}} + {{else if eq $value.Method "POST"}} + {{$badgeMethodClass = "badge-success"}} + {{else if eq $value.Method "DELETE"}} + {{$badgeMethodClass = "badge-danger"}} + {{else if eq $value.Method "PUT"}} + {{$badgeMethodClass = "badge-yellow"}} + {{else if eq $value.Method "PATCH"}} + {{$badgeMethodClass = "badge-cyan"}} + {{else}} + {{$badgeMethodClass = "badge-dark"}} + {{end}} + + {{if eq $value.HTTPCode 200}} + {{$badgeCodeClass = "badge-success"}} + {{else}} + {{$badgeCodeClass = "badge-dark"}} + {{end}} + + + {{if eq $value.HTTPCode 0}} + + {{else}} + + {{end}} + + + +
+
+ +
+
+
{{$value.Content}}
+
+
+
+ {{end}} +
+
+
+
+ + + + + diff --git a/internal/router/router_web.go b/internal/router/router_web.go index 2e14d7d..53d0436 100644 --- a/internal/router/router_web.go +++ b/internal/router/router_web.go @@ -43,9 +43,11 @@ func setWebRouter(r *resource) { web.GET("/authorized/list", authorizedHandler.ListView()) web.GET("/authorized/add", authorizedHandler.AddView()) web.GET("/authorized/api/:id", authorizedHandler.ApiView()) + web.GET("/authorized/demo", authorizedHandler.DemoView()) // 工具箱 web.GET("/tool/hashids", toolHandler.HashIdsView()) + web.GET("/tool/logs", toolHandler.LogsView()) } } diff --git a/internal/web/controller/authorized_handler/func_demoview.go b/internal/web/controller/authorized_handler/func_demoview.go new file mode 100644 index 0000000..254cc7e --- /dev/null +++ b/internal/web/controller/authorized_handler/func_demoview.go @@ -0,0 +1,9 @@ +package authorized_handler + +import "github.com/xinliangnote/go-gin-api/internal/pkg/core" + +func (h *handler) DemoView() core.HandlerFunc { + return func(c core.Context) { + c.HTML("authorized_demo", nil) + } +} diff --git a/internal/web/controller/authorized_handler/handler.go b/internal/web/controller/authorized_handler/handler.go index ecb0a91..24d3268 100644 --- a/internal/web/controller/authorized_handler/handler.go +++ b/internal/web/controller/authorized_handler/handler.go @@ -16,6 +16,7 @@ type Handler interface { AddView() core.HandlerFunc ApiView() core.HandlerFunc ListView() core.HandlerFunc + DemoView() core.HandlerFunc } type handler struct { diff --git a/internal/web/controller/tool_handler/func_logsview.go b/internal/web/controller/tool_handler/func_logsview.go new file mode 100644 index 0000000..4c583c9 --- /dev/null +++ b/internal/web/controller/tool_handler/func_logsview.go @@ -0,0 +1,78 @@ +package tool_handler + +import ( + "encoding/json" + + "github.com/xinliangnote/go-gin-api/configs" + "github.com/xinliangnote/go-gin-api/internal/pkg/core" + "github.com/xinliangnote/go-gin-api/pkg/file" + + "go.uber.org/zap" +) + +type logsViewResponse struct { + Logs []logData `json:"logs"` +} + +type logData struct { + Level string `json:"level"` + Time string `json:"time"` + Path string `json:"path"` + HTTPCode int `json:"http_code"` + Method string `json:"method"` + Msg string `json:"msg"` + TraceID string `json:"trace_id"` + Content string `json:"content"` +} + +func (h *handler) LogsView() core.HandlerFunc { + + type logParseData struct { + Level string `json:"level"` + Time string `json:"time"` + Caller string `json:"caller"` + Msg string `json:"msg"` + Domain string `json:"domain"` + Method string `json:"method"` + Path string `json:"path"` + HTTPCode int `json:"http_code"` + BusinessCode int `json:"business_code"` + Success bool `json:"success"` + CostSeconds float64 `json:"cost_seconds"` + TraceID string `json:"trace_id"` + } + + return func(c core.Context) { + readLineFromEnd, err := file.NewReadLineFromEnd(configs.ProjectLogFile()) + if err != nil { + h.logger.Error("NewReadLineFromEnd err", zap.Error(err)) + } + + logSize := 100 + + obj := new(logsViewResponse) + obj.Logs = make([]logData, logSize) + + for i := 0; i < logSize; i++ { + content, _ := readLineFromEnd.ReadLine() + + var logParse logParseData + _ = json.Unmarshal(content, &logParse) + data := logData{ + Content: string(content), + Level: logParse.Level, + Time: logParse.Time, + Path: logParse.Path, + Method: logParse.Method, + Msg: logParse.Msg, + HTTPCode: logParse.HTTPCode, + TraceID: logParse.TraceID, + } + + if string(content) != "" { + obj.Logs[i] = data + } + } + c.HTML("tool_logs", obj) + } +} diff --git a/internal/web/controller/tool_handler/handler.go b/internal/web/controller/tool_handler/handler.go index 0ee1ca4..7fcac95 100644 --- a/internal/web/controller/tool_handler/handler.go +++ b/internal/web/controller/tool_handler/handler.go @@ -14,6 +14,7 @@ type Handler interface { i() HashIdsView() core.HandlerFunc + LogsView() core.HandlerFunc } type handler struct { diff --git a/pkg/file/file.go b/pkg/file/file.go new file mode 100644 index 0000000..150b3a4 --- /dev/null +++ b/pkg/file/file.go @@ -0,0 +1,183 @@ +package file + +import ( + "bytes" + "fmt" + "io" + "os" +) + +var ( + buffSize = 1 << 20 +) + +// ReadLineFromEnd -- +type ReadLineFromEnd struct { + f *os.File + + fileSize int + bwr *bytes.Buffer + lineBuff []byte + swapBuff []byte + + isFirst bool +} + +// NewReadLineFromEnd +func NewReadLineFromEnd(filename string) (rd *ReadLineFromEnd, err error) { + f, err := os.Open(filename) + if err != nil { + return nil, err + } + + info, err := f.Stat() + if err != nil { + return nil, err + } + + if info.IsDir() { + return nil, fmt.Errorf("not file") + } + + fileSize := int(info.Size()) + + rd = &ReadLineFromEnd{ + f: f, + fileSize: fileSize, + bwr: bytes.NewBuffer([]byte{}), + lineBuff: make([]byte, 0), + swapBuff: make([]byte, buffSize), + isFirst: true, + } + return rd, nil +} + +// ReadLine 结尾包含'\n' +func (c *ReadLineFromEnd) ReadLine() (line []byte, err error) { + var ok bool + for { + ok, err = c.buff() + if err != nil { + return nil, err + } + if ok { + break + } + } + line, err = c.bwr.ReadBytes('\n') + if err == io.EOF && c.fileSize > 0 { + err = nil + } + return line, err +} + +// Close -- +func (c *ReadLineFromEnd) Close() (err error) { + return c.f.Close() +} + +func (c *ReadLineFromEnd) buff() (ok bool, err error) { + if c.fileSize == 0 { + return true, nil + } + + if c.bwr.Len() >= buffSize { + return true, nil + } + + offset := 0 + if c.fileSize > buffSize { + offset = c.fileSize - buffSize + } + _, err = c.f.Seek(int64(offset), 0) + if err != nil { + return false, err + } + + n, err := c.f.Read(c.swapBuff) + if err != nil && err != io.EOF { + return false, err + } + if c.fileSize < n { + n = c.fileSize + } + if n == 0 { + return true, nil + } + + for { + m := bytes.LastIndex(c.swapBuff[:n], []byte{'\n'}) + if m == -1 { + break + } + if m < n-1 { + err = c.writeLine(c.swapBuff[m+1 : n]) + if err != nil { + return false, err + } + ok = true + } else if m == n-1 && !c.isFirst { + err = c.writeLine(nil) + if err != nil { + return false, err + } + ok = true + } + n = m + if n == 0 { + break + } + } + if n > 0 { + reverseBytes(c.swapBuff[:n]) + c.lineBuff = append(c.lineBuff, c.swapBuff[:n]...) + } + if offset == 0 { + err = c.writeLine(nil) + if err != nil { + return false, err + } + ok = true + } + c.fileSize = offset + if c.isFirst { + c.isFirst = false + } + return ok, nil +} + +func (c *ReadLineFromEnd) writeLine(b []byte) (err error) { + if len(b) > 0 { + _, err = c.bwr.Write(b) + if err != nil { + return err + } + } + if len(c.lineBuff) > 0 { + reverseBytes(c.lineBuff) + _, err = c.bwr.Write(c.lineBuff) + if err != nil { + return err + } + c.lineBuff = c.lineBuff[:0] + } + _, err = c.bwr.Write([]byte{'\n'}) + if err != nil { + return err + } + return nil +} + +func reverseBytes(b []byte) { + n := len(b) + if n <= 1 { + return + } + for i := 0; i < n; i++ { + k := n - 1 + if k != i { + b[i], b[k] = b[k], b[i] + } + n-- + } +} diff --git a/pkg/signature/signature.go b/pkg/signature/signature.go new file mode 100644 index 0000000..e5efd80 --- /dev/null +++ b/pkg/signature/signature.go @@ -0,0 +1,52 @@ +package signature + +import ( + "net/http" + "net/url" + "time" +) + +var _ Signature = (*signature)(nil) + +const ( + delimiter = "|" +) + +// 合法的 Methods +var methods = map[string]bool{ + http.MethodGet: true, + http.MethodPost: true, + http.MethodHead: true, + http.MethodPut: true, + http.MethodPatch: true, + http.MethodDelete: true, + http.MethodConnect: true, + http.MethodOptions: true, + http.MethodTrace: true, +} + +type Signature interface { + i() + + // 生成签名 + Generate(path string, method string, params url.Values) (authorization, date string, err error) + + // 验证签名 + Verify(authorization, date string, path string, method string, params url.Values) (ok bool, err error) +} + +type signature struct { + key string + secret string + ttl time.Duration +} + +func New(key, secret string, ttl time.Duration) Signature { + return &signature{ + key: key, + secret: secret, + ttl: ttl, + } +} + +func (s *signature) i() {} diff --git a/pkg/signature/signature_generate.go b/pkg/signature/signature_generate.go new file mode 100644 index 0000000..4897357 --- /dev/null +++ b/pkg/signature/signature_generate.go @@ -0,0 +1,61 @@ +package signature + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "net/url" + "strings" + + "github.com/xinliangnote/go-gin-api/pkg/time_parse" + + "github.com/pkg/errors" +) + +// Generate +// path 请求的路径 (不附带 querystring) +func (s *signature) Generate(path string, method string, params url.Values) (authorization, date string, err error) { + if path == "" { + err = errors.New("path required") + return + } + + if method == "" { + err = errors.New("method required") + return + } + + methodName := strings.ToUpper(method) + if !methods[methodName] { + err = errors.New("method param error") + return + } + + // Date + date = time_parse.GMTLayoutString() + + // Encode() 方法中自带 sorted by key + sortParamsEncode := params.Encode() + + // 加密字符串规则 + buffer := bytes.NewBuffer(nil) + buffer.WriteString(path) + buffer.WriteString(delimiter) + buffer.WriteString(methodName) + buffer.WriteString(delimiter) + buffer.WriteString(sortParamsEncode) + buffer.WriteString(delimiter) + buffer.WriteString(date) + + fmt.Println(string(buffer.Bytes())) + + // 对数据进行 sha256 加密,并进行 base64 encode + hash := hmac.New(sha256.New, []byte(s.secret)) + hash.Write(buffer.Bytes()) + digest := base64.StdEncoding.EncodeToString(hash.Sum(nil)) + + authorization = fmt.Sprintf("%s %s", s.key, digest) + return +} diff --git a/pkg/signature/signature_test.go b/pkg/signature/signature_test.go new file mode 100644 index 0000000..4e58619 --- /dev/null +++ b/pkg/signature/signature_test.go @@ -0,0 +1,45 @@ +package signature + +import ( + "net/url" + "testing" + "time" +) + +const ( + key = "blog" + secret = "i1ydX9RtHyuJTrw7frcu" + ttl = time.Minute * 10 +) + +func TestSignature_Generate(t *testing.T) { + path := "/echo" + method := "POST" + + params := url.Values{} + params.Add("a", "a1") + params.Add("d", "d1") + params.Add("c", "c1") + + authorization, date, err := New(key, secret, ttl).Generate(path, method, params) + t.Log("authorization:", authorization) + t.Log("date:", date) + t.Log("err:", err) +} + +func TestSignature_Verify(t *testing.T) { + + authorization := "blog d3+XC7l0lproeO4Z/WHd2VWZsBct0dy9HUndNOxmPu0=" + date := "Fri, 02 Apr 2021 20:31:28 GMT" + + path := "/echo" + method := "post" + params := url.Values{} + params.Add("a", "a1") + params.Add("d", "d1") + params.Add("c", "c1") + + ok, err := New(key, secret, ttl).Verify(authorization, date, path, method, params) + t.Log(ok) + t.Log(err) +} diff --git a/pkg/signature/signature_verify.go b/pkg/signature/signature_verify.go new file mode 100644 index 0000000..f42fb43 --- /dev/null +++ b/pkg/signature/signature_verify.go @@ -0,0 +1,70 @@ +package signature + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "net/url" + "strings" + "time" + + "github.com/xinliangnote/go-gin-api/pkg/time_parse" + + "github.com/pkg/errors" +) + +func (s *signature) Verify(authorization, date string, path string, method string, params url.Values) (ok bool, err error) { + if date == "" { + err = errors.New("date required") + return + } + + if path == "" { + err = errors.New("path required") + return + } + + if method == "" { + err = errors.New("method required") + return + } + + methodName := strings.ToUpper(method) + if !methods[methodName] { + err = errors.New("method param error") + return + } + + ts, err := time_parse.ParseGMTInLocation(date) + if err != nil { + err = errors.New("date must follow 'Mon, 02 Jan 2006 15:04:05 GMT'") + return + } + + if time_parse.SubInLocation(ts) > float64(s.ttl/time.Second) { + err = errors.Errorf("date exceeds limit %v", s.ttl) + return + } + + // Encode() 方法中自带 sorted by key + sortParamsEncode := params.Encode() + + buffer := bytes.NewBuffer(nil) + buffer.WriteString(path) + buffer.WriteString(delimiter) + buffer.WriteString(methodName) + buffer.WriteString(delimiter) + buffer.WriteString(sortParamsEncode) + buffer.WriteString(delimiter) + buffer.WriteString(date) + + // 对数据进行 hmac 加密,并进行 base64 encode + hash := hmac.New(sha256.New, []byte(s.secret)) + hash.Write(buffer.Bytes()) + digest := base64.StdEncoding.EncodeToString(hash.Sum(nil)) + + ok = authorization == fmt.Sprintf("%s %s", s.key, digest) + return +} diff --git a/pkg/time_parse/time_parse.go b/pkg/time_parse/time_parse.go index 8f7eb0a..bea298e 100644 --- a/pkg/time_parse/time_parse.go +++ b/pkg/time_parse/time_parse.go @@ -1,6 +1,10 @@ package time_parse -import "time" +import ( + "math" + "net/http" + "time" +) var ( cst *time.Location @@ -43,3 +47,19 @@ func CSTLayoutStringToUnix(cstLayoutString string) (int64, error) { } return stamp.Unix(), nil } + +// GMTLayoutString 格式化时间 +// 返回 "Mon, 02 Jan 2006 15:04:05 GMT" 格式的时间 +func GMTLayoutString() string { + return time.Now().In(cst).Format(http.TimeFormat) +} + +// ParseGMTInLocation 格式化时间 +func ParseGMTInLocation(date string) (time.Time, error) { + return time.ParseInLocation(http.TimeFormat, date, cst) +} + +// SubInLocation 计算时间差 +func SubInLocation(ts time.Time) float64 { + return math.Abs(time.Now().In(cst).Sub(ts).Seconds()) +} diff --git a/pkg/time_parse/time_parse_test.go b/pkg/time_parse/time_parse_test.go index 718ef6f..a186835 100644 --- a/pkg/time_parse/time_parse_test.go +++ b/pkg/time_parse/time_parse_test.go @@ -13,3 +13,7 @@ func TestCSTLayoutString(t *testing.T) { func TestCSTLayoutStringToUnix(t *testing.T) { t.Log(CSTLayoutStringToUnix("2020-01-24 21:11:11")) } + +func TestGMTLayoutString(t *testing.T) { + t.Log(GMTLayoutString()) +}