[frontend-gray] support grayKey from localStorage (#1395)

This commit is contained in:
mamba
2024-10-18 13:58:52 +08:00
committed by GitHub
parent c67f494b49
commit 11ff2d1d31
6 changed files with 115 additions and 43 deletions

View File

@@ -17,6 +17,7 @@ description: 前端灰度插件配置参考
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|----------------|--------------|----|-----|----------------------------------------------------------------------------------------------------|
| `grayKey` | string | 非必填 | - | 用户ID的唯一标识可以来自Cookie或者Header中比如 userid如果没有填写则使用`rules[].grayTagKey``rules[].grayTagValue`过滤灰度规则 |
| `localStorageGrayKey` | string | 非必填 | - | 使用JWT鉴权方式用户ID的唯一标识来自`localStorage`中,如果配置了当前参数,则`grayKey`失效 |
| `graySubKey` | string | 非必填 | - | 用户身份信息可能以JSON形式透出比如`userInfo:{ userCode:"001" }`,当前例子`graySubKey`取值为`userCode` |
| `userStickyMaxAge` | int | 非必填 | 172800 | 用户粘滞的时长:单位为秒,默认为`172800`2天时间 |
| `rules` | array of object | 必填 | - | 用户定义不同的灰度规则,适配不同的灰度场景 |
@@ -168,6 +169,30 @@ cookie存在`appInfo`的JSON数据其中包含`userId`字段为当前的唯
否则使用`version: base`版本
### 用户信息存储在LocalStorage
由于网关插件需要识别用户为唯一身份信息HTTP协议进行信息传输只能在Header中传递。如果用户信息存储在LocalStorage在首页注入一段脚本将LocalStorage中的用户信息设置到cookie中。
```
(function() {
var grayKey = '@@X_GRAY_KEY';
var cookies = document.cookie.split('; ').filter(function(row) {
return row.indexOf(grayKey + '=') === 0;
});
try {
if (typeof localStorage !== 'undefined' && localStorage !== null) {
var storageValue = localStorage.getItem(grayKey);
var cookieValue = cookies.length > 0 ? decodeURIComponent(cookies[0].split('=')[1]) : null;
if (storageValue && storageValue.indexOf('=') < 0 && cookieValue && cookieValue !== storageValue) {
document.cookie = grayKey + '=' + encodeURIComponent(storageValue) + '; path=/;';
window.location.reload();
}
}
} catch (error) {
// xx
}
})();
```
### rewrite重写配置
> 一般用于CDN部署场景
```yml

View File

@@ -49,17 +49,18 @@ type BodyInjection struct {
}
type GrayConfig struct {
UserStickyMaxAge string
TotalGrayWeight int
GrayKey string
GraySubKey string
Rules []*GrayRule
Rewrite *Rewrite
Html string
BaseDeployment *Deployment
GrayDeployments []*Deployment
BackendGrayTag string
Injection *Injection
UserStickyMaxAge string
TotalGrayWeight int
GrayKey string
LocalStorageGrayKey string
GraySubKey string
Rules []*GrayRule
Rewrite *Rewrite
Html string
BaseDeployment *Deployment
GrayDeployments []*Deployment
BackendGrayTag string
Injection *Injection
}
func convertToStringList(results []gjson.Result) []string {
@@ -81,7 +82,11 @@ func convertToStringMap(result gjson.Result) map[string]string {
func JsonToGrayConfig(json gjson.Result, grayConfig *GrayConfig) {
// 解析 GrayKey
grayConfig.LocalStorageGrayKey = json.Get("localStorageGrayKey").String()
grayConfig.GrayKey = json.Get("grayKey").String()
if grayConfig.LocalStorageGrayKey != "" {
grayConfig.GrayKey = grayConfig.LocalStorageGrayKey
}
grayConfig.GraySubKey = json.Get("graySubKey").String()
grayConfig.BackendGrayTag = json.Get("backendGrayTag").String()
grayConfig.UserStickyMaxAge = json.Get("userStickyMaxAge").String()

View File

@@ -73,23 +73,22 @@ static_resources:
],
"rewrite": {
"host": "frontend-gray-cn-shanghai.oss-cn-shanghai-internal.aliyuncs.com",
"notFoundUri": "/cygtapi/{version}/333.html",
"indexRouting": {
"/app1": "/cygtapi/{version}/index.html",
"/": "/cygtapi/{version}/index.html"
"/app1": "/mfe/app1/{version}/index.html",
"/": "/mfe/app1/{version}/index.html"
},
"fileRouting": {
"/": "/cygtapi/{version}",
"/app1": "/cygtapi/{version}"
"/": "/mfe/app1/{version}",
"/app1": "/mfe/app1/{version}"
}
},
"baseDeployment": {
"version": "base"
"version": "dev"
},
"grayDeployments": [
{
"name": "beta-user",
"version": "gray",
"version": "0.0.1",
"enabled": true
}
],
@@ -107,8 +106,7 @@ static_resources:
"<script>console.log('hello world after2')</script>"
]
}
},
"html": "<!DOCTYPE html>\n <html lang=\"zh-CN\">\n<head>\n<title>app1</title>\n<meta charset=\"utf-8\" />\n</head>\n<body>\n\t测试替换html版本\n\t<br />\n\t版本: {version}\n\t<br />\n\t<script src=\"./{version}/a.js\"></script>\n</body>\n</html>"
}
}
- name: envoy.filters.http.router
typed_config:

View File

@@ -21,14 +21,13 @@ func main() {
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
wrapper.ProcessResponseHeadersBy(onHttpResponseHeader),
wrapper.ProcessResponseBodyBy(onHttpResponseBody),
wrapper.ProcessStreamingResponseBodyBy(onStreamingResponseBody),
)
}
func parseConfig(json gjson.Result, grayConfig *config.GrayConfig, log wrapper.Log) error {
// 解析json 为GrayConfig
config.JsonToGrayConfig(json, grayConfig)
log.Infof("Rewrite: %v, GrayDeployments: %v", json.Get("rewrite"), json.Get("grayDeployments"))
log.Debugf("Rewrite: %v, GrayDeployments: %v", json.Get("rewrite"), json.Get("grayDeployments"))
return nil
}
@@ -98,15 +97,17 @@ func onHttpRequestHeaders(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
} else {
rewritePath = util.PrefixFileRewrite(path, deployment.Version, grayConfig.Rewrite.File)
}
log.Infof("rewrite path: %s %s %v", path, deployment.Version, rewritePath)
proxywasm.ReplaceHttpRequestHeader(":path", rewritePath)
if path != rewritePath {
log.Infof("rewrite path:%s, rewritePath:%s, Version:%v", path, rewritePath, deployment.Version)
proxywasm.ReplaceHttpRequestHeader(":path", rewritePath)
}
}
return types.ActionContinue
}
func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig, log wrapper.Log) types.Action {
if !util.IsGrayEnabled(grayConfig) {
ctx.DontReadResponseBody()
return types.ActionContinue
}
isPageRequest, ok := ctx.GetContext(config.IsPageRequest).(bool)
@@ -117,6 +118,9 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
if !isPageRequest {
ctx.DontReadResponseBody()
return types.ActionContinue
} else {
// 不会进去Streaming 的Body处理
ctx.BufferResponseBody()
}
status, err := proxywasm.GetHttpResponseHeader(":status")
@@ -159,8 +163,6 @@ func onHttpResponseHeader(ctx wrapper.HttpContext, grayConfig config.GrayConfig,
return types.ActionContinue
}
// 不会进去Streaming 的Body处理
ctx.BufferResponseBody()
proxywasm.ReplaceHttpResponseHeader("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate")
frontendVersion := ctx.GetContext(config.XPreHigressTag).(string)
@@ -184,6 +186,11 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b
if !ok {
isPageRequest = false // 默认值
}
// 只处理首页相关请求
if !isPageRequest {
return types.ActionContinue
}
frontendVersion := ctx.GetContext(config.XPreHigressTag).(string)
isNotFound, ok := ctx.GetContext(config.IsNotFound).(bool)
if !ok {
@@ -212,7 +219,8 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b
return types.ActionContinue
}
if isPageRequest && isNotFound && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" {
// 针对404页面处理
if isNotFound && grayConfig.Rewrite.Host != "" && grayConfig.Rewrite.NotFound != "" {
client := wrapper.NewClusterClient(wrapper.RouteCluster{Host: grayConfig.Rewrite.Host})
client.Get(strings.Replace(grayConfig.Rewrite.NotFound, "{version}", frontendVersion, -1), nil, func(statusCode int, responseHeaders http.Header, responseBody []byte) {
@@ -222,20 +230,18 @@ func onHttpResponseBody(ctx wrapper.HttpContext, grayConfig config.GrayConfig, b
return types.ActionPause
}
if isPageRequest {
// 将原始字节转换为字符串
newBody := string(body)
newBody = util.InjectContent(newBody, grayConfig.Injection)
if err := proxywasm.ReplaceHttpResponseBody([]byte(newBody)); err != nil {
return types.ActionContinue
}
// 处理响应体HTML
newBody := string(body)
newBody = util.InjectContent(newBody, grayConfig.Injection)
if grayConfig.LocalStorageGrayKey != "" {
localStr := strings.ReplaceAll(`<script>
!function(){var o,e,n="@@X_GRAY_KEY",t=document.cookie.split("; ").filter(function(o){return 0===o.indexOf(n+"=")});try{"undefined"!=typeof localStorage&&null!==localStorage&&(o=localStorage.getItem(n),e=0<t.length?decodeURIComponent(t[0].split("=")[1]):null,o)&&o.indexOf("=")<0&&e&&e!==o&&(document.cookie=n+"="+encodeURIComponent(o)+"; path=/;",window.location.reload())}catch(o){}}();
</script>
`, "@@X_GRAY_KEY", grayConfig.LocalStorageGrayKey)
newBody = strings.ReplaceAll(newBody, "<body>", "<body>\n"+localStr)
}
if err := proxywasm.ReplaceHttpResponseBody([]byte(newBody)); err != nil {
return types.ActionContinue
}
return types.ActionContinue
}
func onStreamingResponseBody(ctx wrapper.HttpContext, pluginConfig config.GrayConfig, chunk []byte, isLastChunk bool, log wrapper.Log) []byte {
return chunk
}

View File

@@ -142,8 +142,22 @@ func IsPageRequest(fetchMode string, myPath string) bool {
// 首页Rewrite
func IndexRewrite(path, version string, matchRules map[string]string) string {
for prefix, rewrite := range matchRules {
// Create a slice of keys in matchRules and sort them by length in descending order
keys := make([]string, 0, len(matchRules))
for prefix := range matchRules {
keys = append(keys, prefix)
}
sort.Slice(keys, func(i, j int) bool {
if len(keys[i]) != len(keys[j]) {
return len(keys[i]) > len(keys[j]) // Sort by length
}
return keys[i] < keys[j] // Sort lexicographically
})
// Iterate over sorted keys to find the longest match
for _, prefix := range keys {
if strings.HasPrefix(path, prefix) {
rewrite := matchRules[prefix]
newPath := strings.Replace(rewrite, "{version}", version, -1)
return newPath
}

View File

@@ -53,6 +53,30 @@ func TestIndexRewrite(t *testing.T) {
}
}
func TestIndexRewrite2(t *testing.T) {
matchRules := map[string]string{
"/": "/{version}/index.html",
"/sta": "/sta/{version}/index.html",
"/static": "/static/{version}/index.html",
}
var tests = []struct {
path, output string
}{
{"/static123", "/static/v1.0.0/index.html"},
{"/static", "/static/v1.0.0/index.html"},
{"/sta", "/sta/v1.0.0/index.html"},
{"/", "/v1.0.0/index.html"},
}
for _, test := range tests {
testName := test.path
t.Run(testName, func(t *testing.T) {
output := IndexRewrite(testName, "v1.0.0", matchRules)
assert.Equal(t, test.output, output)
})
}
}
func TestPrefixFileRewrite(t *testing.T) {
matchRules := map[string]string{
// 前缀匹配