惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog RSS Feed
P
Proofpoint News Feed
The GitHub Blog
The GitHub Blog
The Register - Security
The Register - Security
Recorded Future
Recorded Future
D
Docker
I
InfoQ
Recent Announcements
Recent Announcements
MongoDB | Blog
MongoDB | Blog
Microsoft Azure Blog
Microsoft Azure Blog
A
About on SuperTechFans
N
Netflix TechBlog - Medium
H
Hackread – Cybersecurity News, Data Breaches, AI and More
云风的 BLOG
云风的 BLOG
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
F
Full Disclosure
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threatpost
C
Cisco Blogs
P
Proofpoint News Feed
Google DeepMind News
Google DeepMind News
Security Latest
Security Latest
The Hacker News
The Hacker News
Microsoft Security Blog
Microsoft Security Blog
B
Blog
IT之家
IT之家
Latest news
Latest news
D
DataBreaches.Net
T
Tor Project blog
Scott Helme
Scott Helme
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Threat Research - Cisco Blogs
博客园 - 司徒正美
S
SegmentFault 最新的问题
宝玉的分享
宝玉的分享
Project Zero
Project Zero
T
The Exploit Database - CXSecurity.com
P
Privacy International News Feed
Last Week in AI
Last Week in AI
C
CERT Recently Published Vulnerability Notes
WordPress大学
WordPress大学
博客园 - 【当耐特】
C
Cybersecurity and Infrastructure Security Agency CISA
G
GRAHAM CLULEY
小众软件
小众软件
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园 - 聂微东
酷 壳 – CoolShell
酷 壳 – CoolShell
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

Jiajun的技术笔记

你好,2026! TiDB 源码阅读(六):TiDB Coprocessor 源码解析 性能优化的核心思想 TiDB 源码阅读(五):索引 TiDB 源码阅读(四):AST、逻辑计划、物理计划 CockroachDB Serverless Architecture podman 无故退出 Cursor Control-L (CTRL-L) Keyboard Shortcuts in Terminal Replace docker with podman Using xmonad with xfce4 A RC script for freebsd frpc 自己动手写一个k8s controller AI 会取代你的(编程)岗位吗? 自建DERP服务器提升Tailscale连接速度(使用Nginx转发) 自动升级Docker容器 再读《程序员修炼之道-从小工到专家》 让浏览器下载文件 再读《软件随想录》/《黑客与画家》/《软技能》 HTTP 压力测试中的 Coordinated Omission 2的补码 编程语言中的 context 是什么? flutter macOS 构建出错 Flatpak 使用小记 Golang CAS 操作是怎么实现的 PostgreSQL 当MQ来使用 Clash 结合 工作VPN 的网络设计 使用 PostgreSQL 搭建 JuiceFS PostgreSQL 配置优化和日志分析 有GitHub Copilot?那就可以搭建你的ChatGPT4服务 窗口函数的使用(以PG为例) 读《为什么学生不喜欢上学》 OpenAI Prompt Engineering 摘录和总结 读《打造真正的新产品》 VueJS 总结 Linux 自动挂载 alist 提供的webdav FreeBSD 使用 vm-bhyve 安装Debian虚拟机 FreeBSD 和 Linux 网卡聚合实现提速 GPT 帮我搞定了时区转换问题 长任务系统如何处理? macOS/Linux 编译 InputLeap 使用开源软KVM - synergy-core 解决 macOS 终端hostname一直变化问题 KVM 共享 Intel 集成显卡 PromQL 备忘 读《格鲁夫给经理人的第一课》 读《打开心智》 为什么要把复杂的联表操作拆成多个单表查询? 红包系统的设计 MySQL Index Condition Pushdown Optimization Go mod 简明教程 OpenWRT 使用 Android/iOS USB 网络 搭建旁路由 Golang gRPC 错误处理 编写可维护的单元测试代码 OAuth 2 详解(六):Authorization Code Flow with PKCE OAuth 2 详解(五):Device Authorization Flow OAuth 2 详解(三):Resource Owner Password Credentials Grant OAuth 2 详解(四):Client Credentials Flow OAuth 2 详解(二):Implict Grant Flow OAuth 2 详解(一):简介及 Authorization Code 模式 ElasticSearch 学习笔记 三种git流程以及发版模型 错误处理实践 权限模型(RBAC/ABAC) OIDC(OpenID Connect) 简介 任务队列简介 PostgreSQL 操作笔记 使用Drone CI构建CI/CD系统 Golang migrate 做数据库变更管理 使用PostgreSQL做搜索引擎 Nginx 源码阅读(三): 连接池、内存池 Nginx 源码阅读(二): 请求处理 Nginx 源码阅读(一): 启动流程 Go 泛型简明教程 KVM 显卡穿透给 Windows 使用 HTTP Router 处理 Telegram Bot 按钮回调 使用反射(reflect)对结构体赋值 GIN 是如何绑定参数的 你好 2022(2021 年终总结) 用Go导入大型CSV到PostgreSQL 使用 OpenWRT 搭建软路由 使用软KVM切换器 barrier 共享键鼠 SQL 防注入及原理 使用 gomock 测试 Go 代码 gevent不是黑魔法(二): gevent 实现 gevent不是黑魔法(一): greenlet 实现 用 entgo 替代 gorm 应用内使用crontab不是那么方便 单测时要不要 mock 数据库? Sentry 自建指南 用selenium完成自动化任务 用闲置的安卓手机做垃圾电话短信过滤 推荐三个时间管理工具 一次事故反思 当JS遇到uint64:JS整数溢出问题 SQLite3 存储以及ACID原理 Redis源码阅读:pub/sub实现 Redis源码阅读:zset实现 Redis源码阅读:bitmap 位图的运算 Redis源码阅读:set是怎么做交并集运算的?
gRPC鉴权方案
Jiajun Huang · 2020-12-19 · via Jiajun的技术笔记

