package auth import ( "bytes" "crypto/hmac" "crypto/sha1" "encoding/base64" "fmt" "io/ioutil" "net/http" "net/textproto" "sort" "strings" api "github.com/qiniu/go-sdk/v7" "github.com/qiniu/go-sdk/v7/conf" ) const ( IAMKeyLen = 33 IAMKeyPrefix = "IAM-" AuthorizationPrefixQiniu = "Qiniu " AuthorizationPrefixQBox = "QBox " ) // 七牛鉴权类,用于生成Qbox, Qiniu, Upload签名 // AK/SK可以从 https://portal.qiniu.com/user/key 获取 type Credentials struct { AccessKey string SecretKey []byte } // 构建一个Credentials对象 func New(accessKey, secretKey string) *Credentials { return &Credentials{accessKey, []byte(secretKey)} } // Sign 对数据进行签名,一般用于私有空间下载用途 func (ath *Credentials) Sign(data []byte) (token string) { h := hmac.New(sha1.New, ath.SecretKey) h.Write(data) sign := base64.URLEncoding.EncodeToString(h.Sum(nil)) return fmt.Sprintf("%s:%s", ath.AccessKey, sign) } // SignToken 根据t的类型对请求进行签名,并把token加入req中 func (ath *Credentials) AddToken(t TokenType, req *http.Request) error { switch t { case TokenQiniu: token, sErr := ath.SignRequestV2(req) if sErr != nil { return sErr } req.Header.Add("Authorization", AuthorizationPrefixQiniu+token) default: token, err := ath.SignRequest(req) if err != nil { return err } req.Header.Add("Authorization", AuthorizationPrefixQBox+token) } return nil } // SignWithData 对数据进行签名,一般用于上传凭证的生成用途 func (ath *Credentials) SignWithData(b []byte) (token string) { encodedData := base64.URLEncoding.EncodeToString(b) sign := ath.Sign([]byte(encodedData)) return fmt.Sprintf("%s:%s", sign, encodedData) } // IsIAMKey 判断AccessKey是否为IAM的Key func (ath *Credentials) IsIAMKey() bool { return len(ath.AccessKey) == IAMKeyLen*4/3 && strings.HasPrefix(ath.AccessKey, IAMKeyPrefix) } func collectData(req *http.Request) (data []byte, err error) { u := req.URL s := u.Path if u.RawQuery != "" { s += "?" s += u.RawQuery } s += "\n" data = []byte(s) if incBody(req) { s2, rErr := api.BytesFromRequest(req) if rErr != nil { err = rErr return } req.Body = ioutil.NopCloser(bytes.NewReader(s2)) data = append(data, s2...) } return } type ( xQiniuHeaderItem struct { HeaderName string HeaderValue string } xQiniuHeaders []xQiniuHeaderItem ) func (headers xQiniuHeaders) Len() int { return len(headers) } func (headers xQiniuHeaders) Less(i, j int) bool { if headers[i].HeaderName < headers[j].HeaderName { return true } else if headers[i].HeaderName > headers[j].HeaderName { return false } else { return headers[i].HeaderValue < headers[j].HeaderValue } } func (headers xQiniuHeaders) Swap(i, j int) { headers[i], headers[j] = headers[j], headers[i] } func collectDataV2(req *http.Request) (data []byte, err error) { u := req.URL //write method path?query s := fmt.Sprintf("%s %s", req.Method, u.Path) if u.RawQuery != "" { s += "?" s += u.RawQuery } //write host and post s += "\nHost: " + req.Host + "\n" //write content type contentType := req.Header.Get("Content-Type") if contentType == "" { contentType = "application/x-www-form-urlencoded" req.Header.Set("Content-Type", contentType) } s += fmt.Sprintf("Content-Type: %s\n", contentType) xQiniuHeaders := make(xQiniuHeaders, 0, len(req.Header)) for headerName := range req.Header { if len(headerName) > len("X-Qiniu-") && strings.HasPrefix(headerName, "X-Qiniu-") { xQiniuHeaders = append(xQiniuHeaders, xQiniuHeaderItem{ HeaderName: textproto.CanonicalMIMEHeaderKey(headerName), HeaderValue: req.Header.Get(headerName), }) } } if len(xQiniuHeaders) > 0 { sort.Sort(xQiniuHeaders) for _, xQiniuHeader := range xQiniuHeaders { s += fmt.Sprintf("%s: %s\n", xQiniuHeader.HeaderName, xQiniuHeader.HeaderValue) } } s += "\n" data = []byte(s) //write body if incBodyV2(req) { s2, rErr := api.BytesFromRequest(req) if rErr != nil { err = rErr return } req.Body = ioutil.NopCloser(bytes.NewReader(s2)) data = append(data, s2...) } return } // SignRequest 对数据进行签名,一般用于管理凭证的生成 func (ath *Credentials) SignRequest(req *http.Request) (token string, err error) { data, err := collectData(req) if err != nil { return } token = ath.Sign(data) return } // SignRequestV2 对数据进行签名,一般用于高级管理凭证的生成 func (ath *Credentials) SignRequestV2(req *http.Request) (token string, err error) { data, err := collectDataV2(req) if err != nil { return } token = ath.Sign(data) return } // 管理凭证生成时,是否同时对request body进行签名 func incBody(req *http.Request) bool { return req.Body != nil && req.Header.Get("Content-Type") == conf.CONTENT_TYPE_FORM } func incBodyV2(req *http.Request) bool { contentType := req.Header.Get("Content-Type") return req.Body != nil && (contentType == conf.CONTENT_TYPE_FORM || contentType == conf.CONTENT_TYPE_JSON) } // VerifyCallback 验证上传回调请求是否来自七牛 func (ath *Credentials) VerifyCallback(req *http.Request) (bool, error) { auth := req.Header.Get("Authorization") if auth == "" { return false, nil } if strings.HasPrefix(auth, AuthorizationPrefixQiniu) { token, err := ath.SignRequestV2(req) if err != nil { return false, err } return auth == AuthorizationPrefixQiniu+token, nil } else { token, err := ath.SignRequest(req) if err != nil { return false, err } return auth == AuthorizationPrefixQBox+token, nil } }