mirror of
https://github.com/alibaba/higress.git
synced 2026-06-07 11:47:30 +08:00
[frontend-gray] support grayKey from localStorage (#1395)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
// 前缀匹配
|
||||
|
||||
Reference in New Issue
Block a user