昨日与群友讨论,gRPC比较优雅的鉴权方案应该怎么做,本文记录一下最终相对优雅的实践方案。

不过我们仍然要从故事的开头讲起。

起因

我在使用gRPC的时候,一开始是不想使用header的,尽量避免往gRPC里修改一些东西,因此最开始,我是想着,在每个请求的message里, 都带上access_token,例如:

message PingPong {
    string access_token = 1;
}

因此,如果想要做认证的话,就得在每个gRCP实现的方法里,对 access_token 进行校验,例如:

userID, err := getUserIDByAccessToken(req.AccessToken)
if err != nil {
    logrus.Errorf("failed to get userID by accessToken %s: %s", req.AccessToken, err)
    return nil, status.Errorf(codes.Unauthenticated, "AccessToken expired")
}

但是由于Go既没有像Python那样灵活的decorator,可以在函数执行前执行一些代码,又没有像Java那样的annotation可以注入一些 metadata以便根据这些metadata来执行一些操作。所以导致这种方案必须在每一个gRPC方法里都加上上面这样的代码,这就很麻烦。

要想在gRPC里,针对每一个方法都做一点事情,那么最简单的,自然是使用中间件,在gRPC里,叫做 interceptor 。gRPC是可以注入 metadata的,链接见 这里,gRPC提供的options,其中有 一种就叫做 google.protobuf.MethodOptions:

extend google.protobuf.MethodOptions {
  optional MyMessage my_method_option = 50007;
}

然后就可以使用它:

service MyService {
  option (my_service_option) = FOO;

  rpc MyMethod(RequestType) returns(ResponseType) {
    // Note:  my_method_option has type MyMessage.  We can set each field
    //   within it using a separate "option" line.
    option (my_method_option).foo = 567;
    option (my_method_option).bar = "Some string";
  }
}

不过我最终没有选择这种方案,原因是文档极少,而且需要在中间件里把方法的options拿出来,但是中间件函数的签名是:

