mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 20:57:32 +08:00
feat: 【frontend-gray】添加 skipedRoutes以及skipedByHeaders 配置 (#1409)
Co-authored-by: Kent Dong <ch3cho@qq.com>
This commit is contained in:
@@ -20,6 +20,9 @@ description: 前端灰度插件配置参考
|
|||||||
| `localStorageGrayKey` | string | 非必填 | - | 使用JWT鉴权方式,用户ID的唯一标识来自`localStorage`中,如果配置了当前参数,则`grayKey`失效 |
|
| `localStorageGrayKey` | string | 非必填 | - | 使用JWT鉴权方式,用户ID的唯一标识来自`localStorage`中,如果配置了当前参数,则`grayKey`失效 |
|
||||||
| `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` |
|
| `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出,比如:`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` |
|
||||||
| `userStickyMaxAge` | int | 非必填 | 172800 | 用户粘滞的时长:单位为秒,默认为`172800`,2天时间 |
|
| `userStickyMaxAge` | int | 非必填 | 172800 | 用户粘滞的时长:单位为秒,默认为`172800`,2天时间 |
|
||||||
|
| `skippedPathPrefixes` | array of strings | 非必填 | - | 用于排除特定路径,避免当前插件处理这些请求。例如,在 rewrite 场景下,XHR 接口请求 `/api/xxx` 如果经过插件转发逻辑,可能会导致非预期的结果。 |
|
||||||
|
| `skippedByHeaders` | map of string to string | 非必填 | - | 用于通过请求头过滤,指定哪些请求不被当前插件
|
||||||
|
处理。`skippedPathPrefixes` 的优先级高于当前配置,且页面HTML请求不受本配置的影响。若本配置为空,默认会判断`sec-fetch-mode=cors`以及`upgrade=websocket`两个header头,进行过滤 |
|
||||||
| `rules` | array of object | 必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 |
|
| `rules` | array of object | 必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 |
|
||||||
| `rewrite` | object | 必填 | - | 重写配置,一般用于OSS/CDN前端部署的重写配置 |
|
| `rewrite` | object | 必填 | - | 重写配置,一般用于OSS/CDN前端部署的重写配置 |
|
||||||
| `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 |
|
| `baseDeployment` | object | 非必填 | - | 配置Base基线规则的配置 |
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const (
|
|||||||
XPreHigressTag = "x-pre-higress-tag"
|
XPreHigressTag = "x-pre-higress-tag"
|
||||||
IsPageRequest = "is-page-request"
|
IsPageRequest = "is-page-request"
|
||||||
IsNotFound = "is-not-found"
|
IsNotFound = "is-not-found"
|
||||||
|
EnabledGray = "enabled-gray"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LogInfo func(format string, args ...interface{})
|
type LogInfo func(format string, args ...interface{})
|
||||||
@@ -61,6 +62,8 @@ type GrayConfig struct {
|
|||||||
GrayDeployments []*Deployment
|
GrayDeployments []*Deployment
|
||||||
BackendGrayTag string
|
BackendGrayTag string
|
||||||
Injection *Injection
|
Injection *Injection
|
||||||
|
SkippedPathPrefixes []string
|
||||||
|
SkippedByHeaders map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertToStringList(results []gjson.Result) []string {
|
func convertToStringList(results []gjson.Result) []string {
|
||||||
@@ -91,6 +94,8 @@ func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
|
|||||||
grayConfig.BackendGrayTag = json.Get("backendGrayTag").String()
|
grayConfig.BackendGrayTag = json.Get("backendGrayTag").String()
|
||||||
grayConfig.UserStickyMaxAge = json.Get("userStickyMaxAge").String()
|
grayConfig.UserStickyMaxAge = json.Get("userStickyMaxAge").String()
|
||||||
grayConfig.Html = json.Get("html").String()
|
grayConfig.Html = json.Get("html").String()
|
||||||
|
grayConfig.SkippedPathPrefixes = convertToStringList(json.Get("skippedPathPrefixes").Array())
|
||||||
|
grayConfig.SkippedByHeaders = convertToStringMap(json.Get("skippedByHeaders"))
|
||||||
|
|
||||||
if grayConfig.UserStickyMaxAge == "" {
|
if grayConfig.UserStickyMaxAge == "" {
|
||||||
// 默认值2天
|
// 默认值2天
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ static_resources:
|
|||||||
"/app1": "/mfe/app1/{version}"
|
"/app1": "/mfe/app1/{version}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"skippedPathPrefixes": [
|
||||||
|
"/api/"
|
||||||
|
],
|
||||||
"baseDeployment": {
|
"baseDeployment": {
|
||||||
"version": "dev"
|
"version": "dev"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
|
"github.com/alibaba/higress/plugins/wasm-go/extensions/frontend-gray/config"
|
||||||
@@ -32,15 +33,18 @@ func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.L
|
|||||||
}
|
}
|
||||||
|
|
||||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
|
func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
|
||||||
if !util.IsGrayEnabled(grayConfig) {
|
requestPath, _ := proxywasm.GetHttpRequestHeader(":path")
|
||||||
|
requestPath = path.Clean(requestPath)
|
||||||
|
enabledGray := util.IsGrayEnabled(grayConfig, requestPath)
|
||||||
|
ctx.SetContext(config.EnabledGray, enabledGray)
|
||||||
|
|
||||||
|
if !enabledGray {
|
||||||
|
ctx.DontReadRequestBody()
|
||||||
return types.ActionContinue
|
return types.ActionContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
cookies, _ := proxywasm.GetHttpRequestHeader("cookie")
|
cookies, _ := proxywasm.GetHttpRequestHeader("cookie")
|
||||||
path, _ := proxywasm.GetHttpRequestHeader(":path")
|
isPageRequest := util.IsPageRequest(requestPath)
|
||||||
fetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode")
|
|
||||||
|
|
||||||
isPageRequest := util.IsPageRequest(fetchMode, path)
|
|
||||||
hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0
|
hasRewrite := len(grayConfig.Rewrite.File) > 0 || len(grayConfig.Rewrite.Index) > 0
|
||||||
grayKeyValueByCookie := util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey)
|
grayKeyValueByCookie := util.ExtractCookieValueByKey(cookies, grayConfig.GrayKey)
|
||||||
grayKeyValueByHeader, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey)
|
grayKeyValueByHeader, _ := proxywasm.GetHttpRequestHeader(grayConfig.GrayKey)
|
||||||
@@ -73,7 +77,7 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
|
|||||||
} else {
|
} else {
|
||||||
deployment = util.FilterGrayRule(&grayConfig, grayKeyValue)
|
deployment = util.FilterGrayRule(&grayConfig, grayKeyValue)
|
||||||
}
|
}
|
||||||
log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %s,%s", deployment, path, deployment.BackendVersion, preVersion, preUniqueClientId)
|
log.Infof("index deployment: %v, path: %v, backend: %v, xPreHigressVersion: %s,%s", deployment, requestPath, deployment.BackendVersion, preVersion, preUniqueClientId)
|
||||||
} else {
|
} else {
|
||||||
grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue)
|
grayDeployment := util.FilterGrayRule(&grayConfig, grayKeyValue)
|
||||||
deployment = util.GetVersion(grayConfig, grayDeployment, preVersion, isPageRequest)
|
deployment = util.GetVersion(grayConfig, grayDeployment, preVersion, isPageRequest)
|
||||||
@@ -91,14 +95,14 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasRewrite {
|
if hasRewrite {
|
||||||
rewritePath := path
|
rewritePath := requestPath
|
||||||
if isPageRequest {
|
if isPageRequest {
|
||||||
rewritePath = util.IndexRewrite(path, deployment.Version, grayConfig.Rewrite.Index)
|
rewritePath = util.IndexRewrite(requestPath, deployment.Version, grayConfig.Rewrite.Index)
|
||||||
} else {
|
} else {
|
||||||
rewritePath = util.PrefixFileRewrite(path, deployment.Version, grayConfig.Rewrite.File)
|
rewritePath = util.PrefixFileRewrite(requestPath, deployment.Version, grayConfig.Rewrite.File)
|
||||||
}
|
}
|
||||||
if path != rewritePath {
|
if requestPath != rewritePath {
|
||||||
log.Infof("rewrite path:%s, rewritePath:%s, Version:%v", path, rewritePath, deployment.Version)
|
log.Infof("rewrite path:%s, rewritePath:%s, Version:%v", requestPath, rewritePath, deployment.Version)
|
||||||
proxywasm.ReplaceHttpRequestHeader(":path", rewritePath)
|
proxywasm.ReplaceHttpRequestHeader(":path", rewritePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +110,8 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
|
func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
|
||||||
if !util.IsGrayEnabled(grayConfig) {
|
enabledGray, _ := ctx.GetContext(config.EnabledGray).(bool)
|
||||||
|
if !enabledGray {
|
||||||
ctx.DontReadResponseBody()
|
ctx.DontReadResponseBody()
|
||||||
return types.ActionContinue
|
return types.ActionContinue
|
||||||
}
|
}
|
||||||
@@ -179,7 +184,8 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte, log wrapper.Log) types.Action {
|
func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, body []byte, log wrapper.Log) types.Action {
|
||||||
if !util.IsGrayEnabled(grayConfig) {
|
enabledGray, _ := ctx.GetContext(config.EnabledGray).(bool)
|
||||||
|
if !enabledGray {
|
||||||
return types.ActionContinue
|
return types.ActionContinue
|
||||||
}
|
}
|
||||||
isPageRequest, ok := ctx.GetContext(config.IsPageRequest).(bool)
|
isPageRequest, ok := ctx.GetContext(config.IsPageRequest).(bool)
|
||||||
|
|||||||
@@ -47,7 +47,39 @@ func GetRealIpFromXff(xff string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsGrayEnabled(grayConfig config.GrayConfig) bool {
|
func IsRequestSkippedByHeaders(grayConfig config.GrayConfig) bool {
|
||||||
|
secFetchMode, _ := proxywasm.GetHttpRequestHeader("sec-fetch-mode")
|
||||||
|
upgrade, _ := proxywasm.GetHttpRequestHeader("upgrade")
|
||||||
|
if len(grayConfig.SkippedByHeaders) == 0 {
|
||||||
|
// 默认不走插件逻辑的header
|
||||||
|
return secFetchMode == "cors" || upgrade == "websocket"
|
||||||
|
}
|
||||||
|
for headerKey, headerValue := range grayConfig.SkippedByHeaders {
|
||||||
|
requestHeader, _ := proxywasm.GetHttpRequestHeader(headerKey)
|
||||||
|
if requestHeader == headerValue {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsGrayEnabled(grayConfig config.GrayConfig, requestPath string) bool {
|
||||||
|
// 当前路径中前缀为 SkipedRoute,则不走插件逻辑
|
||||||
|
for _, prefix := range grayConfig.SkippedPathPrefixes {
|
||||||
|
if strings.HasPrefix(requestPath, prefix) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是首页,进入插件逻辑
|
||||||
|
if IsPageRequest(requestPath) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// 检查header标识,判断是否需要跳过
|
||||||
|
if IsRequestSkippedByHeaders(grayConfig) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// 检查是否存在重写主机
|
// 检查是否存在重写主机
|
||||||
if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" {
|
if grayConfig.Rewrite != nil && grayConfig.Rewrite.Host != "" {
|
||||||
return true
|
return true
|
||||||
@@ -132,29 +164,35 @@ var indexSuffixes = []string{
|
|||||||
".html", ".htm", ".jsp", ".php", ".asp", ".aspx", ".erb", ".ejs", ".twig",
|
".html", ".htm", ".jsp", ".php", ".asp", ".aspx", ".erb", ".ejs", ".twig",
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsPageRequest(fetchMode string, myPath string) bool {
|
func IsPageRequest(requestPath string) bool {
|
||||||
if fetchMode == "cors" {
|
if requestPath == "/" || requestPath == "" {
|
||||||
return false
|
return true
|
||||||
}
|
}
|
||||||
ext := path.Ext(myPath)
|
ext := path.Ext(requestPath)
|
||||||
return ext == "" || ContainsValue(indexSuffixes, ext)
|
return ext == "" || ContainsValue(indexSuffixes, ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首页Rewrite
|
// SortKeysByLengthAndLexicographically 按长度降序和字典序排序键
|
||||||
func IndexRewrite(path, version string, matchRules map[string]string) string {
|
func SortKeysByLengthAndLexicographically(matchRules map[string]string) []string {
|
||||||
// Create a slice of keys in matchRules and sort them by length in descending order
|
|
||||||
keys := make([]string, 0, len(matchRules))
|
keys := make([]string, 0, len(matchRules))
|
||||||
for prefix := range matchRules {
|
for prefix := range matchRules {
|
||||||
keys = append(keys, prefix)
|
keys = append(keys, prefix)
|
||||||
}
|
}
|
||||||
sort.Slice(keys, func(i, j int) bool {
|
sort.Slice(keys, func(i, j int) bool {
|
||||||
if len(keys[i]) != len(keys[j]) {
|
if len(keys[i]) != len(keys[j]) {
|
||||||
return len(keys[i]) > len(keys[j]) // Sort by length
|
return len(keys[i]) > len(keys[j]) // 按长度排序
|
||||||
}
|
}
|
||||||
return keys[i] < keys[j] // Sort lexicographically
|
return keys[i] < keys[j] // 按字典序排序
|
||||||
})
|
})
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
// Iterate over sorted keys to find the longest match
|
// 首页Rewrite
|
||||||
|
func IndexRewrite(path, version string, matchRules map[string]string) string {
|
||||||
|
// 使用新的排序函数
|
||||||
|
keys := SortKeysByLengthAndLexicographically(matchRules)
|
||||||
|
|
||||||
|
// 遍历排序后的键以找到最长匹配
|
||||||
for _, prefix := range keys {
|
for _, prefix := range keys {
|
||||||
if strings.HasPrefix(path, prefix) {
|
if strings.HasPrefix(path, prefix) {
|
||||||
rewrite := matchRules[prefix]
|
rewrite := matchRules[prefix]
|
||||||
@@ -166,18 +204,21 @@ func IndexRewrite(path, version string, matchRules map[string]string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func PrefixFileRewrite(path, version string, matchRules map[string]string) string {
|
func PrefixFileRewrite(path, version string, matchRules map[string]string) string {
|
||||||
var matchedPrefix, replacement string
|
// 对规则的键进行排序
|
||||||
for prefix, template := range matchRules {
|
sortedKeys := SortKeysByLengthAndLexicographically(matchRules)
|
||||||
|
|
||||||
|
// 遍历排序后的键
|
||||||
|
for _, prefix := range sortedKeys {
|
||||||
if strings.HasPrefix(path, prefix) {
|
if strings.HasPrefix(path, prefix) {
|
||||||
if len(prefix) > len(matchedPrefix) { // 找到更长的前缀
|
// 找到第一个匹配的前缀就停止,因为它是最长的匹配
|
||||||
matchedPrefix = prefix
|
replacement := strings.Replace(matchRules[prefix], "{version}", version, 1)
|
||||||
replacement = strings.Replace(template, "{version}", version, 1)
|
newPath := strings.Replace(path, prefix, replacement+"/", 1)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 将path 中的前缀部分用 replacement 替换掉
|
|
||||||
newPath := strings.Replace(path, matchedPrefix, replacement+"/", 1)
|
|
||||||
return filepath.Clean(newPath)
|
return filepath.Clean(newPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有匹配,返回原始路径
|
||||||
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetVersion(grayConfig config.GrayConfig, deployment *config.Deployment, xPreHigressVersion string, isPageRequest bool) *config.Deployment {
|
func GetVersion(grayConfig config.GrayConfig, deployment *config.Deployment, xPreHigressVersion string, isPageRequest bool) *config.Deployment {
|
||||||
|
|||||||
@@ -108,22 +108,20 @@ func TestPrefixFileRewrite(t *testing.T) {
|
|||||||
|
|
||||||
func TestIsPageRequest(t *testing.T) {
|
func TestIsPageRequest(t *testing.T) {
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
fetchMode string
|
|
||||||
p string
|
p string
|
||||||
output bool
|
output bool
|
||||||
}{
|
}{
|
||||||
{"cors", "/js/a.js", false},
|
{"/js/a.js", false},
|
||||||
{"no-cors", "/js/a.js", false},
|
{"/js/a.js", false},
|
||||||
{"no-cors", "/images/a.png", false},
|
{"/images/a.png", false},
|
||||||
{"no-cors", "/index", true},
|
{"/index", true},
|
||||||
{"cors", "/inde", false},
|
{"/index.html", true},
|
||||||
{"no-cors", "/index.html", true},
|
{"/demo.php", true},
|
||||||
{"no-cors", "/demo.php", true},
|
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
testPath := test.p
|
testPath := test.p
|
||||||
t.Run(testPath, func(t *testing.T) {
|
t.Run(testPath, func(t *testing.T) {
|
||||||
output := IsPageRequest(test.fetchMode, testPath)
|
output := IsPageRequest(testPath)
|
||||||
assert.Equal(t, test.output, output)
|
assert.Equal(t, test.output, output)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user