Nano-kit is a set of utility to build go-micro services.
通过阅读本教程,你将学会
本章节的实战操作,必须用 nano-kit 的 go-micro/v2 稳定版。如何安装可以参考快速开始教程。
一个实用的微服务框架,需要有内建的认证授权机制。具体来说,就是
每个 go-micro 服务,有三种认证的方式备选,由 --auth
启动参数指定
noop
不写 --auth
时缺省可用jwt
需要配置公私钥,使用 JSON Web Token 算法service
只需要配置公钥,使用认证服务 go.micro.auth每种认证的方式,都有如下功能
Generate
根据用户 ID、密码等,生成一个新账号Grant
新增一条访问规则Revoke
删除一条访问规则Rules
获取所有访问规则Verify
根据请求方身份和请求资源,进行权限检查Inspect
从访问令牌 access_token 还原出请求方身份Token
根据账号、密码或者 refresh_token 生成访问令牌 access_token所有功能一起协作,就是 go-micro 框架的认证授权机制。它基于 Role-based access control,核心算法在 Verify
里。值得注意的是,认证方式 jwt
的规则是临时的,需要在服务启动时写代码来装配,参见 micro/cmd/cmd.go
。认证方式 service
的规则是持久化的,储存在 Store 里的,可以用 micro cli 或者写代码来实时修改。
// micro/cmd/cmd.go
// add the system rules if we're using the JWT implementation
// which doesn't have access to the rules in the auth service
if (*cmd.DefaultCmd.Options().Auth).String() == "jwt" {
for _, rule := range inauth.SystemRules {
if err := (*cmd.DefaultCmd.Options().Auth).Grant(rule); err != nil {
return err
}
}
}
上面的简介,看了之后可能会有点懵。没关系,下面我们搭建一个具有认证授权功能的 go-micro 系统,在实战中一步一步去理解。
环境变量必须有
启动命令
$ micro --auth jwt auth
这是我们的认证服务。这个服务作为账号、规则的存储和管理方,用 jwt
方式启动。它启动后,会以 go.micro.auth
为服务名,注册到服务发现中心。
公私钥可以用这个项目里的命令来生成,然后用这个脚本把它们设置成环境变量。注意 macOS 用 base64 -b0
Linux 用 base64 -w0
export MICRO_AUTH_PRIVATE_KEY=$(cat key.pem | base64 -b0)
export MICRO_AUTH_PUBLIC_KEY=$(cat cert.pem | base64 -b0)
服务启动时,框架的初始化代码会生成自己的服务账号 service account。服务账号是服务的身份,可以用来作服务到服务请求的认证授权。区别于用户请求的认证授权,服务到服务请求的认证授权不受用户身份的影响,只要是从某个服务发出的请求,都会通过授权,适用于粗粒度的授权规则。我们现在暂时还用不到它。
// go-micro/util/auth/auth.go
// Generate generates a service account for and continually
// refreshes the access token.
func Generate(id string, name string, a auth.Auth) error {
// ...
}
注意,缺省情况下认证服务用 memory store 来存储需要持久化的账号、规则等信息。生产环境下,可以配置为 file store 或者 sql database store。
环境变量只需有
启动命令
$ micro --auth service api --namespace com.example --type service
这是 API 网关。这个服务作为用户请求的统一入口,会首先鉴别请求方的身份,授权,然后才是其它处理。用 service
方式启动,说明每次有请求到来,它都会去联系认证服务。如果设置了公钥环境变量,则 Inspect
(从访问令牌 access_token 还原出请求方身份)可以本地直接验证,减少一次远程调用。
// go-micro/auth/service/service.go
// Inspect a token
func (s *svc) Inspect(token string) (*auth.Account, error) {
// try to decode JWT locally and fall back to srv if an error occurs
if len(strings.Split(token, ".")) == 3 && s.jwt != nil {
return s.jwt.Inspect(token)
}
// the token is not a JWT or we do not have the keys to decode it,
// fall back to the auth service
rsp, err := s.auth.Inspect(context.TODO(), &pb.InspectRequest{Token: token})
if err != nil {
return nil, err
}
return serializeAccount(rsp.Account), nil
}
另外,Verify
(根据请求方身份和请求资源,进行权限检查)会缓存 30 秒规则,而 RBAC (Role-based access control) 这个算法实际上是在本地计算的。也能一定程度上减少向认证服务的远程调用。
// go-micro/auth/rules/rules.go
// Verify an account has access to a resource using the rules provided. If the account does not have
// access an error will be returned. If there are no rules provided which match the resource, an error
// will be returned
func Verify(rules []*auth.Rule, acc *auth.Account, res *auth.Resource) error {
// ...
}
API 网关的后端服务,可以照常启动。也就是说,它们用的是 noop
方式。互相之间调用不会进行权限检查。由于有统一的请求入口(即 API 网关),位于后端的服务,通常不会开启认证机制,以保证调用性能。值得注意的是,来自网关的用户请求,在认证通过后,会在上下文里保存请求方的身份信息。
获取上下文里的请求方的身份信息,用这个函数
// AccountFromContext gets the account from the context, which
// is set by the auth wrapper at the start of a call. If the account
// is not set, a nil account will be returned. The error is only returned
// when there was a problem retrieving an account
func AccountFromContext(ctx context.Context) (*Account, bool)
框架保证请求方的身份信息能在所有调用链条上一直传递下去。在业务逻辑里,可以根据它识别用户身份,作相关处理。这部分的原理在,比较复杂,我们暂时无需去理解,只要知悉并能运用就好。
// go-micro/util/wrapper/wrapper.go
func (a *authWrapper) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error
func AuthHandler(fn func() auth.Auth) server.HandlerWrapper
现在模拟用户,用 curl 向 API 网关发起请求,能正常响应。
$ curl -XPOST http://127.0.0.1:8080/realworld/Realworld/Call -d '{"name":"Jack","age":1}'
用下面的命令,禁止匿名用户访问,--namespace
表示需要切换到 com.example 这个名字空间,缺省用来 login 的账号 ID 是 default,密码是 password。
$ micro login --namespace com.example default password
You have been logged in
login 成功之后,可以查看当前名字空间下的账号和权限规则。
$ micro auth list accounts
ID Scopes Metadata
default admin n/a
$ micro auth list rules
ID Scope Access Resource Priority
default <public> GRANTED *:*:* 0
缺省的规则,允许匿名请求。为了关闭缺省的规则,我们增加一条规则,
$ micro auth create rule --scope '' --priority 1 --resource '*:*:*' --access denied deny-public
Rule created
$ micro auth list rules
ID Scope Access Resource Priority
deny-public <public> DENIED *:*:* 1
default <public> GRANTED *:*:* 0
再次用 curl 发出请求,被拒绝。说明刚才增加的规则生效了。
$ curl -i -XPOST http://127.0.0.1:8080/realworld/Realworld/Call -d '{"name":"Jack","age":1}'
HTTP/1.1 401 Unauthorized
Unauthorized request
接下来,我们增加一条规则,允许具名用户请求,访问范围是 normal。访问范围(Scope)类似用户组,一个用户可以有多个访问范围(Scope)。用参数 --priority
指定更大的优先级。
$ micro auth create rule --scope normal --priority 1000 --resource '*:*:*' normal-any
Rule created
$ micro auth list rules
ID Scope Access Resource Priority
normal-any normal GRANTED *:*:* 1000
deny-public <public> DENIED *:*:* 1
default <public> GRANTED *:*:* 0
然后,创建一个新用户,ID 是 user001,设定其访问范围是 normal。
$ micro auth create account --secret 123456 --scopes normal user001
Account created: {"id":"user001","type":"","issuer":"com.example","metadata":null,"scopes":["normal"],"secret":"123456"}
创建成功后,获取用户 user001 的访问令牌。
$ micro token --secret 123456 user001
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsInNjb3BlcyI6WyJub3JtYWwiXSwibWV0YWRhdGEiOm51bGwsImV4cCI6MTYxNTExNjk3NSwiaXNzIjoiY29tLmV4YW1wbGUiLCJzdWIiOiJ1c2VyMDAxIn0.mKYr04OltFaES3w5TMcpkgR6jTT-Y35Hd1x3wtexsA-3Go77JAOwwsI_FDrL2NAmhBxjxS_WCqtNm-oDNqM7LZNil9IJzJnNHsXJwZLfWewZaQDeqEMFLhTHVTJkV3R1gfnMAM7plYNff0Cmf32sVTUXg9LPTsVInGYLBj0CVZr8G2SFuyNvct5R7P5WbliaeaOO6OOmJND8l_AHA5MD_-8wEoG4wOv1vmXyS1wchHsJW4PAT0bKCzn9FmT6D2RNaCkqwgz1svGOVMZiFiKoiJIqZLINArQ7Xnw-Kdpen_TeAUBZHEE72XBpXh6jQy8QcPl8AoqLtQpighmnQBGA1g",
"refresh_token": "27d75932-03e0-455e-8cf8-1ff31aa458d8",
}
用这个用户的访问令牌发起请求,也就是把访问令牌填充到 HTTP Authorization
头,能得到响应。
$ curl -H 'Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoidXNlciIsInNjb3BlcyI6WyJub3JtYWwiXSwibWV0YWRhdGEiOm51bGwsImV4cCI6MTYxNTExNjk3NSwiaXNzIjoiY29tLmV4YW1wbGUiLCJzdWIiOiJ1c2VyMDAxIn0.mKYr04OltFaES3w5TMcpkgR6jTT-Y35Hd1x3wtexsA-3Go77JAOwwsI_FDrL2NAmhBxjxS_WCqtNm-oDNqM7LZNil9IJzJnNHsXJwZLfWewZaQDeqEMFLhTHVTJkV3R1gfnMAM7plYNff0Cmf32sVTUXg9LPTsVInGYLBj0CVZr8G2SFuyNvct5R7P5WbliaeaOO6OOmJND8l_AHA5MD_-8wEoG4wOv1vmXyS1wchHsJW4PAT0bKCzn9FmT6D2RNaCkqwgz1svGOVMZiFiKoiJIqZLINArQ7Xnw-Kdpen_TeAUBZHEE72XBpXh6jQy8QcPl8AoqLtQpighmnQBGA1g' -XPOST http://127.0.0.1:8080/realworld/Realworld/Call -d '{"name":"Jack","age":1}'
如果请求被拒绝,可能是访问令牌过期了。用 micro token
命令再次获取新的访问令牌就可以了。
仔细观察 API 网关的调试日志,会发现触发验证通过的规则,以及请求用户。打开调试日志的方法是,在应用启动时,设置环境变量 MICRO_LOG_LEVEL=debug
2021-03-07 19:57:25 file=rules/rules.go:109 level=debug service=api verify ok: rule=&{ID:normal-any Scope:normal Resource:0xc0004ef500 Access:0 Priority:1000}, resource=&{Name:* Type:* Endpoint:*}, account=&{ID:user001 Type:user Issuer:com.example Metadata:map[] Scopes:[normal] Secret:}
在业务逻辑里,用 AccountFromContext
获取上下文里的请求方的身份信息,发现是 user001。至此,我们没有写一行代码,体验了 go-micro 框架的认证授权机制。而且,只有授权的用户,才能访问服务 API。
算法的代码在 go-micro/auth/rules/rules.go
输入
输出
过程
项目 realworld-example-app
这里有一个例子,将 go-micro auth 与 GitHub OAuth 集成起来,关键步骤有
由于要保证账号的实名性,不会被恶意大量注册,我们自己去实现新用户注册的场景并不常见。新用户注册的流程是
限于篇幅,用一篇单独的教程。
这是 go-micro 的内建认证机制的教程。只有具备认证授权机制的微服务框架,才有实用价值。一旦我们能在 API 处理过程中拿到用户的身份,就可以做配额、计费等等。另外,服务间的身份认证(Service Accounts)也很有用:关键服务的访问权限不再需要用 IP 地址白名单控制,用服务身份是更加云原生(Cloud Native)的做法。