go-zero 实战 - User Login

1,802 阅读5分钟

我们通过一个系列文章跟大家详细展示一个 go-zero 微服务实例,整个系列分十三篇文章,目录结构如下:

  1. go-zero 实战 - 服务划分与项目创建
  2. go-zero 实战 - User API Gateway
  3. go-zero 实战 - User Login
  4. go-zero 实战 - User Register
  5. go-zero 实战 - User Userinfo
  6. go-zero 实战 - Food API Gateway
  7. go-zero 实战 - Food Search
  8. go-zero 实战 - Food AddFood
  9. go-zero 实战 - Food DeleteFood
  10. go-zero 实战 - Food Foodlist
  11. go-zero 实战进阶 - rpc 服务
  12. go-zero 实战进阶 - 用户管理 rpc 服务
  13. go-zero 实战进阶 - 食材管理 rpc 服务

期望通过本系列文章带你在本地利用 go-zero 快速开发一个《食谱指南》系统,让你快速上手微服务。

在上一篇 《go-zero 实战 - User API Gateway》 中,我们通过定义 user.api 文件中的内容对 API 类型进行了声明,并通过 goctl api 命令,一键快速生成一个 api 服务,如果仅仅是启动一个 go-zeroapi 演示项目,你甚至都不用编码,就可以完成一个 api 服务开发及正常运行。

本篇将在上一篇的基础上完成以下工作内容:

  1. 创建本地数据库表 user,并定义表结构,以便存储用户基本信息;
  2. 使用 goctl model 自动生成 mysql CRUD 代码;
  3. 添加用户登录接口的逻辑。

创建本地数据库表 user,并定义表结构

在本机 mysql 中创建 foodguides 数据库,并新建 user 表。

CREATE TABLE `user` (
                        `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户Id',
                        `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户名称',
                        `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户密码',
                        `email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户邮箱',
                        `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
                        `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                        PRIMARY KEY (`id`),
                        UNIQUE KEY `name_index` (`name`),
                        UNIQUE KEY `email_index` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

执行如下 SQL 语句新增一条数据:

INSERT INTO `foodguides`.`user`(`id`, `name`, `password`, `email`) VALUES (1, 'RenPanPan', '3a1b7b6dba4af2887d91d2dab0090e9f7c8a4da5d6b17883ff346e026ec27213', 'renpanpan1990@163.com');

使用 goctl model 生成 mysql CRUD 代码

model 下新建 user.sql 文件,将上述创建 user 表的 SQL 语句复制到 user.sql 文件中。执行命令 goctl model 命令生成 CRUD 代码。

$ cd usermanage/model
$ goctl model mysql ddl --src user.sql --dir .
Done.

当你看到 Done. 输出则代表生成成功了,接下来我们来看一下生成的代码内容:

➜  model:
# 列出当前目录下的文件  
$ ls  
user.sql usermodel.go usermodel_gen.go vars.go  
# 查看目录树  
$ tree  
.  
├── user.sql  
├── usermodel.go  // CRUD 代码
├── usermodel_gen.go  
└── vars.go // 定义常量和变量

编写 user login 服务

修改 user-api.yaml 配置文件

编辑 usermanage/api/etc 下的 user-api.yaml 文件,修改服务地址,端口号为 0.0.0.0:8888Mysql 服务配置,Auth 验证配置。

Name: user-api
Host: 0.0.0.0
Port: 8888

Mysql:
  DataSource: root:123456@tcp(127.0.0.1:9528)/foodguides?charset=utf8mb4&parseTime=True&loc=Local

Auth:
  AccessSecret: ad879037-d3fd-tghj-112d-6bfc35d54b7d
  AccessExpire: 86400

Salt: HWVOFkGgPTryzICwd7qnJaZR9KQ2i8xe

Tips

如何配置 DataSource?

添加服务配置实例化

$ vim usermanage/api/internal/config/config.go

package config

import "github.com/zeromicro/go-zero/rest"

type Config struct {
    rest.RestConf
    MySql struct {
       DataSource string
    }
    Auth struct {
       AccessSecret string
       AccessExpire int64
    }
    Salt string
}

注册服务上下文依赖

$ vim usermanage/api/internal/svc/serviceContext.go

package svc

import (
    "FoodGuides/service/usermanage/api/internal/config"
    "FoodGuides/service/usermanage/model"
    "github.com/zeromicro/go-zero/core/stores/sqlx"
)

type ServiceContext struct {
    Config config.Config
    UserModel model.UserModel
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
       Config: c,
       UserModel: model.NewUserModel(sqlx.NewMysql(c.MySql.DataSource)),
    }
}

添加用户登录逻辑 Login

添加 JWT 工具

在根目录 common 下新建 jwtx 工具库,用于生成用户 token

$ vim common/jwtx/jwt.go

package jwtx

import "github.com/golang-jwt/jwt/v4"

// GetJwtToken 
// @secretKey: JWT 加解密密钥
// @iat: 时间戳
// @seconds: 过期时间,单位秒
// @uid: 用户 ID
func GetJwtToken(secretKey string, iat, seconds, uid int64) (string, error) {
    claims := make(jwt.MapClaims)
    claims["exp"] = iat + seconds
    claims["iat"] = iat
    claims["uid"] = uid
    token := jwt.New(jwt.SigningMethodHS256)
    token.Claims = claims
    return token.SignedString([]byte(secretKey))
}

GetJwtToken 方法的实现可参考 官网示例

添加密码加密工具

在根目录 common 下新建 crypt 工具库,此工具方法主要用于密码的加密处理。

$ vim common/cryptx/crypt.go

package cryptx

import (
    "fmt"
    "golang.org/x/crypto/scrypt"
)

