Write the Code. Change the World.

9月 08

上一步中,生成图片验证码接口已经好了。现在完善登录和注册的业务逻辑。这里准备使用 token 的方式来维护用户的状态,选用 jwt 。所以登录或注册成功后,只需返回用户一个 token 就可以了。过期时间暂时先不要。所以登录和注册的返回需要修改一番。

登录注册输出重新定义一下

internal/model/admin/user.go 中定义输入和输出的结构。

type SignInRes struct {
    Token string `json:"token"`
}

type SignUpRes struct {
    Token string `json:"token"`
}

api/user/admin/user.go 中组合输入输出

package admin

import (
    model "goSimpleAdmin/internal/model/admin"

    "github.com/gogf/gf/v2/frame/g"
)

type SignInRes struct {
    model.SignInRes
}

type SignUpRes struct {
    model.SignUpRes
}

因为 api 定义的时候已经定义好了输出。所以 ctrl 和 service 这些不需要再重新生一次了。

但是在 logic 和 service 中没有定义 res。所以需要补上,再重新生一次。 service 就是接口的定义,实现还是在 logic 中。

internal/logic/user/user.go 修改

func (s *sUser) SignIn(ctx context.Context, in model.SignInReq) (res model.SignInRes, err error) {

    err = gerror.New("我的天呀")

    return res, err
}

func (s *sUser) SignUp(ctx context.Context, in model.SignUpReq) (res model.SignUpRes, err error) {

    err = gerror.New("我的地呀")

    return res, err
}

然后再重新生成 service。

gf gen service

这样在 ctrl 中就可以接到 res 或 err 了。就能给到输出了。

登录控制器

先处理 controller,因为 controller 几乎也是死的,灵活的都留给 logic 了。

编辑 internal/controller/user/user_admin_sign_in.go

package user

import (
    "context"

    "goSimpleAdmin/api/user/admin"
    model "goSimpleAdmin/internal/model/admin"
    "goSimpleAdmin/internal/service"
)

func (c *ControllerAdmin) SignIn(ctx context.Context, req *admin.SignInReq) (res *admin.SignInRes, err error) {
    data, err := service.User().SignIn(ctx, model.SignInReq{
        Passport: req.Passport,
        Password: req.Passport,
    })

    if err != nil {
        return
    }

    res = new(admin.SignInRes)
    res.SignInRes = data
    return
}

这里有点别扭的地方就是每次都要对 logic 的 res 要转换一下。还有就是返回给请求端的总是会包裹一层。包裹一层这个问题啰嗦了好久,后边肯定要搞定的。

剩下的逻辑全在 logic 这里了。修改完善 internal/logic/user/user.go 中的 SignIn 方法。

登录业务逻辑

当前登录是账号密码的登录方式。需要处理三个事情。

  • 查找用户
  • 比较用户的密码
  • 生成 token

查找用户通过 orm 就可以。

比较用户的密码就得看密码的来由。通常密码都会是加密的。这里想用 Bcrypt 作为驱动来加密,不用官方封装那几种加密方式。这个加密和判断密码是否正确都有自己的函数,也就是比较密码是否正确不是通过比较两个密码的字符串是否一样。并且可以设置加密的加密系数。

好开干。

# 下载
go get golang.org/x/crypto/bcrypt

# 封装 新建  internal/pkg/hash/hash.go

package hash

import (
    "golang.org/x/crypto/bcrypt"
)

// BcryptHash 使用 bcrypt 对密码进行加密
func BcryptHash(password string) (value string, err error) {
    // GenerateFromPassword 的第二个参数是 cost 值。建议大于 12,数值越大耗费时间越长
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)

    return string(bytes), err
}

