本文相关代码:gitee


文章目录

  • 前言
  • 步骤
  • 一、自定义网关
  • 1.1 新建项目
  • 1.2 测试网关
  • 1.3 Plugin
  • 二、Auth Plugin
  • 2.1 编写插件
  • 2.2 注册插件
  • 2.3 验证
  • 总结
  • 支持一下



前言

熟悉了普通微服务如何集成插件之后,再回顾第四章引入的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中被断路器配置为默认返回值):

网关添加openfeign和loadbalancer依赖后启动失败_jwt

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.keygo-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。

网关添加openfeign和loadbalancer依赖后启动失败_github_02


为了简单验证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即可通过校验。

网关添加openfeign和loadbalancer依赖后启动失败_jwt_03


总结

Plugin和Wrapper主要的区别就在于处理函数的参数不同,因此除了逐个改造Wrapper为Plugin。也可以考虑通过二次封装将Plugin处理函数包装为Wrapper处理函数的形式,直接使用原有的插件,这个各位不妨自己尝试一下。

下一章我们集成断路器。