在基于go的web服务中,常常需要进行用户的权限验证,一般会使用JWT,但是在gin中我们也可以通过自定义中间件来实现用户的权限验证,在gRPC中可以通过拦截器来实现。
目录
1.基于gin中间件的用户验证
2.结合casbin的gin的中间件的用户权限验证
(1) 建立conf文件
(2) 建立用户权限列表csv文件
(3) 使用gin中间件进行权限验证
(4) 测试
(5) 扩展
3.gRPC中的Token认证
(1)gRPC中的拦截器
(2)UnaryServerInterceptor的使用
(3)StreamServerInterceptor的使用
(4)在服务中注册拦截器
4.总结
1.基于gin中间件的用户验证
有些时候我们可能只需要简单的用户认证,例如restful接口,当用户登陆之后后台返回给调用者一个token,之后所有的请求头部都会携带上这个token,然后后端拿到请求的数据,如果请求头部中的token验证通过,那么就认为具有相应的权限。这种情况我们可以通过自定义gin中的中间件来实现。
package main
import (
"/casbin/casbin/v2"
"/gin-gonic/gin"
"log"
"net/http"
)
func main() {
r := gin.Default()
r.Use(authorizationWithCasbin())
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"user": "admin", "secret": "admin@123"})
})
_ = r.Run(":8080")
}
// 自定义的用户验证的中间件
func authorization() gin.HandlerFunc {
return func(c *gin.Context) {
if userName, passWord, ok := c.Request.BasicAuth();!ok{
c.AbortWithStatus(401)
}else{
if userName != "seu" || passWord != "admin@123"{
c.AbortWithStatus(401)
}
}
}
}但是需要注意的的是c.Request.BasicAuth的使用,我们可以看一下它的源代码如下:
// BasicAuth returns the username and password provided in the request's
// Authorization header, if the request uses HTTP Basic Authentication.
// See RFC 2617, Section 2.
func (r *Request) BasicAuth() (username, password string, ok bool) {
auth := r.Header.Get("Authorization")
if auth == "" {
return
}
return parseBasicAuth(auth)
}
// parseBasicAuth parses an HTTP Basic Authentication string.
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
func parseBasicAuth(auth string) (username, password string, ok bool) {
const prefix = "Basic "
// Case insensitive prefix match. See Issue 22736.
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
return
}
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
if err != nil {
return
}
cs := string(c)
s := strings.IndexByte(cs, ':')
if s < 0 {
return
}
return cs[:s], cs[s+1:], true
}可以看到它是先获取请求头部中的Authorization字段,然后再去解析,并且该字段的值必须是以Basic 开头,后面是对应的userName:userInfo的base64编码形式。例如,我们的用户名是seu,密码是admin@123,首先我们获取seu:admin@123的base64编码为c2V1OmFkbWluQDEyMw==(这里为了方便起见我直接将返回的token用用户的密码进行替代),最终请求头部的Authorization字段就是Basic c2V1OmFkbWluQDEyMw==。我们可以使用Goland中的HttpClient发送Get请求进行验证
GET http://localhost:8080/test
Accept: application/json
Authorization: Basic c2V1OmFkbWluQDEyMw==
输出:
# {
"secret": "admin@123",
"user": "admin"
}
GET http://localhost:8080/test
Accept: application/json
Authorization: Basic c2V1OmFkbWluQDEyMw===
输出:
# Response code: 401 (Unauthorized); Time: 96ms; Content length: 0 bytes2.结合casbin的gin的中间件的用户权限验证
有些时候我们需要更复杂的权限验证,比如特定的用户只能请求某些特定的restful接口,这个时候我们可以使用casbin来帮助我们进行权限的验证。关于casbin大家可以参考以下官网文档和相关文章
Github: https:///casbin/casbin
Document: https://casbin.org/docs/en/overview
概括的说就是在使用casbin之前我们需要先建立一个model的控制文件,以及对应的用户权限列表,用户权限列表可以存在mysql中或者是csv中,然后还是和上面一样,根据请求头部中的Authorization字段去获取用户名,然后根据用户名去拉取用户权限,再使用casbin进行权限的比对来判断该用户是否具有相关的权限。
例如现在有2个用户Tom和Bob,其中Tom只能以GET的方式访问/testTom这个接口,Bob只能以POST的方式访问/testBob这个接口,我们可以通过以下的方式来实现这样要求
(1) 建立conf文件
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act其中request_definition中定义的是请求的字段,sub,obj,act可以理解成分别代表用户的名称,用户要操作的对象和用户要进行的操作。相对应的policy_definition中即为对应的用户权限列表。
(2) 建立用户权限列表csv文件
p,Tom, /testTom, GET
p,Bob, /testBob, POST(3) 使用gin中间件进行权限验证
package main
import (
"/casbin/casbin/v2"
"/gin-gonic/gin"
"log"
"net/http"
)
func main() {
r := gin.Default()
r.Use(authorizationWithCasbin())
r.GET("/testTom", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"user": "Tom"})
})
r.POST("/testBob", func(c *gin.Context) {
c.JSON(500, gin.H{"user": "Bob"})
})
_ = r.Run(":8080")
}
func authorizationWithCasbin() gin.HandlerFunc {
return func(c *gin.Context) {
if userName, passWord, ok := c.Request.BasicAuth();!ok{
c.AbortWithStatus(401)
}else{
// 这里可以根据用户名称去拉取密码和用户信息进行进一步的验证
if passWord == "admin@123" {
e, err := casbin.NewSyncedEnforcer("model.conf", "user.csv")
if err != nil{
log.Print(err)
}
sub := userName
obj := c.Request.URL.String()
act := c.Request.Method
if ok, _ := e.Enforce(sub, obj, act);!ok{
c.AbortWithStatus(401)
}
}else{
c.AbortWithStatus(401)
}
}
}
}我们首先获取对应的信息,然后使用casbin进行验证即可
(4) 测试
我们还是使用Goland自带的HttpClient对其进行验证
(1)Tom:admin@123
GET http://localhost:8080/testTom
Accept: application/json
Authorization: Basic VG9tOmFkbWluQDEyMw==
>>> {"user": "Tom"}
(2)Bob:admin@123
GET http://localhost:8080/testTom
Accept: application/json
Authorization: Basic Qm9iOmFkbWluQDEyMw==
>>> Response code: 401 (Unauthorized); Time: 96ms; Content length: 0 bytes
(3)Tom:admin@123
POST http://localhost:8080/testBob
Accept: application/json
Authorization: Basic VG9tOmFkbWluQDEyMw==
>>> Response code: 401 (Unauthorized); Time: 93ms; Content length: 0 bytes
(4)
POST http://localhost:8080/testBob
Accept: application/json
Authorization: Basic VG9tOmFkbWluQDEyMw==
>>> >>> {"user": "Bob"}(5) 扩展
使用casbin我们可以很方便的进行权限修改与扩展,例如我们现在想要所有的用户都可以访问/testTom这个接口,对应的我们只需要修改conf文件和csv文件如下:
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = (r.sub == p.sub && r.obj == p.obj && r.act == p.act) || p.sub == "*"p,*, /testTom, GET
p,Bob, /testBob, POST但是有些时候我们需要自定义验证的方式,例如文件可能存储在其他地方或者数据库中,这个时候我们可以去自己继承casbin中的四个接口
type Adapter interface {
// LoadPolicy loads all policy rules from the storage.
LoadPolicy(model model.Model) error
// SavePolicy saves all policy rules to the storage.
SavePolicy(model model.Model) error
// AddPolicy adds a policy rule to the storage.
// This is part of the Auto-Save feature.
AddPolicy(sec string, ptype string, rule []string) error
// RemovePolicy removes a policy rule from the storage.
// This is part of the Auto-Save feature.
RemovePolicy(sec string, ptype string, rule []string) error
// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
// This is part of the Auto-Save feature.
RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error
}这样我们就可以自定义任何我们需要的验证方式,代码示例可以查看:https:///JustKeepSilence/gdb/blob/master/db/route.go
3.gRPC中的Token认证
上面介绍的是如何在gin中进行token认证,如果使用的是gRPC,同样的我们可以使用官方提供的拦截器很容易的实现token认证,详情可以参考官方文档:https://pkg.go.dev//grpc#UnaryClientInterceptor,接下来就以我自己的项目GDB中的例子来说明如何使用拦截器使用token认证。详细的代码可以参考github: https:///JustKeepSilence/gdb
(1)gRPC中的拦截器
在gRPC中服务端有两种拦截器,针对一般函数的拦截器UnaryServerInterceptor和针对流函数的StreamServerInterceptor拦截器,它们的定义分别如下
# UnaryServerInterceptor
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
# StreamServerInterceptor
type StreamServerInterceptor func(srv interface{}, ss ServerStream, info *StreamServerInfo, handler StreamHandler) error其中,UnaryServerInterceptor的参数分别为client传进来的context,client的请求req,服务端的信息info和用来处理请求的handler。StreamServerInterceptor的参数分别为客户端的请求参数srv,服务端的流信息ss和info以及用来处理请求的handler。
(2)UnaryServerInterceptor的使用
基本逻辑还是和前述一样,在客户端登陆的时候,首先将用户信息写入context中,然后在服务端获取用户信息并进行验证,如果验证通过则返回token信息,以后客户端的每次请求都必须携带这个token信息,我们也可以基于这个token进行更加详细的权限控制。
下面是服务端使用UnaryServerInterceptor进行权限验证的代码
func (s *server) authInterceptor(c context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
if s.configs.Authorization {
methods := strings.Split(info.FullMethod, "/")
if md, ok := metadata.FromIncomingContext(c); !ok {
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
} else {
var userName string
if d, ok := md["userName"]; ok {
userName = d[0]
} else {
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
}
remoteAddress := md.Get(":authority")[0] // address
userAgent := md.Get("user-agent")[0] // user agent
if methods[len(methods)-1] == "UserLogin" {
r := req.(*pb.AuthInfo)
if result, err := s.gdb.userLogin(authInfo{
UserName: r.GetUserName(),
PassWord: r.GetPassWord(),
}, remoteAddress, userAgent); err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
} else {
return &pb.UserToken{Token: result.Token}, nil
}
} else {
var token string
if d, ok := md["token"]; ok {
token = d[0]
} else {
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
}
if v, err := s.gdb.infoDb.Get([]byte(userName+"_token"+"_"+remoteAddress+"_"+userAgent), nil); err != nil || v == nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
} else {
if token != fmt.Sprintf("%s", v) {
return nil, status.Errorf(codes.Unauthenticated, "invalid token")
} else {
// log handler
return handler(c, req)
}
}
}
}
} else {
return handler(c, req)
}
}其中server为对应的rpc服务的结构体。基本的逻辑为首先根据配置文件中的配置来判断用户是否要进行权限控制,如果确定进行权限控制的话,首先判断请求要客户端的方法,如果是登陆方法的话则直接获取用户信息,并进行验证,如果验证通过,则返回token信息,对于其他的请求方法,则获取token字段与数据库中的token信息进行比对
(3)StreamServerInterceptor的使用
StreamServerInterceptor的使用和UnaryServerInterceptor基本相同,下面是服务端的代码
func (s *server) authWithServerStreamInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if s.configs.Authorization {
if !info.IsClientStream {
return status.Errorf(codes.Unknown, "unknown service type")
} else {
if md, ok := metadata.FromIncomingContext(ss.Context()); !ok {
return status.Errorf(codes.Unauthenticated, "invalid token")
} else {
var userName, token string
remoteAddress := md.Get(":authority")[0] // address
userAgent := md.Get("user-agent")[0] // user agent
if d, ok := md["userName"]; ok {
userName = d[0]
} else {
return status.Errorf(codes.Unauthenticated, "invalid token")
}
if d, ok := md["token"]; ok {
token = d[0]
} else {
return status.Errorf(codes.Unauthenticated, "invalid token")
}
if v, err := s.gdb.infoDb.Get([]byte(userName+"_token"+"_"+remoteAddress+"_"+userAgent), nil); err != nil || v == nil {
return status.Errorf(codes.Unauthenticated, "invalid token")
} else {
if token != fmt.Sprintf("%s", v) {
return status.Errorf(codes.Unauthenticated, "invalid token")
} else {
return handler(srv, ss)
}
}
}
}
} else {
return handler(srv, ss)
}
}也是获取请求中的token信息并进行对应的验证
(4)在服务中注册拦截器
在实现完了拦截器之后,我们还需要在启动gRPC服务之前去进行注册,并且现在官方已经支持了链式注册,也就是说我们可以使用多个拦截器,而不需要像以前一样将所有的逻辑都放在一个函数中,对于UnaryServerInterceptor使用ChainUnaryInterceptor,对于StreamServerInterceptor则使用ChainStreamInterceptor,使用代码如下
s = grpc.NewServer(grpc.ChainUnaryInterceptor(se.panicInterceptor, se.authInterceptor, se.logInterceptor),
grpc.ChainStreamInterceptor(se.panicWithServerStreamInterceptor, se.authWithServerStreamInterceptor))可以看到,我这里注册了三个拦截器,分别是panic拦截器,auth拦截器和log拦截器,并且拦截器的运行顺序和注册的顺序是一致的。
4.总结
在go的后台开发中,token的权限验证是很常见的问题,不管是常见的web后台还是grpc后台,我们都可以很容易实现这种需求。如果想要了解更详细的代码细节,大家可以参考我的github:https:///JustKeepSilence/gdb或者与我交流~
















