#21 add tool - view log
This commit is contained in:
@@ -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 = '<p>\n' +
|
||||
'<a href="#!" data-id="' + value.hash_id + '" data-api="' + value.api + '" class="del">' +
|
||||
'<span class="badge badge-secondary"><i class="mdi mdi-window-close"></i></span>\n' +
|
||||
'<span class="badge badge-dark"><i class="mdi mdi-window-close"></i></span>\n' +
|
||||
'</a>\n' +
|
||||
'<span class="badge badge-success">' + value.method + '</span>\n' + value.api
|
||||
'<span class="badge ' + badgeMethodClass + '">' + value.method + '</span>\n' + value.api
|
||||
;
|
||||
|
||||
$(".apis").append(p);
|
||||
|
||||
184
assets/templates/authorized/authorized_demo.html
Normal file
184
assets/templates/authorized/authorized_demo.html
Normal file
@@ -0,0 +1,184 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/>
|
||||
<link href="../../bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="../../bootstrap/css/materialdesignicons.min.css" rel="stylesheet">
|
||||
<link href="../../bootstrap/css/style.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-fluid p-t-15">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<div class="card-title">如何给调用方开通 KEY 和 SECRET?</div>
|
||||
</header>
|
||||
|
||||
<div class="card-body">
|
||||
<p>1. 新增调用方,输入调用方标识、调用方对接人、备注等信息;</p>
|
||||
<p>2. 授权调用方可调用的接口;</p>
|
||||
<p>3. 查看详情,将调用方的 <code>KEY</code>、<code>SECRET</code> 发给调用方;</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<div class="card-title">调用方如何传递 Token?</div>
|
||||
</header>
|
||||
|
||||
<div class="card-body">
|
||||
<p>1. 基于 HTTP Header 中的两个参数 <code>Authorization</code>、<code>Date</code> 存储签名信息,Authorization 存储签名信息,格式:调用方 KEY + 空格分隔符 + 摘要(加密串),例如:</p>
|
||||
<pre>Authorization:blog MjJjMDE1MWFkZjMwOWFmYjFlNzViNDFjYjYwMWFlMmM=</pre>
|
||||
<p>2. Date 存储时间信息,格式:GMT 格林尼治标准时间,使用 <code>Asia/Shanghai</code> 时区,例如;</p>
|
||||
<pre>Date:Sat, 03 Apr 2021 10:14:43 GMT</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<div class="card">
|
||||
<header class="card-header"><div class="card-title">不同语言生成签名的方法,供参考</div></header>
|
||||
<div class="card-body">
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#go" aria-selected="true">Go 语言</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#php" aria-selected="false">PHP 语言</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade active show" id="go">
|
||||
<pre>
|
||||
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)
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="php">
|
||||
<pre>
|
||||
// 模拟数据
|
||||
$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}";
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="../../bootstrap/js/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="../../bootstrap/js/popper.min.js"></script>
|
||||
<script type="text/javascript" src="../../bootstrap/js/bootstrap.min.js"></script>
|
||||
<script type="text/javascript" src="../../bootstrap/js/main.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -48,6 +48,15 @@
|
||||
<a href="javascript:void(0)"><i class="mdi mdi-playlist-check"></i> <span>授权调用方</span></a>
|
||||
<ul class="nav nav-subnav">
|
||||
<li> <a class="multitabs" href="/authorized/list">调用方</a> </li>
|
||||
<li> <a class="multitabs" href="/authorized/demo">使用说明</a> </li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="nav-item nav-item-has-subnav">
|
||||
<a href="javascript:void(0)"><i class="mdi mdi-tools"></i> <span>工具箱</span></a>
|
||||
<ul class="nav nav-subnav">
|
||||
<li> <a class="multitabs" href="/tool/hashids">Hashids</a> </li>
|
||||
<li> <a class="multitabs" href="/tool/logs">调用日志</a> </li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
@@ -55,13 +64,6 @@
|
||||
<li class="nav-item"> <a href="/graphql" target="_blank" ><i class="mdi mdi-file-document-box-search"></i> <span>GraphQL</span></a> </li>
|
||||
<li class="nav-item"> <a href="/metrics" target="_blank" ><i class="mdi mdi-speedometer"></i> <span>接口指标</span></a> </li>
|
||||
|
||||
<li class="nav-item nav-item-has-subnav">
|
||||
<a href="javascript:void(0)"><i class="mdi mdi-tools"></i> <span>工具箱</span></a>
|
||||
<ul class="nav nav-subnav">
|
||||
<li> <a class="multitabs" href="/tool/hashids">hashids</a> </li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
105
assets/templates/tool/tool_logs.html
Normal file
105
assets/templates/tool/tool_logs.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"/>
|
||||
<link href="../../bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="../../bootstrap/css/materialdesignicons.min.css" rel="stylesheet">
|
||||
<link href="../../bootstrap/css/style.min.css" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-fluid p-t-15">
|
||||
|
||||
<div class="alert alert-info alert-dismissible" role="alert">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<p>推荐使用: <code>ELK 组件</code> ,本功能仅仅是读取文本进行展示。</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<div class="card-title">日志列表 <code>仅展示最新的 100 条日志。</code></div>
|
||||
<ul class="card-actions">
|
||||
<li>
|
||||
<a href="#!" onclick="location.reload();" data-toggle="tooltip" title="" data-original-title="刷新"><i class="mdi mdi-refresh"></i></a>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
<div class="card-body">
|
||||
<div class="accordion">
|
||||
{{range $key, $value := .Logs}}
|
||||
{{$badgeLevelClass := ""}}
|
||||
{{$badgeMethodClass := ""}}
|
||||
{{$badgeCodeClass := ""}}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
{{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}}
|
||||
<a data-toggle="collapse" data-target="#collapse{{$key}}" aria-expanded="false" href="#!" class="collapsed">
|
||||
<span class="badge {{$badgeLevelClass}}">{{$value.Level}}</span>
|
||||
<span class="badge badge-brown">{{$value.Time}}</span>
|
||||
<code>{{$value.Msg}}</code>
|
||||
</a>
|
||||
{{else}}
|
||||
<a data-toggle="collapse" data-target="#collapse{{$key}}" aria-expanded="false" href="#!" class="collapsed">
|
||||
<span class="badge {{$badgeLevelClass}}">{{$value.Level}}</span>
|
||||
<span class="badge badge-brown">{{$value.Time}}</span>
|
||||
<span class="badge {{$badgeMethodClass}}">{{$value.Method}}</span>
|
||||
<span class="badge {{$badgeCodeClass}}">{{$value.HTTPCode}}</span>
|
||||
<span class="badge badge-muted">{{$value.TraceID}}</span>
|
||||
<code>{{$value.Path}}</code>
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="collapse{{$key}}" class="collapse">
|
||||
<div class="card-body">
|
||||
<pre>{{$value.Content}}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="../../bootstrap/js/jquery.min.js"></script>
|
||||
<script type="text/javascript" src="../../bootstrap/js/popper.min.js"></script>
|
||||
<script type="text/javascript" src="../../bootstrap/js/bootstrap.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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())
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ type Handler interface {
|
||||
AddView() core.HandlerFunc
|
||||
ApiView() core.HandlerFunc
|
||||
ListView() core.HandlerFunc
|
||||
DemoView() core.HandlerFunc
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
|
||||
78
internal/web/controller/tool_handler/func_logsview.go
Normal file
78
internal/web/controller/tool_handler/func_logsview.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ type Handler interface {
|
||||
i()
|
||||
|
||||
HashIdsView() core.HandlerFunc
|
||||
LogsView() core.HandlerFunc
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
|
||||
183
pkg/file/file.go
Normal file
183
pkg/file/file.go
Normal file
@@ -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--
|
||||
}
|
||||
}
|
||||
52
pkg/signature/signature.go
Normal file
52
pkg/signature/signature.go
Normal file
@@ -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() {}
|
||||
61
pkg/signature/signature_generate.go
Normal file
61
pkg/signature/signature_generate.go
Normal file
@@ -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
|
||||
}
|
||||
45
pkg/signature/signature_test.go
Normal file
45
pkg/signature/signature_test.go
Normal file
@@ -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)
|
||||
}
|
||||
70
pkg/signature/signature_verify.go
Normal file
70
pkg/signature/signature_verify.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user