文章最后附带完整代码
这节主要是如何实现身份的认证,身份认证可以放在网关层,也可以放在单独服务,具体放哪里认证需要根据自己的应用场景,本节是放在Gateway网关层,提供统一的认证入口,通过再转发服务。
认证用的是jwt(json-web-token),基于第三方库"github.com/golang-jwt/jwt
在grpc_gateway
的工具目录utils
新创建一个jwt.go
文件,封装jwt的生成和验证,并复制一份到grpc_user/user
的工具目录中。
具体的jwt其它用法可参考github.com/golang-jwt/jwt
导入第三方包
import (
"github.com/golang-jwt/jwt"
)
声明认证结构体和秘钥,结构体中的属性字段自定义,这里只用了用户id(UserId)
type CustomClaims struct {
UserId int32
jwt.StandardClaims
}
const (
secret = "123456"
)
生成jwt
func GetToken(user_id int32, expire int64) (string, error) {
claims := CustomClaims{
user_id,
jwt.StandardClaims{
ExpiresAt: time.Now().Unix()+expire,
Issuer: "admin",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err := token.SignedString([]byte(secret))
return tokenStr, err
}
验证jwt
func VerityToken(token string) error{
if token == ""{
return errors.New("auth token empty")
}
tokens, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return []byte(secret), nil
})
if claims, ok := tokens.Claims.(jwt.MapClaims); ok && tokens.Valid {
if int(claims["UserId"].(float64)) <= 0 {
return errors.New("auth token: data is 0")
}
return nil
} else {
fmt.Println("auth token:", err)
return err
}
}
获取jwt中自定义属性值
func GetJwtData(token string) (int32, error){
tokens, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return []byte(secret), nil
})
if claims, ok := tokens.Claims.(jwt.MapClaims); ok{
return int32(claims["UserId"].(float64)), nil
}
return 0, err
}
修改user.proto
,把user_id
改成token
,执行pb.bat生成编译
message LoginResp{
int32 status = 1;
string token = 2;
string msg = 3;
}
用户服务的登录接口返回token,这里只贴部分代码,基于第4节 的登录接口,调整UserId为Token
models.Db.Table("user").Select("user.user_id, user.pwd").Where("user.phone = ?", req.Phone).Scan(&res)
if utils.Md5(req.Pwd) != res.Pwd {
resp.Status = common.RESP_ERROR
resp.Msg = "auth error"
return nil
}
resp.Status = common.RESP_SUCCESS
resp.Token, _ = utils.GetToken(res.UserId, 600)
resp.Msg = "success"
return nil
同步第二步的user.proto
修改到grpc_gateway
的user.proto
,执行pb.bat生成
token都是放在Http请求体头部的Authorization字段,通过解析该字段验证。这里我们把测试的认证放在TestUserGet测试,验证不通过则不能请求用户服务,在网关这里就被拦截掉。
func TestUserGet(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
//验证token
token := r.Header.Get("Authorization")
if err :=utils.VerityToken(token); err!=nil{
http.Error(w, err.Error(), 500)
return
}
其它需要验证的网关层接口函数,只需贴上该代码即可
//验证token
token := r.Header.Get("Authorization")
if err :=utils.VerityToken(token); err!=nil{
http.Error(w, err.Error(), 500)
return
}
当然,如果需要传递token带的用户id等属性到用户服务,可以把token放进上下文,带到用户服务解析,怎么放到上下文?
把token通过元数据metadata,类似于 HTTP 请求中的 Cookie 数据,采用的键值对列表的形式。以下是在TestUserGet接口上修改,在用户服务接口TestUser可以通过utils.GetJwtData
获取到用户id
注意,这里导入的包是micro v3提供的github.com/asim/go-micro/v3/metadata
,并非直接用grpc提供的google.golang.org/grpc/metadata
import "github.com/asim/go-micro/v3/metadata"
ctx := metadata.Set(context.Background(), "token", token)
resp, err := service.TestUser(ctx, &pb.TestReq{Id:int32(user_id)})
启动服务基于前面几节细讲,执行gateway.bat
或者gateway.sh
启动网关服务,执行user.bat
或者user.sh
启动用户服务
先登录,获取token,写到文件里(为了测试用)
ioutil.WriteFile("log.txt", []byte(string(resp.Token)), 0655)
这里再写一个http请求客户端grpc_gateway/client/test.go,读取log.txt的token发起请求TestUserGet接口
client := &http.Client{
Timeout: 3 * time.Second,
}
req, err := http.NewRequest("GET", "http://localhost:55001/user/test/11", nil)
//头部带上token
token, _ := ioutil.ReadFile("../log.txt")
req.Header.Add("Authorization", string(token))
if err != nil {
fmt.Println(err)
}
resp, err := client.Do(req)
if err != nil {
fmt.Println(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
}
fmt.Println(string(body))
gitee完整代码链接