声明:此文章为博主个人学习记录,仅供学习和交流,如有侵权请联系博主。
前言
前段时间,因为本地k8s环境一直出问题,线上云环境也用不起,(后面搞定了再慢慢学习)所以就暂时搁置了k8s学习,然后就想着封装一下go微服务框架,看了一些大佬的项目跟着写了一个自己的用户微服务,重构一下微服务框架体系。
项目源码:github地址,觉得有用的话,点个star,谢谢~
该项目涉及的技术栈:
1. 配置viper集成
2. 日志zap集成
3. orm框架gorm集成
4. redis集成
5. protobuf
6. gin框架
7. redis分布式锁实现
8. 服务优雅退出
9. jwt鉴权 token黑名单和续签机制
10. 腾讯云sms sdk
11. 全局错误和响应封装
12. gin中间件 cors跨域
13. 自定义校验器实现
14. grpc
15. 图片验证码
篇幅较长,关键代码会有注释,可以作为微服务练手级项目,加深自己对微服务体系的理解。
用户微服务
用户服务 srv
目录结构
model
定义用户模型
model user.go
// 自定义公有字段 gorm自带 gorm.Model
type BaseModel struct {
ID int32 `gorm:"primarykey"`
CreatedAt time.Time `gorm:"column:add_time"`
UpdatedAt time.Time `gorm:"column:update_time"`
DeletedAt gorm.DeletedAt
IsDeleted bool
}
/*
User
1. 对称加密
2. 非对称加密
3. md5 信息摘要算法
密码如果不可以反解,用户找回密码
*/
type User struct {
BaseModel
Mobile string `gorm:"index:idx_mobile;unique;type:varchar(11);not null"` // index 索引
Password string `gorm:"type:varchar(100);not null"`
NickName string `gorm:"type:varchar(20)"`
Birthday *time.Time `gorm:"type:datetime"`
Gender string `gorm:"column:gender;default:male;type:varchar(6) comment 'female表示女, male表示男'"` // 注意comment
Role int `gorm:"column:role;default:1;type:int comment '1表示普通用户, 2表示管理员'"`
}
同步表结构
mymicro/srvs/user_srv/model/main/main.go
func main() {
dsn := "root:root@tcp(192.168.60.120:3306)/mymicro_user_srv?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // Log level
Colorful: true, // 禁用彩色打印
},
)
// 全局模式
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
Logger: newLogger,
})
if err != nil {
panic(err)
}
_ = db.AutoMigrate(&model.User{})
加密工具
md5
mymicro/srvs/user_srv/utils/cryptx/md5.go
扩展 加盐彩虹表 二次加密
import (
"crypto/md5"
"encoding/hex"
"io"
)
func GenMD5(code string) string {
Md5 := md5.New()
_, _ = io.WriteString(Md5, code)
return hex.EncodeToString(Md5.Sum(nil))
}
encode
mymicro/srvs/user_srv/utils/cryptx/encode.go
github.com/anaskhan96/go-password-encoder 第三方加密库
import (
"crypto/sha512"
"fmt"
"github.com/anaskhan96/go-password-encoder"
"strings"
)
// github.com/anaskhan96/go-password-encoder 加密
var options = &password.Options{16, 100, 32, sha512.New}
func EncodePwd(code string) string {
salt, encodedPwd := password.Encode(code, options)
newPassword := fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd)
return newPassword
}
func VerifyPwd(code, pwd string) bool {
passwordInfo := strings.Split(pwd, "$")
check := password.Verify(code, passwordInfo[2], passwordInfo[3], options)
return check
}
proto生成
命令
protoc -I . user.proto --go_out=. --go-grpc_out=.
proto文件
mymicro/srvs/user_srv/proto/user.proto
pb.go 里面的接口 UserServer
syntax = "proto3";
import "google/protobuf/empty.proto"; // 空结构 message Empty {}
option go_package="./proto";
service User{
rpc GetUserList(PageInfo) returns (UserListResponse); // 用户列表
rpc GetUserByMobile(MobileRequest) returns (UserInfoResponse); //通过mobile查询用户
rpc GetUserById(IdRequest) returns (UserInfoResponse); //通过id查询用户
rpc CreateUser(CreateUserInfo) returns (UserInfoResponse); // 添加用户
rpc UpdateUser(UpdateUserInfo) returns (google.protobuf.Empty); // 更新用户
rpc CheckPassWord(PasswordCheckInfo) returns (CheckResponse); //检查密码
}
message PasswordCheckInfo {
string password = 1;
string encryptedPassword = 2;
}
message CheckResponse{
bool success = 1;
}
message PageInfo {
uint32 pn = 1;
uint32 pSize = 2;
}
message MobileRequest{
string mobile = 1;
}
message IdRequest {
int32 id = 1;
}
message CreateUserInfo {
string nickName = 1;
string passWord = 2;
string mobile = 3;
}
message UpdateUserInfo {
int32 id = 1;
string nickName = 2;
string gender = 3;
uint64 birthDay = 4; // 本质是数字
}
message UserInfoResponse {
int32 id = 1;
string passWord = 2;
string mobile = 3;
string nickName = 4;
uint64 birthDay = 5;
string gender = 6;
int32 role = 7;
}
message UserListResponse {
int32 total = 1;
repeated UserInfoResponse data = 2; // repeated 数组
}
初始化
DB
初始化gorm-mysql数据库
- 连接mysql
- 注意dsn换为自己本地的
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"os"
"time"
)
func InitialDB() *gorm.DB {
dsn := "root:root@tcp(192.168.60.120:3306)/mymicro_user_srv?charset=utf8mb4&parseTime=True&loc=Local"
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // Log level
Colorful: true, // 禁用彩色打印
},
)
// 全局模式
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
Logger: newLogger,
})
if err != nil {
panic(err)
}
fmt.Println("==========connect database success=============")
//_ = db.AutoMigrate(&model.User{})
return db
}
server
开启服务
- 实例grpc服务 grpc.NewServer()
- 注册grpc
- 监听tcp连接
- 开启协程保持grpc连接
- 监听信号,优雅退出服务
import (
"context"
"flag"
"fmt"
"google.golang.org/grpc"
"mymicro/srvs/user_srv/handler"
"mymicro/srvs/user_srv/proto/proto"
"net"
"os"
"os/signal"
"syscall"
"time"
)
func RunServer() {
// flag 解析
addr := flag.String("ip", "localhost", "ip地址")
port := flag.Int("port", 8060, "端口")
flag.Parse()
// 注册
server := grpc.NewServer()
proto.RegisterUserServer(server, &handler.UserServer{})
// 开启监听
lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *addr, *port))
if err != nil {
panic("server listen fail:" + err.Error())
}
go func() {
err = server.Serve(lis)
if err != nil {
panic("failed to start grpc:" + err.Error())
}
}()
fmt.Printf("==============listen %s %d...==============\n", *addr, *port)
// 优雅退出服务
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
fmt.Println("=================quiting server...================")
go func(ctx context.Context) {
server.GracefulStop()
}(ctx)
fmt.Println("=================quit success================")
}
全局
定义全局App变量
- db
import "gorm.io/gorm"
type app struct {
DB *gorm.DB
}
var App = new(app)
用户服务接口
mymicro/srvs/user_srv/handler/user.go
抽离公有方法
用户返回信息 ModelToResponse
func ModelToResponse(user model.User) proto.UserInfoResponse { userInfoRsp := proto.UserInfoResponse{ Id: user.ID, PassWord: user.Password, NickName: user.NickName, Gender: user.Gender, Role: int32(user.Role), Mobile: user.Mobile, } if user.Birthday != nil { userInfoRsp.BirthDay = uint64(user.Birthday.Unix()) } return userInfoRsp }
分页方法 Paginate
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { if page == 0 { page = 1 } switch { case pageSize > 100: pageSize = 100 case pageSize <= 0: pageSize = 10 } offset := (page - 1) * pageSize return db.Offset(offset).Limit(pageSize) // 从offset+1行开始返回pagesize行 } }
用户列表接口
逻辑:
- 定义用户列表切片
- find查询所有用户,scope查出用户数
- 对用户分页处理
- 返回用户列表信息
func (s *UserServer) GetUserList(ctx context.Context, req *proto.PageInfo) (*proto.UserListResponse, error) {
var users []model.User
// 返回所有 list
res := global.App.DB.Find(&users)
if res.Error != nil {
return nil, res.Error
}
rsp := &proto.UserListResponse{}
// 返回字段数 count
rsp.Total = int32(res.RowsAffected)
// 分页
global.App.DB.Scopes(Paginate(int(req.Pn), int(req.PSize))).Find(&users)
for _, user := range users {
userInfo := ModelToResponse(user)
rsp.Data = append(rsp.Data, &userInfo)
}
return rsp, nil
}
通过手机号查询用户
逻辑:
- 定义用户模型
- where根据手机号查询用户,判断
- 返回用户信息
func (s *UserServer) GetUserByMobile(ctx context.Context, req *proto.MobileRequest) (*proto.UserInfoResponse, error) {
var user model.User
res := global.App.DB.Where(&model.User{
Mobile: req.Mobile,
}).First(&user)
if res.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "该用户不存在")
}
if res.Error != nil {
return nil, res.Error
}
userInfoRsp := ModelToResponse(user)
return &userInfoRsp, nil
}
通过id查询用户
逻辑:
- 定义用户模型
- 根据id查询用户,判断
- 返回用户信息
func (s *UserServer) GetUserById(ctx context.Context, req *proto.IdRequest) (*proto.UserInfoResponse, error) {
var user model.User
res := global.App.DB.First(&user, req.Id)
if res.RowsAffected == 0 {
return nil, status.Error(codes.NotFound, "该用户不存在")
}
if res.Error != nil {
return nil, res.Error
}
userInfoRsp := ModelToResponse(user)
return &userInfoRsp, nil
}
创建用户
逻辑:
- 定义用户模型
- 根据手机号查询,判断是否存在该用户
- 根据传入参数,create用户,记得用户加密,判断
- 返回用户信息
func (s *UserServer) CreateUser(ctx context.Context, req *proto.CreateUserInfo) (*proto.UserInfoResponse, error) {
var user model.User
// 判断用户是否存在
res := global.App.DB.Where(&model.User{
Mobile: req.Mobile,
}).First(&user)
if res.RowsAffected == 1 {
return nil, status.Errorf(codes.AlreadyExists, "该用户已存在")
}
user.Mobile = req.Mobile
user.NickName = req.NickName
pwd := cryptx.EncodePwd(req.PassWord)
fmt.Println(pwd)
user.Password = pwd
res = global.App.DB.Create(&user)
if res.Error != nil {
return nil, res.Error
}
userInfoRsp := ModelToResponse(user)
return &userInfoRsp, nil
}
更新用户信息
逻辑:
- 定义用户模型
- 通过id查询,是否存在该用户
- 根据传入参数,Save用户,注意时间类型处理,判断
- 返回响应信息
func (s *UserServer) UpdateUser(ctx context.Context, req *proto.UpdateUserInfo) (*emptypb.Empty, error) {
var user model.User
// 验证
res := global.App.DB.First(&user, req.Id)
if res.RowsAffected == 0 {
return nil, status.Errorf(codes.NotFound, "该用户不存在")
}
if res.Error != nil {
return nil, res.Error
}
birthday := time.Unix(int64(req.BirthDay), 0)
user.Birthday = &birthday
user.NickName = req.NickName
user.Gender = req.Gender
res = global.App.DB.Save(&user)
if res.Error != nil {
return nil, res.Error
}
return &emptypb.Empty{}, nil
}
校验密码
逻辑:
- 根据传入参数调用校验方法
- 返回
func (s *UserServer) CheckPassWord(ctx context.Context, req *proto.PasswordCheckInfo) (*proto.CheckResponse, error) {
check := cryptx.VerifyPwd(req.Password, req.EncryptedPassword)
return &proto.CheckResponse{Success: check}, nil
}
main 启动
srvs/user_srv/main.go
func main() {
// 初始化配置
// 初始化日志
// 初始化数据库
global.App.DB = initialize.InitialDB()
// 启动服务
initialize.RunServer()
}
用户服务 web
选用gin框架
注意:proto文件没有写说明,直接将srv层的proto复制到web就可以了
目录结构
utils工具集
目录判断 directory | redis锁标识 lockstr |加密 md5
directory apis/user_web/utils/directory.go
// PathExists 目录
func PathExists(path string) (bool, error) {
fi, err := os.Stat(path)
if err == nil {
if fi.IsDir() {
return true, nil
}
return false, errors.New("存在同名文件")
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
lockstr apis/user_web/utils/lockstr.go
// RandString 生成锁标识
func RandString(len int) string {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
bytes := make([]byte, len)
for i := 0; i < len; i++ {
b := r.Intn(26) + 65
bytes[i] = byte(b)
}
return string(bytes)
}
md5 apis/user_web/utils/md5.go
func GenMD5(code string) string {
Md5 := md5.New()
_, _ = io.WriteString(Md5, code)
return hex.EncodeToString(Md5.Sum(nil))
}
配置集成
配置文件
开发和生产config
开发 mymicro/apis/user_web/config-debug.yaml
app:
name: user_web
env: debug
port: 8070
url: http://localhost
# viper
# zap
log:
level: info # 开发日志级别
prefix: '[mycro/user_web]' # 日志前缀
format: console # 输出
director: log # 日志存放的文件
encode_level: LowercaseColorLevelEncoder # 编码级别
stacktrace_key: stacktrace # 栈名
max_age: 0 # 日志留存时间
show_line: true # 显示行
log_in_console: true # 输出控制台
# jwt
jwt:
secret: ;d/WOIx0:C}@qGHcpl'XuK2a+]FKS#SfS6$vr%g:Rm/XR6$*>:]`<{B^}v/T`Ow0 # 随机字符
jwt_ttl: 43200 # token 有效期(秒)
jwt_blacklist_grace_period: 10 # 黑名单宽限时间(秒)
refresh_grace_period: 1800
# redis
redis:
host: 127.0.0.1
port: 6379
db: 0
password:
生产 mymicro/apis/user_web/config-prod.yaml
app:
name: user_web
env: production
配置结构体
配置相关 config | app |日志 log |鉴权jwt |redis
mymicro/apis/user_web/config/app.go
package config
type App struct {
Name string `mapstructure:"name" json:"name" yaml:"name"`
Env string `mapstructure:"env" json:"env" yaml:"env"`
Port string `mapstructure:"port" json:"port" yaml:"port"`
Url string `mapstructure:"url" json:"url" yaml:"url"`
}
mymicro/apis/user_web/config/log.go
package config
import (
"go.uber.org/zap/zapcore"
"strings"
)
type Log struct {
Level string `mapstructure:"level" json:"level" yaml:"level"` // 级别
Prefix string `mapstructure:"prefix" json:"prefix" yaml:"prefix"` // 日志前缀
Format string `mapstructure:"format" json:"format" yaml:"format"` // 输出
Director string `mapstructure:"director" json:"director" yaml:"director"` // 日志文件夹
EncodeLevel string `mapstructure:"encode_level" json:"encode_level" yaml:"encode_level"` // 编码级
StacktraceKey string `mapstructure:"stacktrace_key" json:"stacktrace_key" yaml:"stacktrace_key"` // 栈名
MaxAge int `mapstructure:"max_age" json:"max_age" yaml:"max_age"` // 日志留存时间
ShowLine bool `mapstructure:"show_line" json:"show_line" yaml:"show_line"` // 显示行
LogInConsole bool `mapstructure:"log_in_console" json:"log_in_console" yaml:"log_in_console"` // 输出控制台
}
// ZapEncodeLevel 根据 EncodeLevel 返回 zapcore.LevelEncoder
func (l *Log) ZapEncodeLevel() zapcore.LevelEncoder {
switch {
case l.EncodeLevel == "LowercaseLevelEncoder": // 小写编码器(默认)
return zapcore.LowercaseLevelEncoder
case l.EncodeLevel == "LowercaseColorLevelEncoder": // 小写编码器带颜色
return zapcore.LowercaseColorLevelEncoder
case l.EncodeLevel == "CapitalLevelEncoder": // 大写编码器
return zapcore.CapitalLevelEncoder
case l.EncodeLevel == "CapitalColorLevelEncoder": // 大写编码器带颜色
return zapcore.CapitalColorLevelEncoder
default:
return zapcore.LowercaseLevelEncoder
}
}
// TransportLevel 根据字符串转化为 zapcore.Level
func (l *Log) TransportLevel() zapcore.Level {
l.Level = strings.ToLower(l.Level)
switch l.Level {
case "debug":
return zapcore.DebugLevel
case "info":
return zapcore.InfoLevel
case "warn":
return zapcore.WarnLevel
case "error":
return zapcore.WarnLevel
case "dpanic":
return zapcore.DPanicLevel
case "panic":
return zapcore.PanicLevel
case "fatal":
return zapcore.FatalLevel
default:
return zapcore.DebugLevel
}
}
jwt apis/user_web/config/jwt.go
type Config struct {
App App `mapstructure:"app" json:"app" yaml:"app"`
Log Log `mapstructure:"log" json:"log" yaml:"log"`
Jwt Jwt `mapstructure:"jwt" json:"jwt" yaml:"jwt"`
Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"`
}
redis apis/user_web/config/redis.go
type Redis struct {
Host string `mapstructure:"host" json:"host" yaml:"host"`
Port string `mapstructure:"port" json:"port" yaml:"port"`
DB int `mapstructure:"db" json:"db" yaml:"db"`
Password string `mapstructure:"password" json:"password" yaml:"password"`
}
config apis/user_web/config/config.go
type Config struct {
App App `mapstructure:"app" json:"app" yaml:"app"`
Log Log `mapstructure:"log" json:"log" yaml:"log"`
Jwt Jwt `mapstructure:"jwt" json:"jwt" yaml:"jwt"`
Redis Redis `mapstructure:"redis" json:"redis" yaml:"redis"`
}
自定义环境常量
mymicro/apis/user_web/initialize/internal/cfgEnv.go
const (
DebugEnv = "config-debug.yaml"
ProdEnv = "config-prod.yaml"
)
viper集成
mymicro/apis/user_web/initialize/config.go
逻辑:
- 按照
配置文件选择 参数>flag解析>自定义常量>gin环境
进行配置文件选择- new viper实例,配置viper
- readinconfig 读配置
- watchconfig 监控
- 反序列化绑定到全局config struct
package initialize
import (
"flag"
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"os"
"user_web/global"
"user_web/initialize/internal"
)
func InitConfig(args ...string) *viper.Viper {
// 配置文件选择 参数>flag解析>自定义常量>gin环境
var config string
if len(args) == 0 {
flag.StringVar(&config, "c", "", "choose config file")
flag.Parse()
if config == "" {
if os.Getenv(internal.DebugEnv) == "" {
switch gin.Mode() {
case gin.DebugMode:
config = internal.DebugEnv
case gin.ReleaseMode:
config = internal.ProdEnv
}
} else {
config = internal.DebugEnv
}
}
} else {
config = args[0]
}
fmt.Println("config :", config)
// viper配置
vip := viper.New()
vip.SetConfigFile(config)
vip.SetConfigType("yaml")
// 读
err := vip.ReadInConfig()
if err != nil {
panic(err.Error())
}
// 监控
vip.WatchConfig()
// 开启监控
vip.OnConfigChange(func(in fsnotify.Event) {
change := in.Name
fmt.Printf("%s changed", change)
// 反序列化
if err = vip.Unmarshal(&global.App.Config); err != nil {
panic(err.Error())
}
})
// 绑定
if err = vip.Unmarshal(&global.App.Config); err != nil {
panic(err.Error())
}
fmt.Println("==========config init success=============")
return vip
}
全局配置
type app struct {
Config config.Config
Vip *viper.Viper
}
var App = new(app)
日志集成
说明:选择 主流zap日志库 集成到gin
日志切割
mymicro/apis/user_web/initialize/internal/file_rotate_logs.go
- 定义 WriteSyncer
import (
rotatelogs "github.com/lestrrat-go/file-rotatelogs"
"go.uber.org/zap/zapcore"
"os"
"path"
"time"
"user_web/global"
)
var FileRotateLogs = new(fileRotateLogs)
type fileRotateLogs struct {
}
func (f *fileRotateLogs) GetWriteSyncer(level string) (zapcore.WriteSyncer, error) {
fileWriter, err := rotatelogs.New(
// 输出文件名
path.Join(global.App.Config.Log.Director, "%Y-%m-%d", level+".log"),
rotatelogs.WithClock(rotatelogs.Local),
rotatelogs.WithMaxAge(time.Duration(global.App.Config.Log.MaxAge)*24*time.Hour), // 日志留存时间
rotatelogs.WithRotationTime(time.Hour*24))
if global.App.Config.Log.LogInConsole {
return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(fileWriter)), err
}
return zapcore.AddSync(fileWriter), err
}
定制化zap
mymicro/apis/user_web/initialize/internal/zap.go
zap配置
- 编码器
- 日志切割
- 自定义时间输出格式
- 日志等级
import (
"fmt"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"time"
"user_web/global"
)
var Zap = new(_zap)
type _zap struct{}
// GetEncoder 获取 zapcore.Encoder
func (z *_zap) GetEncoder() zapcore.Encoder {
if global.App.Config.Log.Format == "json" {
return zapcore.NewJSONEncoder(z.GetEncoderConfig())
}
return zapcore.NewConsoleEncoder(z.GetEncoderConfig())
}
// GetEncoderConfig 获取zapcore.EncoderConfig
func (z *_zap) GetEncoderConfig() zapcore.EncoderConfig {
return zapcore.EncoderConfig{
MessageKey: "message",
LevelKey: "level",
TimeKey: "time",
NameKey: "logger",
CallerKey: "caller",
StacktraceKey: global.App.Config.Log.StacktraceKey,
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: global.App.Config.Log.ZapEncodeLevel(),
EncodeTime: z.CustomTimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.FullCallerEncoder,
}
}
// GetEncoderCore 获取Encoder的 zapcore.Core
func (z *_zap) GetEncoderCore(l zapcore.Level, level zap.LevelEnablerFunc) zapcore.Core {
writer, err := FileRotateLogs.GetWriteSyncer(l.String()) // 使用file-rotatelogs进行日志分割
if err != nil {
fmt.Printf("Get Write Syncer Failed err:%v", err.Error())
return nil
}
return zapcore.NewCore(z.GetEncoder(), writer, level)
}
// CustomTimeEncoder 自定义日志输出时间格式
func (z *_zap) CustomTimeEncoder(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(global.App.Config.Log.Prefix + " - " + t.Format("2006/01/02 - 15:04:05.000"))
}
// GetZapCores 根据配置文件的Level获取 []zapcore.Core
func (z *_zap) GetZapCores() []zapcore.Core {
cores := make([]zapcore.Core, 0, 7)
for level := global.App.Config.Log.TransportLevel(); level <= zapcore.FatalLevel; level++ {
cores = append(cores, z.GetEncoderCore(level, z.GetLevelPriority(level)))
}
return cores
}
// GetLevelPriority 根据 zapcore.Level 获取 zap.LevelEnablerFunc
func (z *_zap) GetLevelPriority(level zapcore.Level) zap.LevelEnablerFunc {
switch level {
case zapcore.DebugLevel:
return func(level zapcore.Level) bool { // 调试级别
return level == zap.DebugLevel
}
case zapcore.InfoLevel:
return func(level zapcore.Level) bool { // 日志级别
return level == zap.InfoLevel
}
case zapcore.WarnLevel:
return func(level zapcore.Level) bool { // 警告级别
return level == zap.WarnLevel
}
case zapcore.ErrorLevel:
return func(level zapcore.Level) bool { // 错误级别
return level == zap.ErrorLevel
}
case zapcore.DPanicLevel:
return func(level zapcore.Level) bool { // dpanic级别
return level == zap.DPanicLevel
}
case zapcore.PanicLevel:
return func(level zapcore.Level) bool { // panic级别
return level == zap.PanicLevel
}
case zapcore.FatalLevel:
return func(level zapcore.Level) bool { // 终止级别
return level == zap.FatalLevel
}
default:
return func(level zapcore.Level) bool { // 调试级别
return level == zap.DebugLevel
}
}
}
目录工具
mymicro/apis/user_web/utils/directory.go
判断是否存在目录
// PathExists 目录
func PathExists(path string) (bool, error) {
fi, err := os.Stat(path)
if err == nil {
if fi.IsDir() {
return true, nil
}
return false, errors.New("存在同名文件")
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
集成zap
mymicro/apis/user_web/initialize/log.go
逻辑:
- 日志输出目录
- 集成zapcore
func InitLog() *zap.Logger {
if ok, _ := utils.PathExists(global.App.Config.Log.Director); !ok {
log.Printf("create directory %s", global.App.Config.Log.Director)
_ = os.Mkdir(global.App.Config.Log.Director, os.ModePerm)
}
cores := internal.Zap.GetZapCores()
logger := zap.New(zapcore.NewTee(cores...))
if global.App.Config.Log.ShowLine {
logger = logger.WithOptions(zap.AddCaller())
}
log.Println("zap log init success")
return logger
}
自定义校验
校验工具
mymicro/apis/user_web/utils/validator.go
逻辑:
- 定义validator接口
- 定义验证错误信息方法
- 自定义验证规则
type ValidatorMessages map[string]string
type Validator interface {
GetMessages() ValidatorMessages
}
func GetValidatorMsg(req interface{}, err error) string {
if _, isValidatorErr := err.(validator.ValidationErrors); isValidatorErr {
_, isValidator := req.(Validator)
for _, v := range err.(validator.ValidationErrors) {
if isValidator {
if msg, exist := req.(Validator).GetMessages()[v.Field()+"."+v.Tag()]; exist {
return msg
}
}
}
}
return "parameter error"
}
// ValidateMobile 校验mobile
func ValidateMobile(fl validator.FieldLevel) bool {
mobile := fl.Field().String()
ok, _ := regexp.MatchString(`^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$`, mobile)
if !ok {
return false
}
return true
}
// ValidatorEmail 验证email
func ValidatorEmail(fl validator.FieldLevel) bool {
email := fl.Field().String()
ok, _ := regexp.MatchString(`^[a-zA-Z0-9.!#$%&’*+/=?^_`+`){|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$`, email)
if !ok {
return false
}
return true
}
校验规则
mymicro/apis/user_web/forms/user.go
// PageInfoForm 列表分页
type PageInfoForm struct {
Pn uint32 `form:"pn" json:"pn" binding:"required"`
PSize uint32 `form:"p_size" json:"p_size" binding:"required"`
}
type MobileReqForm struct {
Mobile string `binding:"required,mobile" json:"mobile" form:"mobile"`
}
type IdReqForm struct {
Id int32 `binding:"required" form:"id" json:"id"`
}
type CreateInfoForm struct {
Nickname string `binding:"required" form:"nickname" json:"nickname"`
Password string `binding:"required,min=6,max=32" json:"password" form:"password"`
Mobile string `json:"mobile" form:"mobile" binding:"required,mobile"`
//Code string `json:"code" form:"code" binding:"required,min=6,max=6"`
}
type UpdateInfoForm struct {
Id int32 `binding:"required" form:"id" json:"id"`
Nickname string `binding:"required" form:"nickname" json:"nickname"`
Gender string `binding:"required" form:"gender" json:"gender"`
Birthday uint64 `binding:"required" form:"birthday" json:"birthday"`
}
type PasswordCheckForm struct {
Password string `binding:"required,min=6,max=32" json:"password" form:"password"`
}
type LoginInfo struct {
Password string `binding:"required" json:"password" form:"password"`
Mobile string `binding:"required,mobile" json:"mobile" form:"mobile"`
CaptchaCode string `binding:"required,min=5,max=5" json:"captcha_code" form:"captcha_code"`
CaptchaId string `form:"captcha_id" json:"captcha_id" binding:"required"`
}
func (l LoginInfo) GetMessages() utils.ValidatorMessages {
return utils.ValidatorMessages{
"mobile.required": "手机号不能为空",
"mobile.mobile": "手机号格式不对",
"captcha_code.required": "验证码不能为空",
"captcha_id.required": "id不能为空",
}
}
func (p PageInfoForm) GetMessages() utils.ValidatorMessages {
return utils.ValidatorMessages{
"pn.required": "页码不能为空",
"p_size.required": "页数不能为空",
"password.required": "密码不能为空",
}
}
func (m MobileReqForm) GetMessages() utils.ValidatorMessages {
return utils.ValidatorMessages{
"mobile.required": "手机号不能为空",
"mobile.mobile": "手机号格式不对",
}
}
func (I IdReqForm) GetMessages() utils.ValidatorMessages {
return utils.ValidatorMessages{
"id.required": "用户id不能为空",
}
}
func (c CreateInfoForm) GetMessages() utils.ValidatorMessages {
return utils.ValidatorMessages{
"mobile.required": "手机号不能为空",
"mobile.mobile": "手机号格式不对",
"password.required": "密码不能为空",
"nickname.required": "用户昵称不能为空",
//"code.required": "短信验证码不能为空",
}
}
func (u UpdateInfoForm) GetMessages() utils.ValidatorMessages {
return utils.ValidatorMessages{
"id.required": "用户id不能为空",
"nickname.required": "用户昵称不能为空",
"gender.required": "用户性别不能为空",
"birthday.required": "用户生日不能为空",
}
}
func (pw PasswordCheckForm) GetMessages() utils.ValidatorMessages {
return utils.ValidatorMessages{
"password.required": "密码不能为空",
"encryptedPassword.required": "验证密码不能为空",
}
}
集成Validator
mymicro/apis/user_web/initialize/validator.go
逻辑:
- 注册自定义validator
// InitValidator 注册validator
func InitValidator() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册自定义验证器
_ = v.RegisterValidation("mobile", utils.ValidateMobile)
_ = v.RegisterValidation("email", utils.ValidatorEmail)
// 注册自定义 json tag 函数
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
zap.S().Info("validator init success")
} else {
zap.S().Error("validator register failer")
}
}
redis集成
apis/user_web/initialize/redis.go
- new redis 实例并配置连接信息
- 返回redis实例
import (
"context"
"github.com/go-redis/redis/v8"
"go.uber.org/zap"
"user_web/global"
)
func InitializeRedis() *redis.Client {
client := redis.NewClient(&redis.Options{
Addr: global.App.Config.Redis.Host + ":" + global.App.Config.Redis.Port,
Password: global.App.Config.Redis.Password,
DB: global.App.Config.Redis.DB,
})
_, err := client.Ping(context.Background()).Result()
if err != nil {
global.App.Log.Error("redis connect failed err:", zap.Any("err", err))
return nil
}
zap.S().Info("redis init success")
return client
}
全局错误和Response
全局错误
mymicro/apis/user_web/global/errorx/error.go
/*
CustomError
CustomErrors
Errors
*/
type CustomError struct {
ErrorCode int
ErrorMsg string
}
type CustomErrors struct {
ValidatorError CustomError
JWTError CustomError
BusinessError CustomError
}
var Errors = CustomErrors{
ValidatorError: CustomError{
ErrorCode: 401000,
ErrorMsg: "参数验证错误",
},
JWTError: CustomError{
ErrorCode: 404000,
ErrorMsg: "JWT 未授权",
},
BusinessError: CustomError{
ErrorCode: 440000,
ErrorMsg: "业务错误",
},
}
全局response
mymicro/apis/user_web/global/response/response.go
- 成功
- 失败
- 自定义
/*
Response
Success
Fail
FailError
FailByError
ValidatorError
JWTError
*/
type Response struct {
Code int
Msg string
Data interface{}
}
func Success(ctx *gin.Context, data interface{}) {
ctx.JSON(http.StatusOK, Response{
Code: 200,
Msg: "request success",
Data: data,
})
}
func Fail(ctx *gin.Context, code int, msg string) {
ctx.JSON(http.StatusOK, Response{
Code: code,
Msg: msg,
Data: nil,
})
}
func FailByError(ctx *gin.Context, err errorx.CustomError) {
Fail(ctx, err.ErrorCode, err.ErrorMsg)
}
func ValidatorError(ctx *gin.Context, msg string) {
Fail(ctx, errorx.Errors.ValidatorError.ErrorCode, msg)
}
func JWTError(ctx *gin.Context) {
FailByError(ctx, errorx.Errors.JWTError)
}
func BusinessError(ctx *gin.Context) {
FailByError(ctx, errorx.Errors.BusinessError)
}
全局变量
apis/user_web/global/global.go
type app struct {
Config config.Config
Vip *viper.Viper
Srv proto.UserClient
Log *zap.Logger
SrvConn *grpc.ClientConn
Redis *redis.Client
}
var App = new(app)
全局redis 分布式锁
apis/user_web/global/lock.go
import (
"context"
"github.com/go-redis/redis/v8"
"time"
"user_web/utils"
)
type Interface interface {
Get() bool
Block(seconds int64) bool
Release() bool
ForceRelease()
}
type lock struct {
context context.Context
name string // 锁名字
owner string // 锁标识
seconds int64 // 有效期
}
// 释放锁lua脚本,防止任何客户端都能解锁
// 通过两个数组KEYS和ARGV来传递值,KEYS数组用于传递键名,而ARGV数组用于传递其他值。
const releaseLockLuaScript = `
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
`
// Lock 生成锁
func Lock(name string, seconds int64) Interface {
return &lock{
context.Background(),
name,
utils.RandString(16),
seconds,
}
}
// Get 获取锁
func (l *lock) Get() bool {
// SETNX命令获取锁
return App.Redis.SetNX(l.context, l.name, l.owner, time.Duration(l.seconds)*time.Second).Val() // context key value ttl
}
// Block 阻塞一段时间,尝试获取锁
func (l *lock) Block(seconds int64) bool {
starting := time.Now().Unix()
for {
if !l.Get() {
time.Sleep(time.Duration(1) * time.Second)
if time.Now().Unix()-seconds >= starting {
return false
}
} else {
return true
}
}
}
// Release 释放锁
func (l *lock) Release() bool {
luaScript := redis.NewScript(releaseLockLuaScript)
result := luaScript.Run(l.context, App.Redis, []string{l.name}, l.owner).Val().(int64)
return result != 0
}
// ForceRelease 强制释放锁
func (l *lock) ForceRelease() {
App.Redis.Del(l.context, l.name).Val()
}
连接grpc
mymicro/apis/user_web/initialize/rpc_conn.go
- dial
- client
func ConnectUserGrpc() (*proto.UserClient, *grpc.ClientConn) {
conn, err := grpc.Dial("127.0.0.1:8059", grpc.WithInsecure())
if err != nil {
panic(err.Error())
}
client := proto.NewUserClient(conn)
return &client, conn
}
user api
路由
apis/user_web/router/router.go
路由接口
- 根据自己需求,定义接口
- 扩展路由组和使用中间件
func SetRouterGroupApi(r *gin.RouterGroup) {
/* test 测试路由 */
r.GET("/ping", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "pong")
})
r.POST("/valid", func(ctx *gin.Context) {
var register forms.CreateInfoForm
if err := ctx.ShouldBindJSON(®ister); err != nil {
response.ValidatorError(ctx, utils.GetValidatorMsg(register, err))
} else {
response.Success(ctx, "hello world")
}
})
/* 用户接口*/
// 注册
r.POST("/register", api.RegisterHandler)
// 登录
r.POST("/login", api.LoginHandler)
// 图片验证码
r.GET("/captcha", api.GetCaptcha)
// 短信验证码
r.GET("/sms")
authRouter := r.Group("/auth").Use(middlewares.JWTAuth(api.AppGuardName))
{
// 用户列表
authRouter.POST("/userlist", api.UserListHandler)
// 手机查用户
authRouter.POST("/infoBymobile", api.UserByMobileHandler)
// id查用户
authRouter.POST("/infoByid", api.UserByIDHandler)
// 更新用户
authRouter.POST("/updateinfo", api.UpdateHandler)
// 退出登录
authRouter.POST("/logout", api.LogoutHandler)
}
r.Use(middlewares.Cors())
}
中间件
跨域cors | jwt鉴权
cors apis/user_web/middlewares/cors.go
import (
"github.com/gin-gonic/gin"
"net/http"
)
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token, x-token")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH, PUT")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
}
}
jwt apis/user_web/middlewares/jwt.go
import (
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"strconv"
"time"
"user_web/api"
"user_web/global"
"user_web/global/response"
)
func JWTAuth(GuardName string) gin.HandlerFunc {
return func(c *gin.Context) {
tokenStr := c.Request.Header.Get("Authorization")
if tokenStr == "" {
response.JWTError(c)
c.Abort()
return
}
tokenStr = tokenStr[len(api.TokenType)+1:]
// token解析校验
token, err := jwt.ParseWithClaims(tokenStr, &api.CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(global.App.Config.Jwt.Secret), nil
})
// 增加黑名单校验
if err != nil || api.JwtService.IsInBlackList(tokenStr) {
response.JWTError(c)
c.Abort()
return
}
claims := token.Claims.(*api.CustomClaims)
// token发布者校验
if claims.Issuer != GuardName {
response.JWTError(c)
c.Abort()
return
}
// token 续签
if claims.ExpiresAt-time.Now().Unix() < global.App.Config.Jwt.RefreshGracePeriod {
lock := global.Lock("refresh_token_lock", global.App.Config.Jwt.JwtBlacklistGracePeriod)
id, _ := strconv.Atoi(claims.Id)
if lock.Get() {
user, err := api.JwtService.GetUserInfo(GuardName, int32(id))
if err != nil {
global.App.Log.Error(err.Error())
lock.Release()
} else {
tokenData, _, _ := api.JwtService.CreateToken(GuardName, user)
c.Header("new-token", tokenData.AccessToken)
c.Header("new-expires-in", strconv.Itoa(tokenData.ExpiresIn))
_ = api.JwtService.JoinBlackList(token)
}
}
}
c.Set("token", token)
c.Set("id", claims.Id)
}
}
路由handler
用户接口
apis/user_web/api/user.go
import (
"context"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"user_web/forms"
"user_web/global"
"user_web/global/response"
"user_web/proto/proto"
"user_web/utils"
)
// RegisterHandler 注册
func RegisterHandler(ctx *gin.Context) {
var form forms.CreateInfoForm
if err := ctx.ShouldBindJSON(&form); err != nil {
response.ValidatorError(ctx, utils.GetValidatorMsg(form, err))
return
}
// 校验短信
// reg := 正则匹配
//if form.Code != res_.code() {
// response.Fail(ctx,498000,"验证码错误")
//}
res, err := global.App.Srv.CreateUser(context.Background(), &proto.CreateUserInfo{
NickName: form.Nickname,
Mobile: form.Mobile,
PassWord: form.Password,
})
if err != nil {
response.Fail(ctx, 40000, err.Error())
return
}
response.Success(ctx, res)
}
// LoginHandler 登录
func LoginHandler(ctx *gin.Context) {
var login forms.LoginInfo
if err := ctx.ShouldBindJSON(&login); err != nil {
response.ValidatorError(ctx, utils.GetValidatorMsg(login, err))
return
}
// 校验验证码
//fmt.Println(store.Get(login.CaptchaId, false))
//if !store.Verify(login.CaptchaId, login.CaptchaCode, false) {
// response.Fail(ctx, 483000, "验证码错误")
// return
//}
// 校验短信
// 查用户
res, err := global.App.Srv.GetUserByMobile(context.Background(), &proto.MobileRequest{
Mobile: login.Mobile,
})
if err != nil {
response.Fail(ctx, 440000, utils.GetValidatorMsg(login, err))
return
}
// 校验密码
resp, err := global.App.Srv.CheckPassWord(context.Background(), &proto.PasswordCheckInfo{
Password: login.Password,
EncryptedPassword: res.PassWord,
})
if err != nil || !resp.Success {
response.Fail(ctx, 440000, "密码错误")
return
}
tokenData, err, _ := JwtService.CreateToken(AppGuardName, res)
if err != nil {
response.Fail(ctx, 470000, err.Error())
return
}
response.Success(ctx, tokenData)
}
// LogoutHandler 退出登录
func LogoutHandler(ctx *gin.Context) {
err := JwtService.JoinBlackList(ctx.Keys["token"].(*jwt.Token))
if err != nil {
response.Fail(ctx, 43200, "退出登录 fail")
}
response.Success(ctx, "退出登录 success")
}
// UserListHandler 用户列表
func UserListHandler(ctx *gin.Context) {
var list forms.PageInfoForm
if err := ctx.ShouldBindJSON(&list); err != nil {
response.ValidatorError(ctx, utils.GetValidatorMsg(list, err))
return
}
res, err := global.App.Srv.GetUserList(context.Background(), &proto.PageInfo{
Pn: list.Pn,
PSize: list.PSize,
})
if err != nil {
response.Fail(ctx, 45000, err.Error())
return
}
response.Success(ctx, res)
}
// UserByIDHandler 通过id获取用户
func UserByIDHandler(ctx *gin.Context) {
var id forms.IdReqForm
if err := ctx.ShouldBindJSON(&id); err != nil {
response.ValidatorError(ctx, utils.GetValidatorMsg(id, err))
return
}
res, err := global.App.Srv.GetUserById(context.Background(), &proto.IdRequest{
Id: id.Id,
})
if err != nil {
response.Fail(ctx, 43100, err.Error())
}
response.Success(ctx, res)
}
// UpdateHandler 更新用户
func UpdateHandler(ctx *gin.Context) {
var info forms.UpdateInfoForm
if err := ctx.ShouldBindJSON(&info); err != nil {
response.ValidatorError(ctx, utils.GetValidatorMsg(info, err))
return
}
res, err := global.App.Srv.UpdateUser(ctx, &proto.UpdateUserInfo{
Id: info.Id,
NickName: info.Nickname,
Gender: info.Gender,
BirthDay: info.Birthday,
})
if err != nil {
response.Fail(ctx, 45600, err.Error())
return
}
response.Success(ctx, res)
}
// UserByMobileHandler 通过mobile获取用户
func UserByMobileHandler(ctx *gin.Context) {
var mobile forms.MobileReqForm
if err := ctx.ShouldBindJSON(&mobile); err != nil {
response.ValidatorError(ctx, utils.GetValidatorMsg(mobile, err))
return
}
res, err := global.App.Srv.GetUserByMobile(context.Background(), &proto.MobileRequest{
Mobile: mobile.Mobile,
})
if err != nil {
response.Fail(ctx, 457000, err.Error())
return
}
response.Success(ctx, res)
}
腾讯云短信接口
apis/user_web/api/sms.go
/*
腾讯云短信sms接口
Go 1.9版本及以上
*/
import (
"github.com/gin-gonic/gin"
"user_web/global/response"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111"
)
var res_ *sms.SendSmsResponse
func GetSMS(ctx *gin.Context) {
// 实例化一个认证对象,入参需要传入腾讯云账户 SecretId 和 SecretKey,此处还需注意密钥对的保密
// 代码泄露可能会导致 SecretId 和 SecretKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考,建议采用更安全的方式来使用密钥,请参见:https://cloud.tencent.com/document/product/1278/85305
// 密钥可前往官网控制台 https://console.cloud.tencent.com/cam/capi 进行获取
credential := common.NewCredential(
"SecretId",
"SecretKey",
)
// 实例化一个client选项,可选的,没有特殊需求可以跳过
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "sms.tencentcloudapi.com"
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := sms.NewClient(credential, "ap-nanjing", cpf)
// 实例化一个请求对象,每个接口都会对应一个request对象
request := sms.NewSendSmsRequest()
request.PhoneNumberSet = common.StringPtrs([]string{"15533332222"})
request.SmsSdkAppId = common.StringPtr("1400661435")
request.SignName = common.StringPtr("汐瀼测试")
request.TemplateId = common.StringPtr("1362776")
// 返回的resp是一个SendSmsResponse的实例,与请求对象对应
res_, err := client.SendSms(request)
if _, ok := err.(*errors.TencentCloudSDKError); ok {
response.Fail(ctx, 467000, "调用sdk失败")
return
}
if err != nil {
response.Fail(ctx, 467000, err.Error())
}
// 输出json格式的字符串回包
//fmt.Printf("%s", response.ToJsonString())
response.Success(ctx, res_.ToJsonString())
}
jwt授权接口
apis/user_web/api/jwt.go
type jwtService struct {
}
var JwtService = new(jwtService)
// CustomClaims 自定义claims
type CustomClaims struct {
jwt.StandardClaims
}
const (
TokenType = "bearer"
AppGuardName = "app"
)
type TokenOutput struct {
AccessToken string `yaml:"access_token"`
ExpiresIn int `json:"expires-in"`
TokenType string `json:"token-type"`
}
// CreateToken 生成token
func (js *jwtService) CreateToken(GuardName string, user *proto.UserInfoResponse) (tokenData TokenOutput, err error, token *jwt.Token) {
// crypt.claims.secret
token = jwt.NewWithClaims(
jwt.SigningMethodHS256,
CustomClaims{
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Unix() + global.App.Config.Jwt.JwtTtl,
Id: strconv.Itoa(int(user.Id)),
Issuer: GuardName, // 用于在中间件区分不同客户端发的token,避免token跨端使用
NotBefore: time.Now().Unix() - 1000,
},
},
)
tokenStr, err := token.SignedString([]byte(global.App.Config.Jwt.Secret))
tokenData = TokenOutput{
tokenStr,
int(global.App.Config.Jwt.JwtTtl),
TokenType,
}
return
}
// 获取黑名单缓存key
func (js *jwtService) getBlackListKey(tokenStr string) string {
return "jwt_black_list:" + utils.GenMD5(tokenStr)
}
// JoinBlackList token加入黑名单
func (js *jwtService) JoinBlackList(token *jwt.Token) (err error) {
nowUnix := time.Now().Unix()
timer := time.Duration(token.Claims.(*CustomClaims).ExpiresAt-nowUnix) * time.Second
// 将token剩余时间设置为缓存有效期,并将当前时间作为缓存value值
err = global.App.Redis.SetNX(context.Background(), js.getBlackListKey(token.Raw), nowUnix, timer).Err()
return
}
// IsInBlackList token是否在黑名单中
func (js *jwtService) IsInBlackList(tokenStr string) bool {
joinUnixStr, err := global.App.Redis.Get(context.Background(), js.getBlackListKey(tokenStr)).Result()
joinUnix, err := strconv.ParseInt(joinUnixStr, 10, 64)
if joinUnixStr == "" || err != nil {
return false
}
// JwtBlacklistGracePeriod 为黑名单宽限时间,避免并发请求失败
if time.Now().Unix()-joinUnix < global.App.Config.Jwt.JwtBlacklistGracePeriod {
return false
}
return true
}
// GetUserInfo 根据不同客户端token,查询不同用户表数据
func (js *jwtService) GetUserInfo(GuardName string, id int32) (info *proto.UserInfoResponse, err error) {
switch GuardName {
case AppGuardName:
return global.App.Srv.GetUserById(context.Background(), &proto.IdRequest{
Id: id,
})
default:
err = errors.New("guard" + GuardName + "does not exist")
}
return
}
图片验证码接口
apis/user_web/api/captcha.go
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
"user_web/global/response"
)
var store = base64Captcha.DefaultMemStore
func GetCaptcha(ctx *gin.Context) {
driver := base64Captcha.NewDriverDigit(80, 240, 5, 0.7, 80)
cp := base64Captcha.NewCaptcha(driver, store)
id, b64s, err := cp.Generate()
if err != nil {
//zap.S().Errorf("验证码生成失败", err.Error())
response.Fail(ctx, 479000, "验证码生成失败")
return
}
fmt.Println(store.Get(id, false))
response.Success(ctx, gin.H{
"id": id,
"pic": b64s,
})
}
server启动
server启动
mymicro/apis/user_web/initialize/server.go
逻辑
- 实例gin
- 监听服务
- 优雅退出服务
package initialize
import (
"context"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"user_web/global"
"user_web/router"
)
func RunServer() {
r := setRouterApi()
server := &http.Server{
Addr: ":" + global.App.Config.App.Port,
Handler: r,
}
zap.S().Infof("server start on port %s", global.App.Config.App.Port)
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
zap.S().Fatal(err.Error())
}
}()
// 优雅退出
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
_ = global.App.SrvConn.Close()
zap.S().Info("grpc quit")
zap.S().Info("server quiting")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
zap.S().Error(err.Error())
}
zap.S().Info("server quited")
}
func setRouterApi() *gin.Engine {
gin.SetMode(gin.DebugMode)
r := gin.Default()
g := r.Group("/user")
router.SetRouterGroupApi(g)
return r
}
main 启动
apis/user_web/main.go
import (
"go.uber.org/zap"
"user_web/global"
"user_web/initialize"
)
func main() {
// 初始化配置
global.App.Vip = initialize.InitConfig()
// 初始化日志
global.App.Log = initialize.InitLog()
zap.ReplaceGlobals(global.App.Log)
// 初始化验证器
initialize.InitValidator()
// 连接redis
global.App.Redis = initialize.InitializeRedis()
// 初始化 grpc 连接
global.App.Srv, global.App.SrvConn = initialize.ConnectUserGrpc()
// 初始化server
initialize.RunServer()
}