当将对象序列化为JSON时,如何根据运行时条件省略特定字段?

huangapple go评论86阅读模式
英文:

When serializing to JSON, how to omit certain fields based on a run-time condition?

问题

在使用Go实现的Web服务中,我希望能够根据用户的角色限制返回的JSON响应中的字段。

例如,我可能有一个当前登录的用户,其角色为“guest”,还有一个角色为“admin”的用户。

对于管理员,我希望JSON中包含所有的键,例如:

{
  id: 1,
  name: "John",
  role: "admin"
}

对于访客,我希望不包含角色键,例如:

{
  id: 1,
  name: "John"
}

我目前可以将JSON编组,它会返回所有字段。我需要能够进行限制。

英文:

In a web service implemented in Go, I want to be able to restrict fields returned in a JSON response based on a user's role.

For example I may have a currently logged in user who has a role of guest and another with the role of admin

For an admin I want json to have all the keys eg

{
  id: 1,
  name: "John",
  role: "admin"
}

and for a guest to not have the role key eg

{
  id: 1,
  name: "John"
}

I can currently marshal the json and it returns all fields. I need to be able to restrict it.

答案1

得分: 7

你可以按照@Volker的建议,清除用户没有权限的结构字段。这可能是最容易实现的方法。

第二个选项类似,可以创建一个自定义的JSON编码器。只有在角色结构标签与当前用户的角色匹配时,才对字段进行编码。以下是一些伪代码示例:

type T struct {
	currentRole Role   `json:"-"`
	FieldA      string `json:"field_a,omitempty", role:"guest"`
	FieldB      string `json:"field_b,omitempty", role:"guest"`
	FieldC      int    `json:"field_c,omitempty", role:"admin"`
}

// 让 T 实现 encoding/json.Marshaler 接口。
func (t *T) MarshalJSON() ([]byte, error) {
	var buf bytes.Buffer
	
	// 使用一些反射技巧遍历结构字段。
	for _, field := range getStructFields(t) {
		// 更多的反射技巧提取字段标签数据。
		role := getFieldTag(field, "role")
		
		// 如果字段标签的角色与当前角色匹配,
		// 则进行编码;否则跳过该字段。
		if !matchingRole(role, t.currentRole) {
			continue // 跳过该字段
		}

		data, err := json.Marshal(fieldValue(field))
		...
		_, err = buf.Write(data)
		...
	}
	
	return buf.Bytes(), nil
}

然而,如果需要添加新角色,这种方法可能会很麻烦。因此,我不会轻易考虑使用这种方法。

安全问题

我不确定你所寻找的是否是解决你问题的正确方法。这取决于你在代码中使用的上下文,而你的问题并没有提供清楚的信息。但是,如果这涉及到一个网站,用户在网站上的能力仅由role JSON字段的值定义,那么你将面临一个安全漏洞。他们可以简单地进入浏览器调试器,并更改该JSON对象的值,包括"role: "admin"字段。然后,他们就拥有了管理员权限。根据用户角色来渲染页面的某些部分,应该由服务器在模板处理期间处理。就像对服务器提交的所有数据一样,应该进行反复检查,以确保其来自可信源。

如果这些都与你无关,那么请忽略本段内容。

英文:

You can go by the suggestion @Volker made and clear struct fields for which the user has no permissions. This is probably the easiest to implement.

A second option in a similar vein is to create a custom JSON encoder. One which encodes fields only if a role struct tag matches the current user's role. Here is some pseudo code to illustrate:

type T struct {
	currentRole Role   `json:"-"`
	FieldA      string `json:"field_a,omitempty", role:"guest"`
	FieldB      string `json:"field_b,omitempty", role:"guest"`
	FieldC      int    `json:"field_c,omitempty", role:"admin"`
}

// Have T implement the encoding/json.Marshaler interface.
func (t *T) MarshalJSON() ([]byte, error) {
	var buf bytes.Buffer
	
	// Use some reflection magic to iterate over struct fields.
	for _, field := range getStructFields(t) {
		// More reflection magic to extract field tag data.
		role := getFieldTag(field, "role")
		
		// If the field tag's role matches our current role,
		// we are good to go. otherwise, skip this field.
		if !matchingRole(role, t.currentRole) {
			continue // skip this field 
		}

		data, err := json.Marshal(fieldValue(field))
		...
		_, err = buf.Write(data)
		...
	}
	
	return buf.Bytes(), nil
}

