zoukankan      html  css  js  c++  java
  • 【Gin-API系列】Gin中间件之鉴权访问(五)

    在完成中间件的介绍和日志中间件的代码后,我们的程序已经基本能正常跑通了,但如果要上生产,还少了一些必要的功能,例如鉴权、异常捕捉等。本章我们介绍如何编写鉴权中间件。

    鉴权访问,说白了就是给用户的请求增加一些限制条件,过滤掉不符合要求的请求。完善的鉴权模块可以让我们的服务跑得更加安全,特别是面向公共的服务。

    常用的无状态鉴权方式

    • 网络鉴权

    通常有IP白名单方式,通过获取客户端的真实IP来对请求进行过滤

    • 用户鉴权

    通过账号密码或者分配的密钥、Token等方式进行认证,常用的cookies、oauth2.0都是这种方式

    • 加密算法鉴权

    客户端使用加密算法对用户的参数进行计算加密得到Token,并将参数和Token一起发送;服务端使用同样的加密算法对请求参数进行加密后比较Token的值是否一致。

    Gin-IPs 鉴权访问

    为了让我们的服务更加安全,我们通常是混合多种鉴权方式使用。本文采取“用户鉴权”和“加密算法鉴权”混合方式。

    • 鉴权算法介绍

    通过某种方式生成或者手动指定分配公钥和私钥对给用户,用户使用公钥和请求参数组成消息内容,使用私钥对消息内容进行哈希计算,得到固定长度的字符串。
    服务器使用同样的方式对用户请求的参数进行哈希计算后比较。由于这里面的算法和密钥对都是私有的,所以安全性较高,适用于大多数场景。

    • 加密代码
    // 签名算法如下
    /*
    Signature = HMAC-SHA1('SecretKey', UTF-8-Encoding-Of( StringToSign ) ) );
    StringToSign = method + "
    " +
                   URL + "
    " +
                   Sort-UrlParams + "
    " +
                   Content-MD5 + "
    " +  // md5(params)
                   Expires + "
    " +
                   AccessKey;
    */
    func genSignature(accessKey, secretKey, uri, method, urlParams, params, nowTS string) (string, error) {
    	if params != "" {
    		md5Ctx := md5.New()
    		_, _ = io.WriteString(md5Ctx, params)
    		params = fmt.Sprintf("%x", md5Ctx.Sum(nil))
    	}
    
    	strSign := method + "
    " + uri + "
    " + urlParams + "
    "  + params + "
    " + nowTS + "
    " + accessKey
    	sign := hmacSHA1Encrypt(strSign, secretKey)
    	return sign, nil
    }
    
    // hmacSHA1Encrypt encrypt the encryptText use encryptKey
    func hmacSHA1Encrypt(encryptText, encryptKey string) string {
    	key := []byte(encryptKey)
    	mac := hmac.New(sha1.New, key)
    	mac.Write([]byte(encryptText))
    	var str = hex.EncodeToString(mac.Sum(nil))
    	return str
    }
    
    • Gin鉴权中间件使用
    func Validate() gin.HandlerFunc {
    	return func(c *gin.Context) {
    		response := route_response.Response{}
    		response.Data.List = []interface{}{} // 初始化为空切片,而不是空引用
    		traceId := SnowWorker.GetId()
    		c.Writer.Header().Set("X-Request-Trace-Id", traceId)
    
    		uri := c.Request.URL.Path
    		// remoteAddr := c.ClientIP()  // 也可以对客户端IP进行限制
    		contentType := c.Request.Header.Get("Content-Type")
    		if contentType != "application/json" {
    			c.Abort()
    			response.Code, response.Message = configure.RequestParameterTypeError, "Content-Type 类型只支持 application/json"
    			c.JSON(http.StatusUnauthorized, response)
    			return
    		}
    		accessKey := c.DefaultQuery("accesskey", "")
    		expires := c.DefaultQuery("expires", "")
    		signature := c.DefaultQuery("signature", "")
    		if accessKey == "" {
    			c.Abort()
    			response.Code, response.Message = configure.RequestParameterMiss, "Token缺失"
    			c.JSON(http.StatusUnauthorized, response)
    			return
    		}
    		secret, err := dao.FetchSecret(accessKey)
    		if err != nil || "valid" != secret.State {
    			c.Abort()
    			response.Code, response.Message = configure.RequestKeyNotFound, "无效的Token"
    			c.JSON(http.StatusUnauthorized, response)
    			return
    		}
    		c.Writer.Header().Set("X-Request-User", secret.User)
    
    		if expires == "" {
    			c.Abort()
    			response.Code, response.Message = configure.RequestParameterMiss, "有效期参数缺失"
    			c.JSON(http.StatusUnauthorized, response)
    			return
    		}
    		if signature == "" {
    			c.Abort()
    			response.Code, response.Message = configure.RequestParameterMiss, "签名缺失"
    			c.JSON(http.StatusUnauthorized, response)
    			return
    		}
    
    		secretKey := secret.SecretKey
    		if nowTs, err := strconv.ParseInt(expires, 10, 64); err != nil {
    			c.Abort()
    			response.Code, response.Message = configure.RequestParameterTypeError, "有效期参数类型错误"
    			c.JSON(http.StatusUnauthorized, response)
    			return
    		} else {
    			passTime := time.Now().Unix() - nowTs
    			if passTime < 0 || passTime >= configure.GinConfigValue.Expires {
    				// 容错时间越大越容易被攻击,服务器时间必须准
    				c.Abort()
    				response.Code, response.Message = configure.RequestExpired, "请求已过期"
    				c.JSON(http.StatusUnauthorized, response)
    				return
    			}
    		}
    		method := strings.ToUpper(c.Request.Method)
    	
    		var urlParams, params string
    		if "POST" == method || "PUT" == method {
    			body, _ := ioutil.ReadAll(c.Request.Body)
    			c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // 重设body
    			params = string(body)
    		} else if "GET" == method || "DELETE" == method {
    			queryParams := c.Request.URL.Query()
    			allParams := make(map[string]string)
    			for k, v := range queryParams {
    				if k != "accesskey" && k != "expires" && k != "signature" {
    					allParams[k] = v[0] // 如果某个key传入了2个,只用第一个的值
    				}
    			}
    			keys := getMapKeysSorted(allParams)
    			for _, k := range keys {
    				urlParams += k + allParams[k]
    			}
    		}
    		if signatureString, err := genSignature(accessKey, secretKey, uri, method, urlParams, params, expires); err != nil {
    			c.Abort()
    			response.Code, response.Message = configure.ApiGenSignatureError, "API内部错误"
    			c.JSON(http.StatusUnauthorized, response)
    			return
    		} else {
    			if signature != signatureString {
    				c.Abort()
    				response.Code, response.Message = configure.RequestAuthorizedFailed, "API认证失败"
    				c.JSON(http.StatusUnauthorized, response)
    				return
    			}
    		}
    
    		c.Next() 
    	}
    }
    
    

    本文关于鉴权访问的介绍和使用到此为止,下一章我们将使用异常捕捉中间件来完善我们的程序。

    Github 代码

    请访问 Gin-IPs 或者搜索 Gin-IPs

  • 相关阅读:
    typeof 和 Object.prototype.toString 的区别
    获取地理信息的JavaScript 库 -- YQL Geo
    关于html5手机
    我看过的书的示例网站
    解决跨浏览器问题网站收集
    【docker】docker初试与填坑
    sunJCE or ibmJce,was服务器下使用des的注意点
    cxf-webservice-在was6服务器上运行
    微星b85(b85i b85-gaming) 系列dsdt
    IE10的bug?disabled button如何触发事件
  • 原文地址:https://www.cnblogs.com/lxmhhy/p/13603330.html
Copyright © 2011-2022 走看看