文章最后附带完整代码
上一节 使用了身份认证。这节就接着鉴权授权,也就是访问权限,身份认证通过后对其授权是否有权限访问,不同用户具有的访问选项不同。A能访问a,b,c链接,B能访问b,c,d。
这里用的是常用的Rbac模型,主要有三个主体组成,用户,角色和权限。三个主体的关系概括一句话:用户属于某个角色,某个角色具有某些权限。
创建Rbac的几张数据表,用户表之前已有,无需创建,直接用
CREATE TABLE `role` (
`role_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '角色id',
`name` varchar(50) NOT NULL COMMENT '角色名',
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色表';
INSERT INTO `role` VALUES ('1', '普通用户');
INSERT INTO `role` VALUES ('2', '管理员');
CREATE TABLE `method` (
`id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '方法id',
`name` varchar(50) NOT NULL COMMENT '方法名',
`parent_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '所属微服务模块id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='方法权限表';
INSERT INTO `method` VALUES ('1', 'user.TestUser', '1');
INSERT INTO `method` VALUES ('2', 'user.UserReg', '1');
INSERT INTO `method` VALUES ('3', 'user.UserLogin', '1');
CREATE TABLE `role_method` (
`role_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '角色id',
`method_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '方法id',
PRIMARY KEY (`role_id`,`method_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限表';
INSERT INTO `role_method` VALUES ('1', '1');
INSERT INTO `role_method` VALUES ('1', '2');
INSERT INTO `role_method` VALUES ('1', '3');
CREATE TABLE `user_role` (
`user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '用户id',
`role_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '角色id',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户角色表';
INSERT INTO `user_role` VALUES ('43', '1');
//额外添加的,Rbac模型中只需上面4张
CREATE TABLE `service` (
`id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '微服务模块id',
`name` varchar(50) NOT NULL COMMENT '服务模块名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='微服务模块表';
INSERT INTO `service` VALUES ('1', '用户服务模块');
创建完了表结构,需要把表结构也转化成Golang的结构体,支持gorm操作。
这里推荐使用gorm的开源工具gormt github.com/xxjwxc/gormt, 可以将mysql数据库表结构自动生成golang sturct结构,带大驼峰命名规则和json标签。
根据git上文档的安装步骤安装即可,这里用的是window下的打包好可视化工具,直接下载使用,下载连接:https://github.com/xxjwxc/gormt/releases
下载解压之后可直接使用。如果是编译安装,则用编译后的(gormt.exe或者gormt)打开工具(看git文档)。
点击右上角的set配数据库等信息,点击refresh,就如下所示,可以直接复制struct,不符合预期则做修改即可,不用每个表都手动敲struct。把对应的Rbac结构体放到grpc_server/user/models/user_model.go
模型中
其中,Redis权限缓存的命名规则
Redis的命名格式:
--角色对应权限用set集合存储
----格式key rbac_role_角色id
----格式val []string{方法id}
--服务方法用hash类型存储
----格式 key:rbac_method
---- field:rbac_method_方法名
---- val:方法id
在grpc_util中
目录中创建rbac_handler
目录,目录下创建rbac_handler.go
脚本。
脚本内容,只贴main部分,逻辑业务则没贴出来,上gitee看完整代码
const(
KEY_RBAC_REFRESH = "rbac_handler_refresh" //强制刷新rbac的标识key
KEY_RBAC_REFRESH_TIMES = "rbac_handler_times" //累计cron执行检测次数key
KEY_RBAC_ROLE_PREFIX = "rbac_role_" //角色对应权限的key前缀
KEY_RBAC_METHOD = "rbac_method" //服务方法hash结构的key值
KEY_RBAC_METHOD_PREFIX = "rbac_method_" //服务方法hash结构的field前缀
)
func main(){
//创建Redis客户端
addr := fmt.Sprintf("%v:%d", REDIS_ADDR, REDIS_PORT)
redis := redis.NewClient(&redis.Options{Addr:addr})
defer redis.Close()
_, err := redis.Ping().Result()
if err != nil {
log.Println("err :", err)
return
}
//是否有强制刷新标识(在后台设置)
if redis.Get(KEY_RBAC_REFRESH).Val() == "1" {
if refresh_rbac_handler(redis) == nil {
redis.Del(KEY_RBAC_REFRESH)
}
return
}
//判断权限缓存中的方法缓存是否为空
if redis.HLen(KEY_RBAC_METHOD).Val() <= 0 {
refresh_rbac_methods(redis)
}
//判断权限缓存中的角色缓存是否为空,只判断角色1
if redis.SCard(KEY_RBAC_ROLE_PREFIX + "1").Val() <= 0{
refresh_rbac_role_methods(redis)
}
//30分钟则强制刷新一次权限缓存
if redis.Get(KEY_RBAC_REFRESH_TIMES).Val() == "30"{
if refresh_rbac_handler(redis)!= nil{
return
}
redis.Set(KEY_RBAC_REFRESH_TIMES, 1, 0)
}else{
redis.Incr(KEY_RBAC_REFRESH_TIMES)
}
}
执行脚本完成,可以用rbac.bat
或者rbac.sh
测试跑下脚本。
跟着把该脚本放入cron定时任务,1分钟执行一次监控检测
# vi /etc/crontab
*/1 * * * * root /home/tool/golearn/src/emicro_6/grpc_util/rbac_handler/rbac_handler
注册接口增加用户角色的记录,用gorm的事务操作Db
//写进数据库
user.Pwd = utils.Md5(user.Pwd)
tx:= models.Db.Begin()
result := tx.Create(&user)
if result.Error != nil{
tx.Rollback()
return result.Error
}
userRole := models.UserRole{Id: user.Id, RoleId: common.DefaultRoleId}
result = tx.Create(&userRole)
if result.Error != nil{
tx.Rollback()
return result.Error
}
tx.Commit()
resp.Status = common.RESP_SUCCESS
resp.Msg = "success"
登录接口的调整,获取用户的角色,放到token中
type result struct {
UserId int32
Pwd string
RoleId int32
}
var res result
models.Db.Table("user").Select("user.user_id, user.pwd, user_role.role_id").Joins("left join user_role on user.user_id = user_role.user_id").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
}
if res.RoleId <= 0 {
res.RoleId = common.DefaultRoleId
}
resp.Status = common.RESP_SUCCESS
resp.Token, _ = utils.GetToken(res.UserId, res.RoleId, 600)
grpc_server/utils/jwt.go
中jwt自定义字段新增角色idRoleId,以及jwt其它几个Api函数的适配。并同步修改到grpc_gateway/utils/jwt.go
type CustomClaims struct {
UserId int32
RoleId int32
jwt.StandardClaims
}
func GetToken(user_id int32, role_id int32, expire int64) (string, error) {
claims := CustomClaims{
user_id,
role_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
}
...
配置Redis连接配置 ,在grpc_gateway/conf/service.conf
新增
redis_addr = "127.0.0.1"
redis_port = 6379
在grpc_gateway
目录下创建vendors
目录,创建rbac目录和redis目录
redis连接池和操作,eredis.go
const(
USING = 1
FREE = 2
INITNUM = 20
MAXNUM = 60
PINGSTEP = 10 //两次ping之间的间隔
RETRY_TIMES = 3 //重试次数
ALIVE_TIME = 7200 //连接存活时间上限,2个小时,看配置的timeout来设
)
var redisLockAddr = []string{}
type Conn struct{
//Db *redis.ClusterClient //集群对象
Db *redis.Client //单机和主从哨兵对象
status int
pingTime int64 //最后一次ping的时间
time int64 //初始化时间
}
type RedisPool struct{
sync.RWMutex
maxConnNum int //最大连接数
initConnNum int //初始连接数
idleConns chan *Conn //未创建的连接(未初始化)
cacheConns chan *Conn //已创建的空闲连接
pushConnCount int64 //已放回的连接数量
popConnCount int64 //已取出的连接数量
use_pool bool //是否启用连接池
close_status bool //是否关闭状态
}
var ERedis *RedisPool
func init(){
ERedis = OpenPool()
}
//开启连接池
func OpenPool() (*RedisPool){
pool := newPool()
for i:=0; i<pool.maxConnNum; i++{
if i<pool.initConnNum {
conn, err := pool.newConn()
if err != nil{
log.Println("OpenPool error:", err)
pool.AddIdleConn()
continue
}
pool.cacheConns <- conn
}else{
conn := new(Conn)
pool.idleConns <- conn
}
}
return pool
}
rbac权限机制, rbac.go
,解析token后直接读取redis验证
//鉴权-权限验证
func RbacFilter(role_id int32, method_name string) bool{
method_id, err := redis.ERedis.HGet("rbac_method", "rbac_method_" + method_name)
if err != nil{
return false
}
return redis.ERedis.SIsmember("rbac_role_" + utils.GetString(role_id), method_id)
}
身份认证通过后,解析token进行权限验证,通过再继续往下走,转发rpc请求
func TestUserGet(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
//验证token
token := r.Header.Get("Authorization")
log.Println("token:",token)
if err :=utils.VerityToken(token); err!=nil{
http.Error(w, err.Error(), 500)
return
}
//解析token参数
user_id, role_id, err := utils.GetJwtData(token)
if err !=nil{
http.Error(w, err.Error(), 500)
return
}
//权限验证
if !rbac.RbacFilter(role_id, "user.GetUserInfo") {
http.Error(w, errors.New("not permission").Error(), 500)
return
}
授权只需添加解析和验证代码即可
//解析token参数
user_id, role_id, err := utils.GetJwtData(token)
if err !=nil{
http.Error(w, err.Error(), 500)
return
}
//权限验证
if !rbac.RbacFilter(role_id, "user.GetUserInfo") {
http.Error(w, errors.New("not permission").Error(), 500)
return
}
测试验证这步跟随第5节的最后一步,同样测试方式即可。
gitee完整代码链接