This is going to be a pain to maintain if you need new roles though. So this would not be something I would lightly consider doing.

Security concerns

I am not entirely sure that what you are looking for is the right solution to your problem. This depends on the context in which you use your code, which is not clear from your question. But if this concerns a website where a user's abilities on the website are defined solely by the value of the role JSON field, then you are looking at a security hole. They can simply go into a browser debugger and change the value of this JSON object to include the "role: "admin" field. And presto! Instant administrative powers. Whether or not to render certain parts of a page, based on user role, should really be handled by the server, during template processing. Just like any and all data posted to the server should be checked and checked again to ensure it came from a trusted source.

If none of this is applicable to you, then by all means, disregard this paragraph.

答案2

得分: 3

这个问题似乎很旧,但最近我也想做同样的事情。也许这对将来的某个人有所帮助。这里是另一种方法:你可以定义自己的Marshal接口并使用匿名结构体。

// User holds all variables
// even private ones
type User struct {
    ID   int64
    Name string
    Role string
}

// MarshalJSON gives back json user
// but only the public fields!
func (u *User) MarshalJSON() ([]byte, error) {
    return json.Marshal(struct {
        ID   string `json:"id"`
        Name string `json:"name"`
    }{u.ID, u.Name})
}

在这个代码块中,很容易加入一个 if u.Role == "admin" 语句来决定是否序列化剩余的字段。

英文:

This question seems old, but I recently wanted to do the same thing. Maybe this will help someone in the future. Here is another method: you can define your own Marshal interface and use anonymous structs.

//User holds all variables
//even private ones
type User struct {
	ID      int64
    Name    string
    Role    string
}

//MarshalJSON gives back json user
//but only the public fields!
func (u *User) MarshalJSON() ([]byte, error) {
	return json.Marshal(struct {
		ID string `json:"id"`
		Name string `json:"name"`
	}{u.ID, u.Name})
}

it would be pretty easy to put an if u.Role == "admin" statement in the block to decide whether to marshal the rest.

答案3

得分: 3

另一个选项是使用匿名字段来定义来自appengine数据存储查询的结构列表的输出字段集合。

// 使用匿名字段设置相同结构的不同JSON输出字段(灵感来自http://choly.ca/post/go-json-marshalling/)

// 这种替代方案可用于从appengine数据存储查询加载结果集,并为列表项返回自定义字段组合。

package main

import (
"encoding/json"
"fmt"
)

type User struct {
ID string json:"id"
Name string json:"name"
Role string json:"-"
LaunchCode string json:"-"
}

type AdminOutputUser User

func (user *AdminOutputUser) MarshalJSON() ([]byte, error) {
type Alias AdminOutputUser
return json.Marshal(&struct {
*Alias
Role string json:"role"
}{
(*Alias)(user),
user.Role,
})
}

type SuperadminOutputUser User

func (user *SuperadminOutputUser) MarshalJSON() ([]byte, error) {
type Alias SuperadminOutputUser
return json.Marshal(&struct {
*Alias
Role string json:"role"
LaunchCode string json:"code"
}{
(*Alias)(user),
user.Role,
user.LaunchCode,
})
}

func main() {
user := User{"007", "James Bond", "admin", "12345678"}
adminOutput := AdminOutputUser(user)
superadminOutput := SuperadminOutputUser(user)

b, _ := json.Marshal(&user)
fmt.Printf("%s\n\n", string(b))
// {"id":"007","name":"James Bond"}

b, _ = json.Marshal(&adminOutput)
fmt.Printf("%s\n\n", string(b))
// {"id":"007","name":"James Bond","role":"admin"}

b, _ = json.Marshal(&superadminOutput)
fmt.Printf("%s\n\n", string(b))
// {"id":"007","name":"James Bond","role":"admin","code":"12345678"}

}