// UnaryHandler defines the handler invoked by UnaryServerInterceptor to complete the normal
// execution of a unary RPC. If a UnaryHandler returns an error, it should be produced by the
// status package, or else gRPC will use codes.Unknown as the status code and err.Error() as
// the status message of the RPC.
type UnaryHandler func(ctx context.Context, req interface{}) (interface{}, error)

// UnaryServerInfo consists of various information about a unary RPC on
// server side. All per-rpc information may be mutated by the interceptor.
type UnaryServerInfo struct {
	// Server is the service implementation the user provides. This is read-only.
	Server interface{}
	// FullMethod is the full RPC method string, i.e., /package.service/method.
	FullMethod string
}

// UnaryServerInterceptor provides a hook to intercept the execution of a unary RPC on the server. info
// contains all the information of this RPC the interceptor can operate on. And handler is the wrapper
// of the service method implementation. It is the responsibility of the interceptor to invoke handler
// to complete the RPC.
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)

想要拿到option不容易。不过,这倒是引出了方案2。

解决方案2: 手动维护白名单/黑名单

经群友建议,我们可以把该要鉴权,或者不需要鉴权的方法,根据方法名放在一个列表(我使用的是map)里。看上面的 UnaryServerInfo, 此方法可行。还有,与此配合的就是,把 access_token 放在metadata里(其实就是HTTP/2 里的header)传输。

我这里是大部分接口都需要鉴权,因此我把不需要鉴权的接口放在map里:

// 所有不需要用户登录的接口,就放在这里,否则则必须登录
var publicAPIMapper = map[string]bool{
	"/cashapp.CashApp/PingPong": true,
	"/cashapp.CashApp/Register": true,
	"/cashapp.CashApp/Login":    true,
}

func IsPublicAPI(fullMethodName string) bool {
	return publicAPIMapper[fullMethodName]
}

然后就可以在interceptor里做统一处理:

func SentryUnaryServerInterceptor() grpc.UnaryServerInterceptor {
	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		fullMethodName := info.FullMethod

		if !utils.IsPublicAPI(fullMethodName) {
			accessToken := utils.GetAccessToken(ctx)
			userID, err := utils.GetUserIDByAccessToken(accessToken)
			if err != nil || userID == 0 {
				logrus.Infof("failed to find user by %s: %s", accessToken, err)
				return nil, status.Errorf(codes.Unauthenticated, err.Error())
			}

			ctx = context.WithValue(ctx, "access_token", accessToken)
			ctx = context.WithValue(ctx, "user_id", userID)
		}

		logrus.Infof("got request with %v", req)
		result, err := handler(ctx, req)
		if err != nil {
			sentry.CaptureException(fmt.Errorf("%v", err))
			sentry.Flush(time.Second * 5)
		}
		return result, err
	}
}

其中 utils.GetAccessToken 是从ctx里拿metadata,然后从中拿 access-token 的,代码如下:

func GetAccessToken(ctx context.Context) string {
	md, ok := metadata.FromIncomingContext(ctx)
	if !ok {
		return ""
	}

	accessTokenList := md.Get("access-token")
	if len(accessTokenList) == 1 {
		return accessTokenList[0]
	}

	return ""
}

这个时候,客户端只需要统一添加一个 Access-Token 的头部,值为对应用户的 access_token 即可。

注意踩坑,Nginx会把头部做一次转换,比如 Access-Token 会转换成 access-token(HTTP/2 规范要求, 详见RFC:https://tools.ietf.org/html/rfc7540#section-8.1.2 ),而带下划线的头,默认会过滤掉, 详见:http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers ,我一开始传 头部传了 access_token,所以排查了好一会儿。

总结

上面我们看了两种gRPC做鉴权的方案,首先我们还是要承认,access_token这种东西,放在头部还是比较合适的, 并不需要为了不动gRPC的metadata而去迁就。其次,简单粗暴的方案也可以是相对优雅的,所以最后我们选择了 实现上更简单但是代码可读性,维护行好很多的方案2。


ref: