本文相关代码:gitee
前言
熟悉了普通微服务如何集成插件之后,再回顾第四章引入的micro api
网关,作为一个封装服务,他足够的便捷易用,但仍不够强大。
接下来几章,我们基于官方网关开发定制化的网关,逐步为他集成JWT鉴权和断路器等插件功能。
在生产情况下,不论需求大小都建议在项目中自己编译micro工具,确保开发、生产等环境一致。
步骤
一、自定义网关
之前的网关是使用micro api
命令启动的,micro
包官方已经开源,因此我们可以基于此很方便的进行二次开发:github
1.1 新建项目
在项目根目录下新建文件夹gateway
存放网关代码:
> mkdir gateway && cd gateway
既然要对micro包进行二次开发,首先要下载他的源码:
> go get github.com/micro/micro/v2
新建并编辑go-todolist/gateway/main.go
:
package main
import "github.com/micro/micro/v2/cmd"
func main() {
cmd.Init()
}
注意cmd的路径,不要引成github.com/micro/go-micro/v2/config/cmd
路径下的同名包。
编写Makefile
export MICRO_REGISTRY=etcd
export MICRO_REGISTRY_ADDRESS=172.18.0.58:2379
export MICRO_API_HANDLER=http
.PHONY: build
build:
go build -o ./micro main.go
.PHONY: run
run:build
./micro api
1.2 测试网关
执行make run
:
go build -o ./micro main.go
./micro api
2020-09-27 14:00:57 file=api/api.go:259 level=info service=api Registering API HTTP Handler at /{service:[a-zA-Z0-9]+}
2020-09-27 14:00:57 file=http/http.go:90 level=info service=api HTTP API Listening on [::]:8080
2020-09-27 14:00:57 file=v2@v2.9.1/service.go:200 level=info service=api Starting [service] go.micro.api
2020-09-27 14:00:57 file=grpc/grpc.go:864 level=info service=api Server [grpc] Listening on [::]:65235
2020-09-27 14:00:57 file=grpc/grpc.go:697 level=info service=api Registry [etcd] Registering node: go.micro.api-f64af7a4-099c-41e1-af50-21d757e6ea10
通过postman验证接口,就像之前使用官方micro
包一样(这里如果你没有移除之前task-srv
的三秒延迟,就会在task-api
中被断路器配置为默认返回值):
1.3 Plugin
查看micro api
服务启动代码,会发现代码中只调用了一个内置auth wrapper做TLS相关操作,并没有加载其他Wrapper。要增强api
服务,我们需要用到plugin.Plugin
,以下是截取的官方源码:
func Run(ctx *cli.Context, srvOpts ...micro.Option) {
...
// 这里遍历加载了所有全局Plugins和api相关Plugins
// reverse wrap handler
plugins := append(Plugins(), plugin.Plugins()...)
for i := len(plugins); i > 0; i-- {
h = plugins[i-1].Handler()(h)
}
// 这里只注册了一个内置的authWrapper ,并没有加载其他全局wrapper
// create the auth wrapper and the server
authWrapper := auth.Wrapper(rr, nsResolver)
api := httpapi.NewServer(Address, server.WrapHandler(authWrapper))
...
}
二、Auth Plugin
本节我们简单演示如何通过插件的方式集成JWT验证功能,然后编写一个简单的token生成函数生成一个用于校验的token。
一个最最基础的JWT鉴权模块,应包括登录并生成token => 请求头token解析 => 根据用户角色限制访问资源
等三部分,如果再考虑到实际生产应用,以及插件的灵活可配置,功能点就更是繁多。如果你需要集成相对完善的JWT鉴权功能,可以参考下面的代码自行开发:micro-in-cn/starter-kit
开始之前我们先要准备加密揭秘token的密钥,他包含一个密钥和一个 由密钥计算出的公钥,你可以直接使用我在git中上传的文件,也可以在以下网站生成,比用openssl命令生成操作起来简单:在线密钥生成
无论用何种方式取得密钥,请将他们以文件形式保存为go-todolist/gateway/conf/private.key
和go-todolist/gateway/conf/public.key
以便于下面程序调用
2.1 编写插件
下面我们编写一个简单的 Auth Plugin用于鉴权,新建并编辑go-todolist/gateway/plugins/auth/auth.go
:
package auth
import (
"crypto/rsa"
"github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/request"
"github.com/dgrijalva/jwt-go/test"
"github.com/micro/cli/v2"
"github.com/micro/micro/v2/plugin"
"log"
"net/http"
)
// 认证相关参数
// Claims是一些实体(通常指的用户)的状态和额外的元数据
type Claims struct {
// 在jwt默认Claims基础上增加用户ID信息
UserId string `json:"userId"`
jwt.StandardClaims
}
// 这里是我们自己封装的Plugin工厂方法,可以参考官方插件增加一些options参数便于插件的灵活配置
func NewPlugin() plugin.Plugin {
var pubKey *rsa.PublicKey
return plugin.NewPlugin(
// 插件名
plugin.WithName("auth"),
// token解码需要用到公钥,这里顺百年演示了如何配置命令行参数
plugin.WithFlag(
&cli.StringFlag{
Name: "auth_key",
Usage: "auth key file",
Value: "./conf/public.key",
}),
// 配置插件初始化操作,cli.Context中包含了项目启动参数
plugin.WithInit(func(ctx *cli.Context) error {
pubKeyFile := ctx.String("auth_key")
pubKey = test.LoadRSAPublicKeyFromDisk(pubKeyFile)
return nil
}),
// 配置处理函数,注意与wrapper不同,他的参数是http包的ResponseWriter和Request
plugin.WithHandler(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var claims Claims
token, err := request.ParseFromRequest(
r,
request.AuthorizationHeaderExtractor,
func(*jwt.Token) (interface{}, error) {
return pubKey, nil
},
request.WithClaims(&claims),
)
if err != nil {
log.Print("token invalid: ", err.Error())
w.WriteHeader(http.StatusUnauthorized)
return
}
// token.Valid是否成功,取决于jwt中Claims接口定义的Valid() error方法
// 本例中我们直接使用了默认Claims实现jwt.StandardClaims提供的方法,实际生产中可以根据需要重写
if token == nil || !token.Valid {
w.WriteHeader(http.StatusUnauthorized)
return
}
// todo:虽然是有效的token,但并不意味着此用户有权访问所有接口,演示代码省略鉴权细节
// 从Claims种解析userID并加入Header
r.Header.Set("userId", claims.UserId)
// 通过了上述验证后,必须执行下面这一步,保证其他插件和业务代码的执行
h.ServeHTTP(w, r)
})
}),
)
}
2.2 注册插件
修改main.go
文件:
func main() {
// 注册auth插件
err := api.Register(auth.NewPlugin())
if err != nil {
log.Fatal("auth register")
}
cmd.Init()
}
2.3 验证
此时如果你向localhost:8080
发起请求,比如之前的search或者finished,就会收到错误码401。
为了简单验证token解析的有效性,下面简单写一个生成token的函数(顺便附赠解析token函数,本项目中没有用到),用于测试token校验的有效性。
新建并编辑go-todolist/gateway/generate.go
:
package main
import (
"crypto/rsa"
"github.com/dgrijalva/jwt-go"
"github.com/dgrijalva/jwt-go/test"
"go-todolist/gateway/plugins/auth"
"log"
"time"
)
// 加密token的私钥
var priKey *rsa.PrivateKey
// 生成并打印用户ID为123的token
func main() {
priKey = test.LoadRSAPrivateKeyFromDisk("./gateway/conf/private.key")
token, err := GenerateToken("123")
if err != nil {
log.Fatal(err)
} else {
log.Println("token: ", token)
}
}
// 根据用户ID产生token
func GenerateToken(userId string) (string, error) {
// 设置token有效时间
nowTime := time.Now()
expireTime := nowTime.Add(3 * time.Hour)
claims := auth.Claims{
UserId: userId,
StandardClaims: jwt.StandardClaims{
// 过期时间
ExpiresAt: expireTime.Unix(),
// 指定token发行人
Issuer: "micro-auth",
},
}
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
// 该方法内部生成签名字符串,再用于获取完整、已签名的token
token, err := tokenClaims.SignedString(priKey)
return token, err
}
func ParseToken(token string) (*auth.Claims, error) {
// 用于解析鉴权的声明,方法内部主要是具体的解码和校验的过程,最终返回*Token
tokenClaims, err := jwt.ParseWithClaims(token, &auth.Claims{}, func(token *jwt.Token) (interface{}, error) {
return priKey.Public(), nil
})
if tokenClaims != nil {
// 从tokenClaims中获取到Claims对象,并使用断言,将该对象转换为我们自己定义的Claims
// 要传入指针,项目中结构体都是用指针传递,节省空间。
if claims, ok := tokenClaims.Claims.(*auth.Claims); ok && tokenClaims.Valid {
return claims, nil
}
}
return nil, err
}
直接运行上面的代码,就可以获取token,然后在你得请求投增加Authorization: Bearer 生成的token
即可通过校验。
总结
Plugin和Wrapper主要的区别就在于处理函数的参数不同,因此除了逐个改造Wrapper为Plugin。也可以考虑通过二次封装将Plugin处理函数包装为Wrapper处理函数的形式,直接使用原有的插件,这个各位不妨自己尝试一下。
下一章我们集成断路器。
支持一下
原创不易,买杯咖啡,谢谢:p