// 对于appengine,可以执行类似以下操作
// ...
// var users []AdminOutputUser // 或 User 或 SuperadminOutputUser
// q := datastore.NewQuery("User")
// keys, err := q.GetAll(ctx, &users)
// ...

链接:https://play.golang.org/p/ignIz0hP0z

英文:

Another option that also works to define the set of fields in the output for a list of struct that comes from an appengine datastore query.

    // Setting different JSON output field for the same struct, using anonymous
    // fields (inspired by inspired by http://choly.ca/post/go-json-marshalling/)

    // This alternative could be used to load a resultset from an appengine datastore
    // query and returned a custom field combination for the list items.

    package main

    import (
    	"encoding/json"
    	"fmt"
    )

    type User struct {
    	ID         string `json:"id"`
    	Name       string `json:"name"`
    	Role       string `json:"-"`
    	LaunchCode string `json:"-"`
    }

    type AdminOutputUser User

    func (user *AdminOutputUser) MarshalJSON() ([]byte, error) {
    	type Alias AdminOutputUser
    	return json.Marshal(&struct {
    		*Alias
    		Role string `json:"role"`
    	}{
    		(*Alias)(user),
    		user.Role,
    	})
    }

    type SuperadminOutputUser User

    func (user *SuperadminOutputUser) MarshalJSON() ([]byte, error) {
    	type Alias SuperadminOutputUser
    	return json.Marshal(&struct {
    		*Alias
    		Role       string `json:"role"`
    		LaunchCode string `json:"code"`
    	}{
    		(*Alias)(user),
    		user.Role,
    		user.LaunchCode,
    	})
    }

    func main() {
    	user := User{"007", "James Bond", "admin", "12345678"}
    	adminOutput := AdminOutputUser(user)
    	superadminOutput := SuperadminOutputUser(user)

    	b, _ := json.Marshal(&user)
    	fmt.Printf("%s\n\n", string(b))
    	// {"id":"007","name":"James Bond"}

    	b, _ = json.Marshal(&adminOutput)
    	fmt.Printf("%s\n\n", string(b))
    	// {"id":"007","name":"James Bond","role":"admin"}

    	b, _ = json.Marshal(&superadminOutput)
    	fmt.Printf("%s\n\n", string(b))
    	// {"id":"007","name":"James Bond","role":"admin","code":"12345678"}
    }

    // for appengine could do something like
    // ...
    // var users []AdminOutputUser // or User or SuperadminOutputUser
    // q := datastore.NewQuery("User")
    // keys, err := q.GetAll(ctx, &users)
    // ...

https://play.golang.org/p/ignIz0hP0z

答案4

得分: 0

你可以像这样定义你的struct

type User struct {
    ID   int64  `json:"id"`
    Name string `json:"name"`
    Role string `json:"role,omitempty"`
}

然后可以像这样设置它们:

normalUser := User{ID: "boring", Name: "Rubber"}
adminUser := User{ID: "powers", Name: "Ruler", Role: "admin"}

然后像往常一样使用json.Marshal()json.NewEncoder().Encode()进行编码。

How To Use Struct Tags in Go中找到这个信息。

注意:我知道在评论中提到了omitempty,并且在@jimt的代码示例中也提到了它,并且作为第一个选项,尽管没有提供一个简单的示例。对于我这个对Go还比较新的人来说,不清楚它是否会按预期工作。所以我觉得这可能也会对其他人有帮助。

英文:

You might just define your struct like this

type User struct {
	ID   int64 `json:"id"`
	Name string `json:"name"`
	Role string `json:"role,omitempty"`
}

And then set them like this

normalUser := User{ID: "boring", Name: "Rubber"}
adminUser := User{ID: "powers", Name: "Ruler", Role: "admin"}

Then json.Marshal() or json.NewEncoder().Encode() as usual

Found in How To Use Struct Tags in Go

> Note: I know that omitempty was mentioned in a comment and is even
> part of @jimt's code example and mentioned as first option, albeit without a simple example. To me being still pretty new to
> Go wasn't clear that that would just work as expected. So I figured it might help others as well 🤓

huangapple
  • 本文由 发表于 2014年4月3日 19:13:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/22835830.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定