// BcryptCheck 对比明文密码和数据库的哈希值
func BcryptCheck(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

// BcryptIsHashed 判断字符串是否是哈希过的数据
func BcryptIsHashed(str string) bool {
    // bcrypt 加密后的长度等于 60
    return len(str) == 60
}

然后,在 logic 中使用上。(先获取用户,如果用户存在,再比较密码)

var user *entity.User
    err = dao.User.Ctx(ctx).Where(do.User{
        Passport: in.Passport,
    }).WhereOr(do.User{
        Email: in.Passport,
    }).WhereOr(do.User{
        Phone: in.Passport,
    }).Scan(&user)

    if err != nil {
        return res, err
    }

    if user == nil {
        return res, gerror.New("账号不存在")
    }

    check := hash.BcryptCheck(in.Password, user.Password)
    if !check {
        return res, gerror.New("账号或密码错误")
    }

第一个提示对于坏人就有点友好。所以需要在中间件中对请求频率进行控制。这个后边再说。

生成 token,还是使用 jwt 方案。go 的 jwt 封装比较多。这里使用 goframe 官方自己封装的 jwt。

https://goframe.org/display/jwt/gf-jwt

# 先下载
go get github.com/gogf/gf-jwt/v2

再次封装。

package jwt

import (
    "context"
    "sync"
    "time"

    jwtv2 "github.com/gogf/gf-jwt/v2"
    "github.com/gogf/gf/v2/frame/g"
)

type JwtConfig struct {
    Realm      string
    Key        string
    Timeout    int
    MaxRefresh int
}

var authService *jwtv2.GfJWTMiddleware

var once sync.Once

func NewJwt() *jwtv2.GfJWTMiddleware {
    once.Do(func() {

        config := JwtConfig{
            Realm:      "go simple admin",
            Key:        "this is key",
            Timeout:    60 * 24 * 7,
            MaxRefresh: 60 * 24 * 10,
        }

        auth := jwtv2.New(&jwtv2.GfJWTMiddleware{
            Realm:           config.Realm,
            Key:             []byte(config.Key),
            Timeout:         time.Minute * time.Duration(config.Timeout),
            MaxRefresh:      time.Minute * time.Duration(config.MaxRefresh),
            IdentityKey:     "id",
            TokenLookup:     "header: Authorization, query: token, cookie: jwt",
            TokenHeadName:   "Bearer",
            TimeFunc:        time.Now,
            Authenticator:   Authenticator,
            Unauthorized:    Unauthorized,
            PayloadFunc:     PayloadFunc,
            IdentityHandler: IdentityHandler,
        })
        authService = auth
    })
    return authService
}

func PayloadFunc(data interface{}) jwtv2.MapClaims {
    claims := jwtv2.MapClaims{}
    params := data.(map[string]interface{})
    if len(params) > 0 {
        for k, v := range params {
            claims[k] = v
        }
    }
    return claims
}

func IdentityHandler(ctx context.Context) interface{} {
    claims := jwtv2.ExtractClaims(ctx)
    return claims[authService.IdentityKey]
}

func Unauthorized(ctx context.Context, code int, message string) {
    r := g.RequestFromCtx(ctx)
    r.Response.WriteJson(g.Map{
        "code":    code,
        "message": message,
    })
    r.ExitAll()
}

// Authenticator is used to validate login parameters.
// It must return user data as user identifier, it will be stored in Claim Array.
// if your identityKey is 'id', your user data must have 'id'
// Check error (e) to determine the appropriate error message.
func Authenticator(ctx context.Context) (user interface{}, err error) {
    return user, nil
}

本来,是想弄个配置文件的。可是 g.Cfg 获取配置信息需要 context。某些时候,不方便传递 context。暂时就这样,先实现功能。还有这里不需要 Authenticator 。觉得 jwt 就单纯做 jwt 的事情。发放 token 和验证 token。在 jwt 里边搞请求用户数据就有点不太好。没事,不用它,用 TokenGenerator 方法生成 token 就好。

既然这里封装好了 jwt。那么就接着上边的用起来。不是验证用户密码通过后,就可以给用户颁发 token了。

func (s *sUser) SignIn(ctx context.Context, in model.SignInReq) (res model.SignInRes, err error) {
    var user *entity.User
    err = dao.User.Ctx(ctx).Where(do.User{
        Passport: in.Passport,
    }).WhereOr(do.User{
        Email: in.Passport,
    }).WhereOr(do.User{
        Phone: in.Passport,
    }).Scan(&user)

    if err != nil {
        return res, err
    }

    if user == nil {
        return res, gerror.New("账号不存在")
    }

    check := hash.BcryptCheck(in.Password, user.Password)
    if !check {
        return res, gerror.New("账号或密码错误")
    }

    data := g.Map{
        "id":       user.Id,
        "nickname": user.Nickname,
    }

    res.Token, res.Expire, err = jwt.NewJwt().TokenGenerator(data)

    return res, err
}

TokenGenerator 方法中的 data,必须至少有一个 id。这个是用户的唯一识别。和 jwt 配置项里的 IdentityKey 对应。一般,都不会去更改。

好运行起来,用 postman 请求看看。返回了以下数据,看起来,是 ok 的。

{
    "code": 0,
    "message": "",
    "data": {
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTQ3ODQwNTE4OTgsImlkIjo4LCJuaWNrbmFtZSI6IuWGgOWzsCIsIm9yaWdfaWF0IjoxNjk0MTc5MjUxODk4fQ.UJMIiJhBGLo2cuVQ5ibXgTDSN2upxdh7D0ahgdshGZg",
        "expire": "2023-09-15T13:20:51.898854Z"
    }
}

将 token 复制到 https://jwt.io 中解析看看。可以看到 paylod 的数据是。

{
  "exp": 1694784051898,
  "id": 8,
  "nickname": "冀峰",
  "orig_iat": 1694179251898
}

看来是真有用了。

再来搞个 auth 中间件

internal/logic/middleware/middleware.go 自定义中间件放这里。

package middleware

import (
    "github.com/gogf/gf/v2/net/ghttp"

    "goSimpleAdmin/internal/pkg/jwt"
    "goSimpleAdmin/internal/service"
)

type (
    sMiddleware struct{}
)

func init() {
    service.RegisterMiddleware(New())
}

func New() service.IMiddleware {
    return &sMiddleware{}
}

func (s *sMiddleware) Auth(r *ghttp.Request) {
    jwt.NewJwt().MiddlewareFunc()(r)

    r.Middleware.Next()
}

func (s *sMiddleware) CORS(r *ghttp.Request) {
    r.Response.CORSDefault()
    r.Middleware.Next()
}

然后使用 gf gen service 生成对应的 service 文件,就是接口文件。并在 logic.go 中自动注册绑定。

最后,在 cmd 中使用中间件。可以看到所有中间件都共同实现了相同的接口的。

s.Group("/api/admin", func(group *ghttp.RouterGroup) {
                group.Middleware(
                    ghttp.MiddlewareCORS,
                )

                group.ALLMap(g.Map{
                    "/captcha":      user.NewAdmin().Captcha,
                    "/user/sign-in": user.NewAdmin().SignIn,
                    "/user/sign-up": user.NewAdmin().SignUp,
                })

                group.Group("/", func(group *ghttp.RouterGroup) {
                    group.Middleware(service.Middleware().Auth)

                    group.ALLMap(g.Map{
                        "/user/info": user.NewAdmin().UserInfo,
                    })
                })
            })

这里把路由绑定稍微改了下,不再让自动绑定,改为具体绑定了。中间件也加上了。现在可以试试刚才生成的 token 是否有用。

在 postman 中加入 user/info 的请求。在请求头中增加 Authorization 的配置。类别选择 Bearea Token,并把上边的 token 值复制进去。然后请求看看结果。

主要看图片上 1,2,3,4 这四个地方的设置。{{}} 是通过变量实现的。

获取用户信息的实现之前没加,现在加进去。其实,是已经加了,要不也请求不出数据来。

internal/logic/user/user.go

func (s *sUser) UserInfo(ctx context.Context, in model.UserInfoReq) (res model.UserInfoRes, err error) {
    var user *entity.User

    id := jwt.NewJwt().GetIdentity(ctx)

    err = dao.User.Ctx(ctx).Where(do.User{
        Id: id,
    }).Scan(&user)

    if err != nil {
        return res, err
    }

    res.User = *user
    return res, err
}

看这个逻辑,可以看到通过 jwt 获取到 id,再通过 id 获取用户信息的。其实再返回给用户的用户信息时,password 这些信息是不应该有的。后边再处理。

通过 https://goframe.org/pages/viewpage.action?pageId=1114226 这个应该可以。还是没 laravel 好用。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注