• 12、用户微服务


    • 项目目录结构说明
      在这里插入图片描述

    一、创建用户表结构

    • 1、数据库创建
      在这里插入图片描述
    • 2、user_srv\model\user.go
    package model
    
    import (
    	"time"
    
    	"gorm.io/gorm"
    )
    
    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
    }
    
    type User struct {
    	BaseModel
    	Mobile   string     `gorm:"index:idx_mobile;unique;type:varchar(11);not null"`
    	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表示男'"`
    	Role     int        `gorm:"column:role;default:1;type:int comment '1表示普通用户, 2表示管理员'"`
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 3、user_srv\model\main\main.go:用户名密码自行修改
    package main
    
    import (
    	"log"
    	"nd/user_srv/model"
    	"os"
    	"time"
    
    	"gorm.io/driver/mysql"
    	"gorm.io/gorm"
    	"gorm.io/gorm/logger"
    	"gorm.io/gorm/schema"
    )
    
    func main() {
    	dsn := "root:root@tcp(192.168.124.51:3306)/mxshop_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)
    	}
    	// 迁移 schema
    	_ = db.AutoMigrate(&model.User{}) //此处应该有sql语句
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 4、执行user_srv\model\main\main.go:使用gorm自动创建用户表

    在这里插入图片描述
    在这里插入图片描述

    二、md5盐值加密

    1 - 加密算法

    • 为什么需要md5盐值加密:因为用户的密码保存在数据库,我们不希望是以明文的形式保存在数据库中
    • 一般使用密文保存用户密码:因为密文保存密码不可反解
    • 密文加密方法
      • 对称加密:加解密使用的是同一把钥匙,隐患 -> 只要拿到钥匙就可以破解所有数据库的密码
      • 非对称加密:加解密使用的不是同一把钥匙,不满足密码不可反解
      • md5算法:严格来说不是加密算法,是信息摘要算法;可以保证密码不可反解(如果密码不可反解,也就是说用户是无法找回密码的;现代的处理方式是提供链接让用户重新设置密码)

    2 - md5加密算法

    • 什么是md5:Message Digest Algorithm 5,信息摘要算法
    • md5算法的优点
      • ①.压缩性:任意长度的数据,算出md5值长度都是固定的
      • ②.容易计算:从原数据计算出md5值很容易
      • ③.抗修改性:对原数据进行任何修改,哪怕1个字节,md5值差异很大
      • ④.强碰撞:想找到两个不同的数据,使它们具有相同的md5值,非常困难
      • ⑤.不可逆性:不可反解
    • 什么是md5加盐
      • 通过生成随机数和md5生成字符串进行组合
      • 数据库同时存储md5值和salt值,验证正确性使用salt进行md5即可
    package main
    
    import (
    	"crypto/md5"
    	"encoding/hex"
    	"fmt"
    	"io"
    )
    
    func genMd5(code string) string {
    	Md5 := md5.New()
    	_, _ = io.WriteString(Md5, code)
    	return hex.EncodeToString(Md5.Sum(nil))
    }
    
    func main() {
    	fmt.Println(genMd5("123456"))
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3 - md5盐值加密

    package main
    
    import (
    	"crypto/sha512"
    	"fmt"
    
    	"github.com/anaskhan96/go-password-encoder"
    )
    
    func main() {
    	options := &password.Options{SaltLen: 16, Iterations: 100, KeyLen: 32, HashFunction: sha512.New}
    	salt, encodedPwd := password.Encode("generic password", options)
    	fmt.Println(salt)
    	fmt.Println(encodedPwd)
    	check := password.Verify("generic password", salt, encodedPwd, options)
    	fmt.Println(check) // true
    }
    
    //输出
    //lJSijLVv22RCvDqP
    //04440c79158eaa04e1ef7e1d45b00e6724d264624a75eded308269366c221f13
    //true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    4 - 拼接算法、salt、encodedPwd

    • 算法、salt、encodedPwd
      • 拼接这3个字符串一起保存在数据库中
      • 解析的时候需要分割出来
      • 并且需要注意拼接后的长度不要超过数据库定义的长度
    package main
    
    import (
    	"crypto/sha512"
    	"fmt"
    	"strings"
    
    	"github.com/anaskhan96/go-password-encoder"
    )
    
    func main() {
    	options := &password.Options{SaltLen: 16, Iterations: 100, KeyLen: 32, HashFunction: sha512.New}
    	salt, encodedPwd := password.Encode("generic password", options)
    
    	newPassword := fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd) // pbkdf2-sha512是算法,使用$来拼接
    	fmt.Println(len(newPassword))                                        // 要确保长度不会超过数据库字段的定义
    	fmt.Println(newPassword)
    
    	//解析
    	passwordInfo := strings.Split(newPassword, "$")
    	fmt.Println(passwordInfo)
    
    	check := password.Verify("generic password", passwordInfo[2], passwordInfo[3], options)
    	fmt.Println(check) // true
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    三、proto定义

    • user_srv\proto\user.proto:生成命令 -> protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative *.proto
    syntax = "proto3";
    import "google/protobuf/empty.proto";
    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 PageInfo {
        uint32 pn = 1;
        uint32 pSize = 2;
    }
    
    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;
    }
    
    message CreateUserInfo {
        string nickName = 1;
        string passWord = 2;
        string mobile = 3;
    }
    
    message MobileRequest{
        string mobile = 1;
    }
    
    message IdRequest {
        int32 id = 1;
    }
    
    message UpdateUserInfo {
        int32 id = 1;
        string nickName = 2;
        string gender = 3;
        uint64 birthDay = 4;
    }
    
    message PasswordCheckInfo {
        string password = 1;
        string encryptedPassword = 2;
    }
    
    message CheckResponse{
        bool success = 1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    四、用户接口实现

    1 - 查询用户列表接口

    • user_srv\global\global.go:全局初始化db,密码需要自行修改
    package global
    
    import (
    	"log"
    	"os"
    	"time"
    
    	"gorm.io/driver/mysql"
    	"gorm.io/gorm"
    	"gorm.io/gorm/logger"
    	"gorm.io/gorm/schema"
    )
    
    var (
    	DB *gorm.DB
    )
    
    func init() {
    	dsn := "root:root@tcp(192.168.0.101:3306)/mxshop_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,        // 禁用彩色打印
    		},
    	)
    
    	// 全局模式
    	var err error
    	DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
    		NamingStrategy: schema.NamingStrategy{
    			SingularTable: true,
    		},
    		Logger: newLogger,
    	})
    	if err != nil {
    		panic(err)
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • user_srv\handler\user.go:这里使用了gorm的分页功能,并简单进行了改造
    package handler
    
    import (
    	"context"
    	"fmt"
    
    	"nd/user_srv/global"
    	"nd/user_srv/model"
    	"nd/user_srv/proto"
    
    	"gorm.io/gorm"
    )
    
    type UserServer struct {
    	proto.UnimplementedUserServer
    }
    
    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)
    	}
    }
    
    func ModelToRsponse(user model.User) proto.UserInfoResponse {
    	//在grpc的message中字段有默认值,你不能随便赋值nil进去,容易出错
    	//这里要搞清, 哪些字段是有默认值
    	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
    }
    
    func (s *UserServer) GetUserList(ctx context.Context, req *proto.PageInfo) (*proto.UserListResponse, error) {
    	//获取用户列表
    	var users []model.User
    	result := global.DB.Find(&users)
    	if result.Error != nil {
    		return nil, result.Error
    	}
    	fmt.Println("用户列表")
    	rsp := &proto.UserListResponse{}
    	rsp.Total = int32(result.RowsAffected)
    
    	global.DB.Scopes(Paginate(int(req.Pn), int(req.PSize))).Find(&users)
    
    	for _, user := range users {
    		userInfoRsp := ModelToRsponse(user)
    		rsp.Data = append(rsp.Data, &userInfoRsp)
    	}
    	return rsp, nil
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    2 - 根据id和mobile查询用户接口

    • user_srv\handler\user.go
    func (s *UserServer) GetUserByMobile(ctx context.Context, req *proto.MobileRequest) (*proto.UserInfoResponse, error) {
    	//通过手机号码查询用户
    	var user model.User
    	result := global.DB.Where(&model.User{Mobile: req.Mobile}).First(&user)
    	if result.RowsAffected == 0 {
    		return nil, status.Errorf(codes.NotFound, "用户不存在")
    	}
    	if result.Error != nil {
    		return nil, result.Error
    	}
    
    	userInfoRsp := ModelToRsponse(user)
    	return &userInfoRsp, nil
    }
    
    func (s *UserServer) GetUserById(ctx context.Context, req *proto.IdRequest) (*proto.UserInfoResponse, error) {
    	//通过id查询用户
    	var user model.User
    	result := global.DB.First(&user, req.Id)
    	if result.RowsAffected == 0 {
    		return nil, status.Errorf(codes.NotFound, "用户不存在")
    	}
    	if result.Error != nil {
    		return nil, result.Error
    	}
    
    	userInfoRsp := ModelToRsponse(user)
    	return &userInfoRsp, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    3 - 新建用户

    func (s *UserServer) CreateUser(ctx context.Context, req *proto.CreateUserInfo) (*proto.UserInfoResponse, error) {
    	//新建用户
    	var user model.User
    	result := global.DB.Where(&model.User{Mobile: req.Mobile}).First(&user)
    	if result.RowsAffected == 1 {
    		return nil, status.Errorf(codes.AlreadyExists, "用户已存在")
    	}
    
    	user.Mobile = req.Mobile
    	user.NickName = req.NickName
    
    	//密码加密
    	options := &password.Options{SaltLen: 16, Iterations: 100, KeyLen: 32, HashFunction: sha512.New}
    	salt, encodedPwd := password.Encode(req.PassWord, options)
    	user.Password = fmt.Sprintf("$pbkdf2-sha512$%s$%s", salt, encodedPwd)
    
    	result = global.DB.Create(&user)
    	if result.Error != nil {
    		return nil, status.Errorf(codes.Internal, result.Error.Error())
    	}
    
    	userInfoRsp := ModelToRsponse(user)
    	return &userInfoRsp, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    4 - 更新用户

    func (s *UserServer) UpdateUser(ctx context.Context, req *proto.UpdateUserInfo) (*empty.Empty, error) {
    	//个人中心更新用户
    	var user model.User
    	result := global.DB.First(&user, req.Id)
    	if result.RowsAffected == 0 {
    		return nil, status.Errorf(codes.NotFound, "用户不存在")
    	}
    
    	birthDay := time.Unix(int64(req.BirthDay), 0)
    	user.NickName = req.NickName
    	user.Birthday = &birthDay
    	user.Gender = req.Gender
    
    	result = global.DB.Save(&user)
    	if result.Error != nil {
    		return nil, status.Errorf(codes.Internal, result.Error.Error())
    	}
    	return &empty.Empty{}, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    5 - 校验密码

    func (s *UserServer) CheckPassWord(ctx context.Context, req *proto.PasswordCheckInfo) (*proto.CheckResponse, error) {
    	//校验密码
    	options := &password.Options{SaltLen: 16, Iterations: 100, KeyLen: 32, HashFunction: sha512.New}
    	passwordInfo := strings.Split(req.EncryptedPassword, "$")
    	check := password.Verify(req.Password, passwordInfo[2], passwordInfo[3], options)
    	return &proto.CheckResponse{Success: check}, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    五、flag启动grpc

    • user_srv\main.go
    package main
    
    import (
    	"flag"
    	"fmt"
    	"nd/user_srv/handler"
    	"nd/user_srv/proto"
    	"net"
    
    	"google.golang.org/grpc"
    )
    
    func main() {
    	IP := flag.String("ip", "0.0.0.0", "ip地址")
    	Port := flag.Int("port", 50051, "端口号")
    	flag.Parse()
    	fmt.Println("ip...", *IP)
    	fmt.Println("port...", *Port)
    
    	server := grpc.NewServer()
    	proto.RegisterUserServer(server, &handler.UserServer{})
    	lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
    	if err != nil {
    		panic("failed to listen:" + err.Error())
    	}
    	err = server.Serve(lis)
    	if err != nil {
    		panic("failed to start grpc:" + err.Error())
    	}
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 终端启动使用flag的方法
      • 使用go build main.go生成main.exe
      • 使用main.exe -port 50052启动端口为50052的grpc服务
        在这里插入图片描述

    六、接口测试

    • user_srv\tests\user.go
    package main
    
    import (
    	"context"
    	"fmt"
    	"nd/user_srv/proto"
    
    	"google.golang.org/grpc"
    	"google.golang.org/grpc/credentials/insecure"
    )
    
    var userClient proto.UserClient
    var conn *grpc.ClientConn
    
    func Init() {
    	var err error
    	conn, err = grpc.Dial("127.0.0.1:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    	if err != nil {
    		panic(err)
    	}
    	userClient = proto.NewUserClient(conn)
    }
    
    func TestGetUserList() {
    	rsp, err := userClient.GetUserList(context.Background(), &proto.PageInfo{
    		Pn:    1,
    		PSize: 5,
    	})
    	if err != nil {
    		panic(err)
    	}
    	for _, user := range rsp.Data {
    		fmt.Println(user.Mobile, user.NickName, user.PassWord)
    		checkRsp, err := userClient.CheckPassWord(context.Background(), &proto.PasswordCheckInfo{
    			Password:          "admin123",
    			EncryptedPassword: user.PassWord,
    		})
    		if err != nil {
    			panic(err)
    		}
    		fmt.Println(checkRsp.Success)
    	}
    }
    
    func TestCreateUser() {
    	for i := 0; i < 10; i++ {
    		rsp, err := userClient.CreateUser(context.Background(), &proto.CreateUserInfo{
    			NickName: fmt.Sprintf("bobby%d", i),
    			Mobile:   fmt.Sprintf("1878222222%d", i),
    			PassWord: "admin123",
    		})
    		if err != nil {
    			panic(err)
    		}
    		fmt.Println(rsp.Id)
    	}
    }
    
    func main() {
    	Init()
    	// TestCreateUser()
    	TestGetUserList()
    
    	conn.Close()
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    在这里插入图片描述

  • 相关阅读:
    python连接sqlserver
    操作系统·操作系统引论
    收藏:不能不刷的数字后端面试题,含解析
    深入理解Java多线程之线程间的通信方式(上)
    报告解读下载 | 11月《中国数据库行业分析报告》发布,精彩抢先看
    如何将gif变成视频?3个转换方法
    【USMA】N1CTF2022-praymoon
    计算机毕业设计(附源码)python学生实训管理网站
    React之组件实例的三大属性之rel
    docker-compose在虚拟机上搭建zookeeper+kafka3.0.0集群
  • 原文地址:https://blog.csdn.net/qq23001186/article/details/125881609