func PasswordEncrypt(salt, password string) string {
    dk, _ := scrypt.Key([]byte(password), []byte(salt), 32768, 8, 1, 32)
    return fmt.Sprintf("%x", string(dk))
}

添加用户登录逻辑

用户登录流程:过邮箱查询判断用户是否是注册用户,如果是注册用户,需要将用户输入的密码进行加密与数据库中用户加密密码进行对比验证。当两者相等时,即认为用户登录成功,我们将使用用户信息生成对应的 token 以及 token 的有效期。

$ vim usermanage/api/internal/logic/loginlogic.go

package logic

import (
    "FoodGuides/common/cryptx"
    "FoodGuides/common/jwtx"
    "FoodGuides/service/usermanage/model"
    "context"
    "errors"
    "time"

    "FoodGuides/service/usermanage/api/internal/svc"
    "FoodGuides/service/usermanage/api/internal/types"

    "github.com/zeromicro/go-zero/core/logx"
)

type LoginLogic struct {
    logx.Logger
    ctx    context.Context
    svcCtx *svc.ServiceContext
}

func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
    return &LoginLogic{
       Logger: logx.WithContext(ctx),
       ctx:    ctx,
       svcCtx: svcCtx,
    }
}

func (l *LoginLogic) Login(req *types.LoginRequest) (*types.LoginResponse, error) {
    // 查询用户是否存在
    res, err := l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email)
    if err != nil {
       if err == model.ErrNotFound {
          return nil, errors.New("用户不存在")
       }
       return nil, err
    }

    // 判断密码是否正确
    password := cryptx.PasswordEncrypt(l.svcCtx.Config.Salt, req.Password)
    if password != res.Password {
       return nil, errors.New("密码错误")
    }

    now := time.Now().Unix()
    accessExpire := l.svcCtx.Config.Auth.AccessExpire
    jwtToken, err := jwtx.GetJwtToken(l.svcCtx.Config.Auth.AccessSecret, now, accessExpire, res.Id)
    if err != nil {
       return nil, err
    }
    
    token := types.JwtToken{
       AccessToken:  jwtToken,
       AccessExpire: now + accessExpire,
       RefreshAfter: now + accessExpire/2,
    }
    
    response := types.UserReply{
       Id:       res.Id,
       Username: res.Name,
       Email:    res.Email,
       JwtToken: token,
    }
    
    return &types.LoginResponse{UserReply: response}, nil
}

启动服务

运行如下命令以启动 user api 服务, 运行成功后,user api 则运行在本机的 8888 端口:

➜  service:
$ go run usermanage/api/user.go -f usermanage/api/etc/user-api.yaml
Starting server at 0.0.0.0:8888...

我们用 Postman 尝试请求 /users/login 接口:

  1. PostmanBody 选项中选择 raw,并设置 JSON 内容为 {"email": "renpanpan1990@163.com","password": "809161"}
  2. 点击发送请求按钮,有如下截图的响应说明接口运行正常。

image.png

修改 Response 返回格式

我希望客户端接口请求数据的返回格式是这样子的:

{
    "code": 1,
    "msg": "登录成功",
    "data": {
        "id": 1,
        "username": "RenPanPan",
        "email": "renpanpan1990@163.com",
        "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTc2OTk3NDQsImlhdCI6MTY5NzYxMzM0NCwidWlkIjoxfQ.KF7woznVmjuPz85o1vYXnNTRvvnlZXASn7N9kWRL0nA",
        "accessExpire": 1697699744,
        "refreshAfter": 1697656544
    }
}

目前如果需要实现这种格式响应,有 2 种做法:

  1. 自定义响应格式
  2. 使用 go-zero 扩展包来实现

我们用做法 1 来演示一下,做法 2 可参考官方文档的演示实例 HTTP 扩展

  • 在根目录 common 下新建 response 工具库,用来统一请求响应返回格式
$ vim common/responsex/response.go

package responsex

type HttpResponse struct {
    Code    int         `json:"code"`
    Message string      `json:"msg"`
    Data    interface{} `json:"data"`
}

func SuccessResponse(resData interface{}, message string) HttpResponse {
    return HttpResponse{Code: 1, Message: message, Data: resData}
}

func FailureResponse(resData interface{}, message string, code int) HttpResponse {
    return HttpResponse{Code: code, Message: message, Data: resData}
}
  • 修改 loginhandler.go 文件
$ vim usermanage/api/internal/handler/loginhandler.go

package handler

import (
    "FoodGuides/common/responsex"
    "net/http"

    "FoodGuides/service/usermanage/api/internal/logic"
    "FoodGuides/service/usermanage/api/internal/svc"
    "FoodGuides/service/usermanage/api/internal/types"
    "github.com/zeromicro/go-zero/rest/httpx"
)

func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
       var req types.LoginRequest
       if err := httpx.Parse(r, &req); err != nil {
          // httpx.ErrorCtx(r.Context(), w, err)
          httpx.OkJson(w, responsex.FailureResponse(nil, err.Error(), 1000))
          return
       }

       l := logic.NewLoginLogic(r.Context(), svcCtx)
       resp, err := l.Login(&req)
       if err != nil {
          // httpx.ErrorCtx(r.Context(), w, err)
          httpx.OkJson(w, responsex.FailureResponse(nil, err.Error(), 1000))
       } else {
          // httpx.OkJsonCtx(r.Context(), w, resp)
          httpx.OkJson(w, responsex.SuccessResponse(resp, "登录成功"))
       }
    }
}
  • 重启服务,并发起 /users/login 请求

image.png

发现请求响应已变成我们想要的格式。至此,我们就完成了用户登录接口的搭建,下一篇我们将采用相同的方法搭建用户注册接口。

上一篇《go-zero 实战 - User API Gateway》

下一篇《go-zero 实战 - User Register》