书接上文 Go Grpc Jwt身份认证 ,本文我们尝试把gateway也加进来,有关gatewa大家可以参考 go学习笔记 grpc-gateway和swagger。直接开干吧
Grpc Jwt GateWay的集成【包含跨域问题的解决】
1.修改api/api.proto文件
syntax = "proto3";
package api;
// 1 导入 gateway 相关的proto 以及 swagger 相关的 proto
import "google/api/annotations.proto";
import "protoc-gen-swagger/options/annotations.proto";
// 2 定义 swagger 相关的内容
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
info: {
title: "grpc gateway sample";
version: "1.0";
license: {
name: "MIT";
};
};
schemes: HTTP;
consumes: "application/json";
produces: "application/json";
};
service Ping {
rpc Login (LoginRequest) returns (LoginReply) {
option (google.api.http) = {
post: "/login"
body: "*"
};
}
rpc SayHello(PingMessage) returns (PingMessage) {
option (google.api.http) = {
post: "/sayhello"
body: "*"
};
}
}
message LoginRequest{
string username=1;
string password=2;
}
message LoginReply{
string status=1;
string token=2;
}
message PingMessage {
string greeting = 1;
}
2.编译api/api.proto
protoc -ID:\Go\include -I. --go_out=plugins=grpc:. ./api/api.proto
protoc -ID:\Go\include -I. --grpc-gateway_out=logtostderr=true:. ./api/api.proto
3. 这次我们吧server 和client 分开, 分成两个文件夹,上文中获取token 用的是metadata.FromIncomingContext(ctx)方法, 这次我们该用metautils.ExtractIncoming(ctx).Get(headerAuthorize)方法比较简单。修改后的的authtoken.go 如下:
package api
import (
"context"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
)
var (
headerAuthorize = "authorization"
)
func CreateToken(userName string) (tokenString string) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": "lora-app-server",
"aud": "lora-app-server",
"nbf": time.Now().Unix(),
"exp": time.Now().Add(time.Hour).Unix(),
"sub": "user",
"username": userName,
})
tokenString, err := token.SignedString([]byte("verysecret"))
if err != nil {
panic(err)
}
return tokenString
}
// AuthToekn 自定义认证
type AuthToekn struct {
Token string
}
func (c AuthToekn) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
headerAuthorize: c.Token,
}, nil
}
func (c AuthToekn) RequireTransportSecurity() bool {
return false
}
// Claims defines the struct containing the token claims.
type Claims struct {
jwt.StandardClaims
// Username defines the identity of the user.
Username string `json:"username"`
}
// Step1. 从 context 的 metadata 中,取出 token
func getTokenFromContext(ctx context.Context) string {
val := metautils.ExtractIncoming(ctx).Get(headerAuthorize)
return val
}
func CheckAuth(ctx context.Context) (username string) {
tokenStr := getTokenFromContext(ctx)
if len(tokenStr) == 0 {
panic("get token from context error")
}
var clientClaims Claims
token, err := jwt.ParseWithClaims(tokenStr, &clientClaims, func(token *jwt.Token) (interface{}, error) {
if token.Header["alg"] != "HS256" {
panic("ErrInvalidAlgorithm")
}
return []byte("verysecret"), nil
})
if err != nil {
panic("jwt parse error")
}
if !token.Valid {
panic("ErrInvalidToken")
}
return clientClaims.Username
}
4.server的main.go 我们增加了跨域请求的设置,同时也罢 grpc server 和http 的server整合在一起【原理很简单 就是整合一个handler 监听一个端口, 判断进来的是grpc 还是json,grpc交由grpc 服务处理】,server/main.go代码如下:
package main
import (
"context"
"fmt"
"log"
"net/http"
"strings"
"jwtdemo/api"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"google.golang.org/grpc"
)
const (
port = ":8080"
)
func main() {
// 創建grpc-gateway服務,轉發到grpc的8080端口
gwmux := runtime.NewServeMux()
opt := []grpc.DialOption{grpc.WithInsecure()}
err := api.RegisterPingHandlerFromEndpoint(context.Background(), gwmux, "localhost"+port, opt)
if err != nil {
log.Fatal(err)
}
// 創建grpc服務
rpcServer := grpc.NewServer()
api.RegisterPingServer(rpcServer, new(api.Server))
// 創建http服務,監聽8080端口,並調用上面的兩個服務來處理請求
http.ListenAndServe(
port,
grpcHandlerFunc(rpcServer, gwmux),
)
}
// grpcHandlerFunc 根據請求頭判斷是grpc請求還是grpc-gateway請求
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
grpcServer.ServeHTTP(w, r)
} else {
allowCORS(otherHandler).ServeHTTP(w, r)
}
}), &http2.Server{})
}
func preflightHandler(w http.ResponseWriter, r *http.Request) {
headers := []string{"Content-Type", "Accept", "Authorization"}
w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ","))
methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE"}
w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
fmt.Println("preflight request for:", r.URL.Path)
return
}
// allowCORS allows Cross Origin Resoruce Sharing from any origin.
// Don't do this without consideration in production systems.
func allowCORS(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if origin := r.Header.Get("Origin"); origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" {
preflightHandler(w, r)
return
}
}
h.ServeHTTP(w, r)
})
}
5客户端我们增加了 http的调用, client/main.go实现如下:
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"jwtdemo/api"
"google.golang.org/grpc"
)
func main() {
grpcCall()
fmt.Println("http call.....")
httpCall()
}
const (
grpcPort = ":8080"
httpPort = ":8080"
)
func grpcCall() {
var conn *grpc.ClientConn
//call Login
conn, err := grpc.Dial(grpcPort, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %s", err)
}
defer conn.Close()
c := api.NewPingClient(conn)
loginReply, err := c.Login(context.Background(), &api.LoginRequest{Username: "gavin", Password: "gavin"})
if err != nil {
log.Fatalf("Error when calling SayHello: %s", err)
}
//fmt.Println("Login Reply:", loginReply)
//Call SayHello
requestToken := new(api.AuthToekn)
requestToken.Token = loginReply.Token
conn, err = grpc.Dial(grpcPort, grpc.WithInsecure(), grpc.WithPerRPCCredentials(requestToken))
if err != nil {
log.Fatalf("did not connect: %s", err)
}
defer conn.Close()
c = api.NewPingClient(conn)
helloreply, err := c.SayHello(context.Background(), &api.PingMessage{Greeting: "foo"})
if err != nil {
log.Fatalf("Error when calling SayHello: %s", err)
}
log.Printf("Response from server: %s", helloreply.Greeting)
}
func httpCall() {
urlpfx := "http://localhost" + httpPort
//call login
loginRequest := api.LoginRequest{Username: "gavin", Password: "gavin"}
loginrequestByte, _ := json.Marshal(loginRequest)
request, _ := http.NewRequest("POST", urlpfx+"/login", strings.NewReader(string(loginrequestByte)))
request.Header.Set("Content-Type", "application/json")
loginResponse, _ := http.DefaultClient.Do(request)
loginReplyBytes, _ := ioutil.ReadAll(loginResponse.Body)
defer loginResponse.Body.Close()
var loginReply api.LoginReply
json.Unmarshal(loginReplyBytes, &loginReply)
//fmt.Println("token:" + loginReply.Token)
///call say hello
sayhelloRequest := api.PingMessage{Greeting: "gavin say "}
sayhelloRequestByte, _ := json.Marshal(sayhelloRequest)
request, _ = http.NewRequest("POST", urlpfx+"/sayhello", strings.NewReader(string(sayhelloRequestByte)))
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Authorization", loginReply.Token)
sayhelloResponse, err := http.DefaultClient.Do(request)
if err != nil {
fmt.Println(err)
}
sayhelloReplyBytes, err := ioutil.ReadAll(sayhelloResponse.Body)
if err != nil {
fmt.Println(err)
}
log.Printf(string(sayhelloReplyBytes))
}
6.为了验证跨域问题, 我们增加了一个html/hello.html页面 内容如下:
<html>
<head>
<title>grpc gate way test</title>
</head>
<body>
<div id="divtoke"></div> <input type="button" value="token" id="btnToken"><br>
<div id="divhelllo"></div><input type="button" value="Sayhello" id="btnHello"><br>
<script type="text/javascript" src="./jquery-2.2.3.min.js"></script>
<script>
var prfx="http://localhost:8080/";
$("#btnToken").click(function(){
var obj={ username:"gavin",password:"gavin"};
var objstr= JSON.stringify(obj);
$.ajax({
"type": "POST",
"contentType": "application/json",
"url": prfx + "login",
"dataType": "json",
"data": objstr ,
"success": function(data, status, xhr) {
$("#divtoke").html(data.token)
}
});
});
$("#btnHello").click(function(){
var obj={greeting:"world"};
var objstr= JSON.stringify(obj);
var userToken=$("#divtoke").html();
$.ajax({
"headers": {"Authorization":userToken},
"type": "POST",
"contentType": "application/json",
"url": prfx + "sayhello",
"dataType": "json",
"data": objstr,
"success": function(data, status, xhr) {
$("#divhelllo").html(data.greeting)
}
});
});
</script>
</body>
</html>
7。 为了便于之间看文章的朋友我吧 api/handler.go的代码附上:
package api
import (
"fmt"
"golang.org/x/net/context"
)
// Server represents the gRPC server
type Server struct {
}
func (s *Server) Login(ctx context.Context, in *LoginRequest) (*LoginReply, error) {
fmt.Println("Loginrequest: ", in.Username)
if in.Username == "gavin" && in.Password == "gavin" {
tokenString := CreateToken(in.Username)
return &LoginReply{Status: "200", Token: tokenString}, nil
} else {
return &LoginReply{Status: "403", Token: ""}, nil
}
}
// SayHello generates response to a Ping request
func (s *Server) SayHello(ctx context.Context, in *PingMessage) (*PingMessage, error) {
msg := "bar"
userName := CheckAuth(ctx)
msg += " " + userName
return &PingMessage{Greeting: msg}, nil
}
8.运行结果如下:
------------------------------------------------------------------------------------------------------------------------------------------------------------
Https双向认证的集成
到目前为止我们 还没有使用证书,为了方便先前的code 跑起来, 我新建servertls 和clienttls文件夹,关于证书的生成利用MySSL测试证书生成工具我们可以很简单的生成两张证书,要是用https首先需要修改api/api.proto文件的schemes 为https 然后重新编译, 为了让AuthToekn兼容http和https 我们修改为如下:
// AuthToekn 自定义认证
type AuthToekn struct {
Token string
Tsl bool
}
func (c AuthToekn) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
return map[string]string{
headerAuthorize: c.Token,
}, nil
}
func (c AuthToekn) RequireTransportSecurity() bool {
return c.Tsl
//return false
}
最后我们来看看 servertls/main.go如何实现:
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
api "jwtdemo/api"
"log"
"net/http"
"strings"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
port = ":8283"
serverPem = "../certs/server.pem"
serverkey = "../certs/server.key"
rootPem = "../certs/ca.pem"
)
func main() {
cert, _ := tls.LoadX509KeyPair(serverPem, serverkey)
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile(rootPem)
certPool.AppendCertsFromPEM(ca)
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
})
// 創建grpc-gateway服務,轉發到grpc的8080端口
gwmux := runtime.NewServeMux()
creds = credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: certPool,
})
opt := []grpc.DialOption{grpc.WithTransportCredentials(creds)}
err := api.RegisterPingHandlerFromEndpoint(context.Background(), gwmux, "localhost"+port, opt)
if err != nil {
log.Fatal(err)
}
// 創建grpc服務
rpcServer := grpc.NewServer()
api.RegisterPingServer(rpcServer, new(api.Server))
// 創建http服務,監聽8080端口,並調用上面的兩個服務來處理請求
http.ListenAndServeTLS(port, serverPem, serverkey, grpcHandlerFunc(rpcServer, gwmux))
}
// grpcHandlerFunc 根據請求頭判斷是grpc請求還是grpc-gateway請求
func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler {
return h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
grpcServer.ServeHTTP(w, r)
} else {
allowCORS(otherHandler).ServeHTTP(w, r)
}
}), &http2.Server{})
}
func preflightHandler(w http.ResponseWriter, r *http.Request) {
headers := []string{"Content-Type", "Accept", "Authorization"}
w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ","))
methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE"}
w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
fmt.Println("preflight request for:", r.URL.Path)
return
}
// allowCORS allows Cross Origin Resoruce Sharing from any origin.
// Don't do this without consideration in production systems.
func allowCORS(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if origin := r.Header.Get("Origin"); origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" {
preflightHandler(w, r)
return
}
}
h.ServeHTTP(w, r)
})
}
func getTLSConfig(host, caCertFile string, certOpt tls.ClientAuthType) *tls.Config {
var caCert []byte
var err error
var caCertPool *x509.CertPool
if certOpt > tls.RequestClientCert {
caCert, err = ioutil.ReadFile(caCertFile)
if err != nil {
fmt.Printf("Error opening cert file %s error: %v", caCertFile, err)
}
caCertPool = x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
}
return &tls.Config{
ServerName: host,
ClientAuth: certOpt,
ClientCAs: caCertPool,
MinVersion: tls.VersionTLS12, // TLS versions below 1.2 are considered insecure - see https://www.rfc-editor.org/rfc/rfc7525.txt for details
}
}
最后clienttls/main.go修改后如下:
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"jwtdemo/api"
"golang.org/x/net/http2"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func main() {
grpcCall()
fmt.Println("http call.....")
httpCall()
}
const (
port = ":8283"
clientPem = "../certs/server.pem"
clientkey = "../certs/server.key"
rootPem = "../certs/ca.pem"
)
func grpcCall() {
var conn *grpc.ClientConn
cert, _ := tls.LoadX509KeyPair(clientPem, clientkey)
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile(rootPem)
certPool.AppendCertsFromPEM(ca)
creds := credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ServerName: "localhost",
RootCAs: certPool,
})
//call Login
conn, err := grpc.Dial("localhost"+port, grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatalf("did not connect: %s", err)
}
defer conn.Close()
//c := api.NewPingClient(conn)
c := api.NewPingClient(conn)
loginReply, err := c.Login(context.Background(), &api.LoginRequest{Username: "gavin", Password: "gavin"})
if err != nil {
log.Fatalf("Error when calling Login: %s", err)
}
//fmt.Println("Login Reply:", loginReply)
//Call SayHello
requestToken := new(api.AuthToekn)
requestToken.Token = loginReply.Token
requestToken.Tsl = true
conn, err = grpc.Dial(port, grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(requestToken))
if err != nil {
log.Fatalf("did not connect: %s", err)
}
defer conn.Close()
c = api.NewPingClient(conn)
helloreply, err := c.SayHello(context.Background(), &api.PingMessage{Greeting: "foo"})
if err != nil {
log.Fatalf("Error when calling SayHello: %s", err)
}
log.Printf("Response from server: %s", helloreply.Greeting)
}
func httpCall() {
urlpfx := "https://localhost" + port
cert, _ := tls.LoadX509KeyPair(clientPem, clientkey)
certPool := x509.NewCertPool()
ca, _ := ioutil.ReadFile(rootPem)
certPool.AppendCertsFromPEM(ca)
t := &http2.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: certPool,
},
}
httpClient := http.Client{Transport: t}
//call login
loginRequest := api.LoginRequest{Username: "gavin", Password: "gavin"}
loginrequestByte, _ := json.Marshal(loginRequest)
request, _ := http.NewRequest("POST", urlpfx+"/login", strings.NewReader(string(loginrequestByte)))
request.Header.Set("Content-Type", "application/json")
loginResponse, _ := httpClient.Do(request)
loginReplyBytes, _ := ioutil.ReadAll(loginResponse.Body)
defer loginResponse.Body.Close()
var loginReply api.LoginReply
json.Unmarshal(loginReplyBytes, &loginReply)
//fmt.Println("token:" + loginReply.Token)
///call say hello
sayhelloRequest := api.PingMessage{Greeting: "gavin say "}
sayhelloRequestByte, _ := json.Marshal(sayhelloRequest)
request, _ = http.NewRequest("POST", urlpfx+"/sayhello", strings.NewReader(string(sayhelloRequestByte)))
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Authorization", loginReply.Token)
sayhelloResponse, err := httpClient.Do(request)
if err != nil {
fmt.Println(err)
}
sayhelloReplyBytes, err := ioutil.ReadAll(sayhelloResponse.Body)
if err != nil {
fmt.Println(err)
}
log.Printf(string(sayhelloReplyBytes))
}
最后运行成功!!!!!!
备注 在win7 如果提示证书握手失败, 请安装ca.crt证书 到受信任中心 【openssl x509 -outform der -in ca.pem -out ca.crt】
下载地址 https://github.com/dz45693/gogrpcjwt.git
参考:
https://www.mdeditor.tw/pl/p1Vq/zh-hk
https://github.com/Bingjian-Zhu/go-grpc-example
https://razeencheng.com/post/how-to-use-grpc-in-golang-03